#!/bin/bash
set -euo pipefail
# Copyright (c) 2011 Zotero
# Center for History and New Media
# George Mason University, Fairfax, Virginia, USA
# http://zotero.org
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
. "$APP_ROOT_DIR/config.sh"
. "$SCRIPT_DIR/utils.sh"
cd "$APP_ROOT_DIR"
usage() {
cat >&2 <<'DONE'
Usage: fetch_xulrunner.sh -p [-a ARCH]
Required
-p PLATFORM m (Mac), w (Windows), l (Linux)
Optional / Conditional
-a ARCH - Mac: disallowed (builds are universal)
- Windows: x64 | arm64 | win32
- Linux: x64 (or x86_64) | arm64
If omitted on Windows/Linux, all standard archs are fetched.
DONE
exit 1
}
BUILD_MAC=0
BUILD_WIN=0
BUILD_LINUX=0
arch=""
while getopts 'p:a:' opt; do
case $opt in
p)
for (( i=0; i<${#OPTARG}; i++ )); do
case ${OPTARG:i:1} in
m) BUILD_MAC=1;;
w) BUILD_WIN=1;;
l) BUILD_LINUX=1;;
*) echo "Invalid platform: ${OPTARG:i:1}"; echo; usage;;
esac
done;;
a) arch="$OPTARG";;
*) usage;;
esac
shift $((OPTIND-1)); OPTIND=1
done
# Require at least one platform
if [[ $BUILD_MAC == 0 ]] && [[ $BUILD_WIN == 0 ]] && [[ $BUILD_LINUX == 0 ]]; then
usage
fi
# Sanity‑check arch flag vs platform selections
if [[ $BUILD_MAC == 1 ]]; then
[[ -n $arch ]] && { echo "-a is not allowed for Mac" >&2; echo; usage; }
fi
if [[ $BUILD_WIN == 1 || $BUILD_LINUX == 1 ]]; then
# arch optional; if provided validate & map aliases now
if [[ -n $arch ]]; then
if [[ $BUILD_WIN == 1 && $BUILD_LINUX == 1 ]]; then
echo "-a cannot be used when building both Windows and Linux simultaneously" >&2; echo; usage
fi
if [[ $BUILD_WIN == 1 ]]; then
case $arch in
x64) arch="win-x64" ;;
arm64) arch="win-arm64" ;;
win32|win-x64|win-arm64) ;;
*) echo "Invalid Windows arch: $arch" >&2; echo; usage;;
esac
fi
if [[ $BUILD_LINUX == 1 ]]; then
[[ $arch == x64 ]] && arch="x86_64"
case $arch in x86_64|arm64) ;; *) echo "Invalid Linux arch: $arch" >&2; echo; usage;; esac
fi
fi
fi
function get_utf16_chars {
str=$(echo -n "$1" | xxd -p | fold -w 2 | sed -r 's/(.+)/\\\\x{\1}\\\\x{00}/')
# Add NUL padding
if [ -n "${2:-}" ]; then
# Multiply characters x 2 for UTF-16
for i in `seq 1 $(($2 * 2))`; do
str+=$(echo '\\x{00}')
done
fi
echo $str | xargs | sed 's/ //g'
}
#
# Make various modifications to the stock Firefox app
#
function modify_omni {
platform=$1
mkdir omni
mv omni.ja omni
cd omni
# omni.ja is an "optimized" ZIP file, so use a script from Mozilla to avoid a warning from unzip
# here and to make it work after rezipping below
python3 "$APP_ROOT_DIR/scripts/optimizejars.py" --deoptimize ./ ./ ./
rm -f omni.ja.log
unzip omni.ja
rm omni.ja
rm -r chrome/pdfjs
rm -r modules/narrate
#rm -r modules/translation
rm actors/AboutTranslations{Parent,Child}.sys.mjs
rm actors/CookieBanner{Parent,Child}.sys.mjs
rm actors/PictureInPictureChild.sys.mjs
rm actors/ThumbnailsChild.sys.mjs
rm actors/Translations{Engine,}{Parent,Child}.sys.mjs
rm actors/AudioPlayback{Parent,Child}.sys.mjs
replace_line 'BROWSER_CHROME_URL:.+' 'BROWSER_CHROME_URL: "chrome:\/\/zotero\/content\/zoteroPane.xhtml",' modules/AppConstants.sys.mjs
# Used by OSKeyStore as the master-key label, visible in macOS Keychain Access.
# Verify that OSKeyStore still derives the label from MOZ_APP_BASENAME, so a
# future Mozilla change to a hardcoded string doesn't silently rebrand the
# keychain entry back to "Firefox Encrypted Storage".
replace_line 'MOZ_APP_BASENAME: "Firefox"' 'MOZ_APP_BASENAME: "Zotero"' modules/AppConstants.sys.mjs
check_line 'STORE_LABEL: AppConstants\.MOZ_APP_BASENAME \+ " Encrypted Storage"' modules/OSKeyStore.sys.mjs
# https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html
#
# It's not clear that most of these do anything anymore when not compiled in, but just in case
replace_line 'MOZ_REQUIRE_SIGNING:' 'MOZ_REQUIRE_SIGNING: false \&\&' modules/AppConstants.sys.mjs
replace_line 'MOZ_DATA_REPORTING:' 'MOZ_DATA_REPORTING: false \&\&' modules/AppConstants.sys.mjs
replace_line 'MOZ_TELEMETRY_REPORTING:' 'MOZ_TELEMETRY_REPORTING: false \&\&' modules/AppConstants.sys.mjs
replace_line 'MOZ_CRASHREPORTER:' 'MOZ_CRASHREPORTER: false \&\&' modules/AppConstants.sys.mjs
replace_line 'MOZ_UPDATE_CHANNEL:.+' 'MOZ_UPDATE_CHANNEL: "none",' modules/AppConstants.sys.mjs
replace_line '"https:\/\/[^\/]+mozilla.com.+"' '""' modules/AppConstants.sys.mjs
# Don't use Mozilla Maintenance Service on Windows
replace_line 'MOZ_MAINTENANCE_SERVICE:' 'MOZ_MAINTENANCE_SERVICE: false \&\&' modules/AppConstants.sys.mjs
# Continue using app.update.auto in prefs.js on Windows
replace_line 'PER_INSTALLATION_PREFS_PLATFORMS = \["win"\]' 'PER_INSTALLATION_PREFS_PLATFORMS = []' modules/UpdateUtils.sys.mjs
# Prompt if major update is available instead of installing automatically on restart
replace_line 'if \(!updateAuto\) \{' 'if (update.type == "major") {
LOG("UpdateService:_selectAndInstallUpdate - prompting because it is a major update");
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF);
Services.obs.notifyObservers(update, "update-available", "show-prompt");
return;
}
if (!updateAuto) {' modules/UpdateService.sys.mjs
# Avoid console warning about resource://gre/modules/FxAccountsCommon.js
replace_line 'const logins = this._data.logins;' 'const logins = this._data.logins; if (this._data.logins.length != -1) return;' modules/LoginStore.sys.mjs
# Prevent error during network requests
replace_line 'async lazyInit\(\) \{' 'async lazyInit() { if (this.features) return false;' modules/UrlClassifierExceptionListService.sys.mjs
# Silence CrashManager "Could not get children of [...]/Crash Reports/events" spam
replace_line '\/\/ Missing events folders are allowed' '\/\/ Missing events folders are allowed (silence warning)
if (ex) return entries;' modules/CrashManager.sys.mjs
# Several services pull in services-settings (which we strip) and throw
# at profile-after-change. Remove their #init() methods to prevent errors.
replace_line 'async #init\(\) \{' 'async #init() {} async _unused_init() {' modules/EssentialDomainsRemoteSettings.sys.mjs
replace_line 'async #init\(\) \{' 'async #init() {} async _unused_init() {' modules/SimpleURIUnknownSchemesRemoteObserver.sys.mjs
# RemotePermissionService uses a top-level import, so even loading the
# module throws
replace_line 'import \{ RemoteSettings \} from "resource:\/\/services-settings\/remote-settings.sys.mjs";' \
'const RemoteSettings = () => ({ on() {}, get: async () => [] });' \
modules/RemotePermissionService.sys.mjs
replace_line 'pref\("network.captive-portal-service.enabled".+' 'pref("network.captive-portal-service.enabled", false);' greprefs.js
replace_line 'pref\("network.connectivity-service.enabled".+' 'pref("network.connectivity-service.enabled", false);' greprefs.js
replace_line 'pref\("toolkit.telemetry.server".+' 'pref("toolkit.telemetry.server", "");' greprefs.js
replace_line 'pref\("toolkit.telemetry.unified".+' 'pref("toolkit.telemetry.unified", false);' greprefs.js
replace_line 'pref\("media.gmp-manager.url".+' 'pref("media.gmp-manager.url", "");' greprefs.js
#
# # Disable transaction timeout
# perl -pi -e 's/let timeoutPromise/\/*let timeoutPromise/' modules/Sqlite.jsm
# perl -pi -e 's/return Promise.race\(\[transactionPromise, timeoutPromise\]\);/*\/return transactionPromise;/' modules/Sqlite.jsm
# rm -f jsloader/resource/gre/modules/Sqlite.jsm
#
# Disable unwanted components
remove_line '(RemoteSettings|services-|telemetry|Telemetry|URLDecorationAnnotationsService)' components/components.manifest
file="modules/ActorManagerParent.sys.mjs"
# Remove deleted actors
# Entries in the JSWINDOWACTORS/JSPROCESSACTORS literals:
remove_between '^ AudioPlayback: \{' '^ },' $file
remove_between '^ CookieBanner: \{' '^ },' $file
remove_between '^ Thumbnails: \{' '^ },' $file
remove_between '^ Translations: \{' '^ },' $file
remove_between '^ TranslationsEngine: \{' '^ },' $file
# Entries added conditionally after the literals:
remove_between '^ JSWINDOWACTORS\.AboutTranslations = \{' '^ \};' $file
remove_between '^ JSWINDOWACTORS\.PictureInPictureLauncher = \{' '^ \};' $file
remove_between '^ JSWINDOWACTORS\.PictureInPictureToggle = \{' '^ \};' $file
remove_between '^ JSWINDOWACTORS\.PictureInPicture = \{' '^ \};' $file
# Do not trigger LoginManager event that logs an error on autocomplete submission
remove_line 'DOMInputPasswordAdded: \{\},' $file
# On Mac/Linux, ignore relative paths in PATH in Subprocess.pathSearch(), used when a bare command is passed to Utilities.Internal.subprocess()
if [[ $platform != win* ]]; then
replace_line 'let path = PathUtils.join\(dir, bin\);' 'if (!dir.startsWith("\/")) continue; let path = PathUtils.join(dir, bin);' modules/subprocess/subprocess_unix.sys.mjs
fi
# Remove unwanted files
pushd modules
rm Bookmarks*
rm FxAccounts*
echo '' > FingerprintingWebCompatService.sys.mjs
echo '' > PartitioningExceptionListService.sys.mjs
rm Places*
echo '' > PlacesExpiration.sys.mjs
echo '' > PlacesFrecencyRecalculator.sys.mjs
rm -rf services-settings/*
touch services-settings/remote-settings.sys.mjs
# Causes a startup error -- try an empty file or a shim instead?
#rm Telemetry*
rm URLDecorationAnnotationsService.sys.mjs
popd
pushd chrome/toolkit/content/mozapps/extensions
rm -rf default-theme
popd
# This file loads remote-settings.sys.mjs, but if it's empty Zotero crashes
replace_line 'async #init\(\) \{' 'async #init() { if (true) { return; }' modules/URLQueryStrippingListService.sys.mjs
# Clear most WebExtension manifest properties
replace_line 'manifest = normalized.value;' 'manifest = normalized.value;
if (this.type == "extension") {
if (!manifest.applications?.zotero?.id) {
this.manifestError("applications.zotero.id not provided");
}
if (!manifest.applications?.zotero?.update_url) {
this.manifestError("applications.zotero.update_url not provided");
}
if (!manifest.applications?.zotero?.strict_max_version) {
this.manifestError("applications.zotero.strict_max_version not provided");
}
manifest.browser_specific_settings = undefined;
manifest.content_scripts = [];
manifest.permissions = [];
manifest.host_permissions = [];
manifest.web_accessible_resources = undefined;
manifest.experiment_apis = {};
}' modules/Extension.sys.mjs
# Use applications.zotero instead of applications.gecko
replace_line 'let bss = manifest.applications\?.gecko' 'let bss = manifest.applications?.zotero' modules/addons/XPIInstall.sys.mjs
replace_line 'manifest.applications\?.gecko' 'manifest.applications?.zotero' modules/Extension.sys.mjs
# When installing addon, use app version instead of toolkit version for targetApplication
replace_line "id: TOOLKIT_ID," "id: '$APP_ID'," modules/addons/XPIInstall.sys.mjs
# Set strictCompatibility to false in beta/dev/source builds to skip strict_max_version check
replace_line 'addon.strictCompatibility = true;' \
'let version = Services.appinfo.version;
addon.strictCompatibility = !version.includes("-beta") && !version.includes("-dev") && !version.includes("SOURCE");' modules/addons/XPIInstall.sys.mjs
# In beta/dev/source builds, ignore the persisted strictCompatibility flag for already-installed
# plugins, so plugins installed on a stable build (where the flag was set to true) aren't marked
# incompatible after upgrading. The manifest isn't re-parsed on app upgrade, so the flag set at
# install time sticks around.
replace_line '!this\.addon\.strictCompatibility' \
'(!this.addon.strictCompatibility || Services.appinfo.version.includes("-beta") || Services.appinfo.version.includes("-dev") || Services.appinfo.version.includes("SOURCE"))' modules/addons/XPIInstall.sys.mjs
replace_line '!this\.strictCompatibility &&' \
'(!this.strictCompatibility || Services.appinfo.version.includes("-beta") || Services.appinfo.version.includes("-dev") || Services.appinfo.version.includes("SOURCE")) &&' modules/addons/XPIDatabase.sys.mjs
# Accept zotero@chnm.gmu.edu for target application to allow Zotero 6 plugins to remain
# installed in Zotero 7
replace_line "if \(targetApp.id == Services.appinfo.ID\) \{" "if (targetApp.id == 'zotero\@chnm.gmu.edu') targetApp.id = '$APP_ID'; if (targetApp.id == Services.appinfo.ID) {" modules/addons/XPIDatabase.sys.mjs
# TEMP: Skip addons without ids until we figure out what's going on with the default theme.
# This fixes plugin updating and other things.
replace_line 'return addons;' 'return addons.filter(addon => addon.id);' modules/addons/XPIDatabase.sys.mjs
# For updates, look for applications.zotero instead of applications.gecko in manifest.json and
# use the app id and version for strict_min_version/strict_max_version comparisons
replace_line 'gecko: \{\},' 'zotero: {},' modules/addons/AddonUpdateChecker.sys.mjs
replace_line 'if \(!\("gecko" in applications\)\) \{' 'if (!("zotero" in applications)) {' modules/addons/AddonUpdateChecker.sys.mjs
replace_line '"gecko not in application entry' '"zotero not in application entry' modules/addons/AddonUpdateChecker.sys.mjs
replace_line 'let app = getProperty\(applications, "gecko", "object"\);' 'let app = getProperty(applications, "zotero", "object");' modules/addons/AddonUpdateChecker.sys.mjs
replace_line "id: TOOLKIT_ID," "id: '$APP_ID'," modules/addons/AddonUpdateChecker.sys.mjs
replace_line 'lazy.AddonManagerPrivate.webExtensionsMinPlatformVersion' '"7.0"' modules/addons/AddonUpdateChecker.sys.mjs
replace_line 'result.targetApplications.push' 'false && result.targetApplications.push' modules/addons/AddonUpdateChecker.sys.mjs
# In beta/dev/source builds, don't set strictCompatibility on parsed update entries so that
# AddonUpdateChecker doesn't reject an available update whose maxVersion is lower than the
# current (non-stable) app version.
replace_line 'result\.strictCompatibility = appEntry\.maxVersion != "\*";' \
'let appVer = Services.appinfo.version; result.strictCompatibility = appEntry.maxVersion != "*" && !appVer.includes("-beta") && !appVer.includes("-dev") && !appVer.includes("SOURCE");' modules/addons/AddonUpdateChecker.sys.mjs
# Set uninstall flag to true if the update config has it set
replace_line 'targetApplications: \[appEntry\],' \
'targetApplications: \[appEntry\],
uninstall: getProperty(update, "uninstall", "boolean"),' modules/addons/AddonUpdateChecker.sys.mjs
# Prevent automatic installation of default theme
# (chrome/toolkit/content/mozapps/extensions/default-theme, removed above)
replace_line 'this.maybeInstallBuiltinAddon\(' 'if (false) this.maybeInstallBuiltinAddon(' modules/addons/XPIProvider.sys.mjs
# Don't log a warning when checking 'distribution/extensions' for addons
replace_line 'logger.warn\(`Checking' 'logger.debug(`Checking' modules/addons/XPIProvider.sys.mjs
# Allow addon installation by bypassing confirmation dialogs. If we want a confirmation dialog,
# we need to either add gXPInstallObserver from browser-addons.js [1][2] or provide our own with
# Ci.amIWebInstallPrompt [3].
#
# [1] https://searchfox.org/mozilla-esr102/rev/5a6d529652045050c5cdedc0558238949b113741/browser/base/content/browser.js#1902-1923
# [2] https://searchfox.org/mozilla-esr102/rev/5a6d529652045050c5cdedc0558238949b113741/browser/base/content/browser-addons.js#201
# [3] https://searchfox.org/mozilla-esr102/rev/5a6d529652045050c5cdedc0558238949b113741/toolkit/mozapps/extensions/AddonManager.sys.mjs#3114-3124
replace_line 'if \(info.addon.installPermissions\) \{' 'if (false) {' modules/AddonManager.sys.mjs
replace_line '\} else if \(info.addon.sitePermissions\) \{' '} else if (false) {' modules/AddonManager.sys.mjs
replace_line '\} else if \(requireConfirm\) \{' '} else if (false) {' modules/AddonManager.sys.mjs
# Make addon listener methods wait for promises, to allow calling asynchronous plugin `shutdown`
# and `uninstall` methods in our `onInstalling` handler
replace_line 'callAddonListeners\(aMethod' 'async callAddonListeners(aMethod' modules/AddonManager.sys.mjs
# Don't need this one to be async, but we can't easily avoid modifying its `listener[aMethod].apply()` call
replace_line 'callManagerListeners\(aMethod' 'async callManagerListeners(aMethod' modules/AddonManager.sys.mjs
replace_line 'AddonManagerInternal.callAddonListeners.apply\(AddonManagerInternal, aArgs\);' \
'return AddonManagerInternal.callAddonListeners.apply(AddonManagerInternal, aArgs);' modules/AddonManager.sys.mjs
replace_line 'listener\[aMethod\].apply\(listener, aArgs\);' \
'let maybePromise = listener[aMethod].apply(listener, aArgs);
if (maybePromise && maybePromise.then) await maybePromise;' modules/AddonManager.sys.mjs
replace_line 'AddonManagerPrivate.callAddonListeners' 'await AddonManagerPrivate.callAddonListeners' modules/addons/XPIInstall.sys.mjs
replace_line 'let uninstall = \(\) => \{' 'let uninstall = async () => {' modules/addons/XPIInstall.sys.mjs
replace_line 'cancelUninstallAddon\(aAddon\)' 'async cancelUninstallAddon(aAddon)' modules/addons/XPIInstall.sys.mjs
# Check for `uninstall` flag in update config and uninstall the addon if set
replace_line 'if \(update && !this.addon.location.locked\) \{' \
'if (update && !this.addon.location.locked) {
if (update.uninstall) {
let aId = this.addon.id;
logger.debug(`AddonUpdateChecker: uninstall addon \${aId} with uninstall flag`);
let addon = await AddonManager.getAddonByID(aId);
if (addon && (addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
try {
addon.uninstall();
} catch (err) {
logger.error(`AddonUpdateChecker: error uninstalling addon \${aId} with uninstall flag`, err);
}
sendUpdateAvailableMessages(this, null);
return;
}
else {
logger.error(`AddonUpdateChecker: addon \${aId} with uninstall flag not found or cannot be uninstalled`);
sendUpdateAvailableMessages(this, null);
return;
}
}
' modules/addons/XPIInstall.sys.mjs
# Prevent login manager from trying to load FxAccountsCommon.sys.mjs
replace_line 'return login.hostname == lazy.FXA_PWDMGR_HOST;' 'return false;' modules/storage-json.sys.mjs
# No idea why this is necessary, but without it initialization fails with "TypeError: "constructor" is read-only"
replace_line 'LoginStore.prototype.constructor = LoginStore;' '\/\/LoginStore.prototype.constructor = LoginStore;' modules/LoginStore.sys.mjs
#
# # Allow proxy password saving
# perl -pi -e 's/get _inPrivateBrowsing\(\) \{/get _inPrivateBrowsing() {if (true) { return false; }/' components/nsLoginManagerPrompter.js
#
# # Change text in update dialog
# perl -pi -e 's/A security and stability update for/A new version of/' chrome/en-US/locale/en-US/mozapps/update/updates.properties
# perl -pi -e 's/updateType_major=New Version/updateType_major=New Major Version/' chrome/en-US/locale/en-US/mozapps/update/updates.properties
# perl -pi -e 's/updateType_minor=Security Update/updateType_minor=New Version/' chrome/en-US/locale/en-US/mozapps/update/updates.properties
# perl -pi -e 's/update for &brandShortName; as soon as possible/update as soon as possible/' chrome/en-US/locale/en-US/mozapps/update/updates.dtd
#
# Set available locales
cp "$APP_ROOT_DIR/assets/multilocale.txt" res/multilocale.txt
# Use Zotero URL opening in Mozilla dialogs (e.g., app update dialog)
replace_line 'function openURL\(aURL\) \{' 'function openURL(aURL) {let {Zotero} = ChromeUtils.importESModule("chrome:\/\/zotero\/content\/zotero.mjs"); Zotero.launchURL(aURL); if (true) { return; }' chrome/toolkit/content/global/contentAreaUtils.js
#
# Modify Add-ons window
#
# Prevent display: block from overriding display: none on s
replace_line 'display: block !important;' 'display: block;' chrome/toolkit/content/global/elements/panel-list.css
file="chrome/toolkit/content/mozapps/extensions/aboutaddons.css"
echo >> $file
# Hide the search bar and the whole sidebar, since we only ever show plugins and the main pane
# is already headed "Manage Your Plugins"
echo '.main-search, #sidebar { display: none; }' >> $file
# Center the content in the window now that it isn't offset by the sidebar
echo '#full { grid-template-columns: 1fr; }' >> $file
echo '#content { max-width: calc(var(--page-main-content-width) + var(--main-margin-start)); margin-inline: auto; }' >> $file
echo '.main-heading { margin-top: 2em; }' >> $file
# Hide Details/Permissions tabs in addon details so we only show details
echo 'addon-details > button-group { display: none !important; }' >> $file
# Hide "Run on sites with restrictions" row and helper message
echo '.addon-detail-row-quarantined-domains { display: none; }' >> $file
echo '.addon-detail-help-row:has([data-l10n-id="addon-detail-quarantined-domains-help"]) { display: none; }' >> $file
# Hide "Report" button from plugin menu
echo 'addon-card addon-options hr:first-of-type { display: none }' >> $file
echo '[data-l10n-id="report-addon-button"] { display: none }' >> $file
# Hide "Debug Addons" and "Manage Extension Shortcuts"
echo 'panel-item[action="debug-addons"], panel-item[action="reset-update-states"] + hr, panel-item[action="manage-shortcuts"] { display: none }' >> $file
# Show cursor feedback on plugin homepage links
echo '.addon-detail-row-homepage .text-link { cursor: pointer; color: LinkText; }' >> $file
echo '.addon-detail-row-homepage .text-link:hover { text-decoration: underline; }' >> $file
file="chrome/toolkit/content/mozapps/extensions/aboutaddons-utils.mjs"
# Hide unsigned-addon warning
replace_line 'export function isUnsignedWarningMessageDisabled\(\) \{' \
'export function isUnsignedWarningMessageDisabled() {if (true) return true;' $file
# Use our own localized string for blocked plugin notification
replace_line 'details-notification-hard-blocked-\$\{typeSuffix\}' 'plugins-blocked-plugin' $file
# Hide Recommendations tab in sidebar and recommendations in main pane
replace_line 'function isDiscoverEnabled\(\) \{' 'function isDiscoverEnabled() {return false;' $file
file="chrome/toolkit/content/mozapps/extensions/components/addon-details.mjs"
# Hide Private Browsing setting in addon details
replace_line 'pbRow\.' '\/\/pbRow.' $file
replace_line 'let isAllowed = await isAllowedInPrivateBrowsing' '\/\/let isAllowed = await isAllowedInPrivateBrowsing' $file
# Open plugin links in external browser
replace_line 'let homepageURL = homepageRow.querySelector\(\"a\"\);' 'let homepageURL = homepageRow.querySelector(\"\.text-link\");' $file
replace_line 'homepageURL.href = addon.homepageURL;' 'homepageURL.setAttribute("href", addon.homepageURL);' $file
replace_line '<\/a>' \
'