diff --git a/browser-extension/prepareExtension.js b/browser-extension/prepareExtension.js index 9e3db16c7d..dfcb1820a1 100644 --- a/browser-extension/prepareExtension.js +++ b/browser-extension/prepareExtension.js @@ -1,5 +1,5 @@ import { - getForEnv, + selectValue, generateImportWrappers, writeManifest, writeConfig, @@ -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', @@ -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', @@ -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', @@ -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'); })(); diff --git a/browser-extension/prepareTools.js b/browser-extension/prepareTools.js index 37cc214ec6..a7479fe55b 100644 --- a/browser-extension/prepareTools.js +++ b/browser-extension/prepareTools.js @@ -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( @@ -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); }) ); }) diff --git a/browser-extension/src/.eslintrc.json b/browser-extension/src/.eslintrc.json index 2659a2c24a..24c9f91303 100644 --- a/browser-extension/src/.eslintrc.json +++ b/browser-extension/src/.eslintrc.json @@ -6,7 +6,7 @@ "webextensions": true }, "parserOptions": { - "ecmaVersion": 11, + "ecmaVersion": 12, "sourceType": "module", "ecmaFeatures": { "modules": true diff --git a/browser-extension/src/background.js b/browser-extension/src/background.js index 6a81596e29..dac140e174 100644 --- a/browser-extension/src/background.js +++ b/browser-extension/src/background.js @@ -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) { @@ -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 ); } } @@ -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); diff --git a/browser-extension/src/utils.js b/browser-extension/src/utils.js index 94056d0a77..e56a73d0b1 100644 --- a/browser-extension/src/utils.js +++ b/browser-extension/src/utils.js @@ -1,4 +1,4 @@ -import { apiUrl } from './config.js'; +import { apiUrl, manifestVersion } from './config.js'; export const getAccessToken = async () => { return new Promise((resolve) => { @@ -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 = {}) => {