#!/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" cd "$APP_ROOT_DIR" function usage { cat >&2 <&1 exit 1 fi } function remove_line { pattern=$1 file=$2 if egrep -q "$pattern" "$file"; then egrep -v "$pattern" "$file" > "$file.tmp" mv "$file.tmp" "$file" else echo "$pattern" not found in "$infile" -- aborting 2>&1 exit 1 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 { 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 replace_line 'BROWSER_CHROME_URL:.+' 'BROWSER_CHROME_URL: "chrome:\/\/zotero\/content\/zoteroPane.xhtml",' modules/AppConstants.jsm # 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.jsm replace_line 'MOZ_DATA_REPORTING:' 'MOZ_DATA_REPORTING: false \&\&' modules/AppConstants.jsm replace_line 'MOZ_SERVICES_HEALTHREPORT:' 'MOZ_SERVICES_HEALTHREPORT: false \&\&' modules/AppConstants.jsm replace_line 'MOZ_TELEMETRY_REPORTING:' 'MOZ_TELEMETRY_REPORTING: false \&\&' modules/AppConstants.jsm replace_line 'MOZ_TELEMETRY_ON_BY_DEFAULT:' 'MOZ_TELEMETRY_ON_BY_DEFAULT: false \&\&' modules/AppConstants.jsm replace_line 'MOZ_CRASHREPORTER:' 'MOZ_CRASHREPORTER: false \&\&' modules/AppConstants.jsm replace_line 'MOZ_UPDATE_CHANNEL:.+' 'MOZ_UPDATE_CHANNEL: "none",' modules/AppConstants.jsm replace_line '"https:\/\/[^\/]+mozilla.com.+"' '""' modules/AppConstants.jsm # Don't use Mozilla Maintenance Service on Windows replace_line 'MOZ_MAINTENANCE_SERVICE:' 'MOZ_MAINTENANCE_SERVICE: false \&\&' modules/AppConstants.jsm # 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.jsm # 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.jsm # Prevent error during network requests replace_line 'async lazyInit\(\) \{' 'async lazyInit() { if (this.features) return false;' modules/UrlClassifierExceptionListService.jsm 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 # Remove unwanted files rm modules/FxAccounts* # Causes a startup error -- try an empty file or a shim instead? #rm modules/Telemetry* rm modules/URLDecorationAnnotationsService.jsm rm -rf modules/services-* # 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.jsm # Use applications.zotero instead of applications.gecko replace_line 'let bss = manifest.applications\?.gecko' 'let bss = manifest.applications?.zotero' modules/addons/XPIInstall.jsm replace_line 'manifest.applications\?.gecko' 'manifest.applications?.zotero' modules/Extension.jsm # When installing addon, use app version instead of toolkit version for targetApplication replace_line "id: TOOLKIT_ID," "id: '$APP_ID'," modules/addons/XPIInstall.jsm # 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.jsm # 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.jsm replace_line 'if \(!\("gecko" in applications\)\) \{' 'if (!("zotero" in applications)) {' modules/addons/AddonUpdateChecker.jsm replace_line '"gecko not in application entry' '"zotero not in application entry' modules/addons/AddonUpdateChecker.jsm replace_line 'let app = getProperty\(applications, "gecko", "object"\);' 'let app = getProperty(applications, "zotero", "object");' modules/addons/AddonUpdateChecker.jsm replace_line "id: TOOLKIT_ID," "id: '$APP_ID'," modules/addons/AddonUpdateChecker.jsm replace_line 'AddonManagerPrivate.webExtensionsMinPlatformVersion' '7.0' modules/addons/AddonUpdateChecker.jsm replace_line 'result.targetApplications.push' 'false && result.targetApplications.push' modules/addons/AddonUpdateChecker.jsm # 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.jsm#3114-3124 replace_line 'if \(info.addon.userPermissions\) \{' 'if (false) {' modules/AddonManager.jsm replace_line '\} else if \(info.addon.sitePermissions\) \{' '} else if (false) {' modules/AddonManager.jsm replace_line '\} else if \(requireConfirm\) \{' '} else if (false) {' modules/AddonManager.jsm # 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.jsm # 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.jsm replace_line 'AddonManagerInternal.callAddonListeners.apply\(AddonManagerInternal, aArgs\);' \ 'return AddonManagerInternal.callAddonListeners.apply(AddonManagerInternal, aArgs);' modules/AddonManager.jsm replace_line 'listener\[aMethod\].apply\(listener, aArgs\);' \ 'let maybePromise = listener[aMethod].apply(listener, aArgs); if (maybePromise && maybePromise.then) await maybePromise;' modules/AddonManager.jsm replace_line 'AddonManagerPrivate.callAddonListeners' 'await AddonManagerPrivate.callAddonListeners' modules/addons/XPIInstall.jsm replace_line 'let uninstall = \(\) => \{' 'let uninstall = async () => {' modules/addons/XPIInstall.jsm replace_line 'cancelUninstallAddon\(aAddon\)' 'async cancelUninstallAddon(aAddon)' modules/addons/XPIInstall.jsm # 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.jsm # # # 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.import("chrome:\/\/zotero\/content\/include.jsm"); Zotero.launchURL(aURL); if (true) { return; }' chrome/toolkit/content/global/contentAreaUtils.js # # Modify Add-ons window # file="chrome/toolkit/content/mozapps/extensions/aboutaddons.css" echo >> $file # Hide search bar, Themes and Plugins tabs, and sidebar footer echo '.main-search, button[name="theme"], button[name="plugin"], sidebar-footer { display: none; }' >> $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 "Debug Addons" and "Manage Extension Shortcuts" echo 'panel-item[action="debug-addons"], panel-item[action="reset-update-states"] + panel-item-separator, panel-item[action="manage-shortcuts"] { display: none }' >> $file file="chrome/toolkit/content/mozapps/extensions/aboutaddons.js" # Hide unsigned-addon warning replace_line 'if \(!isCorrectlySigned\(addon\)\) \{' 'if (!isCorrectlySigned(addon)) {return {};' $file # Hide Private Browsing setting in addon details replace_line 'pbRow\.' '\/\/pbRow.' $file replace_line 'let isAllowed = await isAllowedInPrivateBrowsing' '\/\/let isAllowed = await isAllowedInPrivateBrowsing' $file # Use our own strings for the removal prompt replace_line 'let \{ BrowserAddonUI \} = windowRoot.ownerGlobal;' '' $file replace_line 'await BrowserAddonUI.promptRemoveExtension' 'promptRemoveExtension' $file # Customize empty-list message replace_line 'createEmptyListMessage\(\) {' 'createEmptyListMessage() { var p = document.createElement("p"); p.id = "empty-list-message"; return p;' $file # Swap in include.js, which we need for Zotero.getString(), for abuse-reports.js, which we don't need # Hide Recommendations tab in sidebar and recommendations in main pane replace_line 'function isDiscoverEnabled\(\) \{' 'function isDiscoverEnabled() {return false;' chrome/toolkit/content/mozapps/extensions/aboutaddonsCommon.js replace_line 'pref\("extensions.htmlaboutaddons.recommendations.enabled".+' 'pref("extensions.htmlaboutaddons.recommendations.enabled", false);' greprefs.js # Hide Report option replace_line 'pref\("extensions.abuseReport.enabled".+' 'pref("extensions.abuseReport.enabled", false);' greprefs.js # The first displayed Services.prompt dialog's size jumps around because sizeToContent() is called twice # Fix by preventing the first sizeToContent() call if the icon hasn't been loaded yet replace_line 'window.sizeToContent\(\);' 'if (ui.infoIcon.complete) window.sizeToContent();' chrome/toolkit/content/global/commonDialog.js replace_line 'ui.infoIcon.addEventListener' 'if (!ui.infoIcon.complete) ui.infoIcon.addEventListener' chrome/toolkit/content/global/commonDialog.js # Use native checkbox instead of Firefox-themed version in prompt dialogs replace_line ' CE appends its autoscroll to document.documentElement, which doesn't work in zoteroPane.xhtml # (in Firefox browser.xhtml, document.documentElement is , which can contain s; in zoteroPane.xhtml, it's # , which can't) # Fix by appending it to a instead replace_line 'document.documentElement.appendChild\(this._autoScrollPopup\);' 'let popupset = document.querySelector("popupset"); if (!popupset) { popupset = document.createXULElement("popupset"); document.documentElement.appendChild(popupset); } popupset.appendChild(this._autoScrollPopup);' chrome/toolkit/content/global/elements/browser-custom-element.js zip -qr9XD omni.ja * mv omni.ja .. cd .. python3 "$APP_ROOT_DIR/scripts/optimizejars.py" --optimize ./ ./ ./ rm -rf omni # Unzip browser/omni.ja and leave unzipped cd browser mkdir omni mv omni.ja omni cd omni ls -la set +e unzip omni.ja set -e rm omni.ja # Remove Firefox update URLs remove_line 'pref\("app.update.url.(manual|details)' defaults/preferences/firefox-branding.js # Remove Firefox overrides (e.g., to use Firefox-specific strings for connection errors) remove_line '(override)' chrome/chrome.manifest # Remove WebExtension APIs remove_line ext-browser.json components/components.manifest # Don't open a second window if app is already open when launching .exe (Windows) or running via # command line (macOS) replace_line 'function dch_handle\(cmdLine\) \{' 'function dch_handle(cmdLine) { if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) { return; } ' modules/BrowserContentHandler.jsm } mkdir -p xulrunner cd xulrunner if [ $BUILD_MAC == 1 ]; then GECKO_VERSION="$GECKO_VERSION_MAC" DOWNLOAD_URL="https://ftp.mozilla.org/pub/firefox/releases/$GECKO_VERSION" rm -rf Firefox.app if [ -e "Firefox $GECKO_VERSION.app.zip" ]; then echo "Using Firefox $GECKO_VERSION.app.zip" unzip "Firefox $GECKO_VERSION.app.zip" else curl -o Firefox.dmg "$DOWNLOAD_URL/mac/en-US/Firefox%20$GECKO_VERSION.dmg" set +e hdiutil detach -quiet /Volumes/Firefox 2>/dev/null set -e hdiutil attach -quiet Firefox.dmg cp -a /Volumes/Firefox/Firefox.app . hdiutil detach -quiet /Volumes/Firefox fi # Download custom components #echo #rm -rf MacOS #if [ -e "Firefox $GECKO_VERSION MacOS.zip" ]; then # echo "Using Firefox $GECKO_VERSION MacOS.zip" # unzip "Firefox $GECKO_VERSION MacOS.zip" #else # echo "Downloading Firefox $GECKO_VERSION MacOS.zip" # curl -o MacOS.zip "${custom_components_url}Firefox%20$GECKO_VERSION%20MacOS.zip" # unzip MacOS.zip #fi #echo pushd Firefox.app/Contents/Resources modify_omni mac popd # Replace "FirefoxCP" with "ZoteroCP" for subprocesses ("Isolated Web Content", "Socket Process", "Web Content", etc.) info_plist=Firefox.app/Contents/MacOS/plugin-container.app/Contents/Resources/English.lproj/InfoPlist.strings from=$(get_utf16_chars "FirefoxCP") to=$(get_utf16_chars "ZoteroCP") perl -pi -e "s/$from/$to/" $info_plist # Check for UTF-16 "ZoteroCP" if ! grep -a -q "Z.o.t.e.r.o.C.P." $info_plist; then echo '"ZoteroCP" not found in InfoPlist.strings after replacement' exit 1 fi if [ ! -e "Firefox $GECKO_VERSION.app.zip" ]; then rm "Firefox.dmg" fi #if [ ! -e "Firefox $GECKO_VERSION MacOS.zip" ]; then # rm "MacOS.zip" #fi echo $("$SCRIPT_DIR/xulrunner_hash" -p m) > hash-mac fi if [ $BUILD_WIN == 1 ]; then GECKO_VERSION="$GECKO_VERSION_WIN" DOWNLOAD_URL="https://ftp.mozilla.org/pub/firefox/releases/$GECKO_VERSION" for arch in win32 win-x64; do xdir=firefox-$arch rm -rf $xdir mkdir $xdir if [ -e "Firefox Setup $GECKO_VERSION-$arch.exe" ]; then echo "Using Firefox Setup $GECKO_VERSION-$arch.exe" cp "Firefox Setup $GECKO_VERSION-$arch.exe" "Firefox%20Setup%20$GECKO_VERSION.exe" else if [ $arch = "win-x64" ]; then curl -O "$DOWNLOAD_URL/win64/en-US/Firefox%20Setup%20$GECKO_VERSION.exe" else curl -O "$DOWNLOAD_URL/$arch/en-US/Firefox%20Setup%20$GECKO_VERSION.exe" fi fi 7z x Firefox%20Setup%20$GECKO_VERSION.exe -o$xdir 'core/*' mv $xdir/core $xdir-core rm -rf $xdir mv $xdir-core $xdir pushd $xdir # Replace "Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38" with "Zotero" for C:\ProgramData directory # # Mozilla uses a UUID in the path because they previously used just "Mozilla" and needed to # recreate the folder with correct permissions for security reasons, but we never had a folder in # ProgramData, so we can just create it as "Zotero". Instead of using a custom xul.dll, just # replace the hard-coded string with "Zotero" and add a bunch of NULs. from=$(get_utf16_chars "Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38") to=$(get_utf16_chars "Zotero" 38) perl -pe "s/$from/$to/" < xul.dll > xul.dll.new mv xul.dll.new xul.dll # Check for UTF-16 "Zotero" in DLL # # (The macOS strings command doesn't have an encoding option, so skip on macOS. We could # require binutils, which has GNU strings, but you need to build on Windows to make an # installer, so a Windows build from macOS likely isn't being deployed, and there's no # reason the Perl command above should fail anyway.) if [[ "`uname`" != "Darwin" ]] && [[ -z "$(strings -e l xul.dll | grep -m 1 Zotero)" ]]; then echo '"Zotero" not found in xul.dll after replacement' exit 1 fi modify_omni $arch # Disable skeleton UI window # https://forums.zotero.org/discussion/comment/437636/#Comment_437636 replace_line 'pref\("browser\.startup\.preXulSkeletonUI", true\);' 'pref("browser.startup.preXulSkeletonUI", false);' defaults/preferences/firefox.js popd # Uncomment to create local copies for reuse #cp "Firefox%20Setup%20$GECKO_VERSION.exe" "Firefox Setup $GECKO_VERSION-$arch.exe" rm "Firefox%20Setup%20$GECKO_VERSION.exe" echo echo done echo $("$SCRIPT_DIR/xulrunner_hash" -p w) > hash-win fi if [ $BUILD_LINUX == 1 ]; then GECKO_VERSION="$GECKO_VERSION_LINUX" DOWNLOAD_URL="https://ftp.mozilla.org/pub/firefox/releases/$GECKO_VERSION" # Include 32-bit build if not in CI if [[ "${CI:-}" = "1" ]] || [[ "${SKIP_32:-}" = "1" ]]; then arches="x86_64" else arches="i686 x86_64" fi for arch in $arches; do xdir="firefox-$arch" rm -rf $xdir archived_file="firefox-$GECKO_VERSION-$arch.tar.bz2" if [ -e "$archived_file" ]; then echo "Using $archived_file" cp "$archived_file" "firefox-$GECKO_VERSION.tar.bz2" else curl -O "$DOWNLOAD_URL/linux-$arch/en-US/firefox-$GECKO_VERSION.tar.bz2" fi tar xvf firefox-$GECKO_VERSION.tar.bz2 mv firefox firefox-$arch pushd firefox-$arch modify_omni popd echo $($SCRIPT_DIR/xulrunner_hash -p l) > hash-linux rm "firefox-$GECKO_VERSION.tar.bz2" done fi echo Done