diff --git a/ext/html/offscreen.html b/ext/html/offscreen.html new file mode 100644 index 0000000..c707b01 --- /dev/null +++ b/ext/html/offscreen.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/ext/js/background.js b/ext/js/background.js index f647453..fd4f9a0 100644 --- a/ext/js/background.js +++ b/ext/js/background.js @@ -1,3 +1,11 @@ +if (typeof importScripts !== 'undefined') { + importScripts( + "../vendor/browser-polyfill.js", + "config.js", + "common.js", + ); +} + (function () { 'use strict'; @@ -16,6 +24,19 @@ const contestProblemList = {}; + /** + * The offscreen document being currently created, if any. + */ + let offscreenBeingCreated; + + /** + * Return the web browser name this extension is running on. + * @returns {string} 'chrome' or 'firefox' depending on the browser + */ + function currentBrowser() { + return browser.runtime.getURL('').startsWith('chrome-extension://') ? 'chrome' : 'firefox'; + } + function displayStatusNotification(submitID, problemCode, status) { browser.notifications.create({ type: 'basic', @@ -48,14 +69,14 @@ } /** - * Display extension's page action. - * @param tab tab object + * Display extension's action. */ - function enablePageAction(tab) { - browser.pageAction.show(tab.id); - browser.pageAction.onClicked.addListener(() => - browser.runtime.openOptionsPage(), - ); + function enableAction() { + const listener = () => { + browser.runtime.openOptionsPage(); + }; + + browser.action.onClicked.addListener(listener); } /** @@ -180,17 +201,18 @@ /** * Add highlight.js CSS to the page using the selected style. */ - function injectHighlightJsCss(tab) { - storage + async function injectHighlightJsCss(tab) { + await storage .get({ [HIGHLIGHT_JS_STYLE_KEY]: DEFAULT_SETTINGS[HIGHLIGHT_JS_STYLE_KEY], }) - .then((response) => { + .then(async (response) => { let style = response.highlightJsStyle; if (style !== 'none') { - browser.tabs.insertCSS(tab.id, { - file: `vendor/bower/hjsstyles/${style}.css`, + await browser.scripting.insertCSS({ + files: [`vendor/bower/hjsstyles/${style}.css`], + target: { tabId: tab.id }, }); } }); @@ -285,9 +307,21 @@ if (!response.ok) { throw new Error(`HTTP Status ${response.status}`); } - contestProblemList[contestID] = parseProblemList( - $.parseHTML(await response.text()), - ); + const responseText = await response.text(); + + if (currentBrowser() === 'chrome') { + await setupOffscreenDocument("/html/offscreen.html"); + const parseResponse = await chrome.runtime.sendMessage({ + type: 'parseProblemList', + target: 'offscreen', + data: responseText + }); + await chrome.offscreen.closeDocument(); + + contestProblemList[contestID] = parseResponse; + } else { + contestProblemList[contestID] = parseProblemList(responseText); + } } catch (error) { console.error(error); } @@ -295,6 +329,35 @@ return contestProblemList[contestID] ?? {}; } + /** + * Setup offscreen document for parsing HTML + * @param path path to the offscreen document + * @returns {Promise} promise that resolves when the document is ready + */ + async function setupOffscreenDocument(path) { + const offscreenUrl = chrome.runtime.getURL(path); + const existingContexts = await chrome.runtime.getContexts({ + contextTypes: ['OFFSCREEN_DOCUMENT'], + documentUrls: [offscreenUrl] + }); + + if (existingContexts.length > 0) { + return; + } + + if (offscreenBeingCreated) { + await offscreenBeingCreated; + } else { + offscreenBeingCreated = chrome.offscreen.createDocument({ + url: path, + reasons: [chrome.offscreen.Reason.DOM_PARSER], + justification: 'Parse DOM', + }); + await offscreenBeingCreated; + offscreenBeingCreated = null; + } + } + /** * Save joined contest list in Storage * @param {array} contestList contest list @@ -316,17 +379,20 @@ }); setUpSessionCookies(); - browser.runtime.onMessage.addListener((request, sender) => { - if (request.action === 'enablePageAction') { - enablePageAction(sender.tab); + browser.runtime.onMessage.addListener(async (request, sender) => { + if (request.action === 'enableAction') { + enableAction(sender.tab); + return new Promise((resolve) => resolve(null)); } else if (request.action === 'saveLastContestID') { saveLastContestID(getContestID(sender.url)); + return new Promise((resolve) => resolve(null)); } else if (request.action === 'displayStatusNotification') { displayStatusNotification( request.submitID, request.problemCode, request.problemStatus, ); + return new Promise((resolve) => resolve(null)); } else if (request.action === 'modifyContestItemList') { modifyContestItemList( request.listName, @@ -334,6 +400,7 @@ request.value, request.add, ); + return new Promise((resolve) => resolve(null)); } else if (request.action === 'getContestItemList') { return new Promise((resolve) => { getContestItemList( @@ -343,16 +410,21 @@ ); }); } else if (request.action === 'injectHighlightJsCss') { - injectHighlightJsCss(sender.tab); + await injectHighlightJsCss(sender.tab); + return new Promise((resolve) => resolve(null)); } else if (request.action === 'saveContestProblemList') { saveContestProblemList(request.contestID, request.problems); + return new Promise((resolve) => resolve(null)); } else if (request.action === 'getContestProblemList') { return getProblemList(request.contestID); } else if (request.action === 'setJoinedContestList') { saveJoinedContestList(request.contestList); + return new Promise((resolve) => resolve(null)); } else if (request.action === 'getJoinedContestList') { return getJoinedContestList(); } - return new Promise((resolve) => resolve(null)); + + console.warn(`Unexpected message type received: '${request.action}'.`); + return false; }); })(); diff --git a/ext/js/common.js b/ext/js/common.js index eb8c55f..5e76a97 100644 --- a/ext/js/common.js +++ b/ext/js/common.js @@ -27,7 +27,9 @@ if (typeof module !== 'undefined') { }; } -function parseProblemList(jqueryHandles) { +function parseProblemList(htmlText) { + const jqueryHandles = $.parseHTML(htmlText); + return Object.fromEntries( jqueryHandles.flatMap((el) => [ diff --git a/ext/js/general.js b/ext/js/general.js index 75efb05..a80285d 100644 --- a/ext/js/general.js +++ b/ext/js/general.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - browser.runtime.sendMessage({ action: 'enablePageAction' }); + browser.runtime.sendMessage({ action: 'enableAction' }); const BANNER_URL = browser.runtime.getURL('images/satori_banner.png'); const TCS_LOGO_URL = browser.runtime.getURL('images/tcslogo.svg'); diff --git a/ext/js/offscreen.html b/ext/js/offscreen.html deleted file mode 100644 index 177e040..0000000 --- a/ext/js/offscreen.html +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/ext/js/offscreen.js b/ext/js/offscreen.js index e26ae88..20c0aa1 100644 --- a/ext/js/offscreen.js +++ b/ext/js/offscreen.js @@ -1,48 +1,12 @@ -importScripts( - "../vendor/browser-polyfill.js", - "config.js", - "../vendor/bower/jquery.min.js" -); - -browser.runtime.onMessage.addListener(handleMessages); - -async function handleMessages(message) { - if (message.target !== 'offscreen') { +browser.runtime.onMessage.addListener(async (request) => { + if (request.target !== 'offscreen') { return false; } - if (message.type === 'parseProblemList') { - parseProblemList(message.data); - } else { - console.warn(`Unexpected message type received: '${message.type}'.`); - return false; + if (request.type === 'parseProblemList') { + return parseProblemList(request.data); } -} - -function parseProblemList(htmlText) { - const jqueryHandles = $.parseHTML(htmlText); - - return Object.fromEntries( - jqueryHandles.flatMap((el) => - [ - ...$(el).find('#content table.results tr:not(:first-of-type)'), - ].map((tr) => [ - $(tr).find('td:nth-child(1)').text(), - { - title: $(tr).find('td:nth-child(2)').text(), - href: $(tr).find('td:nth-child(2) a').attr('href'), - pdfHref: $(tr).find('td:nth-child(3) a').attr('href'), - submitHref: $(tr).find('td:nth-child(5) a').attr('href'), - }, - ]), - ), - ); -} -function sendToBackground(type, data) { - browser.runtime.sendMessage({ - type, - target: 'background', - data - }); -} \ No newline at end of file + console.warn(`Unexpected message type received: '${request.type}'.`); + return false; +}); diff --git a/ext/js/results.js b/ext/js/results.js index 2800c79..021bc16 100644 --- a/ext/js/results.js +++ b/ext/js/results.js @@ -17,9 +17,7 @@ let problemStatus = tr.find('td:last').text(); let url = document.location.href; - console.log("XD"); const contestID = getContestID(url); - console.log("XD2"); /** diff --git a/ext/js/service-worker.js b/ext/js/service-worker.js deleted file mode 100644 index 32c3739..0000000 --- a/ext/js/service-worker.js +++ /dev/null @@ -1,368 +0,0 @@ -importScripts( - "../vendor/browser-polyfill.js", - "config.js", -); - -(function () { - 'use strict'; - - const SATORI_URL_CONTEST = SATORI_URL_HTTPS + 'contest/'; - - let storage = browser.storage.sync || browser.storage.local; - - let lastContestID; - /** - * A set that stores information about which tabs have Satori open. It is - * used to redirect the user to the last contest (if they are already - * browsing the website, then we shouldn't redirect). - * @type {Set} - */ - let satoriTabs = new Set(); - - const contestProblemList = {}; - - function displayStatusNotification(submitID, problemCode, status) { - browser.notifications.create({ - type: 'basic', - title: 'Submit ' + submitID + ' (' + problemCode + ') status', - message: 'New problem status: ' + status, - iconUrl: 'icon48.png', - }); - } - - /** - * Retrieve the ID of the last visited contest and store it in lastContestID - * variable. - * @see setUpLastContestRedirect - */ - function retrieveLastContestID() { - storage.get('lastContestID').then((response) => { - lastContestID = response.lastContestID; - }); - } - - /** - * Save last contest ID both in Storage and our local variable. - * @param {string} contestID last contest ID - * @see setUpLastContestRedirect - */ - function saveLastContestID(contestID) { - storage.set({ lastContestID: contestID }); - lastContestID = contestID; - } - - /** - * Display extension's page action. - * @param tab tab object - */ - function enablePageAction(tab) { - const listener = () => { - browser.runtime.openOptionsPage(); - }; - - if (chrome.action !== undefined) { - chrome.action.onClicked.addListener(listener); - } else { - browser.pageAction.show(tab.id); - browser.pageAction.onClicked.addListener(listener); - } - } - - /** - * Add or remove a value to given list bound of a specified contest. - * @param {string} listName name of the list to look for in the storage - * @param {string} contestID ID of the contest to modify its list - * @param {string} value value to add or remove - * @param {boolean} add whether to add or remove given element - * @see getContestItemList - */ - function modifyContestItemList(listName, contestID, value, add) { - storage - .get({ - [listName]: {}, - }) - .then((items) => { - let list = items[listName]; - if (!(contestID in list)) { - list[contestID] = []; - } - - let s = new Set(list[contestID]); - if (add) { - s.add(value); - } else { - s.delete(value); - } - list[contestID] = [...s]; - - storage.set({ [listName]: list }); - }); - } - - /** - * Retrieve a list bound to given contest. - * - * This function is used e.g. to store the list of problem hidden in for a - * contest. - * - * @param {string} listName name of the list to get - * @param {string} contestID ID of the contest to get the list for - * @param {function} sendResponse function to call with the data retrieved - * @see modifyContestItemList - */ - function getContestItemList(listName, contestID, sendResponse) { - storage - .get({ - [listName]: {}, - }) - .then((items) => { - if (contestID in items[listName]) { - sendResponse(items[listName][contestID]); - } else { - sendResponse([]); - } - }); - } - - /** - * Add an onBeforeRequest listener that redirects to the last contest - * if the user just entered Satori webpage. - * - * Also, the function adds webNavigation.onCommitted and tabs.onRemoved - * listeners to keep the list of Satori tabs. - */ - function setUpLastContestRedirect() { - // Store which tabs have Satori open - browser.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { - if ( - tab.url && - (tab.url.startsWith(SATORI_URL_HTTP) || - tab.url.startsWith(SATORI_URL_HTTPS)) - ) { - satoriTabs.add(tabId); - } else { - satoriTabs.delete(tabId); - } - }); - - browser.tabs.onRemoved.addListener((tabId) => satoriTabs.delete(tabId)); - - // browser.webRequest.onBeforeRequest.addListener( - // function (details) { - // if ( - // typeof lastContestID === 'undefined' || - // lastContestID === null || - // satoriTabs.has(details.tabId) - // ) { - // // If we haven't saved any contest yet, then do nothing - // // Also, don't redirect if the user is already browsing - // // Satori - // return; - // } - // - // return { - // redirectUrl: SATORI_URL_CONTEST + lastContestID + '/', - // }; - // }, - // { - // urls: [ - // '*://satori.tcs.uj.edu.pl/', - // '*://satori.tcs.uj.edu.pl/news', - // ], - // types: ['main_frame'], - // }, - // ['blocking'], - // ); - } - - /** - * Add highlight.js CSS to the page using the selected style. - */ - async function injectHighlightJsCss(tab) { - await storage - .get({ - [HIGHLIGHT_JS_STYLE_KEY]: - DEFAULT_SETTINGS[HIGHLIGHT_JS_STYLE_KEY], - }) - .then(async (response) => { - let style = response.highlightJsStyle; - if (style !== 'none') { - await browser.scripting.insertCSS({ - files: [`vendor/bower/hjsstyles/${style}.css`], - target: { tabId: tab.id }, - }); - } - }); - } - - const satoriDomain = new URL(SATORI_URL_HTTPS).hostname; - - function getKeepSignedInDuration() { - return storage - .get({ - [KEEP_SIGNED_IN_DURATION_KEY]: - DEFAULT_SETTINGS[KEEP_SIGNED_IN_DURATION_KEY], - }) - .then((response) => { - const duration = response[KEEP_SIGNED_IN_DURATION_KEY]; - if (duration === 'none') { - return null; - } - return parseInt(duration, 10); - }); - } - - function updateCookie(cookie) { - return getKeepSignedInDuration().then((duration) => { - if (duration === null) { - return; - } - const newCookie = { - expirationDate: - Math.round(Date.now() / 1000) + duration * 24 * 60 * 60, - httpOnly: cookie.httpOnly, - name: cookie.name, - path: cookie.path, - sameSite: cookie.sameSite, - secure: cookie.secure, - storeId: cookie.storeId, - value: cookie.value, - url: SATORI_URL_HTTPS, - }; - return browser.cookies.set(newCookie); - }); - } - - function getTokenCookies() { - return browser.cookies.getAll({ - domain: satoriDomain, - name: 'satori_token', - path: '/', - }); - } - - function updateExistingCookie() { - return getTokenCookies().then((cookies) => { - if (cookies.length === 0) { - return; - } - if (cookies.length > 1) { - console.warn('Too many satori_token cookies'); - return; - } - const [cookie] = cookies; - if (cookie.expirationDate === undefined && cookie.value !== '') { - return updateCookie(cookie); - } - }); - } - - function setUpSessionCookies() { - updateExistingCookie().catch(console.error); - browser.cookies.onChanged.addListener(({ removed, cookie }) => { - if ( - !removed && - cookie.domain === satoriDomain && - cookie.name === 'satori_token' && - cookie.path === '/' - ) { - updateExistingCookie().catch(console.error); - } - }); - } - - function saveContestProblemList(contestID, problems) { - contestProblemList[contestID] = problems; - } - - async function getProblemList(contestID) { - if (!contestProblemList[contestID]) { - try { - const response = await fetch( - `${SATORI_URL_HTTPS}contest/${contestID}/problems`, - ); - if (!response.ok) { - throw new Error(`HTTP Status ${response.status}`); - } - - await chrome.offscreen.createDocument({ - url: "offscreen.html", - reasons: [chrome.offscreen.Reason.DOM_PARSER], - justification: 'Parse DOM' - }); - const parseResponse = await chrome.runtime.sendMessage({ - type: 'parseProblemList', - target: 'offscreen', - data: await response.text() - }); - await chrome.offscreen.closeDocument(); - console.log(parseResponse); - - contestProblemList[contestID] = parseResponse; - } catch (error) { - console.error(error); - } - } - return contestProblemList[contestID] ?? {}; - } - - /** - * Save joined contest list in Storage - * @param {array} contestList contest list - */ - function saveJoinedContestList(contestList) { - storage.set({ contestList }); - } - - /** - * Save joined contest list in Storage - * @return {array} contest list - */ - async function getJoinedContestList() { - return (await storage.get('contestList')).contestList ?? []; - } - - retrieveLastContestID(); - setUpLastContestRedirect(); - setUpSessionCookies(); - - browser.runtime.onMessage.addListener(async (request, sender) => { - if (request.action === 'enablePageAction') { - enablePageAction(sender.tab); - } else if (request.action === 'saveLastContestID') { - saveLastContestID(getContestID(sender.url)); - } else if (request.action === 'displayStatusNotification') { - displayStatusNotification( - request.submitID, - request.problemCode, - request.problemStatus, - ); - } else if (request.action === 'modifyContestItemList') { - modifyContestItemList( - request.listName, - getContestID(sender.url), - request.value, - request.add, - ); - } else if (request.action === 'getContestItemList') { - return new Promise((resolve) => { - getContestItemList( - request.listName, - getContestID(sender.url), - resolve, - ); - }); - } else if (request.action === 'injectHighlightJsCss') { - await injectHighlightJsCss(sender.tab); - } else if (request.action === 'saveContestProblemList') { - saveContestProblemList(request.contestID, request.problems); - } else if (request.action === 'getContestProblemList') { - return getProblemList(request.contestID); - } else if (request.action === 'setJoinedContestList') { - saveJoinedContestList(request.contestList); - } else if (request.action === 'getJoinedContestList') { - return getJoinedContestList(); - } - return new Promise((resolve) => resolve(null)); - }); -})(); diff --git a/ext/manifest.json b/ext/manifest.json index f061764..30b1140 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -10,7 +10,7 @@ "browser_specific_settings": { "gecko": { "id": "{a7a2f4f4-8648-4221-8e88-af047e0fe5ca}", - "strict_min_version": "42.0" + "strict_min_version": "132.0" } }, @@ -44,7 +44,14 @@ }, "background": { - "service_worker": "js/service-worker.js" + "service_worker": "js/background.js", + "scripts": [ + "vendor/browser-polyfill.js", + "js/config.js", + "vendor/bower/jquery.min.js", + "js/common.js", + "js/background.js" + ] }, "web_accessible_resources": [ diff --git a/ext/scss/options.scss b/ext/scss/options.scss index aee1589..e83b3a8 100644 --- a/ext/scss/options.scss +++ b/ext/scss/options.scss @@ -1,3 +1,18 @@ +html, body { + font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.5; + color: #333; + background-color: #fff; +} + +body { + margin: 1em; +} + +h2 { + font-weight: 400; +} + h2:not(:first-child) { margin-top: 35px; } diff --git a/gulpfile.js b/gulpfile.js index 394cae9..1d1ee62 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -37,6 +37,7 @@ let distFiles = [ 'icon48.png', 'icon128.png', 'options.html', + 'html/**/*', 'css/**/*', 'js/**/*', 'images/**/*',