#!/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
}
#
# Make various modifications to the stock Firefox app
#
function modify_omni {
	local 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
	
	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
	
	# 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); 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 '/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
	
	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.
		perl -pe 's/\x{4D}\x{00}\x{6F}\x{00}\x{7A}\x{00}\x{69}\x{00}\x{6C}\x{00}\x{6C}\x{00}\x{61}\x{00}\x{2D}\x{00}\x{31}\x{00}\x{64}\x{00}\x{65}\x{00}\x{34}\x{00}\x{65}\x{00}\x{65}\x{00}\x{63}\x{00}\x{38}\x{00}\x{2D}\x{00}\x{31}\x{00}\x{32}\x{00}\x{34}\x{00}\x{31}\x{00}\x{2D}\x{00}\x{34}\x{00}\x{31}\x{00}\x{37}\x{00}\x{37}\x{00}\x{2D}\x{00}\x{61}\x{00}\x{38}\x{00}\x{36}\x{00}\x{34}\x{00}\x{2D}\x{00}\x{65}\x{00}\x{35}\x{00}\x{39}\x{00}\x{34}\x{00}\x{65}\x{00}\x{38}\x{00}\x{64}\x{00}\x{31}\x{00}\x{66}\x{00}\x{62}\x{00}\x{33}\x{00}\x{38}\x{00}/\x{5A}\x{00}\x{6F}\x{00}\x{74}\x{00}\x{65}\x{00}\x{72}\x{00}\x{6F}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}\x{00}/' < 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_cmd -e l xul.dll | grep -m 1 Zotero)" ]]; then
			echo '"Zotero" not found in xul.dll after replacement'
			exit 1
		fi
		
		modify_omni $arch
		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"
	
	rm -rf firefox
	
	# Include 32-bit build if not in CI
	if [ -z "${CI:-}" ]; then
		curl -O "$DOWNLOAD_URL/linux-i686/en-US/firefox-$GECKO_VERSION.tar.bz2"
		rm -rf firefox-i686
		tar xvf firefox-$GECKO_VERSION.tar.bz2
		mv firefox firefox-i686
		
		pushd firefox-i686
		modify_omni linux32
		popd
		
		rm "firefox-$GECKO_VERSION.tar.bz2"
	fi
	
	curl -O "$DOWNLOAD_URL/linux-x86_64/en-US/firefox-$GECKO_VERSION.tar.bz2"
	rm -rf firefox-x86_64
	tar xvf firefox-$GECKO_VERSION.tar.bz2
	mv firefox firefox-x86_64
	
	pushd firefox-x86_64
	modify_omni linux64
	popd
	echo $($SCRIPT_DIR/xulrunner_hash -p l) > hash-linux
	rm "firefox-$GECKO_VERSION.tar.bz2"
fi
echo Done