From f41da95f7e3b11792b894bdc440663f5866a10bc Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Sun, 23 Feb 2025 17:54:58 +1100 Subject: [PATCH] feat: magic button that crashes chrome tabs (yes rly idk why) --- src/SEQTA.ts | 27 ++++- src/background.ts | 195 ++++++++++++++++------------------ src/manifests/manifest.json | 2 +- src/pageState.js | 116 ++++++++++++++++++++ src/seqta/utils/ReactFiber.ts | 67 ++++++++++++ vite.config.ts | 3 +- 6 files changed, 304 insertions(+), 106 deletions(-) create mode 100644 src/pageState.js create mode 100644 src/seqta/utils/ReactFiber.ts diff --git a/src/SEQTA.ts b/src/SEQTA.ts index d69f1ae6..c70d9220 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -38,6 +38,7 @@ import documentLoadCSS from '@/css/documentload.scss?inline' import renderSvelte from '@/interface/main' import Settings from '@/interface/pages/settings.svelte' import { settingsPopup } from './interface/hooks/SettingsPopup' +import ReactFiber from './seqta/utils/ReactFiber' let SettingsClicked = false export let MenuOptionsOpen = false @@ -90,6 +91,26 @@ async function init() { } console.info('[BetterSEQTA+] Successfully initalised BetterSEQTA+, starting to load assets.') main() + + waitForElm('.Viewer__Viewer___32BH-', true).then(() => { + console.log('Element exists') + + const button = document.createElement('button') + button.innerHTML = 'Click me' + button.onclick = () => { + // function call to run onclick + ReactFiber.find(".Viewer__Viewer___32BH-", { debug: true }) + .setState(prev => ({ ...prev, selected: new Set([999999]) })) + } + button.style.position = 'fixed' + button.style.top = '0' + button.style.right = '0' + button.style.padding = '10px' + button.style.zIndex = '9999' + button.style.backgroundColor = 'red' + button.style.color = 'white' + document.body.appendChild(button) + }) } catch (error: any) { console.error(error) } @@ -624,10 +645,11 @@ export async function waitForElm(selector: string, usePolling: boolean = false, } else { return new Promise((resolve) => { const registerObserver = () => { - const { unregister } = eventManager.register(`${selector}`, { + const { unregister } = eventManager.register(`${selector}`, { customCheck: (element) => element.matches(selector) - }, (element) => { + }, async (element) => { resolve(element); + await delay(1); unregister(); // Remove the listener once the element is found }); return unregister; @@ -2976,7 +2998,6 @@ async function handleAssessments(node: Element): Promise { } const preaverage = numaverage.toFixed(0) as unknown as number const prepaverage = Math.ceil(preaverage / 5) * 5; - console.info(prepaverage) const NumberGradeMap: Record = { 100: "A+", 95: "A", diff --git a/src/background.ts b/src/background.ts index bab99afd..3038b12d 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,62 +1,6 @@ import browser from 'webextension-polyfill' import type { SettingsState } from "@/types/storage"; -export const openDB = () => { - return new Promise((resolve, reject) => { - const request = indexedDB.open('MyDatabase', 1); - - request.onupgradeneeded = (event: any) => { - const db = event.target.result; - db.createObjectStore('backgrounds', { keyPath: 'id' }); - }; - - request.onsuccess = () => { - resolve(request.result); - }; - - request.onerror = (event: any) => { - reject('Error opening database: ' + event.target.errorCode); - }; - }); -}; - -export const writeData = async (type: any, data: any) => { - const db: any = await openDB(); - - const tx = db.transaction('backgrounds', 'readwrite'); - const store = tx.objectStore('backgrounds'); - const request = await store.put({ id: 'customBackground', type, data }); - - return request.result; -}; - -export const readData = () => { - return new Promise((resolve, reject) => { - openDB() - .then((db: any) => { - const tx = db.transaction('backgrounds', 'readonly'); - const store = tx.objectStore('backgrounds'); - - // Retrieve the custom background - const getRequest = store.get('customBackground'); - - // Attach success and error event handlers - getRequest.onsuccess = function(event: any) { - resolve(event.target.result); - }; - - getRequest.onerror = function(event: any) { - console.error('An error occurred:', event); - reject(event); - }; - }) - .catch(error => { - console.error('An error occurred:', error); - reject(error); - }); - }); -}; - function reloadSeqtaPages() { const result = browser.tabs.query({}) function open (tabs: any) { @@ -69,56 +13,104 @@ function reloadSeqtaPages() { result.then(open, console.error) } +const injectedTabs = new Set(); // Keep track of injected tabs + +async function injectPageState(tabId: number) { + if (injectedTabs.has(tabId)) { + return; // Already injected + } + + try { + await browser.scripting.executeScript({ + target: { tabId }, + files: ["pageState.js"], + // @ts-ignore + world: "MAIN", + }); + injectedTabs.add(tabId); + console.log(`[background] Injected pageState.js into tab ${tabId}`); + } catch (err) { + console.error(`[background] Failed to inject pageState.js into tab ${tabId}:`, err); + } +} + +// Remove the script when a tab is closed +browser.tabs.onRemoved.addListener((tabId) => { + injectedTabs.delete(tabId); +}); + // Main message listener -browser.runtime.onMessage.addListener((request: any, _sender: any, sendResponse: any) => { +browser.runtime.onMessage.addListener((request: any, sender: any, sendResponse: (response?: any) => void) => { + switch (request.type) { - case 'reloadTabs': - reloadSeqtaPages(); - break; - - case 'extensionPages': - browser.tabs.query({}).then(function (tabs) { - for (let tab of tabs) { - if (tab.url?.includes('chrome-extension://')) { - browser.tabs.sendMessage(tab.id!, request); - } - } - }); - break; - - case 'currentTab': - browser.tabs.query({ active: true, currentWindow: true }).then(function (tabs) { - browser.tabs.sendMessage(tabs[0].id!, request).then(function (response) { - sendResponse(response); - }); - }); - return true; - - case 'githubTab': - browser.tabs.create({ url: 'github.com/BetterSEQTA/BetterSEQTA-Plus' }); - break; + case 'reloadTabs': + reloadSeqtaPages(); + break; - case 'setDefaultStorage': - SetStorageValue(DefaultValues); - break; + case 'extensionPages': + browser.tabs.query({}).then(function (tabs) { + for (let tab of tabs) { + if (tab.url?.includes('chrome-extension://')) { + browser.tabs.sendMessage(tab.id!, request); + } + } + }); + break; + + case 'currentTab': + browser.tabs.query({ active: true, currentWindow: true }).then(function (tabs) { + browser.tabs.sendMessage(tabs[0].id!, request).then(function (response) { + sendResponse(response); + }); + }); + return true; // Keep message channel open for async response - case 'sendNews': - const date = new Date(); - - const from = - date.getFullYear() + - '-' + - (date.getMonth() + 1) + - '-' + - (date.getDate() - 5); - - const url = `https://newsapi.org/v2/everything?domains=abc.net.au&from=${from}&apiKey=17c0da766ba347c89d094449504e3080`; - - GetNews(sendResponse, url); - return true; + case 'githubTab': + browser.tabs.create({ url: 'github.com/BetterSEQTA/BetterSEQTA-Plus' }); + break; - default: - console.log('Unknown request type'); + case 'setDefaultStorage': + SetStorageValue(DefaultValues); + break; + + case 'sendNews': + const date = new Date(); + + const from = + date.getFullYear() + + '-' + + (date.getMonth() + 1) + + '-' + + (date.getDate() - 5); + + const url = `https://newsapi.org/v2/everything?domains=abc.net.au&from=${from}&apiKey=17c0da766ba347c89d094449504e3080`; + + GetNews(sendResponse, url); + return true; + + case 'reactFiberRequest': { + //const { tabId, /* selector, action, payload, debug */ } = request; + const tabId = sender.tab.id; + + // Ensure pageState.js is injected + injectPageState(tabId).then(() => { + // Forward the request to the injected script + browser.tabs.sendMessage(tabId, { ...request, type: "reactFiberAction" }, { frameId: 0 }) // Target the main world + .then(response => { + sendResponse(response); // Send the response back to the content script + }) + .catch(err => { + console.error(`[background] Error communicating with injected script in tab ${tabId}:`, err); + }); + }).catch(err => { + console.error("[background] Failed to inject", err); + }); + return true; // Keep the message channel open for the async response + } + + + default: + console.log('Unknown request type'); } }); @@ -220,6 +212,7 @@ const DefaultValues: SettingsState = { }, ], customshortcuts: [], + lettergrade: false, }; function SetStorageValue(object: any) { diff --git a/src/manifests/manifest.json b/src/manifests/manifest.json index 3c34412c..d7612deb 100644 --- a/src/manifests/manifest.json +++ b/src/manifests/manifest.json @@ -15,7 +15,7 @@ "64": "resources/icons/icon-64.png" } }, - "permissions": ["tabs", "notifications", "storage"], + "permissions": ["tabs", "notifications", "storage", "scripting", "activeTab"], "host_permissions": ["https://newsapi.org/", "*://*/*"], "background": { "service_worker": "background.ts" diff --git a/src/pageState.js b/src/pageState.js new file mode 100644 index 00000000..6cdcf0fd --- /dev/null +++ b/src/pageState.js @@ -0,0 +1,116 @@ +class ReactFiber { + constructor(selector, options = {}) { + this.selector = selector; + this.debug = options.debug || false; + this.nodes = [...document.querySelectorAll(selector)]; // Support multiple elements + this.fibers = this.nodes.map(node => this.getFiberNode(node)); + this.components = this.fibers.map(fiber => this.getOwnerComponent(fiber)); + + if (this.debug) { + console.log("📌 Selected Nodes:", this.nodes); + console.log("🔍 Found Fibers:", this.fibers); + console.log("🛠 Found Components:", this.components); + } + } + + static find(selector, options = {}) { + return new ReactFiber(selector, options); + } + + getFiberNode(node) { + if (!node) return null; + const fiberKey = Object.getOwnPropertyNames(node).find(name => + name.startsWith('__reactFiber') || name.startsWith('__reactInternalInstance') + ); + return fiberKey ? node[fiberKey] : null; + } + + getOwnerComponent(fiberNode) { + let current = fiberNode; + while (current) { + if (current.stateNode && (current.stateNode.setState || current.stateNode.forceUpdate)) { + return current.stateNode; + } + current = current.return; + } + return null; + } + + getState(key = null) { + if (!this.components.length) return null; + const state = this.components[0]?.state || null; + return key ? state?.[key] : state; + } + + setState(updateFn) { + this.components.forEach(component => { + if (component?.setState) { + component.setState(prevState => { + const newState = updateFn(prevState); + if (this.debug) console.log("✅ Updated State:", newState); + return newState; + }); + } + }); + return this; // Enable chaining + } + + getProp(propName) { + if (!this.fibers.length) return null; + return this.fibers[0]?.memoizedProps?.[propName] || null; + } + + setProp(propName, value) { + this.fibers.forEach(fiber => { + if (fiber?.memoizedProps) { + fiber.memoizedProps[propName] = value; + } + }); + return this.forceUpdate(); // Apply the change and return this for chaining + } + + forceUpdate() { + this.components.forEach(component => { + if (component?.forceUpdate) { + component.forceUpdate(); + if (this.debug) console.log("🔄 Forced React Re-render"); + } + }); + return this; // Enable chaining + } +} + +// Message listener for communication with the background script +browser.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.type === "reactFiberAction") { + const { selector, action, payload, debug } = request; + const fiberInstance = ReactFiber.find(selector, {debug}); // Use the class + + switch (action) { + case "getState": + sendResponse(fiberInstance.getState(payload.key)); + break; + case "setState": + // Very important: Eval the function string in the context of the page + const updateFn = eval(`(${payload.updateFn})`); + fiberInstance.setState(updateFn); + sendResponse({}); // Send acknowledgement + break; + case "getProp": + sendResponse(fiberInstance.getProp(payload.propName)); + break; + case "setProp": + fiberInstance.setProp(payload.propName, payload.value); + sendResponse({}); // Send acknowledgement + break; + case "forceUpdate": + fiberInstance.forceUpdate(); + sendResponse({}); // Send acknowledgement + break; + default: + console.warn(`[pageState] Unknown action: ${action}`); + sendResponse(null); + } + return true; // Keep message channel open (for consistency, even if not always needed) + } +}); \ No newline at end of file diff --git a/src/seqta/utils/ReactFiber.ts b/src/seqta/utils/ReactFiber.ts new file mode 100644 index 00000000..0eab49a5 --- /dev/null +++ b/src/seqta/utils/ReactFiber.ts @@ -0,0 +1,67 @@ +import browser from 'webextension-polyfill'; + +class ReactFiber { +private selector: string; + private debug: boolean; + + constructor(selector: string, options: { debug?: boolean } = {}) { + this.selector = selector; + this.debug = options.debug || false; + } + + static find(selector: string, options: { debug?: boolean } = {}) { + return new ReactFiber(selector, options); + } + + private async sendMessage(action: string, payload: any = {}): Promise { + const message = { + type: "reactFiberRequest", + selector: this.selector, + action, + payload, + debug: this.debug + }; + + try { + const response = await browser.runtime.sendMessage(message); + + if (this.debug) { + console.log("Content Received Response:", response); + } + return response; + } catch (error) { + console.error("Error sending message:", error); + return null; // or throw error, depending on desired behavior + } + } + + + async getState(key?: string): Promise { + return this.sendMessage("getState", { key }); + } + + async setState(updateFn: (prevState: any) => any): Promise { + // Serialize the update function to a string. This is the tricky part. + // We can only send strings/JSON-serializable data through messages. + const updateFnString = updateFn.toString(); + await this.sendMessage("setState", { updateFn: updateFnString }); + return this; + } + + async getProp(propName: string): Promise { + return this.sendMessage("getProp", { propName }); + } + + async setProp(propName: string, value: any): Promise { + await this.sendMessage("setProp", { propName, value }); + return this; + } + + async forceUpdate(): Promise { + await this.sendMessage("forceUpdate"); + return this; + } +} + + +export default ReactFiber \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 51bc6f77..25582b7f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -76,7 +76,8 @@ export default defineConfig(({ command }) => ({ rollupOptions: { input: { settings: join(__dirname, 'src', 'interface', 'index.html'), - migration: join(__dirname, 'src', 'seqta', 'utils', 'migration', 'migrate.html') + migration: join(__dirname, 'src', 'seqta', 'utils', 'migration', 'migrate.html'), + pageState: join(__dirname, 'src', 'pageState.js'), } } }