import { GM_fetch, GM_head, headerStringToObject } from './requests'; export const lqueue = {} as any; const localSet = (key: string, value: any) => localStorage.setItem('__pee__' + key, JSON.stringify(value)); export let port1: MessagePort; console.log(execution_mode, isBackground); /* A web worker has no access to the dom, so things like remote fetches are proxied through the main frame */ let iframe: HTMLIFrameElement; export const genPort = () => { const nmc = new MessageChannel(); const port1 = nmc.port1; const port2 = nmc.port2; iframe.contentWindow?.postMessage('', '*', [port2]); return port1; }; export const initMainIPC = async () => { iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.name = location.origin; const iframeloaded = new Promise(_ => { iframe.onload = _; }); iframe.src = `${chrome.runtime.getURL('')}options.html`; //const meself = new URL(chrome.runtime.getURL('')).origin; document.documentElement.appendChild(iframe); await iframeloaded; port1 = genPort(); port1.onmessage = (ev) => { lqueue[ev.data.id](ev.data); }; }; let msgBuff: [any, Transferable[] | undefined][] = []; export const setupPort = (port: MessagePort) => { port1 = port; port1.onmessage = (ev) => { lqueue[ev.data.id](ev.data); }; if (execution_mode == "worker") { for (const msg of msgBuff) { port.postMessage(msg[0], { transfer: msg[1] }); } msgBuff = []; } }; // will be later overwritten if it's not launched from the userscript if (execution_mode == "worker") { port1 = { onmessage(ev) { lqueue[ev.data.id](ev.data); }, postMessage(msg, tr?: Transferable[]) { msgBuff.push([msg, tr]); } } as MessagePort; } // hack let gid = 0; const visit = (e: any, cb: (e: any) => true | undefined) => { if (typeof e == "object") { if (!cb(e)) // true if we don't want to visit deeper for (const p in e) visit(e[p], cb); } else cb(e); }; export const sendCmd = (cmd: any, tr?: Transferable[], overwrite = false, todelete = false) => { const prom = new Promise(_ => { const id = gid++; if (overwrite) cmd.id = id; lqueue[id] = (e: any) => { _(e.res); if (todelete) delete lqueue[id]; }; port1.postMessage({ id, ...cmd }, tr || []); }); return prom; }; const bridge = V>(name: string, f: T) => { if (execution_mode == 'userscript') return f; if (isBackground) return f; // It has to be the background script return (...args: U) => { return sendCmd({ name, args }); }; }; // eslint-disable-next-line @typescript-eslint/ban-types const Bridged = (ctor: any) => { const keys = Object.getOwnPropertyNames(ctor).filter(k => typeof ctor[k] == "function"); for (const k of keys) ctor[k] = bridge(k, ctor[k]); }; const altdomains = [ "desuarchive.org", "archived.moe", "archive.nyafuu.org", "arch.b4k.co", "archive.4plebs.org", "archive.wakarimasen.moe", "b4k.co", "fireden.net", "thebarchive.com", "archiveofsins.com", ]; export function supportedAltDomain(s: string) { return altdomains.includes(s); } export function supportedMainDomain(s: string) { return ['boards.4channel.org', 'boards.4chan.org'].includes(s); } let popupport: browser.runtime.Port; const pendingcmds: Record void> = {}; if (execution_mode == "chrome_api") { popupport = chrome.runtime.connect({ name: 'popup' }); popupport.onMessage.addListener((msg: any) => { if (msg.id in pendingcmds) { pendingcmds[msg.id](msg); delete pendingcmds[msg.id]; } }); } // Used to call background-only APIs from content scripts @Bridged export class Platform { static cmdid = 0; static async openInTab(src: string, opts: { active: boolean, insert: boolean }) { if (execution_mode == 'userscript') { return GM.openInTab(src, opts); } const obj = execution_mode == "chrome_api" ? chrome : browser; let i: number | undefined; if (opts.insert) i = (await obj.tabs.getCurrent()).index + 1; return obj.tabs.create({ active: opts.active, url: src, index: i }); } static async getValue(key: string, def: T) { const isinls = ('__pee__' + key) in localStorage; let ret: T; if (isinls) { let it = localStorage.getItem('__pee__' + key); if (it === "undefined") it = null; ret = { ...def, ...JSON.parse(it || '{}') } as T; } else ret = def; if (execution_mode != "userscript") { if (isinls) { delete localStorage[('__pee__' + key)]; await chrome.storage.local.set({ [key]: JSON.stringify(ret) }); } else { const d = await chrome.storage.local.get([key]); if (typeof d[key] == "string") return { ...def, ...(await JSON.parse('' + d[key] || '{}')) } as T; } } return ret; } static setValue(name: string, val: any) { localSet(name, val); } } let cmdid = 0; export function request(domain: string): void { try { popupport.postMessage({ id: cmdid, type: 'grant', domain }); cmdid++; } catch (e) { if ((e as Error).message.includes("disconnected")) { popupport = chrome.runtime.connect({ name: 'popup' }); popupport.onMessage.addListener((msg: any) => { if (msg.id in pendingcmds) { pendingcmds[msg.id](msg); delete pendingcmds[msg.id]; } }); return request(domain); } } } async function braveserialize(root: any): Promise { const transfer: Transferable[] = []; const ser = async (src: any): Promise => { if (src instanceof FormData) { const value = []; for (const kv of src) value.push([kv[0], await Promise.all(src.getAll(kv[0]).map(ser))]); return { cls: 'FormData', value, }; } if (src instanceof File) { const { name, type, lastModified } = src; const value = await src.arrayBuffer(); transfer.push(value); return { cls: 'File', name, type, lastModified, value, }; } if (src instanceof Blob) { const { type } = src; const value = await src.arrayBuffer(); transfer.push(value); return { cls: 'Blob', type, value, }; } if (src === null || src === undefined || typeof src != "object") return src; const ret = { cls: 'Object', value: {} } as any; for (const prop in src) { ret.value[prop] = await ser(src[prop]); } return ret; }; return [await ser(root), transfer]; } export const corsFetch = async (input: string, init?: RequestInit, lsn?: EventTarget) => { const id = gid++; let transfer: Transferable[] = []; if (init?.body) { // Chrom* can't pass around FormData and File/Blobs between // the content and bg scripts, so the data is passed through bloburls if (execution_mode == "chrome_api") { [init.body, transfer] = await braveserialize(init.body); } } const prom = new Promise>>((_, rej) => { let gcontroller: ReadableStreamController | undefined; let buffer: Uint8Array[] = []; let finished = false; const rs = new ReadableStream({ // I think start is not called immediately, but when something tries to pull the response start(controller) { // something is finally ready to read gcontroller = controller; // at this point the background script already read all that it needed // so we free up memory allocated for the request // flush buffer buffer.forEach(b => gcontroller?.enqueue(b)); buffer = []; if (finished) { gcontroller.close(); } } }); // seq num... see background script for explanation let s: number; s = 0; const cmdbuff: any[] = []; lqueue[id] = (async (e: any) => { // this is computed from the background script because the content script may // request everything to be delivered in one chunk, defeating the purpose if (e.progress) { if (lsn) lsn.dispatchEvent(new CustomEvent("progress", { detail: e.progress })); } if (e.pushData) { if (e.s > s) { // insert before an hypothetical cmd with a higher seq number // -1 will be returned on empty arrays, which still results in correct insertion let idx = 0; while (idx < cmdbuff.length) { if (cmdbuff[idx].s > e.s) break; idx++; } cmdbuff.splice(idx, 0, e); return; } // since we start from 0 and // don't accept command s > local s, // then these must be equal // this also means that cmdbuff must contain 0 or more ordered commands that must be processed // afterward until discontinuity const processCmd = (e: any) => { if (e.pushData.data) { const data = new Uint8Array(e.pushData.data); if (gcontroller) gcontroller.enqueue(data); else buffer.push(data); } else { if (gcontroller) gcontroller?.close(); else finished = true; } }; await processCmd(e); s++; // process remaining sequential buffered commands while (cmdbuff[0]?.s == s) { await processCmd(cmdbuff.shift()); s++; } } if (e.setRes) { const arrayBuffer = async () => { // read the response fully const r = rs.getReader(); await sendCmd({ name: 'fullyRead', fid: id }); const abs: Uint8Array[] = []; let res: ReadableStreamDefaultReadResult; do { res = await r.read(); if (res.done) break; abs.push(res.value); } while (!res.done); const sum = abs.reduce((a, b) => a + b.byteLength, 0); const ret = new Uint8Array(sum); abs.reduce((ptr, arr) => { ret.set(arr, ptr); return ptr + arr.byteLength; }, 0); r.releaseLock(); return ret; }; const blob = async () => new Blob([await arrayBuffer()]); const text = async () => new TextDecoder().decode(await arrayBuffer()); const json = async () => JSON.parse(await text()); if (e.ok) _({ body: rs, ok: e.ok, headers: e.headers, redirected: e.redirected, type: e.type, url: e.url, status: e.status, bodyUsed: e.bodyUsed, statusText: e.statusText, clone() { return this as Response; }, arrayBuffer, blob, text, json, async formData() { return new FormData; } }); else { rej(new Error(`${e.url} - ${e.status}`)); } } }); port1.postMessage({ id, name: 'corsFetch', args: [input, init] }, transfer); }); return prom; }; export async function getHeaders(s: string) { if (execution_mode == 'userscript') return headerStringToObject(await GM_head(s)); const res = await ifetch(s, { method: "HEAD" }); return res.headers as any as Record; } export async function ifetch(...[url, opt, lisn]: [...Parameters, EventTarget?]): ReturnType { if (execution_mode != "userscript") return corsFetch(url.toString(), opt, lisn); return GM_fetch(url, opt, lisn); } // most pngs are encoded with 65k idat chunks export async function* streamRemote(url: string, chunkSize = 72 * 1024, fetchRestOnNonCanceled = true) { // if (false) { // const res = await corsFetch(url); // const reader = res.body; // const stream = reader?.getReader(); // while (!stream?.closed) { // const buff = await stream?.read(); // if (buff?.done) { // break; // } // if (buff?.value) { // const e = (yield buff.value) as boolean; // if (e) { // stream?.cancel(); // reader?.cancel(); // break; // } // } // } // stream?.releaseLock(); // return; // } //const headers = await getHeaders(url); let size = Number.POSITIVE_INFINITY; let ptr = 0; let fetchSize = chunkSize; while (ptr != size) { //console.log('doing a fetch of ', url, ptr, ptr + fetchSize - 1); let obj: Record; const fres = await ifetch(url, { headers: { range: `bytes=${ptr}-${ptr + fetchSize - 1}` } }); if (execution_mode == "userscript") { obj = headerStringToObject((fres as any as Tampermonkey.Response).responseHeaders); } else { obj = (fres as any as Response).headers as any; } if (!('content-length' in obj)) { console.warn("no content lenght???", url); break; } if ('content-range' in obj) { size = +obj['content-range'].split('/')[1]; } const len = +obj['content-length']; ptr += len; if (fetchRestOnNonCanceled) fetchSize = size; const val = Buffer.from(await (fres as any).arrayBuffer()); const e = (yield val) as boolean; //console.log('yeieledd, a', e); if (e) { break; } } }