webext-manifest-browser-polyfill
Version:
Makes webextensions compatible with older browsers
1,299 lines (1,080 loc) • 55.3 kB
JavaScript
/* 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;
function browserMatchSome (query) {
const subBrowsers = browserslist(query);
return subBrowsers.some((item) => browsers.includes(item));
}
function comesBefore (array, firstItem, secondItem, defaultValue) {
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 handleManifestBackground ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}, supportedStores, optionalBackgroundStrategy) {
const legacyBackgroundPage = manifestJson.background_page;
const backgroundDataPresent = 'background' in manifestJson;
// no background found
if (!legacyBackgroundPage && !backgroundDataPresent) return;
const backgroundData = manifestJson.background || {};
/**
* setup
*/
const initialServiceWorker = backgroundData.service_worker;
const initialPage = backgroundData.page;
const initialScripts = backgroundData.scripts;
const initialScriptsIsValid = Array.isArray(initialScripts) && initialScripts.length > 0;
const validInitialScripts = initialScriptsIsValid ? initialScripts : null;
const initialDocumentEnvironment = legacyBackgroundPage || initialPage || validInitialScripts;
const initialPreferredEnvironment = backgroundData.preferred_environment;
/* TODO Opera >= 74, Samsung >= 11, where 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');
let onlyUseServiceWorker = false;
let onlyUseDocument = false;
// background strategy
if (targetVersion === 2) {
onlyUseServiceWorker = (() => {
// using self invoked as it simplifies the if statements by allowing return
if (optionalBackgroundStrategy === 'service_worker') return true;
if (optionalBackgroundStrategy === 'document') return false;
if (initialPreferredEnvironment) {
if (comesBefore(initialPreferredEnvironment, 'service_worker', 'document')) return !someServiceWorkerUnsupported;
if (comesBefore(initialPreferredEnvironment, 'document', 'service_worker')) return false;
}
if (someServiceWorkerUnsupported) return false;
return !(initialPage || validInitialScripts);
})();
// INFO mv2 has no dual mode support
onlyUseDocument = !onlyUseServiceWorker;
if (onlyUseServiceWorker && someServiceWorkerUnsupported) {
manifestWarnings.push('The background script will now only be a service worker. This reduces support for older browsers and Firefox.');
}
} else if (targetVersion >= 3) {
const backgroundStrategy = optionalBackgroundStrategy || 'service_worker';
if (backgroundStrategy === 'service_worker' && disallowOtherAlongsideServiceWorker) {
onlyUseServiceWorker = true;
manifestWarnings.push('The background script will now only be a service worker. This makes it incompatible with Firefox.');
// report incompatible with firefox
} else if (backgroundStrategy === 'document' && disallowOtherAlongsideServiceWorker) {
onlyUseDocument = true;
// report incompatible with chrome
} else {
// use dual mode
const resultDoesNotMatchFullBrowserList = disallowOtherAlongsideServiceWorker;
if (resultDoesNotMatchFullBrowserList) {
manifestWarnings.push('Both a service_worker and document background environment will be included. This makes it incompatible with some older browsers.');
}
}
}
/**
* page key
*/
if (legacyBackgroundPage) {
manifestWarnings.push('[p2] "background_page" is deprecated. Please only use "background.page".');
if (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.');
}
}
if (targetVersion === 1) {
if (!legacyBackgroundPage) {
if (initialPage) {
manifestJson.background_page = initialPage;
manifestChanges.push('Added "background_page" based on "background.page".');
} else {
const fallbackPath = '/background.html';
manifestWarnings.push(`[p1] No "background.page" set. It will be assumed a background page is present under "${fallbackPath}".`);
manifestJson.background_page = fallbackPath;
manifestChanges.push(`Added "background_page" with value "${fallbackPath}".`);
}
}
} else {
if (legacyBackgroundPage) {
delete manifestJson.background_page;
manifestChanges.push('Removed "background_page" in favor of the "background" object.');
}
if (onlyUseServiceWorker) {
if (initialPage) {
delete backgroundData.page;
manifestChanges.push('Removed "background.page" in favor of "background.service_worker". See https://crbug.com/1418934');
}
} else if (legacyBackgroundPage && !initialPage && !validInitialScripts) {
backgroundData.page = legacyBackgroundPage;
manifestChanges.push('Added "background.page" based on the "background_page" value.');
}
}
/**
* scripts key
*/
if (targetVersion >= 2) {
// TODO simplify onlyUseServiceWorker and edgeScriptsNotSupported
const scriptsNotSupported = onlyUseServiceWorker || (targetVersion >= 3 && supportedStores.includes('edge'));
if (initialScripts) {
if (!validInitialScripts) {
delete backgroundData.scripts;
manifestChanges.push('Removed "background.scripts" as it is empty or malformed');
} else if (onlyUseServiceWorker) {
delete backgroundData.scripts;
manifestChanges.push('Removed "background.scripts" in favor of "background.service_worker". See https://crbug.com/1418934');
} else if (initialPage) {
delete backgroundData.scripts;
manifestChanges.push('Removed "background.scripts" in favor of "background.page".');
} else if (scriptsNotSupported) {
delete backgroundData.scripts;
manifestChanges.push('Removed "background.scripts" as it is not accepted in manifest v3 by the Edge store. See: https://github.com/microsoft/MicrosoftEdge-Extensions/issues/136');
}
}
if (!validInitialScripts && !scriptsNotSupported && initialServiceWorker && !initialPage) {
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".');
}
}
/**
* service_worker key
*/
if (initialServiceWorker && onlyUseDocument) {
delete backgroundData.service_worker;
manifestChanges.push('Removed "background.service_worker" in favor of "background.scripts" or "background.page".');
}
if (onlyUseServiceWorker && !initialServiceWorker) {
if (validInitialScripts && validInitialScripts.length === 1) {
backgroundData.service_worker = validInitialScripts[0];
manifestChanges.push('Added "background.service_worker" based on "background.scripts[0]".');
} else {
const fallbackPath = '/background.js';
manifestWarnings.push(`[p1] No usable "background.scripts" fallback found for "background.service_worker". For a proper fallback, "background.scripts" is required to include exactly one script file. As last resort, hope a service_worker is present under ${fallbackPath}`);
backgroundData.service_worker = fallbackPath;
manifestChanges.push(`Added "background.service_worker" set to ${fallbackPath}`);
}
}
/**
* persistent key
*/
// TODO confirm this, install tech preview on older macbook
// TODO implement this logic
// TODO Safari seems to allow persistent: true in mv2 alongside only service_worker. Does this have any effect?
const initialPersistent = backgroundData.persistent;
const finalResultHasServiceWorker = 'service_worker' in backgroundData;
if (initialDocumentEnvironment && initialPersistent !== false) {
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 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).');
}
}
if (targetVersion >= 2) {
if (targetVersion === 2 && browserMatchSome('Safari >= 14') && !browserMatchSome('iOS >= 15')) {
manifestInfo.push('Safari 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 handling.');
}
// SAFARI mv3 persistent true:
// Extensions with manifest_version equal to 3 must have a non-persistent background page. The persistent key will be ignored.
let persistentUnsupported = false;
if (targetVersion >= 3) {
persistentUnsupported = '"persistent" key in "background" removed since only the value false is supported, which is the default in mv3 and above.';
} else if (disallowOtherAlongsideServiceWorker && finalResultHasServiceWorker) {
// TODO should probably be removed in more situations
persistentUnsupported = '"persistent" key in "background" removed 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') || (!finalResultHasServiceWorker && 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 makePersistent = initialDocumentEnvironment && !initialServiceWorker;
backgroundData.persistent = makePersistent;
// edge requires this browserMatchSome('Edge >= 14 and Edge < 50')
manifestChanges.push(`"persistent" key in "background" set to ${makePersistent}. This improves compatibility with older Edge versions and makes sure the "background.service_worker" fallback works.`);
}
}
}
/**
* type key
*/
if ('type' in backgroundData) {
if (targetVersion >= 2 && 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
*/
const allEnvironments = [backgroundData.service_worker, backgroundData.scripts, backgroundData.page];
const environmentCount = allEnvironments.filter(Boolean).length;
if (initialPreferredEnvironment && getIsMalformedPreferredEnvironment(initialPreferredEnvironment)) {
manifestWarnings.push('The only acceptable environments are "service_worker" and "document"');
}
if (environmentCount === 0) {
manifestWarnings.push('[p1] No supported background environment is defined.');
} else if (environmentCount > 1) {
if (!initialPreferredEnvironment) {
if (initialDocumentEnvironment && initialServiceWorker) {
// do not create preferred_environment as the order can not be determined
manifestInfo.push('Both "service_worker" and "document" environments are included. Keep in mind "service_worker" is preferred if a browser supports extension service workers.');
} 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.');
}
}
} else if (initialPreferredEnvironment && !getIsMalformedPreferredEnvironment(initialPreferredEnvironment)) {
delete backgroundData.preferred_environment;
manifestChanges.push('Removed "background.preferred_environment" key as only one environment defined.');
}
/**
* background key
*/
if (!backgroundData.scripts && !backgroundData.page && !backgroundData.service_worker) {
delete manifestJson.background;
manifestChanges.push('Removed the "background" key as no valid background scripts are specified.');
} else if (!manifestJson.background) {
manifestChanges.push('Added the "background" key based on `background_page`.');
manifestJson.background = backgroundData;
}
}
function handleManifestVersion ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}) {
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, manifestErrors,
}) {
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, manifestErrors,
}) {
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) {
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;
return Object.keys(lightIconsObject).filter((key) => {
return darkIconsObject[key] && (key | 0);
}).map((key) => {
return {
dark: lightIconsObject[key],
light: darkIconsObject[key],
size: key | 0,
};
});
}
function handleManifestIconVariants ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}) {
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".');
}
}
function handleActionIcons ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}, 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 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);
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);
if (potentialThemeIcons) {
action.theme_icons = potentialThemeIcons;
manifestChanges.push(`Adding ${actionKey}.theme_icons based on "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.`);
}
}
function handleManifestAction ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}, 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('whale'); // 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;
/**
* app key
*/
if ('app' in manifestJson) {
manifestWarnings.push('[p2] Legacy packaged apps are currently not supported by this tool.');
manifestChanges.push('Removed "app" as packaged apps are not supported by this tool.');
delete manifestJson.app;
}
/**
* 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');
}
}
/**
* 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 {
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.');
}
}
}
/**
* action icons
*/
handleActionIcons({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}, 'action');
handleActionIcons({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}, 'browser_action');
}
function handleManifestActionCommands ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}) {
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_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_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_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, manifestErrors,
},
) {
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, manifestErrors,
},
) {
const workingCsp = transformAnyCsp(
'extension_pages',
originalCsp,
{
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
},
);
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 = workingCsp.toLowerCase().includes('frame-ancestors');
const addAboutProtocolToFrameAncestors = hasInlineOptions && isOlderFirefoxESR && hasFrameAncestors;
if (!removeSandboxForSafari && !addAboutProtocolToFrameAncestors) {
return workingCsp;
}
let cspChanged = false;
let finalCsp = workingCsp;
try {
finalCsp = workingCsp.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 workingCsp;
}
if (cspChanged) {
return finalCsp;
}
return workingCsp;
}
function handleManifestSandboxAndCsp ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}) {
const legacySandboxCsp = manifestJson.sandbox && manifestJson.sandbox.content_security_policy;
const definedCsp = manifestJson.content_security_policy;
// no CSP found
if (!definedCsp && !legacySandboxCsp) return;
let cspObject = definedCsp;
if (definedCsp && typeof definedCsp !== 'object') {
manifestWarnings.push('[p2] "content_security_policy" should be an object.');
cspObject = {
extension_pages: definedCsp,
};
manifestJson.content_security_policy = cspObject;
manifestChanges.push('Upgraded "content_security_policy" to object syntax.');
}
if (legacySandboxCsp) {
manifestWarnings.push('[p2] "content_security_policy" of "sandbox" should be under "content_security_policy.sandbox".');
if (!cspObject) {
cspObject = {};
manifestJson.content_security_policy = cspObject;
}
if (!cspObject.sandbox) {
cspObject.sandbox = legacySandboxCsp;
manifestChanges.push('Replacing "sandbox.content_security_policy" with "content_security_policy.sandbox".');
} else {
manifestWarnings.push('[p1] Both "sandbox.content_security_policy" and "content_security_policy.sandbox" are defined, please use just one of them.');
manifestChanges.push('Removing "sandbox.content_security_policy" in favor of "content_security_policy.sandbox".');
}
}
if (!cspObject) return;
const useLegacySyntax = targetVersion <= 2;
// todo in the future some browsers support the new syntax on manifest v2
if (useLegacySyntax) {
if (cspObject.extension_pages) {
manifestJson.content_security_policy = transformExtensionCsp(
cspObject.extension_pages,
{
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
},
);
manifestChanges.push('Replaced value of "content_security_policy" with "extension_pages".');
} else {
delete manifestJson.content_security_policy;
manifestChanges.push('Removed "content_security_policy" as no "extension_pages" is defined.');
}
if (manifestJson.sandbox && cspObject.sandbox) {
manifestJson.sandbox.content_security_policy = transformAnyCsp(
'sandbox',
cspObject.sandbox,
{
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
},
);
}
return;
}
// target manifest is version 3 or up
if (cspObject.extension_pages) {
cspObject.extension_pages = transformExtensionCsp(
cspObject.extension_pages,
{
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
},
);
}
if (cspObject.content_scripts) {
cspObject.content_scripts = transformAnyCsp(
'content_scripts',
cspObject.content_scripts,
{
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
},
);
}
if (cspObject.sandbox) {
cspObject.sandbox = transformAnyCsp(
'sandbox',
cspObject.sandbox,
{
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
},
);
}
}
function handleManifestWebAccessibleResources ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}) {
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, manifestErrors,
}) {
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 handleScriptingPermissions ({ manifestJson, manifestChanges, containerId }) {
const permissions = manifestJson[containerId];
if (!Array.isArray(permissions)) return;
if (!permissions.includes('scripting')) return;
permissions.splice(permissions.indexOf('scripting'), 1);
manifestChanges.push(`Removed scripting from "${containerId}" as Google considers it a violation in manifest v2`);
}
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] = finalPermissions;
}
delete manifestJson[sourceContainerId];
manifestChanges.push(`Moved "${sourceContainerId}" values to "${targetContainerId}".`);
}
function movePermissionsUpgrade ({
manifestJson, manifestChanges, sourceContainerId, targetContainerId,
}) {
const sourcePermissions = manifestJson[sourceContainerId];
if (!Array.isArray(sourcePermissions)) return;
const hostPermissions = sourcePermissions.filter(isHostPermission);
if (hostPermissions.length === 0) return;
const targetPermissions = manifestJson[targetContainerId];
const finalPermissions = targetPermissions || [];
for (const permission of hostPermissions) {
if (targetPermissions.includes(permission)) continue;
targetPermissions.push(permission);
}
const notHostPermissions = sourcePermissions.filter((permission) => {
return !isHostPermission(permission);
});
if (notHostPermissions.length === 0) {
delete manifestJson[sourceContainerId];
} else {
manifestJson[sourceContainerId] = notHostPermissions;
}
// assure existance of new key
if (!targetPermissions) {
manifestJson[targetContainerId] = finalPermissions;
}
manifestChanges.push(`Moved "${sourceContainerId}" values to "${targetContainerId}".`);
}
function handleManifestPermissions ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}) {
if (manifestJson.permissions) {
manifestJson.permissions.forEach((permission) => {
if (isHostPermission(permission)) {
manifestWarnings.push(`[p2] "permissions" include host match ${permission}. This should be moved to "host_permissions".`);
}
});
}
if (manifestJson.optional_permissions) {
manifestJson.optional_permissions.forEach((permission) => {
if (isHostPermission(permission)) {
manifestWarnings.push(`[p2] "optional_permissions" include host match ${permission}. This should be moved to "host_permissions" or "optional_host_permissions".`);
}
});
}
if (targetVersion <= 2) {
handleScriptingPermissions({
manifestJson,
manifestChanges,
containerId: 'permissions',
});
handleScriptingPermissions({
manifestJson,
manifestChanges,
containerId: 'optional_permissions',
});
movePermissions({
manifestJson,
manifestChanges,
sourceContainerId: 'host_permissions',
targetContainerId: 'permissions',
});
movePermissions({
manifestJson,
manifestChanges,
sourceContainerId: 'optional_host_permissions',
targetContainerId: 'optional_permissions',
});
return;
}
// target manifest is version 3 and up
movePermissionsUpgrade({
manifestJson,
manifestChanges,
sourceContainerId: 'permissions',
targetContainerId: 'host_permissions',
});
movePermissionsUpgrade({
manifestJson,
manifestChanges,
sourceContainerId: 'optional_permissions',
targetContainerId: 'optional_host_permissions',
});
}
function truncatePropertyValue (manifestJson, key, limit, manifestChanges) {
const originalValue = manifestJson[key];
if (typeof originalValue !== 'string') return;
// do not truncate if it includes i18n replacements
if (originalValue.includes('__MSG_')) return;
// only truncate if exceeding the limits
if (originalValue.length <= limit) return;
let newValue = originalValue.replace(/\s/g, '');
if (newValue.length > limit) {
newValue = originalValue.slice(0, limit);
}
manifestJson[key] = newValue;
manifestChanges.push(`Truncated "${key}": "${originalValue}" to "${newValue}".`);
}
function getNameLimit (supportedStores) {
if (supportedStores.includes('chrome')) return 75;
if (supportedStores.includes('firefox')) return 45;
return Infinity;
}
function getShortNameLimit (supportedStores) {
if (supportedStores.includes('opera')) return 12;
if (supportedStores.includes('chrome')) return 45;
if (supportedStores.includes('firefox')) return 45;
return Infinity;
}
function getDescriptionLimit (supportedStores) {
if (supportedStores.includes('safari')) return 112;
if (supportedStores.includes('chrome')) return 132;
return Infinity;
}
function handleManifestNameAndDescription ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}, supportedStores) {
/**
* name
*/
const extensionName = manifestJson.name;
if (!extensionName) {
manifestWarnings.push('[p1] Manifest is missing "name"');
}
if (supportedStores.includes('whale') && typeof extensionName === 'string' && extensionName.includes('™')) {
// remove tm symbol if found, as the store doesn't render it correctly
manifestJson.name = extensionName.replace(/™/g, '');
manifestChanges.push('Removed ™ symbol from name. See: https://forum.whale.naver.com/topic/39748/');
}
const nameLimit = getNameLimit(supportedStores);
truncatePropertyValue(manifestJson, 'name', nameLimit, manifestChanges);
/**
* short name
*/
const shortNameLimit = getShortNameLimit(supportedStores);
truncatePropertyValue(manifestJson, 'short_name', shortNameLimit, manifestChanges);
/**
* description
*/
if (!manifestJson.description) {
manifestWarnings.push('[p1] Manifest is missing "description"');
}
const descriptionLimit = getDescriptionLimit(supportedStores);
truncatePropertyValue(manifestJson, 'description', descriptionLimit, manifestChanges);
}
function handleManifestMinimumVersion ({
manifestJson, targetVersion, manifestChanges, manifestInfo, manifestWarnings, manifestErrors,
}, supportedStores) {
if (!supportedStores.includes('chrome')) return;
const manifestVersion = manifestJson.manifest_version;
if (targetVersion < 3) return;
const minimumChromeVersion = manifestJson.minimum_chrome_version;
if (!minimumChromeVersion) return;
const mainVersionNumber = minimumChromeVersion | 0;
if (mainVersionNumber > 87) return;
packageChanges.push('Removed minimum_chrome_version property as it requires to be at least 88 with manifest v3.');
delete m