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

[ext] Manifest V3 #1912

Merged
merged 3 commits into from
Feb 15, 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
151 changes: 103 additions & 48 deletions browser-extension/prepareExtension.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
getForEnv,
selectValue,
generateImportWrappers,
writeManifest,
writeConfig,
Expand All @@ -8,43 +8,94 @@ import {

const env = process.env.TOURNESOL_ENV || 'production';

const browser = process.env.EXTENSION_BROWSER || 'firefox';
if (process.env.EXTENSION_BROWSER && !process.env.MANIFEST_VERSION) {
throw new Error(`MANIFEST_VERSION is required with EXTENSION_BROWSER`);
}

const manifestVersion = parseInt(process.env.MANIFEST_VERSION || 2);
if (manifestVersion != 2 && manifestVersion != 3)
throw new Error(`Invalid manifest version: ${manifestVersion}`);
if (manifestVersion === 2) {
console.info(
`Extension will be configured with manifest version ${manifestVersion}.`
);
} else {
console.info(
`Extension will be configured for ${browser} with manifest version ${manifestVersion}.`
);
}

const { version } = await readPackage();
const hostPermissions = [
...selectValue(env, {
production: ['https://tournesol.app/', 'https://api.tournesol.app/'],
'dev-env': [
'http://localhost/',
'http://localhost:3000/',
'http://localhost:8000/',
],
}),
'https://www.youtube.com/',
];

const permissions = [
'activeTab',
'contextMenus',
'storage',
'webNavigation',
// webRequest and webReauestBlocking were used to overwrite
// headers in the API response. This is no longer the case
// with version > 3.5.2.
// These permissions can be removed as soon as we are confident
// the next release works as expected.
'webRequest',
'webRequestBlocking',
...selectValue(manifestVersion, { 2: [], 3: ['scripting'] }),
];

const allPermissions = selectValue(manifestVersion, {
2: { permissions: [...hostPermissions, ...permissions] },
3: { permissions, host_permissions: hostPermissions },
});

const webAccessibleResourcesFromYouTube = [
'Logo128.png',
'html/*',
'images/*',
'utils.js',
'models/*',
'config.js',
];

const manifest = {
name: 'Tournesol Extension',
version,
description: 'Open Tournesol directly from YouTube',
permissions: [
...getForEnv(
{
production: ['https://tournesol.app/', 'https://api.tournesol.app/'],
'dev-env': [
'http://localhost/',
'http://localhost:3000/',
'http://localhost:8000/',
],
},
env
),
'https://www.youtube.com/',
'activeTab',
'contextMenus',
'storage',
'webNavigation',
'webRequest',
'webRequestBlocking',
],
manifest_version: 2,
...allPermissions,
manifest_version: manifestVersion,
icons: {
64: 'Logo64.png',
128: 'Logo128.png',
512: 'Logo512.png',
},
background: {
page: 'background.html',
persistent: true,
},
browser_action: {
background: selectValue(manifestVersion, {
2: { page: 'background.html', persistent: true },
3: selectValue(browser, {
// It's possible to make a browser-independent background value but
// Chrome only supports that since version 121 released in January 2024.
// See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background
firefox: {
scripts: ['background.js'],
type: 'module',
},
chrome: {
service_worker: 'background.js',
type: 'module',
},
}),
}),
[selectValue(manifestVersion, { 2: 'browser_action', 3: 'action' })]: {
default_icon: {
16: 'Logo16.png',
64: 'Logo64.png',
Expand All @@ -68,13 +119,10 @@ const manifest = {
all_frames: true,
},
{
matches: getForEnv(
{
production: ['https://tournesol.app/*'],
'dev-env': ['http://localhost:3000/*'],
},
env
),
matches: selectValue(env, {
production: ['https://tournesol.app/*'],
'dev-env': ['http://localhost:3000/*'],
}),
js: [
'fetchTournesolToken.js',
'fetchTournesolRecommendationsLanguages.js',
Expand All @@ -88,20 +136,28 @@ const manifest = {
open_in_tab: true,
},
default_locale: 'en',
web_accessible_resources: [
'Logo128.png',
'html/*',
'images/*',
'utils.js',
'models/*',
'config.js',
],
web_accessible_resources: selectValue(manifestVersion, {
2: webAccessibleResourcesFromYouTube,
3: [
{
matches: [
'https://*.youtube.com/*',
selectValue(env, {
production: 'https://tournesol.app/*',
'dev-env': 'http://localhost:3000/*',
}),
],
resources: webAccessibleResourcesFromYouTube,
},
],
}),
};

// Please DO NOT add a trailing slash to front end URL, this prevents
// creating duplicates in our web analytics tool
const config = getForEnv(
{
const config = {
manifestVersion,
...selectValue(env, {
production: {
frontendUrl: 'https://tournesol.app',
frontendHost: 'tournesol.app',
Expand All @@ -112,12 +168,11 @@ const config = getForEnv(
frontendHost: 'localhost:3000',
apiUrl: 'http://localhost:8000',
},
},
env
);
}),
};

(async () => {
await generateImportWrappers(manifest);
await generateImportWrappers(manifest, webAccessibleResourcesFromYouTube);
await writeManifest(manifest, 'src/manifest.json');
await writeConfig(config, 'src/config.js');
})();
15 changes: 8 additions & 7 deletions browser-extension/prepareTools.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { writeFile, mkdir, readFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';

export const getForEnv = (object, env) => {
const result = object[env];
export const selectValue = (key, options) => {
const result = options[key];
if (result === undefined) {
throw new Error(
`No value found for the environment ${JSON.stringify(env)}`
);
throw new Error(`No value found for the key ${JSON.stringify(key)}`);
}
return result;
};

export const generateImportWrappers = async (manifest) => {
export const generateImportWrappers = async (
manifest,
webAccessibleResources
) => {
await Promise.all(
manifest['content_scripts'].map(async (contentScript) => {
await Promise.all(
Expand All @@ -22,7 +23,7 @@ export const generateImportWrappers = async (manifest) => {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, content);
contentScript.js[i] = newJs;
manifest['web_accessible_resources'].push(js);
webAccessibleResources.push(js);
})
);
})
Expand Down
2 changes: 1 addition & 1 deletion browser-extension/src/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"webextensions": true
},
"parserOptions": {
"ecmaVersion": 11,
"ecmaVersion": 12,
"sourceType": "module",
"ecmaFeatures": {
"modules": true
Expand Down
33 changes: 4 additions & 29 deletions browser-extension/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ const createContextMenu = function createContextMenu() {
});
});

chrome.contextMenus.onClicked.addListener(function (e) {
chrome.contextMenus.onClicked.addListener(function (e, tab) {
var videoId = new URL(e.linkUrl).searchParams.get('v');
if (!videoId) {
alertUseOnLinkToYoutube();
alertUseOnLinkToYoutube(tab);
} else {
addRateLater(videoId).then((response) => {
if (!response.success) {
Expand All @@ -47,7 +47,8 @@ const createContextMenu = function createContextMenu() {
function (response) {
if (!response.success) {
alertOnCurrentTab(
'Sorry, an error occured while opening the Tournesol login form.'
'Sorry, an error occured while opening the Tournesol login form.',
tab
);
}
}
Expand All @@ -61,32 +62,6 @@ const createContextMenu = function createContextMenu() {
};
createContextMenu();

/**
* Remove the X-FRAME-OPTIONS and FRAME-OPTIONS headers included in the
* Tournesol application HTTP answers. It allows the extension to display
* the application in an iframe without enabling all website to do the same.
*/
chrome.webRequest.onHeadersReceived.addListener(
function (info) {
const headers = info.responseHeaders.filter(
(h) =>
!['x-frame-options', 'frame-options'].includes(h.name.toLowerCase())
);
return { responseHeaders: headers };
},
{
urls: ['https://tournesol.app/*'],
types: ['sub_frame'],
},
[
'blocking',
'responseHeaders',
// Modern Chrome needs 'extraHeaders' to see and change this header,
// so the following code evaluates to 'extraHeaders' only in modern Chrome.
chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
].filter(Boolean)
);

function getDateThreeWeeksAgo() {
// format a string to properly display years months and day: 2011 -> 11, 5 -> 05, 12 -> 12
const threeWeeksAgo = new Date(Date.now() - 3 * 7 * 24 * 3600000);
Expand Down
32 changes: 25 additions & 7 deletions browser-extension/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { apiUrl } from './config.js';
import { apiUrl, manifestVersion } from './config.js';

export const getAccessToken = async () => {
return new Promise((resolve) => {
Expand All @@ -8,14 +8,32 @@ export const getAccessToken = async () => {
});
};

export const alertOnCurrentTab = async (msg) => {
chrome.tabs.executeScript({
code: `alert("${msg}", 'ok')`,
});
const getCurrentTab = async () => {
const queryOptions = { active: true, lastFocusedWindow: true };
const [tab] = await chrome.tabs.query(queryOptions);
return tab;
};

export const alertOnCurrentTab = async (msg, tab) => {
if (manifestVersion === 2) {
chrome.tabs.executeScript({
code: `alert("${msg}", 'ok')`,
});
} else {
tab ??= await getCurrentTab();
const windowAlert = (msg, btn) => {
window.alert(msg, btn);
};
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: windowAlert,
args: [msg, 'ok'],
});
}
};

export const alertUseOnLinkToYoutube = () => {
alertOnCurrentTab('This must be used on a link to a youtube video');
export const alertUseOnLinkToYoutube = (tab) => {
alertOnCurrentTab('This must be used on a link to a youtube video', tab);
};

export const fetchTournesolApi = async (path, options = {}) => {
Expand Down
Loading