Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate to Manifest V3 #46

Merged
merged 3 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ satori-enhancements is a WebExtension built for Jagiellonian University's
to improve usability, ease of use and add some useful features, that should
have been there since the beginning (but for some reason, they aren't).

![Satori Enhancements results page](screenshots/results.png)
![Satori Enhancements results page](screenshots/results-2x.png)

## Features

Expand Down
6 changes: 6 additions & 0 deletions ext/html/offscreen.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!doctype html>
<script src="/vendor/browser-polyfill.js"></script>
<script src="/js/config.js"></script>
<script src="/vendor/bower/jquery.min.js"></script>
<script src="/js/common.js"></script>
<script src="/js/offscreen.js"></script>
197 changes: 141 additions & 56 deletions ext/js/background.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
if (typeof importScripts !== 'undefined') {
importScripts('../vendor/browser-polyfill.js', 'config.js', 'common.js');
}

(function () {
'use strict';

Expand All @@ -16,6 +20,21 @@

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',
Expand All @@ -30,31 +49,32 @@
* variable.
* @see setUpLastContestRedirect
*/
function retrieveLastContestID() {
storage.get('lastContestID').then((response) => {
lastContestID = response.lastContestID;
});
async function retrieveLastContestID() {
const response = await storage.get('lastContestID');
lastContestID = response.lastContestID;
}

/**
* Save last contest ID both in Storage and our local variable.
* Save last contest ID both in Storage and our local variable
* and update redirect rules.
* @param {string} contestID last contest ID
* @see setUpLastContestRedirect
*/
function saveLastContestID(contestID) {
storage.set({ lastContestID: contestID });
lastContestID = contestID;
updateLastContestRedirectRules();
}

/**
* 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);
}

/**
Expand Down Expand Up @@ -114,13 +134,48 @@
}

/**
* Add an onBeforeRequest listener that redirects to the last contest
* Sets up a declarative net request rules to redirect to the last contest
* if the user just entered Satori webpage.
*/
function updateLastContestRedirectRules() {
const addRules = [];
if (typeof lastContestID !== 'undefined' && lastContestID !== null) {
addRules.push(
...[
{ id: 1, urlFilter: '||satori.tcs.uj.edu.pl/|' },
{ id: 2, urlFilter: '||satori.tcs.uj.edu.pl/news' },
].map(({ id, urlFilter }) => ({
id,
condition: {
urlFilter,
// Redirect only if the user just opened Satori on a given tab
excludedTabIds: [...satoriTabs.values()],
resourceTypes: ['main_frame'],
},
action: {
type: 'redirect',
redirect: {
url: SATORI_URL_CONTEST + lastContestID + '/',
},
},
})),
);
}
browser.declarativeNetRequest.updateSessionRules({
removeRuleIds: [1, 2],
addRules,
});
}

/**
* Enables the last contest redirect.
*
* Also, the function adds webNavigation.onCommitted and tabs.onRemoved
* listeners to keep the list of Satori tabs.
*/
function setUpLastContestRedirect() {
updateLastContestRedirectRules();

// Store which tabs have Satori open
browser.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
if (
Expand All @@ -132,52 +187,30 @@
} else {
satoriTabs.delete(tabId);
}
updateLastContestRedirectRules();
});

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'],
);
browser.tabs.onRemoved.addListener((tabId) => {
satoriTabs.delete(tabId);
updateLastContestRedirectRules();
});
}

/**
* 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 },
});
}
});
Expand Down Expand Up @@ -272,16 +305,58 @@
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);
}
}
return contestProblemList[contestID] ?? {};
}

/**
* Setup offscreen document for parsing HTML
* @param path path to the offscreen document
* @returns {Promise<void>} 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
Expand All @@ -298,28 +373,33 @@
return (await storage.get('contestList')).contestList ?? [];
}

retrieveLastContestID();
setUpLastContestRedirect();
retrieveLastContestID().then(() => {
setUpLastContestRedirect();
});
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,
getContestID(sender.url),
request.value,
request.add,
);
return new Promise((resolve) => resolve(null));
} else if (request.action === 'getContestItemList') {
return new Promise((resolve) => {
getContestItemList(
Expand All @@ -329,16 +409,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;
});
})();
4 changes: 3 additions & 1 deletion ext/js/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ if (typeof module !== 'undefined') {
};
}

function parseProblemList(jqueryHandles) {
function parseProblemList(htmlText) {
const jqueryHandles = $.parseHTML(htmlText);

return Object.fromEntries(
jqueryHandles.flatMap((el) =>
[
Expand Down
8 changes: 4 additions & 4 deletions ext/js/general.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
(function () {
'use strict';

browser.runtime.sendMessage({ action: 'enablePageAction' });
browser.runtime.sendMessage({ action: 'enableAction' });

const BANNER_URL = browser.extension.getURL('images/satori_banner.png');
const TCS_LOGO_URL = browser.extension.getURL('images/tcslogo.svg');
const ALT_TCS_LOGO_URL = browser.extension.getURL('images/alttcslogo.png');
const BANNER_URL = browser.runtime.getURL('images/satori_banner.png');
const TCS_LOGO_URL = browser.runtime.getURL('images/tcslogo.svg');
const ALT_TCS_LOGO_URL = browser.runtime.getURL('images/alttcslogo.png');

let storage = browser.storage.sync || browser.storage.local;

Expand Down
12 changes: 12 additions & 0 deletions ext/js/offscreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
browser.runtime.onMessage.addListener(async (request) => {
if (request.target !== 'offscreen') {
return false;
}

if (request.type === 'parseProblemList') {
return parseProblemList(request.data);
}

console.warn(`Unexpected message type received: '${request.type}'.`);
return false;
});
Loading
Loading