diff --git a/components/toolbar/toolbar.tsx b/components/toolbar/toolbar.tsx index bfcae33..515b81a 100644 --- a/components/toolbar/toolbar.tsx +++ b/components/toolbar/toolbar.tsx @@ -26,11 +26,16 @@ const Toolbar = () => { React.useEffect(() => { const listener = async () => { const tab = await getCurrentTab(); - const url = new URL(tab.url); - if (url.origin.startsWith('moz-extension')) { - return; + + if (tab !== undefined) { + const url = new URL(tab.url); + if (url.origin.startsWith('moz-extension')) { + return; + } + setOrigin(url.origin); + } else { + console.warn('Out of the tab scope'); } - setOrigin(url.origin); }; browser.tabs.onUpdated.addListener(listener); @@ -217,7 +222,7 @@ const Toolbar = () => { 'fullscreen=yes', ].join(','); window.open( - `/report-window/report-window.html?origin=${origin}`, + `/components/report-window/report-window.html?origin=${origin}`, 'new_window', params ); diff --git a/extended-request.ts b/extended-request.ts index 14af85f..cd23f20 100644 --- a/extended-request.ts +++ b/extended-request.ts @@ -1,413 +1,406 @@ -import { StolenDataEntry } from "./stolen-data-entry"; +import { StolenDataEntry } from './stolen-data-entry'; import { - flattenObjectEntries, - getshorthost, - parseCookie, - Request, - safeDecodeURIComponent, -} from "./util"; + flattenObjectEntries, + getshorthost, + parseCookie, + Request, + safeDecodeURIComponent, +} from './util'; type NameValue = { name: string; value: string }; export type HAREntry = { - pageref: string; - startedDateTime: string; - request: { - bodySize: number; - cookies: NameValue[]; - headers: NameValue[]; - headersSize: number; - httpVersion: string; - method: string; - postData?: { - mimeType: string; - params: (NameValue & { - fileName: string; - contentType: string; - comment: ""; - })[]; - text: string; + pageref: string; + startedDateTime: string; + request: { + bodySize: number; + cookies: NameValue[]; + headers: NameValue[]; + headersSize: number; + httpVersion: string; + method: string; + postData?: { + mimeType: string; + params: (NameValue & { + fileName: string; + contentType: string; + comment: ''; + })[]; + text: string; + }; + queryString: NameValue[]; + url: string; }; - queryString: NameValue[]; - url: string; - }; - response: { - status: number; - statusText: string; - httpVersion: string; - headers: NameValue[]; - cookies: NameValue[]; - content: { - mimeType: string; - size: number; - encoding: "base64"; - text: string; - }; - redirectURL: ""; - headersSize: number; - bodySize: number; - }; // not relevant - cache: {}; - timings: {}; - time: number; - _securityState: string; - serverIPAddress: string; - connection: string; + response: { + status: number; + statusText: string; + httpVersion: string; + headers: NameValue[]; + cookies: NameValue[]; + content: { + mimeType: string; + size: number; + encoding: 'base64'; + text: string; + }; + redirectURL: ''; + headersSize: number; + bodySize: number; + }; // not relevant + cache: {}; + timings: {}; + time: number; + _securityState: string; + serverIPAddress: string; + connection: string; }; const whitelisted_cookies = [ - /^Accept.*$/, - /^Host$/, - /^Connection$/, - /^Sec-Fetch-.*$/, - /^Content-Type$/, - /^Cookie$/, // we're extracting it in getCookie separately anyway - /^User-Agent$/, + /^Accept.*$/, + /^Host$/, + /^Connection$/, + /^Sec-Fetch-.*$/, + /^Content-Type$/, + /^Cookie$/, // we're extracting it in getCookie separately anyway + /^User-Agent$/, ]; type RequestBody = { - error?: string; - formData?: Record; - raw?: { bytes: ArrayBuffer; file?: string }[]; + error?: string; + formData?: Record; + raw?: { bytes: ArrayBuffer; file?: string }[]; }; export default class ExtendedRequest { - public tabId: number; - public url: string; - public shorthost: string; - public requestHeaders: Request["requestHeaders"] = []; - public originalURL: string; - public origin: string; - public initialized = false; - public stolenData: StolenDataEntry[]; - public originalPathname: string; - public requestBody: RequestBody; + public tabId: number; + public url: string; + public shorthost: string; + public requestHeaders: Request['requestHeaders'] = []; + public originalURL: string; + public origin: string; + public initialized = false; + public stolenData: StolenDataEntry[]; + public originalPathname: string; + public requestBody: RequestBody; - static by_id = {} as Record; + static by_id = {} as Record; + public data: Request; - constructor(public data: Request) { - this.tabId = data.tabId; - this.url = data.url; - this.shorthost = getshorthost(data.url); - this.requestBody = - ((data as any).requestBody as undefined | RequestBody) || {}; - if (this.url.includes("criteo")) { - console.log(this); - } - ExtendedRequest.by_id[data.requestId] = this; - } - - addHeaders(headers: Request["requestHeaders"]) { - this.requestHeaders = headers; - return this; - } - - async init() { - await this.cacheOrigin(); - this.initialized = true; - this.stolenData = this.getAllStolenData(); - } - - async cacheOrigin(): Promise { - let url: string; - if (this.data.tabId && this.data.tabId >= 0) { - const tab = await browser.tabs.get(this.data.tabId); - url = tab.url; - } else if ((this.data as any)?.frameAncestors) { - url = (this.data as any).frameAncestors[0].url || ""; - } else { - const headers = Object.fromEntries( - this.requestHeaders.map(({ name, value }) => [name, value]) - ); - if (headers.Referer) { - url = headers.Referer; - } else { - url = this.data.url; - } - } - - this.originalURL = url; - this.origin = new URL(url).origin; - this.originalPathname = new URL(url).pathname; - } - - isThirdParty() { - const request_url = new URL(this.data.url); - const origin_url = new URL(this.originalURL); - if (request_url.host.includes(origin_url.host)) { - return false; - } - if (getshorthost(request_url.host) == getshorthost(origin_url.host)) { - return false; - } - return ( - request_url.origin != origin_url.origin || - (this.data as any).urlClassification.thirdParty.length > 0 - ); - } - - getReferer() { - return ( - this.requestHeaders.filter((h) => h.name === "Referer")[0]?.value || - "missing-referrer" - ); - } - - exposesOrigin() { - const url = new URL(this.originalURL); - const host = url.host; - const path = url.pathname; - const shorthost = getshorthost(host); - if (this.getReferer().includes(shorthost)) { - return true; - } - for (const entry of this.stolenData) { - if ( - entry.value.includes(host) || - entry.value.includes(path) || - entry.value.includes(shorthost) - ) { - return true; - } - } - return false; - } - - private getAllStolenData(): StolenDataEntry[] { - return [ - ...this.getPathParams(), - ...this.getCookieData(), - ...this.getQueryParams(), - ...this.getHeadersData(), - ...this.getRequestBodyData(), - ]; - } - - getCookieData(): StolenDataEntry[] { - if (!this.hasCookie() || this.getCookie() === undefined) { - return []; - } - return flattenObjectEntries( - Object.entries(parseCookie(this.getCookie())).map(([key, value]) => [ - key, - value || "", - ]), - StolenDataEntry.parseValue - ).map(([key, value]) => new StolenDataEntry(this, "cookie", key, value)); - } - - getRequestBodyData(): StolenDataEntry[] { - const ret = flattenObjectEntries( - Object.entries({ - ...this.requestBody.formData, - ...Object.fromEntries( - Object.entries( - this.requestBody.raw || {} - ).map(([key, value], index) => [`${key}.${index}`, value]) - ), - }).map(([key, value]) => { - // to handle how ocdn.eu encrypts POST body on https://businessinsider.com.pl/ - if ( - (Array.isArray(value) && value.length === 1 && !value[0]) || - !value - ) { - return ["requestBody", key]; - } else if (!Array.isArray(value)) { - return [ - "raw", - String.fromCharCode.apply(null, new Uint8Array(value.bytes)), - ]; - } else { - return [key, value || ""]; + constructor(data: Request) { + this.tabId = data.tabId; + this.url = data.url; + this.shorthost = getshorthost(data.url); + this.requestBody = ((data as any).requestBody as undefined | RequestBody) || {}; + if (this.url.includes('criteo')) { + console.log(this); } - }), - StolenDataEntry.parseValue - ).map( - ([key, value]) => new StolenDataEntry(this, "request_body", key, value) - ); - return ret; - } + ExtendedRequest.by_id[data.requestId] = this; - hasReferer() { - return this.requestHeaders.some((h) => h.name === "Referer"); - } + this.data = Object.assign({}, data); + (this.data as any).frameAncestors = [ + ...(data as any).frameAncestors.map((e: any) => ({ url: e.url })), + ]; - hasCookie() { - return this.requestHeaders.some((h) => h.name === "Cookie"); - } - - getCookie(): string { - return this.requestHeaders.find((h) => h.name == "Cookie")?.value; - } - - getPathParams(): StolenDataEntry[] { - const url = new URL(this.data.url); - const path = url.pathname; - if (!path.includes(";")) { - return []; + // console.log('→→→',(this.data as any).frameAncestors, (data as any).frameAncestors); } - return flattenObjectEntries( - path - .split(";") - .map((e) => e.split("=")) - .map(([key, value]) => [key, value || ""]) - .map(([key, value]) => { - return [ - key, - StolenDataEntry.parseValue(safeDecodeURIComponent(value)), - ]; - }) - ).map(([key, value]) => new StolenDataEntry(this, "pathname", key, value)); - } - getQueryParams(): StolenDataEntry[] { - const url = new URL(this.data.url); - return flattenObjectEntries( - Array.from((url.searchParams as any).entries()) - .map(([key, value]) => [key, value || ""]) - .map(([key, value]) => { - return [ - key, - StolenDataEntry.parseValue(safeDecodeURIComponent(value)), - ]; - }) - ).map(([key, value]) => { - return new StolenDataEntry(this, "queryparams", key, value); - }); - } + addHeaders(headers: Request['requestHeaders']) { + this.requestHeaders = headers; + return this; + } - getHeadersData(): StolenDataEntry[] { - return flattenObjectEntries( - this.requestHeaders - .filter((header) => { - for (const regex of whitelisted_cookies) { - if (regex.test(header.name)) { - return false; + async init() { + await this.cacheOrigin(); + this.initialized = true; + this.stolenData = this.getAllStolenData(); + } + + async cacheOrigin(): Promise { + let url: string; + if (this.data.tabId && this.data.tabId >= 0) { + const tab = await browser.tabs.get(this.data.tabId); + url = tab.url; + } else if ( + (this.data as any)?.frameAncestors && + (this.data as any).frameAncestors[0] !== undefined + ) { + url = (this.data as any).frameAncestors[0].url || ''; + } else { + const headers = Object.fromEntries( + this.requestHeaders.map(({ name, value }) => [name, value]) + ); + if (headers.Referer) { + url = headers.Referer; + } else { + url = this.data.url; } - } - return true; - }) - .map((header) => { - return [ - header.name, - StolenDataEntry.parseValue(safeDecodeURIComponent(header.value)), - ]; - }) - ).map(([key, value]) => new StolenDataEntry(this, "header", key, value)); - } + } - hasMark() { - return this.stolenData.some((data) => data.isMarked); - } - - getMarkedEntries() { - return this.stolenData.filter((data) => data.isMarked); - } - - getHost() { - return new URL(this.url).host; - } - - matchesHAREntry(har: HAREntry): boolean { - const rq = this.data; - const hrq = har.request; - return rq.url == hrq.url; - } - - toHAR(): HAREntry { - return { - pageref: "page_1", - startedDateTime: `${new Date().toJSON().replace("Z", "+01:00")}`, - request: { - bodySize: - JSON.stringify(this.requestBody.formData || {}).length + - (this.requestBody.raw || []) - .map((e) => e.bytes.byteLength) - .reduce((a, b) => a + b, 0), - method: this.data.method, - url: this.data.url, - headersSize: JSON.stringify(this.requestHeaders).length, - httpVersion: "HTTP/2", - headers: this.requestHeaders as NameValue[], - cookies: this.getCookieData().map((cookie) => ({ - name: cookie.name, - value: cookie.value, - })), - queryString: this.getQueryParams().map((param) => ({ - name: param.name, - value: param.value, - })), - postData: { - mimeType: "application/x-www-form-urlencoded", - params: this.stolenData - .filter((e) => e.source == "request_body") - .map((e) => ({ - name: e.name, - value: e.value, - fileName: "--" + Math.ceil(Math.random() * 1000000000), - contentType: "text/plain", - comment: "", - })), - text: this.stolenData - .filter((e) => e.source == "request_body") - .map((e) => `${e.name}:\t${StolenDataEntry.parseValue(e.value)}`) - .join("\n\n"), - }, - }, - response: { - status: 200, - statusText: "OK", - httpVersion: "HTTP/2", - headers: [], - cookies: [], - content: { - mimeType: "text/plain", - size: this.getBalancedPriority(), - encoding: "base64", - text: "ZG9lc24ndCBtYXR0ZXIK", - }, - redirectURL: "", - headersSize: 15, - bodySize: 15, - }, - cache: {}, - timings: { - blocked: -1, - dns: 0, - connect: 0, - ssl: 0, - send: 0, - wait: 79, - receive: 0, - }, - time: 79, - _securityState: "secure", - serverIPAddress: "31.13.92.36", - connection: "443", - }; - } - - getMaxPriority(): number { - return Math.max(...this.stolenData.map((entry) => entry.getPriority())); - } - - getBalancedPriority(): number { - let result = 0; - if (this.stolenData.some((e) => e.exposesPath())) { - result += 50; + this.originalURL = url; + this.origin = new URL(url).origin; + this.originalPathname = new URL(url).pathname; } - if (this.stolenData.some((e) => e.exposesHost())) { - result += 50; + + isThirdParty() { + const request_url = new URL(this.data.url); + const origin_url = new URL(this.originalURL); + if (request_url.host.includes(origin_url.host)) { + return false; + } + if (getshorthost(request_url.host) == getshorthost(origin_url.host)) { + return false; + } + return ( + request_url.origin != origin_url.origin || + (this.data as any).urlClassification.thirdParty.length > 0 + ); } - if (this.hasCookie()) { - result += 50; + + getReferer() { + return ( + this.requestHeaders.filter((h) => h.name === 'Referer')[0]?.value || 'missing-referrer' + ); } - if (this.stolenData.some((e) => e.classification === "location")) { - result += 300; + + exposesOrigin() { + const url = new URL(this.originalURL); + const host = url.host; + const path = url.pathname; + const shorthost = getshorthost(host); + if (this.getReferer().includes(shorthost)) { + return true; + } + for (const entry of this.stolenData) { + if ( + entry.value.includes(host) || + entry.value.includes(path) || + entry.value.includes(shorthost) + ) { + return true; + } + } + return false; } - if (this.url.includes("facebook")) { - result += 50; + + private getAllStolenData(): StolenDataEntry[] { + return [ + ...this.getPathParams(), + ...this.getCookieData(), + ...this.getQueryParams(), + ...this.getHeadersData(), + ...this.getRequestBodyData(), + ]; + } + + getCookieData(): StolenDataEntry[] { + if (!this.hasCookie() || this.getCookie() === undefined) { + return []; + } + return flattenObjectEntries( + Object.entries(parseCookie(this.getCookie())).map(([key, value]) => [key, value || '']), + StolenDataEntry.parseValue + ).map(([key, value]) => new StolenDataEntry(this, 'cookie', key, value)); + } + + getRequestBodyData(): StolenDataEntry[] { + const ret = flattenObjectEntries( + Object.entries({ + ...this.requestBody.formData, + ...Object.fromEntries( + Object.entries(this.requestBody.raw || {}).map(([key, value], index) => [ + `${key}.${index}`, + value, + ]) + ), + }).map(([key, value]) => { + // to handle how ocdn.eu encrypts POST body on https://businessinsider.com.pl/ + if ((Array.isArray(value) && value.length === 1 && !value[0]) || !value) { + return ['requestBody', key]; + } else if (!Array.isArray(value)) { + return ['raw', String.fromCharCode.apply(null, new Uint8Array(value.bytes))]; + } else { + return [key, value || '']; + } + }), + StolenDataEntry.parseValue + ).map(([key, value]) => new StolenDataEntry(this, 'request_body', key, value)); + return ret; + } + + hasReferer() { + return this.requestHeaders.some((h) => h.name === 'Referer'); + } + + hasCookie() { + return this.requestHeaders.some((h) => h.name === 'Cookie'); + } + + getCookie(): string { + return this.requestHeaders.find((h) => h.name == 'Cookie')?.value; + } + + getPathParams(): StolenDataEntry[] { + const url = new URL(this.data.url); + const path = url.pathname; + if (!path.includes(';')) { + return []; + } + return flattenObjectEntries( + path + .split(';') + .map((e) => e.split('=')) + .map(([key, value]) => [key, value || '']) + .map(([key, value]) => { + return [key, StolenDataEntry.parseValue(safeDecodeURIComponent(value))]; + }) + ).map(([key, value]) => new StolenDataEntry(this, 'pathname', key, value)); + } + + getQueryParams(): StolenDataEntry[] { + const url = new URL(this.data.url); + return flattenObjectEntries( + Array.from((url.searchParams as any).entries()) + .map(([key, value]) => [key, value || '']) + .map(([key, value]) => { + return [key, StolenDataEntry.parseValue(safeDecodeURIComponent(value))]; + }) + ).map(([key, value]) => { + return new StolenDataEntry(this, 'queryparams', key, value); + }); + } + + getHeadersData(): StolenDataEntry[] { + return flattenObjectEntries( + this.requestHeaders + .filter((header) => { + for (const regex of whitelisted_cookies) { + if (regex.test(header.name)) { + return false; + } + } + return true; + }) + .map((header) => { + return [ + header.name, + StolenDataEntry.parseValue(safeDecodeURIComponent(header.value)), + ]; + }) + ).map(([key, value]) => new StolenDataEntry(this, 'header', key, value)); + } + + hasMark() { + return this.stolenData.some((data) => data.isMarked); + } + + getMarkedEntries() { + return this.stolenData.filter((data) => data.isMarked); + } + + getHost() { + return new URL(this.url).host; + } + + matchesHAREntry(har: HAREntry): boolean { + const rq = this.data; + const hrq = har.request; + return rq.url == hrq.url; + } + + toHAR(): HAREntry { + return { + pageref: 'page_1', + startedDateTime: `${new Date().toJSON().replace('Z', '+01:00')}`, + request: { + bodySize: + JSON.stringify(this.requestBody.formData || {}).length + + (this.requestBody.raw || []) + .map((e) => e.bytes.byteLength) + .reduce((a, b) => a + b, 0), + method: this.data.method, + url: this.data.url, + headersSize: JSON.stringify(this.requestHeaders).length, + httpVersion: 'HTTP/2', + headers: this.requestHeaders as NameValue[], + cookies: this.getCookieData().map((cookie) => ({ + name: cookie.name, + value: cookie.value, + })), + queryString: this.getQueryParams().map((param) => ({ + name: param.name, + value: param.value, + })), + postData: { + mimeType: 'application/x-www-form-urlencoded', + params: this.stolenData + .filter((e) => e.source == 'request_body') + .map((e) => ({ + name: e.name, + value: e.value, + fileName: '--' + Math.ceil(Math.random() * 1000000000), + contentType: 'text/plain', + comment: '', + })), + text: this.stolenData + .filter((e) => e.source == 'request_body') + .map((e) => `${e.name}:\t${StolenDataEntry.parseValue(e.value)}`) + .join('\n\n'), + }, + }, + response: { + status: 200, + statusText: 'OK', + httpVersion: 'HTTP/2', + headers: [], + cookies: [], + content: { + mimeType: 'text/plain', + size: this.getBalancedPriority(), + encoding: 'base64', + text: 'ZG9lc24ndCBtYXR0ZXIK', + }, + redirectURL: '', + headersSize: 15, + bodySize: 15, + }, + cache: {}, + timings: { + blocked: -1, + dns: 0, + connect: 0, + ssl: 0, + send: 0, + wait: 79, + receive: 0, + }, + time: 79, + _securityState: 'secure', + serverIPAddress: '31.13.92.36', + connection: '443', + }; + } + + getMaxPriority(): number { + return Math.max(...this.stolenData.map((entry) => entry.getPriority())); + } + + getBalancedPriority(): number { + let result = 0; + if (this.stolenData.some((e) => e.exposesPath())) { + result += 50; + } + if (this.stolenData.some((e) => e.exposesHost())) { + result += 50; + } + if (this.hasCookie()) { + result += 50; + } + if (this.stolenData.some((e) => e.classification === 'location')) { + result += 300; + } + if (this.url.includes('facebook')) { + result += 50; + } + return result; } - return result; - } } diff --git a/memory.ts b/memory.ts index 678f6e1..f4dac8c 100644 --- a/memory.ts +++ b/memory.ts @@ -4,7 +4,7 @@ import { EventEmitter } from 'events'; import { RequestCluster } from './request-cluster'; function setDomainsNumber(counter: number, tabId: number) { - browser.browserAction.setBadgeText({ text: counter.toString(), tabId }); + browser.browserAction.setBadgeText({ text: counter < 0 ? '0' : counter.toString(), tabId }); browser.browserAction.setTitle({ title: 'Rentgen', tabId, @@ -64,22 +64,67 @@ export default class Memory extends EventEmitter { ); } - emit(eventName: string, immediate = false, data = 'any', reason: string) { - console.log('emitting!', eventName, data, reason); - setTimeout(() => super.emit(eventName, data), 0); - return; - try { - if (immediate) { - super.emit(eventName, data); - return; - } else { - this.throttle(() => super.emit(eventName, data)); + private originalEmit(type: string, ...args: unknown[]) { + var doError = type === 'error'; + + var events = (this as any)._events; + if (events !== undefined) doError = doError && events.error === undefined; + else if (!doError) return false; + + // If there is no 'error' event listener then throw. + if (doError) { + var er; + if (args.length > 0) er = args[0]; + if (er instanceof Error) { + // Note: The comments on the `throw` lines are intentional, they show + // up in Node's output if this results in an unhandled exception. + throw er; // Unhandled 'error' event } - return true; - } catch (e) { - // debugger; - console.error(e); + // At least give some kind of context to the user + var err = new Error('Unhandled error.' + (er ? ' (' + (er as any).message + ')' : '')); + (err as any).context = er; + throw err; // Unhandled 'error' event } + + var handler = events[type]; + + if (handler === undefined) return false; + + if (typeof handler === 'function') { + try { + Reflect.apply(handler, this, args); + } catch (error) { + events[type] = undefined; + } + } else { + // var len = handler.length; + var listeners = [...handler]; + + listeners + .filter((e) => { + try { + e.call; + } catch (error) { + return false; + } + return true; + }) + .forEach((listener) => { + try { + Reflect.apply(listener, this, args); + } catch (error) { + console.error(error); + debugger; + } + }); + } + + return true; + } + + emit(eventName: string, immediate = false, data = 'any', reason: string): boolean { + setTimeout(() => this.originalEmit(eventName, data), 0); + return; } getClustersForOrigin(origin: string): Record { diff --git a/util.ts b/util.ts index d42f849..caae362 100644 --- a/util.ts +++ b/util.ts @@ -6,273 +6,268 @@ export type Unarray = T extends Array ? R : T; export type Tab = Unarray>>; export type Request = { - cookieStoreId?: string; - documentUrl?: string; // RL of the document in which the resource will be loaded. For example, if the web page at "https://example.com" contains an image or an iframe, then the documentUrl for the image or iframe will be "https://example.com". For a top-level document, documentUrl is undefined. - frameId: number; - incognito?: boolean; - method: string; - originUrl: string; - parentFrameId: number; - proxyInfo?: { - host: string; - port: number; - type: string; - username: string; - proxyDNS: boolean; - failoverTimeout: number; - }; - requestHeaders?: { name: string; value?: string; binaryValue?: number[] }[]; - requestId: string; - tabId: number; - thirdParty?: boolean; - timeStamp: number; - type: string; - url: string; // the target of the request; - urlClassification?: { firstParty: string[]; thirdParty: string[] }; + cookieStoreId?: string; + documentUrl?: string; // RL of the document in which the resource will be loaded. For example, if the web page at "https://example.com" contains an image or an iframe, then the documentUrl for the image or iframe will be "https://example.com". For a top-level document, documentUrl is undefined. + frameId: number; + incognito?: boolean; + method: string; + originUrl: string; + parentFrameId: number; + proxyInfo?: { + host: string; + port: number; + type: string; + username: string; + proxyDNS: boolean; + failoverTimeout: number; + }; + requestHeaders?: { name: string; value?: string; binaryValue?: number[] }[]; + requestId: string; + tabId: number; + thirdParty?: boolean; + timeStamp: number; + type: string; + url: string; // the target of the request; + urlClassification?: { firstParty: string[]; thirdParty: string[] }; }; export function getshorthost(host: string) { - const parts = host - .replace(/^.*:\/\//, '') - .replace(/\/.*$/, '') - .split('.'); - let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3; - if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') { - lookback = -4; // to distinguish between google ads and stats - } else if (parts.at(-2) == 'google') { - lookback = -3; // to distinguish various google services - } - return parts.slice(lookback).join('.'); + const parts = host + .replace(/^.*:\/\//, '') + .replace(/\/.*$/, '') + .split('.'); + let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3; + if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') { + lookback = -4; // to distinguish between google ads and stats + } else if (parts.at(-2) == 'google') { + lookback = -3; // to distinguish various google services + } + return parts.slice(lookback).join('.'); } export function useEmitter( - e: EventEmitter + e: EventEmitter ): [ - Record, - React.Dispatch>> + Record, + React.Dispatch>> ] { - const [eventCounts, setEventCounts] = React.useState>({ - '*': 0, - }); - React.useEffect(() => { - const callback = (eventSubtype: string) => { - setEventCounts((eventCounts) => { - console.log({ - ...eventCounts, - ...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 }, - ...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 }, - }); - return { - ...eventCounts, - ...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 }, - ...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 }, - }; - }); - }; - e.on('change', callback); - return () => { - e.removeListener('change', callback); - }; - }, []); - return [eventCounts, setEventCounts]; + const [eventCounts, setEventCounts] = React.useState>({ + '*': 0, + }); + React.useEffect(() => { + const callback = (eventSubtype: string) => { + setEventCounts((eventCounts) => ({ + ...eventCounts, + ...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 }, + ...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 }, + })); + }; + e.on('change', callback); + return () => { + e.removeListener('change', callback); + }; + }, []); + return [eventCounts, setEventCounts]; } export function parseCookie(cookie: string): Record { - return cookie - .split(';') - .map((l) => l.split('=')) - .reduce( - (acc, [key, value]) => ({ - ...acc, - [key]: value, - }), - {} - ); + return cookie + .split(';') + .map((l) => l.split('=')) + .reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: value, + }), + {} + ); } export async function getTabByID(id: number) { - const tabs = await browser.tabs.query({ currentWindow: true }); - return tabs.find((tab) => tab.id == id); + const tabs = await browser.tabs.query({ currentWindow: true }); + return tabs.find((tab) => tab.id == id); } export function parseToObject(str: unknown): Record { - let result: Record; - let original_string: string; - if (typeof str === 'string') { - original_string = str; - result = JSON.parse(str); - } else if (typeof str == 'object') { - result = str as Record; - original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str); - } - result[Symbol.for('originalString')] = original_string; - return result; + let result: Record; + let original_string: string; + if (typeof str === 'string') { + original_string = str; + result = JSON.parse(str); + } else if (typeof str == 'object') { + result = str as Record; + original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str); + } + result[Symbol.for('originalString')] = original_string; + return result; } export function isJSONObject(str: unknown): str is Record | string | number { - try { - const firstChar = JSON.stringify(parseToObject(str))[0]; - return ['{', '['].includes(firstChar); - } catch (e) { - return false; - } + try { + const firstChar = JSON.stringify(parseToObject(str))[0]; + return ['{', '['].includes(firstChar); + } catch (e) { + return false; + } } export function isURL(str: unknown): str is string { - try { - return !!(typeof str === 'string' && new URL(str)); - } catch (e) { - return false; - } + try { + return !!(typeof str === 'string' && new URL(str)); + } catch (e) { + return false; + } } export function hyphenate(str: string): string { - return str.replace(/[_\[A-Z]/g, `${String.fromCharCode(173)}$&`); + return str.replace(/[_\[A-Z]/g, `${String.fromCharCode(173)}$&`); } export function unique(array: T[]): Array { - return Array.from(new Set(array)); + return Array.from(new Set(array)); } export function allSubhosts(host: string) { - const parts = host.split('.'); - const result = []; - for (let i = 0; i < parts.length - 2; i++) { - result.push(parts.slice(i).join('.')); - } - return result; + const parts = host.split('.'); + const result = []; + for (let i = 0; i < parts.length - 2; i++) { + result.push(parts.slice(i).join('.')); + } + return result; } export function reduceConcat(a: T[], b: T[]): T[] { - return a.concat(b); + return a.concat(b); } export function getDate() { - const d = new Date(); - return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d - .getDate() - .toString() - .padStart(2, '0')}`; + const d = new Date(); + return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d + .getDate() + .toString() + .padStart(2, '0')}`; } export function toBase64(file: File): Promise { - return new Promise((resolve) => { - const FR = new FileReader(); - FR.addEventListener('load', (e) => { - resolve(e.target.result as string); - }); - FR.readAsDataURL(file); - }); + return new Promise((resolve) => { + const FR = new FileReader(); + FR.addEventListener('load', (e) => { + resolve(e.target.result as string); + }); + FR.readAsDataURL(file); + }); } export function makeThrottle(interval: number) { - let last_emit = 0; - function emit(callback: () => void) { - if (Date.now() - last_emit > interval) { - callback(); - last_emit = Date.now(); - return true; - } else { - return false; - } - } - return function (callback: () => void) { - if (!emit(callback)) { - setTimeout(() => emit(callback), interval); - } - }; + let last_emit = 0; + function emit(callback: () => void) { + if (Date.now() - last_emit > interval) { + callback(); + last_emit = Date.now(); + return true; + } else { + return false; + } + } + return function (callback: () => void) { + if (!emit(callback)) { + setTimeout(() => emit(callback), interval); + } + }; } export function isSameURL(url1: string, url2: string): boolean { - if (url1 === url2) { - return true; - } - url1 = url1.replace(/^https?:\/\//, '').replace(/\/$/, ''); - url2 = url2.replace(/^https?:\/\//, '').replace(/\/$/, ''); - return url1 === url2; + if (url1 === url2) { + return true; + } + url1 = url1.replace(/^https?:\/\//, '').replace(/\/$/, ''); + url2 = url2.replace(/^https?:\/\//, '').replace(/\/$/, ''); + return url1 === url2; } export function isBase64(s: string): boolean { - try { - atob(s); - return true; - } catch (e) {} - return false; + try { + atob(s); + return true; + } catch (e) {} + return false; } export function isBase64JSON(s: unknown): s is string { - return typeof s === 'string' && isBase64(s) && isJSONObject(atob(s)); + return typeof s === 'string' && isBase64(s) && isJSONObject(atob(s)); } export function flattenObject( - obj: unknown, - parser: (to_parse: unknown) => string | Record = (id) => id.toString(), - key = '', - ret = [] as [string, string][], - parsed = false + obj: unknown, + parser: (to_parse: unknown) => string | Record = (id) => id.toString(), + key = '', + ret = [] as [string, string][], + parsed = false ): [string, string][] { - const prefix = key === '' ? '' : `${key}.`; - if (Array.isArray(obj)) { - if (obj.length == 1) { - flattenObject(obj[0], parser, key, ret); - } else { - for (let i in obj) { - flattenObject(obj[i], parser, prefix + i, ret); - } - } - } else if (typeof obj === 'object') { - for (const [subkey, value] of Object.entries(obj)) { - flattenObject(value, parser, prefix + subkey, ret); - } - } else if (!parsed) { - flattenObject(parser(obj), parser, key, ret, true); - } else if (typeof obj === 'string') { - ret.push([key, obj]); - } else { - throw new Error('Something went wrong when parsing ' + obj); - } - return ret; + const prefix = key === '' ? '' : `${key}.`; + if (Array.isArray(obj)) { + if (obj.length == 1) { + flattenObject(obj[0], parser, key, ret); + } else { + for (let i in obj) { + flattenObject(obj[i], parser, prefix + i, ret); + } + } + } else if (obj === null) { + ret.push([key, '']); + } else if (typeof obj === 'object') { + for (const [subkey, value] of Object.entries(obj)) { + flattenObject(value, parser, prefix + subkey, ret); + } + } else if (!parsed) { + flattenObject(parser(obj), parser, key, ret, true); + } else if (typeof obj === 'string') { + ret.push([key, obj]); + } else { + throw new Error('Something went wrong when parsing ' + obj); + } + return ret; } export function flattenObjectEntries( - entries: [string, unknown][], - parser: (to_parse: unknown) => string | Record = (id) => id.toString() + entries: [string, unknown][], + parser: (to_parse: unknown) => string | Record = (id) => id.toString() ): [string, string][] { - return flattenObject(Object.fromEntries(entries), parser); + return flattenObject(Object.fromEntries(entries), parser); } export function maskString( - str: string, - max_fraction_remaining: number, - max_chars_total: number + str: string, + max_fraction_remaining: number, + max_chars_total: number ): string { - const amount_of_chars_to_cut = - str.length - Math.min(str.length * max_fraction_remaining, max_chars_total); - if (amount_of_chars_to_cut == 0) { - return str; - } - return ( - str.slice(0, str.length / 2 - amount_of_chars_to_cut / 2) + - '(...)' + - str.slice(str.length / 2 + amount_of_chars_to_cut / 2) - ); + const amount_of_chars_to_cut = + str.length - Math.min(str.length * max_fraction_remaining, max_chars_total); + if (amount_of_chars_to_cut == 0) { + return str; + } + return ( + str.slice(0, str.length / 2 - amount_of_chars_to_cut / 2) + + '(...)' + + str.slice(str.length / 2 + amount_of_chars_to_cut / 2) + ); } export function safeDecodeURIComponent(s: string) { - try { - return decodeURIComponent(s); - } catch (e) { - return s; - } + try { + return decodeURIComponent(s); + } catch (e) { + return s; + } } export function normalizeForClassname(string: string) { - return string.replace(/[^a-z0-9]/gi, '-'); + return string.replace(/[^a-z0-9]/gi, '-'); } export function wordlist(words: string[]) { - return words.reduce( - (acc, word, i) => `${acc}${i > 0 ? (i < words.length - 1 ? ',' : ' i') : ''} ${word}`, - '' - ); + return words.reduce( + (acc, word, i) => `${acc}${i > 0 ? (i < words.length - 1 ? ',' : ' i') : ''} ${word}`, + '' + ); }