UNPKG

webext-manifest-browser-polyfill

Version:
1,269 lines (1,073 loc) 64 kB
/* eslint-disable no-restricted-syntax */ import fs from 'node:fs'; import browserslist from 'browserslist'; // TODO FirefoxAndroid always returns only the latest version /** * priorities: * 1 - leads to issues * 2 - no compromise easy fix * 3 - requires migration */ let browsers = browserslist(); const maximumCompatibility = true; const STORE_ID_CHROME = 'chrome'; const STORE_ID_EDGE = 'edge'; const STORE_ID_OPERA = 'opera'; const STORE_ID_WHALE = 'whale'; const STORE_ID_SAMSUNG = 'samsung'; const STORE_ID_FIREFOX = 'firefox'; const STORE_ID_APPLE = 'safari'; function browserMatchSome (query) { const subBrowsers = browserslist(query); return subBrowsers.some((item) => browsers.includes(item)); } function comesBefore (array, firstItem, secondItem, defaultValue) { if (Array.isArray(array)) { const hasFirstItem = array.includes(firstItem); const hasSecondItem = array.includes(secondItem); if (hasFirstItem) { if (!hasSecondItem) return true; return array.indexOf(firstItem) < array.indexOf(secondItem); } if (hasSecondItem) return false; } return defaultValue || false; } function getIsMalformedPreferredEnvironment (preferredEnvironment) { const environmentsWeUnderstand = ['document', 'service_worker']; return preferredEnvironment.some((environment) => !environmentsWeUnderstand.includes(environment)); } function getBackgroundStrategy (optionalBackgroundStrategy, initialPreferredEnvironment) { if (optionalBackgroundStrategy) return optionalBackgroundStrategy; if (false && Array.isArray(initialPreferredEnvironment)) { // TODO either remove false or remove this block const hasDocumentEnvironment = initialPreferredEnvironment.includes('document'); const hasServiceWorkerEnvironment = initialPreferredEnvironment.includes('service_worker'); if (hasDocumentEnvironment && hasServiceWorkerEnvironment) return 'hybrid'; if (hasDocumentEnvironment) return 'document'; if (hasServiceWorkerEnvironment) return 'service_worker'; } return 'compatible'; } function handleManifestBackground ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, supportedStores, optionalBackgroundStrategy) { /** * setup */ if ('app' in manifestJson && manifestJson.app !== null) { manifestWarnings.push('[p2] Legacy packaged apps are currently not supported by this tool.'); } let legacyPage; if ('background_page' in manifestJson && manifestJson.background_page !== null) { manifestWarnings.push('[p2] "background_page" is deprecated. Please only use "background.page".'); if (typeof manifestJson.background_page === 'string') { legacyPage = manifestJson.background_page; } } const backgroundDataPresent = 'background' in manifestJson && manifestJson.background !== null; if (!legacyPage && !backgroundDataPresent) { // no background found return; } const backgroundData = manifestJson.background || {}; const hasUnsupportedBackgroundSyntax = backgroundDataPresent && (Array.isArray(backgroundData) || typeof manifestJson.background !== 'object'); if (hasUnsupportedBackgroundSyntax) { manifestWarnings.push('[p1] This tool only supports the background property as object.'); return; } const initialServiceWorker = backgroundData.service_worker; const initialPage = backgroundData.page; if (legacyPage && initialPage) { manifestInfo.push('If both "background_page" and "background.page" are specified. "background_page" will be used for mv1, otherwise "background.page" will be used.'); } let validInitialScripts; const initialScripts = backgroundData.scripts; if (Array.isArray(initialScripts) && initialScripts.length > 0) { validInitialScripts = initialScripts; } const hasInitialDocumentEnvironment = legacyPage || initialPage || validInitialScripts; const hasAnyBackgroundEnvironment = hasInitialDocumentEnvironment || initialServiceWorker; if (!hasAnyBackgroundEnvironment) { if (backgroundDataPresent) { delete manifestJson.background; manifestChanges.push('Removed the "background" key as no valid background environments are specified.'); } return; } const initialPreferredEnvironment = backgroundData.preferred_environment; /* TODO Opera >= 74, Samsung >= 11, were in below matcher as well. We need to find the upper limit before proceeding */ const disallowOtherAlongsideServiceWorker = browserMatchSome('Chrome >= 88 and Chrome < 121, Edge >= 88 and Edge < 121, Firefox >= 109 and Firefox < 121'); const someServiceWorkerUnsupported = browserMatchSome('Chrome < 88, Edge < 88, Opera < 74, Firefox < 200, Safari < 15.4, iOS < 15.4'); const fallbackServiceWorker = validInitialScripts && validInitialScripts[0]; const completeServiceWorkerFallback = fallbackServiceWorker && validInitialScripts.length === 1; const backgroundStrategy = getBackgroundStrategy(optionalBackgroundStrategy, initialPreferredEnvironment); let haveDocument = false; let haveServiceWorker = false; if (targetVersion <= 2) { // INFO mv2 has no dual mode support if (backgroundStrategy === 'document') { haveDocument = true; } else if (backgroundStrategy === 'service_worker' && (initialServiceWorker || fallbackServiceWorker)) { haveServiceWorker = true; if (someServiceWorkerUnsupported) { manifestWarnings.push('The background script will now only be a service worker. This reduces support for older browsers and Firefox.'); } } else if (someServiceWorkerUnsupported) { haveDocument = true; } else if (!hasInitialDocumentEnvironment && initialServiceWorker) { haveServiceWorker = true; } else if (comesBefore(initialPreferredEnvironment, 'service_worker', 'document') && completeServiceWorkerFallback) { haveServiceWorker = true; } else { haveDocument = true; } } else if (targetVersion >= 3) { if (supportedStores.includes(STORE_ID_EDGE)) { haveServiceWorker = true; // TODO add serviceWorker usage reason when edge mv3, 'Removed "background.scripts" as it is not accepted in mv3 by the Edge store. See: https://github.com/microsoft/MicrosoftEdge-Extensions/issues/136' // TODO validate if Edge Store supports background.page alongside background.service_worker in mv3 } else if ((backgroundStrategy === 'service_worker' || backgroundStrategy === 'compatible') && disallowOtherAlongsideServiceWorker) { haveServiceWorker = true; } else if (backgroundStrategy === 'document' && disallowOtherAlongsideServiceWorker) { haveDocument = true; } else { const supportScriptsAsServiceWorkerWithPreferredEnvironment = browserMatchSome('Safari >= 18'); const preferScriptsForServiceWorker = supportScriptsAsServiceWorkerWithPreferredEnvironment && comesBefore(initialPreferredEnvironment, 'service_worker', 'document') && !initialServiceWorker && validInitialScripts && validInitialScripts.length > 1; haveServiceWorker = !preferScriptsForServiceWorker; haveDocument = true; if (disallowOtherAlongsideServiceWorker) { manifestWarnings.push('Both a service_worker and document background environment will be included. This makes it incompatible with some older browsers.'); } } } let haveScripts = false; let havePage = false; if (haveDocument) { if (!haveServiceWorker && validInitialScripts && comesBefore(initialPreferredEnvironment, 'service_worker', 'document')) { // only keep scripts, which will allow Safari to load them as service_worker haveScripts = true; } else if (initialPage) { havePage = true; } else if (validInitialScripts) { haveScripts = true; } else if (legacyPage) { havePage = true; } else { haveScripts = true; } } if (!haveDocument) { manifestWarnings.push('The background script will now only be a service worker. This makes it incompatible with Firefox.'); } if (!haveServiceWorker && targetVersion >= 3) { manifestWarnings.push('The background script will now only be a document. This makes it incompatible with Chromium-based browsers.'); } /** * background_page key */ if (targetVersion <= 1) { if (!legacyPage) { if (initialPage) { manifestJson.background_page = initialPage; manifestChanges.push('Added "background_page" based on "background.page".'); } else { manifestWarnings.push('[p1] No "background.page" set. No background page will be loaded.'); } } } else if (legacyPage) { // TODO potentially keep when maximumCompatibility is set to true? delete manifestJson.background_page; manifestChanges.push('Removed "background_page" in favor of the "background" object.'); } /** * background key */ if (!manifestJson.background) { if (targetVersion <= 1) return; manifestChanges.push('Added the "background" key based on `background_page`.'); manifestJson.background = backgroundData; } /** * page key */ if (havePage) { if (!initialPage) { backgroundData.page = legacyPage; manifestChanges.push('Added "background.page" based on the "background_page" value.'); } } else if (initialPage) { delete backgroundData.page; manifestChanges.push(`Removed "background.page" in favor of "background.${haveScripts ? 'scripts' : 'service_worker'}". See https://crbug.com/1418934`); } /** * scripts key */ if (haveScripts) { if (!validInitialScripts) { manifestInfo.push('The background script may now run in a document environment. Make sure your background script can deal with this and is not using any serviceworker-only features.'); backgroundData.scripts = [initialServiceWorker]; manifestChanges.push('Added "background.scripts" based on "background.service_worker".'); } } else if ('scripts' in backgroundData) { delete backgroundData.scripts; if (!validInitialScripts) { manifestChanges.push('Removed "background.scripts" as it is empty or malformed. This prevents loading in some Firefox versions.'); } else if (havePage) { manifestChanges.push('Removed "background.scripts" in favor of "background.page".'); } else { manifestChanges.push('Removed "background.scripts" in favor of "background.service_worker". See https://crbug.com/1418934'); } } /** * service_worker key */ if (haveServiceWorker) { if (!initialServiceWorker) { backgroundData.service_worker = fallbackServiceWorker; manifestChanges.push(`Added "background.service_worker" with value ${fallbackServiceWorker} derived from "background.scripts[0]".`); if (!completeServiceWorkerFallback) { manifestWarnings.push('[p1] The background.scripts fallback for service_worker includes more than one script. Only the first script will be used. Consider adding "background.service_worker" or limit "background.scripts" to one script file.'); } } } else if ('service_worker' in backgroundData) { delete backgroundData.service_worker; manifestChanges.push(`Removed "background.service_worker" in favor of "background.${haveScripts ? 'scripts' : 'page'}".`); } /** * persistent key */ if (targetVersion === 2 && browserMatchSome('Safari >= 14') && !browserMatchSome('iOS >= 15')) { manifestInfo.push('Safari mv2 support is requested. Keep in mind Safari does not include iOS Safari. If your extension needs to support iOS Safari, make sure to add iOS to browserlist as this has consequences for the background.persistent handling.'); } // INFO persistent key not supported if scripts=[] and no page or service_worker exists in chrome // TODO confirm this, install tech preview on older macbook // TODO Safari seems to allow persistent: true in mv2 alongside only service_worker. Does this have any effect? // TODO SAFARI mv3 persistent true? const initialPersistent = backgroundData.persistent; if (hasInitialDocumentEnvironment && initialPersistent !== false) { if (haveServiceWorker || targetVersion >= 3) { manifestInfo.push('Background scripts are not necessarily persistent in all browsers. Make sure your scripts can deal with this as vendors want to discontinue persistent background page in favor of non-persistent background pages.'); } if (initialPersistent !== true) { manifestWarnings.push('[p1] Make sure to explicitly set the "background.persistent" key to true or false to indicate if the background page is preferred to be persistent or not. Suggested is to set it to false to indicate your background page will be non persistent (an event page/script).'); } } let persistentUnsupported = false; if (targetVersion >= 3) { persistentUnsupported = 'Removed "background.persistent" as mv3 only allows it to be set to false which is the default value.'; } else if (!haveDocument) { delete backgroundData.persistent; persistentUnsupported = 'Removed "background.persistent" as "background" has no document environment.'; } else if (disallowOtherAlongsideServiceWorker && haveServiceWorker) { // TODO should probably be removed in more situations? // TODO revisit this, why is disallowOtherAlongsideServiceWorker a condition for this? persistentUnsupported = 'Removed "background.persistent" as it is not supported alongside service_workers in chromium-based browsers. See https://crbug.com/1418934'; } if (persistentUnsupported) { if ('persistent' in backgroundData) { delete backgroundData.persistent; manifestChanges.push(persistentUnsupported); } } else if (targetVersion === 2) { // iOS safari in mv3 or if using serviceworker assumes persistent false const persistentRequiredFalse = browserMatchSome('iOS >= 15 and iOS < 15.4') || (!haveServiceWorker && browserMatchSome('iOS >= 15.4')); if (persistentRequiredFalse) { if (initialPersistent !== false) { backgroundData.persistent = false; manifestChanges.push('"persistent" key in "background" set to false for compatibility with iOS Safari.'); } } else if (typeof initialPersistent !== 'boolean') { // TODO let nonPersistentUnsupported = browserMatchSome('Safari >= 14 and Safari < 14.1'); const assumeWishingNonPersistent = manifestJson.manifest_version >= 3 || (!hasInitialDocumentEnvironment && initialServiceWorker); const newValue = !assumeWishingNonPersistent; backgroundData.persistent = newValue; // edge requires this browserMatchSome('Edge >= 14 and Edge < 50') manifestChanges.push(`"persistent" key in "background" set to ${newValue}. This improves compatibility with older Edge versions and makes sure the "background.service_worker" fallback works.`); } } /** * type key */ if ('type' in backgroundData) { if (targetVersion <= 3 && backgroundData.type === 'classic') { delete backgroundData.type; manifestChanges.push('Removed "background.type" key as it is "classic" by default. Removing it improves compatibility.'); } if (backgroundData.type === 'module') { manifestInfo.push('"background.type" "module" is not supported in every browser for every background type.'); // see https://github.com/w3c/webextensions/issues/289 } } /** * preferred_environment key */ if (initialPreferredEnvironment) { if (getIsMalformedPreferredEnvironment(initialPreferredEnvironment)) { manifestWarnings.push('Currently the only supported environments are "service_worker" and "document"'); } if (!haveDocument || !haveServiceWorker) { delete backgroundData.preferred_environment; manifestChanges.push('Removing "background.preferred_environment" as only one environment will be specified.'); } } else if (haveDocument && haveServiceWorker) { // preferred environment is needed if (hasInitialDocumentEnvironment && initialServiceWorker) { // do not create preferred_environment as the preferred order can not be determined manifestInfo.push('Both "service_worker" and "document" environments are included. "service_worker" is preferred in Chromium-based browsers, while "page" and "scripts" are preferred in Firefox and Safari. Consider adding "background.preferred_environment".'); } else if (initialServiceWorker) { backgroundData.preferred_environment = ['service_worker', 'document']; manifestChanges.push('Adding "background.preferred_environment" to indicate "service_worker" is the preferred environment.'); } else { backgroundData.preferred_environment = ['document', 'service_worker']; manifestChanges.push('Adding "background.preferred_environment" to indicate "document" is the preferred environment.'); } } } function handleManifestVersion ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }) { const currentVersion = manifestJson.manifest_version; if (currentVersion !== 3) { manifestWarnings.push('[p2] "manifest_version" should be set to 3.'); } if (targetVersion <= 1) { delete manifestJson.manifest_version; manifestChanges.push('Removed "manifest_version" key.'); return; } if (currentVersion !== targetVersion) { manifestJson.manifest_version = targetVersion; manifestChanges.push(`Manifest version changed from ${currentVersion} to ${targetVersion}`); } } function handleManifestDeveloperHomepageUrlAuthor ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }) { const author = manifestJson.author; const homepageUrl = manifestJson.homepage_url; const developerName = manifestJson.developer?.name; const developerUrl = manifestJson.developer?.url; /** * author key */ if (!author) { if (developerName) { manifestJson.author = developerName; manifestChanges.push('Added "author" key based on "developer.name".'); } else if (browserMatchSome('Edge >= 14 and Edge < 50')) { manifestJson.author = manifestJson.name; manifestChanges.push('Added "author" key based on "name". It is a required key in Edge < 50'); } } /** * homepage_url key */ if (developerUrl && !homepageUrl) { manifestJson.homepage_url = developerUrl; manifestChanges.push('Added "homepage_url" based on "developer.url".'); } /** * developer key */ const addDeveloperKeys = maximumCompatibility || browserMatchSome('Opera >= 15, Firefox >= 42'); if (author && !developerName && addDeveloperKeys) { manifestJson.developer ||= {}; manifestJson.developer.name = author; manifestChanges.push('Added "developer.name" based on "author".'); } if (homepageUrl && !developerUrl && addDeveloperKeys) { manifestJson.developer ||= {}; manifestJson.developer.url = homepageUrl; manifestChanges.push('Added "developer.url" based on "homepage_url".'); } } function handleManifestOptions ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }) { const legacyOptionsPage = manifestJson.options_page; let optionsUi = manifestJson.options_ui; // no options page found if (!legacyOptionsPage && !optionsUi) return; if (!optionsUi) { optionsUi = { browser_style: false, open_in_tab: true, page: legacyOptionsPage, }; manifestJson.options_ui = optionsUi; manifestChanges.push('Added "options_ui" based on "options_page".'); } if (typeof optionsUi.page !== 'string') { manifestWarnings.push('[p1] "options_ui.page" is missing.'); } if (optionsUi.chrome_style || optionsUi.chrome_style === false) { if (optionsUi.chrome_style === true) { manifestWarnings.push('[p3] "options_ui.chrome_style" is deprecated.'); } else { manifestWarnings.push('[p2] "options_ui.chrome_style" is deprecated and false by default. Please remove this key. See https://web.archive.org/web/20150321054040/https://developer.chrome.com/extensions/optionsV2#chrome_style'); } if (targetVersion >= 3) { delete optionsUi.chrome_style; manifestChanges.push('Removed incompatible "options_ui.chrome_style".'); } else if (maximumCompatibility && optionsUi.chrome_style !== true) { delete optionsUi.chrome_style; manifestChanges.push('Removed no-op "options_ui.chrome_style".'); } } if (manifestJson?.options_ui?.browser_style === true) { manifestWarnings.push('[p3] "options_ui.browser_style" set to true is deprecated.'); if (optionsUi.browser_style === true && targetVersion >= 3 && browserMatchSome('Firefox >= 118, FirefoxAndroid >= 118')) { delete optionsUi.browser_style; manifestChanges.push('Removed incompatible "browser_style: true" on "options_ui" key.'); } } if (legacyOptionsPage) { manifestWarnings.push('[p2] "options_page" is deprecated, use "options_ui" instead.'); } else if (targetVersion <= 2 && optionsUi.page) { const addFallback = maximumCompatibility || (targetVersion <= 2 && browserMatchSome('Chrome < 40, Edge < 50, Opera < 27, Firefox < 48')); if (addFallback) { manifestJson.options_page = optionsUi.page; manifestChanges.push('Added "options_page" based on "options_ui".'); } } } function hasPngIcon (iconVariantsObject) { if (!iconVariantsObject) return false; return JSON.stringify(iconVariantsObject).includes('.png'); } function hasSvgIcon (iconVariantsObject) { if (!iconVariantsObject) return false; return JSON.stringify(iconVariantsObject).includes('.svg'); } function constructThemeIconsBasedOnIconVariants (iconVariants, defaultIcons) { if (iconVariants.length === 1) return null; let darkIconsObject; let lightIconsObject; for (const iconVariantsObject of iconVariants) { const colorSchemes = iconVariantsObject.color_schemes || ['dark', 'light']; if (!darkIconsObject && colorSchemes.includes('dark')) { darkIconsObject = iconVariantsObject; } if (!lightIconsObject && colorSchemes.includes('light')) { lightIconsObject = iconVariantsObject; } } if (!darkIconsObject || !lightIconsObject) return null; if (darkIconsObject === lightIconsObject && !hasSvgIcon(lightIconsObject)) return null; const availableSizes = new Set(); const result = Object.keys(lightIconsObject).filter((key) => { return darkIconsObject[key] && (key | 0); }).map((key) => { const size = key | 0; availableSizes.add(size); return { dark: lightIconsObject[key], light: darkIconsObject[key], size: size, }; }); if (availableSizes.length === 0) return null; const requiredSizes = new Set(); for (const sizeKey of Object.keys(defaultIcons)) { const sizeAsInt = sizeKey | 0; if (!sizeAsInt) continue; requiredSizes.add(sizeAsInt); } if (requiredSizes.size === 0) return result; const missingSizes = requiredSizes.difference(availableSizes); const sizeMap = { 32: 16, 38: 19, 48: 16, 64: 32, }; result.sort((a, b) => a.size - b.size); for (const size of missingSizes) { const preferredFallback = sizeMap[size]; const fallbackItem = result.find((item) => item.size === preferredFallback) || result.find((item) => item.size > size) || result[result.length - 1]; const item = { ...fallbackItem, size, }; result.push(item); } result.sort((a, b) => a.size - b.size); return result; } function handleManifestIconVariants ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, supportedStores) { if (manifestJson.icons) { manifestWarnings.push('[p2] "icons" is deprecated, use "icon_variants" instead.'); } const iconVariants = manifestJson.icon_variants; if (iconVariants) { const lastIconVariantsObject = iconVariants[iconVariants.length - 1]; if (!hasPngIcon(lastIconVariantsObject)) { manifestWarnings.push('[p1] "icon_variants" is missing png fallback icons.'); } if (!manifestJson.icons) { manifestJson.icons = lastIconVariantsObject; manifestChanges.push('Added "icons" based on "icon_variants".'); } } else { manifestWarnings.push('[p1] missing "icon_variants".'); } if (supportedStores.includes(STORE_ID_CHROME) && manifestJson.icons) { const unsupportedSizes = ['16', '32', '48', '128']; const regex = /^\//; for (const size of unsupportedSizes) { const sizeDeclaration = manifestJson.icons[size]; const absolutePath = regex.test(sizeDeclaration); if (!absolutePath) continue; manifestJson.icons[size] = sizeDeclaration.replace('/', ''); manifestChanges.push(`Replaced "icons[${size}]" absolute path to relative path.`); } } } function handleActionIcons ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, actionKey) { const action = manifestJson[actionKey]; if (!action) return; const iconVariants = action.icon_variants; let lastIconVariantsObject; if (iconVariants) { lastIconVariantsObject = iconVariants[iconVariants.length - 1]; if (!hasPngIcon(lastIconVariantsObject)) { manifestWarnings.push(`[p1] "${actionKey}.icon_variants" is missing png fallback icons.`); } } else { manifestWarnings.push(`[p1] missing "${actionKey}.icon_variants".`); } const defaultIcon = action.default_icon; if (defaultIcon) { manifestWarnings.push(`[p2] "${actionKey}.default_icon" is deprecated, use "icon_variants" instead.`); } else if (lastIconVariantsObject) { action.default_icon = lastIconVariantsObject; manifestChanges.push(`Adding "${actionKey}.default_icon" based on "${actionKey}.icon_variants".`); } else if (manifestJson.icons) { action.default_icon = manifestJson.icons; manifestChanges.push(`Adding "${actionKey}.default_icon" based on "icons"`); } if (typeof action.default_icon !== 'object' || !action.default_icon['19']) { manifestWarnings.push(`[p2] Missing "${actionKey}.default_icon['19']". This is required for action icons in Naver Whale.`); } if (typeof action.default_icon !== 'object' || !action.default_icon['38']) { manifestWarnings.push(`[p2] Missing "${actionKey}.default_icon['38']". This is required for action icons in Naver Whale.`); } const defaultIconObject = (typeof action.default_icon === 'object' && action.default_icon) || {}; const themeIcons = action.theme_icons; if (themeIcons) { manifestWarnings.push(`[p2] "${actionKey}.theme_icons" is deprecated, use "icon_variants" instead.`); } else if (iconVariants) { const potentialThemeIcons = constructThemeIconsBasedOnIconVariants(iconVariants, defaultIconObject); if (potentialThemeIcons) { action.theme_icons = potentialThemeIcons; manifestChanges.push(`Adding "${actionKey}.theme_icons" based on "${actionKey}.icon_variants"`); } } else if (manifestJson.icon_variants && !action.default_icon) { action.icon_variants = manifestJson.icon_variants; manifestChanges.push(`Adding "${actionKey}.icon_variants" based on "icon_variants"`); const potentialThemeIcons = constructThemeIconsBasedOnIconVariants(manifestJson.icon_variants, defaultIconObject); if (potentialThemeIcons) { action.theme_icons = potentialThemeIcons; manifestChanges.push(`Adding "${actionKey}.theme_icons" based on "icon_variants"`); } } } function handleManifestAction ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, supportedStores) { const disallowBrowserStyle = browserMatchSome('Firefox >= 118, FirefoxAndroid >= 118'); const disallowPageAction = targetVersion >= 3 && browserMatchSome('Chrome >= 88, Edge >= 80, Opera >= 15, Samsung >= 11'); // TODO Opera and Edge should have later versions const disallowPageAlongsideBrowserAction = browserMatchSome('Chrome >= 4, Opera >= 15, Safari >= 48, Edge >= 50, Samsung >= 11'); // TODO this needs validation and source, Edge is probably wrong const disallowSidebarActionAlongsideOtherActions = supportedStores.includes(STORE_ID_WHALE) || supportedStores.includes(STORE_ID_CHROME); // TODO switch to browser matching? see https://github.com/Fyrd/caniuse/issues/6004 const initialAction = manifestJson.action; const initialPageAction = manifestJson.page_action; const initialBrowserAction = manifestJson.browser_action; const initialSidebarAction = manifestJson.sidebar_action; /** * browser_action key */ if (initialBrowserAction) { manifestWarnings.push('[p3] "browser_action" is deprecated. Please use "action" going forward.'); } if (targetVersion <= 2 && initialAction && !initialBrowserAction) { manifestJson.browser_action = initialAction; manifestChanges.push('Added "browser_action" based on "action" key.'); } if (targetVersion >= 3 && initialBrowserAction) { delete manifestJson.browser_action; manifestChanges.push('Removed "browser_action" in favor of "action" key.'); } /** * action key */ if (targetVersion <= 2 && initialAction) { delete manifestJson.action; manifestChanges.push('Removed "action" in favor of "browser_action" key.'); } if (targetVersion >= 3 && !initialAction) { if (initialBrowserAction) { manifestJson.action = initialBrowserAction; manifestChanges.push('Added "action" based on "browser_action" key.'); } else if (disallowPageAction && initialPageAction) { manifestJson.action = initialPageAction; manifestChanges.push('Added "action" based on "page_action" key.'); } } const finalAction = manifestJson.action; if (finalAction && disallowBrowserStyle && finalAction.browser_style === true) { delete finalAction.browser_style; manifestChanges.push('Removed incompatible "browser_style: true" on "action" key. see: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action#syntax'); } /** * page_action key */ if (initialPageAction) { if (disallowPageAlongsideBrowserAction && (initialBrowserAction || initialAction || disallowPageAction)) { delete manifestJson.page_action; const replacementKey = targetVersion >= 3 ? 'action' : 'browser_action'; manifestChanges.push(`Removed "page_action" in favor of "${replacementKey}" key.`); } else if (targetVersion >= 3 && initialPageAction.browser_style === true && disallowBrowserStyle) { delete initialPageAction.browser_style; manifestChanges.push('Removed incompatible "browser_style: true" on "page_action" key. see: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action#syntax'); } } /** * action icons */ handleActionIcons({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, 'action'); handleActionIcons({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, 'browser_action'); /** * sidebar_action key */ if (initialSidebarAction) { const dropSidebarAction = disallowSidebarActionAlongsideOtherActions && (manifestJson.action || manifestJson.browser_action || manifestJson.page_action); if (dropSidebarAction) { delete manifestJson.sidebar_action; manifestChanges.push('Removed "sidebar_action" as Naver Whale does not allow it alongside other actions.'); } else { if (!manifestJson.side_panel) { manifestWarnings.push('"sidebar_action" is specified, but "side_panel" is missing. Chrome and Edge only understand "side_panel". And Vivaldi does not support "sidePanel" without specifying "manifest.side_panel"'); } const defaultPanel = initialSidebarAction.default_panel; if (defaultPanel && !initialSidebarAction.default_page) { initialSidebarAction.default_page = defaultPanel; manifestChanges.push('Added "sidebar_action.default_page" based on "sidebar_action.default_panel".'); } if (targetVersion >= 3 && initialSidebarAction.browser_style === true && disallowBrowserStyle) { delete initialSidebarAction.browser_style; manifestChanges.push('Removed incompatible "browser_style: true" on "sidebar_action" key. see: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action#syntax'); } if ('chrome_style' in initialSidebarAction) { manifestWarnings.push('[p2] "sidebar_action.chrome_style" has never been supported. Please remove this property.'); delete initialSidebarAction.chrome_style; manifestChanges.push('Removed unsupported "sidebar_action.chrome_style" property.'); } if (!initialSidebarAction.default_icon) { manifestWarnings.push('"sidebar_action.default_icon" is required by the Opera store'); if (supportedStores.includes(STORE_ID_OPERA)) { initialSidebarAction.default_icon = manifestJson.action?.default_icon || manifestJson.browser_action?.default_icon || manifestJson.icons; manifestChanges.push('Added "sidebar_action.default_icon" based on other manifest icons to support the Opera stores'); } } if (!('open_at_install' in initialSidebarAction)) { manifestWarnings.push('"sidebar_action.open_at_install" is missing. If not set to false, Firefox will open the sidebar on installation.'); } } } else if (manifestJson.side_panel) { manifestWarnings.push('"side_panel" is specified, but "sidebar_action" is missing. Opera and Firefox only understand "sidebar_action".'); } } function handleManifestActionCommands ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }) { const commands = manifestJson.commands; if (!commands) return; const actionCommand = commands._execute_action; const pageActionCommand = commands._execute_page_action; const browserActionCommand = commands._execute_browser_action; const sidebarActionCommand = commands._execute_sidebar_action; // https://bugzilla.mozilla.org/show_bug.cgi?id=1797811 // https://bugs.chromium.org/p/chromium/issues/detail?id=1465265 // https://bugs.chromium.org/p/chromium/issues/detail?id=1375790 // https://bugs.chromium.org/p/chromium/issues/detail?id=1492991 // https://bugs.chromium.org/p/chromium/issues/detail?id=1342360 // https://bugs.chromium.org/p/chromium/issues/detail?id=1344809 /** * _execute_browser_action */ if (browserActionCommand) { manifestWarnings.push('[p1] "commands._execute_browser_action" is deprecated. Please use "commands._execute_action" instead.'); if (!manifestJson.browser_action) { delete commands._execute_browser_action; manifestChanges.push('Removed "commands._execute_browser_action" as "browser_action" is not set.'); } } else if (actionCommand && manifestJson.browser_action) { commands._execute_browser_action = actionCommand; manifestChanges.push('Added "commands._execute_browser_action" based on "commands._execute_action"'); } /** * _execute_action */ if (!actionCommand && manifestJson.action) { if (browserActionCommand) { commands._execute_action = browserActionCommand; manifestChanges.push('Added "commands._execute_action" based on "commands._execute_browser_action"'); } else if (pageActionCommand && !manifestJson.page_action) { commands._execute_action = pageActionCommand; manifestChanges.push('Added "commands._execute_action" based on "commands._execute_page_action"'); } } else if (actionCommand && !manifestJson.action) { delete commands._execute_action; manifestChanges.push('Removed "commands._execute_action" as "action" is not set.'); } /** * _execute_page_action */ if (pageActionCommand && !manifestJson.page_action) { delete commands._execute_page_action; manifestChanges.push('Removed "commands._execute_page_action" as "page_action" is not set.'); } /** * _execute_sidebar_action */ if (sidebarActionCommand && !sidebarActionCommand.description) { sidebarActionCommand.description = 'Open extension sidebar'; manifestChanges.push('Added "commands._execute_sidebar_action.description" for compatibility with browsers other than firefox.'); } } function transformAnyCsp ( cspType, originalCsp, { manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, ) { const anyFirefox = browserMatchSome('Firefox >= 42, FirefoxAndroid >= 42'); if (!anyFirefox) return originalCsp; const hasReportSample = originalCsp.includes(' \'report-sample\''); const hasStrictDynamic = originalCsp.includes(' \'strict-dynamic\''); if (!hasReportSample && !hasStrictDynamic) return originalCsp; let newCsp = originalCsp; if (hasReportSample) { newCsp = newCsp.replace(/ 'report-sample'/g, ''); manifestChanges.push(`Removed 'report-sample' from "content_security_policy.${cspType}". see: https://bugzilla.mozilla.org/show_bug.cgi?id=1618141`); } if (hasStrictDynamic) { newCsp = newCsp.replace(/ 'strict-dynamic'/g, ''); manifestChanges.push(`Removed 'strict-dynamic' from "content_security_policy.${cspType}". see: https://bugzilla.mozilla.org/show_bug.cgi?id=1618141`); } return newCsp; } function transformExtensionCsp ( originalCsp, { manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, ) { const cspHasSandbox = originalCsp.toLowerCase().includes('sandbox '); const supportSafariVersionWithSandboxBug = browserMatchSome('Safari >= 14 and Safari < 16.3'); // TODO issue might have been fixed sooner than 16.3 const removeSandboxForSafari = supportSafariVersionWithSandboxBug && cspHasSandbox; const hasInlineOptions = manifestJson.options_ui && !manifestJson.options_ui.open_in_tab; const isOlderFirefoxESR = targetVersion <= 2 && browserMatchSome('Firefox < 56, FirefoxAndroid < 56'); const hasFrameAncestors = originalCsp.toLowerCase().includes('frame-ancestors'); const addAboutProtocolToFrameAncestors = hasInlineOptions && isOlderFirefoxESR && hasFrameAncestors; if (!removeSandboxForSafari && !addAboutProtocolToFrameAncestors) { return originalCsp; } let cspChanged = false; let finalCsp = originalCsp; try { finalCsp = originalCsp.split(/,+ ?/).map((singleCsp) => singleCsp.split(/;+ ?/).filter((part) => { if (removeSandboxForSafari && part.toLowerCase().startsWith('sandbox ')) { manifestChanges.push('Removed sandbox directive from "content_security_policy.extension_pages" as it prevents loading of styles.'); cspChanged = true; return false; } return true; }).map((part) => { if (!addAboutProtocolToFrameAncestors) return part; if (!part.toLowerCase().startsWith('frame-ancestors')) return part; if ((part + ' ').includes(' about: ')) return part; manifestChanges.push('Added about: to frame-ancestors directive in "content_security_policy.extension_pages" to fix the inline options page in older firefox ESR versions.'); cspChanged = true; if (part.startsWith('frame-ancestors \'none\'')) { part = part.replace('frame-ancestors \'none\'', 'frame-ancestors'); } return part + ' about:'; }).join('; ')).join(', '); } catch { manifestWarnings.push('[p1] Error parsing "content_security_policy".'); return originalCsp; } if (cspChanged) { return finalCsp; } return originalCsp; } function handleManifestSandboxAndCsp ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }) { let manifestCsp = manifestJson.content_security_policy; const legacySandboxCsp = manifestJson.sandbox?.content_security_policy; const useLegacySyntax = targetVersion <= 2; // TODO in the future some browsers support the new syntax on mv2 // no CSP found if (!manifestCsp && !legacySandboxCsp) return; const manifestUsesObject = typeof manifestCsp === 'object'; if (!manifestUsesObject) { manifestWarnings.push('[p2] "content_security_policy" should be an object.'); } const modernSandboxCsp = manifestUsesObject && manifestCsp.sandbox; let sandboxCsp = modernSandboxCsp || legacySandboxCsp; if (legacySandboxCsp) { manifestWarnings.push('[p2] "content_security_policy" of "sandbox" should be under "content_security_policy.sandbox".'); if (modernSandboxCsp) { manifestWarnings.push('[p1] Both "sandbox.content_security_policy" and "content_security_policy.sandbox" are defined, please use just one of them.'); if (useLegacySyntax) { sandboxCsp = legacySandboxCsp; } } } let correctedSandboxCsp; if (typeof sandboxCsp === 'string') { correctedSandboxCsp = transformAnyCsp( 'sandbox', sandboxCsp, { manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, ); } const contentScriptCsp = manifestUsesObject && manifestCsp.content_scripts; let correctedContentScriptCsp; if (targetVersion >= 3 && typeof contentScriptCsp === 'string') { correctedContentScriptCsp = transformAnyCsp( 'content_scripts', contentScriptCsp, { manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, ); } const extensionCsp = manifestUsesObject ? manifestCsp.extension_pages : manifestCsp; let correctedExtensionCsp; if (typeof extensionCsp === 'string') { correctedExtensionCsp = transformAnyCsp( 'extension_pages', extensionCsp, { manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, ); correctedExtensionCsp = transformExtensionCsp( correctedExtensionCsp, { manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }, ); } if (useLegacySyntax) { if (typeof correctedExtensionCsp === 'string') { manifestJson.content_security_policy = correctedExtensionCsp; if (manifestUsesObject) { manifestChanges.push('Replaced value of "content_security_policy" with "extension_pages".'); } } else if (manifestCsp) { delete manifestJson.content_security_policy; manifestChanges.push('Removed "content_security_policy" as it is missing "extension_pages".'); } if (typeof correctedSandboxCsp === 'string' && typeof manifestJson.sandbox === 'object') { manifestJson.sandbox = correctedSandboxCsp; } } else { if (!manifestUsesObject) { manifestCsp = {}; manifestJson.content_security_policy = manifestCsp; manifestChanges.push('Upgraded "content_security_policy" to object syntax.'); } if (typeof correctedExtensionCsp === 'string') { manifestCsp.extension_pages = correctedExtensionCsp; } if (typeof correctedSandboxCsp === 'string') { manifestCsp.sandbox = correctedSandboxCsp; } if (typeof correctedContentScriptCsp === 'string') { manifestCsp.content_scripts = correctedContentScriptCsp; } if (typeof manifestJson.sandbox === 'object' && 'content_security_policy' in manifestJson.sandbox) { delete manifestJson.sandbox.content_security_policy; manifestChanges.push('Removing "sandbox.content_security_policy" in favor of "content_security_policy.sandbox".'); } } } function handleManifestWebAccessibleResources ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }) { let webAccessibleResources = manifestJson.web_accessible_resources; if (!webAccessibleResources) return; const legacySyntax = typeof webAccessibleResources[0] === 'string'; if (legacySyntax) { manifestWarnings.push('[p2] Please use the new syntax for "web_accessible_resources".'); if (targetVersion >= 3) { webAccessibleResources = [{ matches: ['<all_urls>'], resources: webAccessibleResources, use_dynamic_url: false, }]; manifestJson.web_accessible_resources = webAccessibleResources; manifestChanges.push('Upgraded web_accessible_resources to the new syntax.'); } return; } if (targetVersion <= 2) { const newResources = []; for (const itemData of webAccessibleResources) { newResources.push(...itemData.resources); } manifestJson.web_accessible_resources = newResources; manifestChanges.push('Downgraded web_accessible_resources to legacy syntax.'); } } function getGeckoIdRequired (manifestJson) { if (browserMatchSome('Firefox < 48, FirefoxAndroid >= 42')) return true; if (manifestJson.dictionaries) return true; const allPermissions = Array.prototype.concat( manifestJson.permissions, manifestJson.optional_permissions, ); if ( allPermissions.includes('storage') || allPermissions.includes('identity') || allPermissions.includes('pkcs11') ) return true; return false; } function handleManifestBrowserSpecificSettings ({ manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, }) { const anyFirefox = browserMatchSome('Firefox >= 42, FirefoxAndroid >= 42'); const firefoxPriorToVersion69 = anyFirefox && browserMatchSome('Firefox < 69, FirefoxAndroid < 69'); function cleanupBrowserSpecificSettings (objectName) { const object = manifestJson[objectName]; if (object && firefoxPriorToVersion69) { const otherKeys = Object.keys(object).filter((key) => key !== 'gecko'); if (otherKeys.length > 0) { for (const key of otherKeys) { delete object[key]; } manifestChanges.push(`Remove "${otherKeys.join('", "')}" from "${objectName}" key. See https://bugzilla.mozilla.org/show_bug.cgi?id=1542351`); } } return object; } const browserSpecificSettings = cleanupBrowserSpecificSettings('browser_specific_settings'); let legacyApplications = cleanupBrowserSpecificSettings('applications'); if (!browserSpecificSettings && !legacyApplications) return; if (legacyApplications) { manifestWarnings.push('[p1] "applications" is deprecated, please use "browser_specific_settings" instead.'); if (targetVersion !== 2) { delete manifestJson.applications; manifestChanges.push('Removed "applications" as it is only valid in mv2.'); } } if (legacyApplications && !browserSpecificSettings) { manifestChanges.push('Added "browser_specific_settings" based on "applications" key.'); manifestJson.browser_specific_settings = legacyApplications; } const attemptLegacyApplicationsFallback = targetVersion === 2 && (maximumCompatibility || browserMatchSome('Firefox < 48, FirefoxAndroid < 48')); if (attemptLegacyApplicationsFallback && !legacyApplications?.gecko && browserSpecificSettings?.gecko) { if (!legacyApplications) { legacyApplications = {}; manifestJson.applications = legacyApplications; } legacyApplications.gecko = browserSpecificSettings.gecko; manifestChanges.push('Added "applications.gecko" based on "browser_specific_settings.gecko".'); } const geckoIdUseful = anyFirefox; if (!geckoIdUseful) return; const geckoIdRequired = getGeckoIdRequired(manifestJson); const geckoId = legacyApplications?.gecko?.id || browserSpecificSettings?.gecko?.id; if (geckoId) return; manifestWarnings.push(`${geckoIdRequired ? '[p1] ' : '[p2]'} "browser_specific_settings.gecko.id" is missing and should be added`); } function isHostPermission (permission) { if (typeof permission !== 'string') return false; if (permission === '<all_urls>') return true; if (permission.includes('://')) return true; return false; } function removeValueFromArray (array, value) { array.splice(array.indexOf(value), 1); } function movePermissionToOptional ({ manifestJson, permissionName, manifestChanges, reason = '', }) { const containsPermission = manifestJson.permissions?.includes(permissionName); if (!containsPermission) return; removeValueFromArray(manifestJson.permissions, permissionName); if (!manifestJson.optional_permissions) { manifestJson.optional_permissions = []; } manifestJson.optional_permissions.push(permissionName); manifestChanges.push(`Moved ${permissionName} from "permissions" to "optional_permissions". ${reason}`); } function removePermission ({ manifestJson, permissionName, manifestChanges, containerId, reason, }) { const permissions = manifestJson[containerId]; if (!Array.isArray(permissions)) return; if (!permissions.includes(permissionName)) return; removeValueFromArray(permissions, permissionName); manifestChanges.push(`Removed "${permissionName}" from "${containerId}". ${reason}`); } function removePermanentAndOptionalPermission ({ manifestJson, manifestChanges, permissionName, reason, }) { removePermission({ manifestJson, manifestChanges, permissionName, reason, containerId: 'permissions', }); removePermission({ manifestJson, manifestChanges, permissionName, reason, containerId: 'optional_permissions', }); } function movePermissions ({ manifestJson, manifestChanges, sourceContainerId, targetContainerId, }) { const sourcePermissions = manifestJson[sourceContainerId]; if (!Array.isArray(sourcePermissions)) return; const targetPermissions = manifestJson[targetContainerId]; const finalPermissions = targetPermissions || []; for (const permission of sourcePermissions) { if (finalPermissions && finalPermissions.includes(permission)) continue; finalPermissions.push(permission); } // assure existance of new key if (!targetPermissions) { manifestJson[targetContainerId]