diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000000..3c72d46bde
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,25 @@
+{
+ "compact": false,
+ "retainLines": true,
+ "presets": [
+ "@babel/preset-react"
+ ],
+ "ignore": [
+ "chrome/content/zotero/include.js",
+ "chrome/content/zotero/xpcom/citeproc.js",
+ "chrome/content/ace/*",
+ "chrome/content/scaffold/templates/*",
+ "resource/react.js",
+ "resource/react-dom.js",
+ "resource/react-virtualized.js",
+ "test/resource/*.js"
+ ],
+ "plugins": [
+ [
+ "transform-es2015-modules-commonjs",
+ {
+ "strictMode": false
+ }
+ ]
+ ]
+}
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000000..8c171129bc
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,17 @@
+build/*
+chrome/content/zotero/xpcom/citeproc.js
+chrome/content/zotero/xpcom/isbn.js
+chrome/content/zotero/xpcom/rdf/*
+chrome/content/zotero/xpcom/xregexp/*
+chrome/content/zotero/xpcom/translation/tlds.js
+chrome/skin/default/zotero/timeline/bundle.js
+chrome/skin/default/zotero/timeline/timeline-api.js
+resource/bluebird/*
+resource/classnames.js
+resource/csl-validator.js
+resource/jspath.js
+resource/prop-types.js
+resource/react*
+resource/tinymce/*
+test/resource/*
+translators/*
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000000..640ce90410
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,63 @@
+{
+ "env": {
+ "browser": true,
+ "es6": true,
+ "node": true
+ },
+ "globals": {
+ "Zotero": false,
+ "ZOTERO_CONFIG": false,
+ "AddonManager": false,
+ "Cc": false,
+ "Ci": false,
+ "ChromeUtils": false,
+ "Components": false,
+ "ConcurrentCaller": false,
+ "ctypes": false,
+ "OS": false,
+ "MozXULElement": false,
+ "PluralForm": false,
+ "Services": false,
+ "windowUtils": false,
+ "XPCOMUtils": false,
+ "XRegExp": false,
+ "XULElement": false,
+ "XULElementBase": false,
+ "ItemPaneSectionElementBase": false,
+ "Cu": false,
+ "ChromeWorker": false,
+ "Localization": false,
+ "L10nFileSource": false,
+ "L10nRegistry": false,
+ "ZoteroPane_Local": false,
+ "ZoteroPane": false,
+ "Zotero_Tabs": false,
+ "ZoteroContextPane": false,
+ "IOUtils": false,
+ "NetUtil": false,
+ "FileUtils": false,
+ "globalThis": false
+ },
+ "extends": [
+ "@zotero",
+ "plugin:react/recommended"
+ ],
+ "parser": "@babel/eslint-parser",
+ "parserOptions": {
+ "ecmaVersion": 2018,
+ "ecmaFeatures": {
+ "jsx": true
+ },
+ "sourceType": "module"
+ },
+ "plugins": [
+ "react",
+ "@babel"
+ ],
+ "settings": {
+ "react": {
+ "version": "17.0"
+ }
+ },
+ "rules": {}
+}
diff --git a/.forgejo/workflows/generate-tarball.yml b/.forgejo/workflows/generate-tarball.yml
deleted file mode 100644
index 5de03640f5..0000000000
--- a/.forgejo/workflows/generate-tarball.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-on:
- workflow_dispatch:
- inputs:
- ref_name:
- description: 'Tag or commit'
- required: true
- type: string
-
- push:
- tags:
- - '*'
-
-jobs:
- build-tarball:
- name: Build tarball w/ submodules
- runs-on: x86_64
- container:
- image: alpine:latest
- env:
- CI_PROJECT_NAME: zotero
- steps:
- - name: Environment setup
- run: apk add nodejs git git-archive-all gzip
- - name: Repo pull
- uses: actions/checkout@v4
- with:
- fetch-depth: 500
- ref: ${{ inputs.ref_name }}
- - name: Package build
- run: |
- if test $GITHUB_REF_NAME == "ci" ; then
- CI_REF_NAME=${{ inputs.ref_name }}
- else
- CI_REF_NAME=$GITHUB_REF_NAME
- fi
- echo "building tarball for $CI_REF_NAME"
- git-archive-all --force-submodules $CI_PROJECT_NAME-$CI_REF_NAME.tar.gz
- echo "Generating sha512sum"
- sha512sum $CI_PROJECT_NAME-$CI_REF_NAME.tar.gz > $CI_PROJECT_NAME-$CI_REF_NAME.tar.gz.sha512sum
- - name: Package upload
- uses: forgejo/upload-artifact@v3
- with:
- name: tarball
- path: zotero-*.tar*
- upload-tarball:
- name: Upload to generic repo
- runs-on: x86_64
- needs: [build-tarball]
- container:
- image: alpine:latest
- steps:
- - name: Environment setup
- run: apk add nodejs curl
- - name: Package download
- uses: forgejo/download-artifact@v3
- - name: Package deployment
- run: |
- if test $GITHUB_REF_NAME == "ci" ; then
- CI_REF_NAME=${{ inputs.ref_name }}
- else
- CI_REF_NAME=$GITHUB_REF_NAME
- fi
- curl --user ${{ vars.CODE_FORGEJO_USER }}:${{ secrets.CODE_FORGEJO_TOKEN }} --upload-file ./tarball/zotero-*.tar.gz ${{ github.server_url }}/api/packages/mirrors/generic/zotero/$CI_REF_NAME/zotero-$CI_REF_NAME.tar.gz
- curl --user ${{ vars.CODE_FORGEJO_USER }}:${{ secrets.CODE_FORGEJO_TOKEN }} --upload-file ./tarball/zotero-*.tar.gz.sha512sum ${{ github.server_url }}/api/packages/mirrors/generic/zotero/$CI_REF_NAME/zotero-$CI_REF_NAME.tar.gz.sha512sum
diff --git a/.forgejo/workflows/mirror-repository.yml b/.forgejo/workflows/mirror-repository.yml
deleted file mode 100644
index 36ad9d5504..0000000000
--- a/.forgejo/workflows/mirror-repository.yml
+++ /dev/null
@@ -1,70 +0,0 @@
-on:
- workflow_dispatch:
- inputs:
- ref_name:
- description: 'Tag or commit'
- required: false
- type: string
- is_commit:
- description: 'Is commit?'
- required: false
- type: boolean
-
-
- schedule:
- - cron: '@hourly'
-
-jobs:
- mirror:
- name: Pull from upstream
- runs-on: x86_64
- container:
- image: alpine:latest
- env:
- upstream: https://github.com/zotero/zotero
- tags: '7.*'
- steps:
- - name: Environment setup
- run: apk add grep git sed coreutils bash nodejs
- - name: Fetch destination
- uses: actions/checkout@v4
- with:
- fetch_depth: 1
- ref: ci
- token: ${{ secrets.CODE_FORGEJO_TOKEN }}
- - name: Missing tag detecting
- run: |
- if [ -n "${{ inputs.ref_name }}" ]; then
- echo ${{ inputs.ref_name }} > missing_tags
- else
- git ls-remote $upstream "refs/tags/$tags" | grep -v '{' | sed 's|.*/||' > upstream_tags
- git ls-remote ${{ github.server_url}}/${{ github.repository }} "refs/tags/$tags" | grep -v '{' | sed 's|.*/||' > destination_tags
- cat upstream_tags destination_tags | tr ' ' '\n' | sort | uniq -u > missing_tags
- fi
- echo "Missing tags:"
- cat missing_tags
- - name: Missing tag fetch
- run: |
- git remote add upstream $upstream
- while read tag; do
- if test "${{ inputs.is_commit }}" == "true"; then
- git fetch upstream $tag --no-tags
- else
- git fetch upstream tag $tag --no-tags
- fi
- done < missing_tags
- - name: Packaging workflow injection
- run: |
- while read tag; do
- git checkout $tag
- if test "${{ inputs.is_commit }}" != "true"; then
- git tag -d $tag
- fi
- git checkout ci -- ./.forgejo
- git config user.name "forgejo-actions[bot]"
- git config user.email "dev@ayakael.net"
- git commit -m 'Inject custom workflow'
- git tag -a $tag -m $tag
- done < missing_tags
- - name: Push to destination
- run: git push --force origin refs/tags/*:refs/tags/* --tags
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..4fb3d77151
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+*.sql text eol=lf
+app/win/zotero.exe.tar.xz filter=lfs diff=lfs merge=lfs -text
+app/mac/updater.tar.xz filter=lfs diff=lfs merge=lfs -text
+app/win/updater.exe.tar.xz filter=lfs diff=lfs merge=lfs -text
+app/linux/updater.tar.xz filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000000..91292d7a56
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,9 @@
+**READ THIS BEFORE CREATING AN ISSUE**
+
+Zotero does not use GitHub Issues for bug reports or feature requests.
+
+Please post all such requests to the Zotero Forums at https://forums.zotero.org, where Zotero developers and many others can help. For confirmed bugs or agreed-upon changes, Zotero developers will create new issues in the relevant repositories.
+
+Development questions involving code, APIs, or other technical topics can be posted to the zotero-dev mailing list at http://groups.google.com/group/zotero-dev.
+
+See https://www.zotero.org/support/zotero_support for more information on how Zotero support works.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000000..2a1e744cc4
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,93 @@
+name: CI
+on: [push, pull_request]
+concurrency:
+ group: ${{ github.ref }}
+ cancel-in-progress: true
+jobs:
+ build:
+ name: Build, Upload, Test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ lfs: true
+
+ - name: Install Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
+ #cache: npm
+
+ # On GitHub
+ - name: Install xvfb
+ if: env.ACT != 'true'
+ run: sudo apt update && sudo apt install -y xvfb
+
+ # Local via act
+ - name: Install packages for act
+ if: env.ACT == 'true'
+ run: apt update && apt install -y zstd xvfb dbus-x11 libgtk-3-0 libx11-xcb1 libdbus-glib-1-2 libxt6
+
+ - name: Cache xulrunner
+ id: xulrunner-cache
+ uses: actions/cache@v4
+ with:
+ path: app/xulrunner/firefox-x86_64
+ key: xulrunner-${{ hashFiles('app/config.sh', 'app/scripts/fetch_xulrunner') }}
+
+ - name: Fetch xulrunner
+ if: steps.xulrunner-cache.outputs.cache-hit != 'true'
+ run: app/scripts/fetch_xulrunner -p l
+
+ - name: Cache Node modules
+ id: node-cache
+ uses: actions/cache@v4
+ with:
+ path: node_modules
+ key: node-modules-${{ hashFiles('package-lock.json') }}
+
+ - name: Install Node modules
+ if: steps.node-cache.outputs.cache-hit != 'true'
+ run: npm install
+
+ - name: Build Zotero
+ run: npm run build
+ # Currently necessary for pdf-worker Webpack: https://stackoverflow.com/a/69746937
+ env:
+ NODE_OPTIONS: --openssl-legacy-provider
+
+ - name: Upload deployment ZIP
+ if: |
+ env.ACT != 'true'
+ && github.repository == 'zotero/zotero'
+ && github.event_name == 'push'
+ && (github.ref == 'refs/heads/main' || endsWith(github.ref, '-hotfix'))
+ env:
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ run: |
+ mkdir build-zip
+ cd build
+ zip -r ../build-zip/$GITHUB_SHA.zip *
+ cd ..
+ sudo gem install --no-document dpl dpl-s3
+ dpl --provider=s3 --bucket=zotero-download --local-dir=build-zip --upload-dir=ci/client --acl=public-read --skip_cleanup=true
+
+ - name: Run tests
+ run: xvfb-run test/runtests.sh -f
+
+ - name: Cache utilities Node modules
+ id: utilities-node-cache
+ uses: actions/cache@v4
+ with:
+ path: chrome/content/zotero/xpcom/utilities/node_modules
+ key: utilities-node-modules-${{ hashFiles('chrome/content/zotero/xpcom/utilities/package-lock.json') }}
+
+ - name: Install utilities Node modules
+ if: steps.utilities-node-cache.outputs.cache-hit != 'true'
+ run: npm install --prefix chrome/content/zotero/xpcom/utilities
+
+ - name: Run utilities tests
+ run: |
+ npm test --prefix chrome/content/zotero/xpcom/utilities -- -j resource/schema/global/schema.json
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..3e51b5ea8c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.DS_Store
+node_modules
+build/
+.signatures.json
+tmp
+app/win/codesign
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000..ffb7808f55
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,58 @@
+[submodule "translators"]
+ path = translators
+ url = https://github.com/zotero/translators.git
+ branch = master
+[submodule "chrome/content/zotero/locale/csl"]
+ path = chrome/content/zotero/locale/csl
+ url = https://github.com/citation-style-language/locales.git
+ branch = master
+[submodule "styles"]
+ path = styles
+ url = https://github.com/zotero/bundled-styles.git
+ branch = master
+[submodule "test/resource/chai"]
+ path = test/resource/chai
+ url = https://github.com/chaijs/chai.git
+ branch = master
+[submodule "test/resource/mocha"]
+ path = test/resource/mocha
+ url = https://github.com/mochajs/mocha.git
+ branch = master
+[submodule "test/resource/chai-as-promised"]
+ path = test/resource/chai-as-promised
+ url = https://github.com/domenic/chai-as-promised.git
+ branch = master
+[submodule "resource/schema/global"]
+ path = resource/schema/global
+ url = https://github.com/zotero/zotero-schema.git
+ branch = master
+[submodule "resource/SingleFile"]
+ path = resource/SingleFile
+ url = https://github.com/gildas-lormeau/SingleFile.git
+[submodule "pdf-reader"]
+ path = reader
+ url = https://github.com/zotero/reader.git
+ branch = master
+[submodule "pdf-worker"]
+ path = pdf-worker
+ url = https://github.com/zotero/pdf-worker.git
+ branch = master
+[submodule "note-editor"]
+ path = note-editor
+ url = https://github.com/zotero/note-editor.git
+ branch = master
+[submodule "chrome/content/zotero/xpcom/utilities"]
+ path = chrome/content/zotero/xpcom/utilities
+ url = https://github.com/zotero/utilities.git
+[submodule "chrome/content/zotero/xpcom/translate"]
+ path = chrome/content/zotero/xpcom/translate
+ url = https://github.com/zotero/translate.git
+[submodule "app/modules/zotero-word-for-mac-integration"]
+ path = app/modules/zotero-word-for-mac-integration
+ url = https://github.com/zotero/zotero-word-for-mac-integration.git
+[submodule "app/modules/zotero-word-for-windows-integration"]
+ path = app/modules/zotero-word-for-windows-integration
+ url = https://github.com/zotero/zotero-word-for-windows-integration.git
+[submodule "app/modules/zotero-libreoffice-integration"]
+ path = app/modules/zotero-libreoffice-integration
+ url = https://github.com/zotero/zotero-libreoffice-integration.git
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..905c2c0531
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,11 @@
+# Contributing to Zotero
+
+## Bug Reports and Feature Requests
+
+Zotero does not use GitHub Issues for bug reports, feature requests, or support questions. Please post all such requests to the [Zotero Forums](https://forums.zotero.org), where Zotero developers and many others can help. See [How Zotero Support Works](https://www.zotero.org/support/zotero_support) for more information.
+
+For confirmed bugs or agreed-upon changes, Zotero developers will create new issues in the relevant repositories.
+
+## Working with Zotero Code
+
+See [Zotero Source Code](https://www.zotero.org/support/dev/source_code).
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000000..b14d531f57
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,681 @@
+Zotero is Copyright © 2018 Corporation for Digital Scholarship,
+Vienna, Virginia, USA http://digitalscholar.org
+
+Copyright © 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017
+Roy Rosenzweig Center for History and New Media, George Mason University,
+Fairfax, Virginia, USA http://zotero.org
+
+The Corporation for Digital Scholarship distributes the Zotero source code
+under the GNU Affero General Public License, version 3 (AGPLv3). The full text
+of this license is given below.
+
+The Zotero name is a registered trademark of the Corporation for Digital Scholarship.
+See http://zotero.org/trademark for more information.
+
+Third-party copyright in this distribution is noted where applicable.
+
+All rights not expressly granted are reserved.
+
+=========================================================================
+
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..b1f49f3314
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+Zotero
+======
+[](https://github.com/zotero/zotero/actions/workflows/ci.yml)
+
+[Zotero](https://www.zotero.org/) is a free, easy-to-use tool to help you collect, organize, cite, and share your research sources.
+
+Please post feature requests or bug reports to the [Zotero Forums](https://forums.zotero.org/). If you're having trouble with Zotero, see [Getting Help](https://www.zotero.org/support/getting_help).
+
+For more information on how to use this source code, see the [Zotero documentation](https://www.zotero.org/support/dev/source_code).
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000000..c1310cfcac
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,11 @@
+*~
+cache
+config-custom.sh
+dist
+lastrev
+staging
+tmp
+xulrunner
+win/resource_hacker
+win/firefox-*.win32.zip
+win/firefox-*.win64.zip
diff --git a/app/README.md b/app/README.md
new file mode 100644
index 0000000000..f96b3a3d69
--- /dev/null
+++ b/app/README.md
@@ -0,0 +1,4 @@
+# Zotero Standalone build utility
+These files are used to bundle the [Zotero core](https://github.com/zotero/zotero) into distributable bundles for Mac, Windows, and Linux.
+
+Instructions for building and packaging are available on the [Zotero wiki](https://www.zotero.org/support/dev/client_coding/building_the_standalone_client).
diff --git a/app/assets/application.ini b/app/assets/application.ini
new file mode 100644
index 0000000000..73990c30ff
--- /dev/null
+++ b/app/assets/application.ini
@@ -0,0 +1,18 @@
+[App]
+Vendor=Zotero
+Name=Zotero
+Version={{VERSION}}
+BuildID={{BUILDID}}
+Copyright=Copyright (c) 2006-2022 Contributors
+ID=zotero@zotero.org
+
+[Gecko]
+MinVersion=115.0
+MaxVersion=115.99.*
+
+[XRE]
+EnableExtensionManager=1
+EnableProfileMigrator=1
+
+[AppUpdate]
+URL=https://www.zotero.org/download/client/update/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/update.xml
diff --git a/app/assets/branding/locale/brand.dtd b/app/assets/branding/locale/brand.dtd
new file mode 100644
index 0000000000..cba785bc63
--- /dev/null
+++ b/app/assets/branding/locale/brand.dtd
@@ -0,0 +1 @@
+
diff --git a/app/assets/branding/locale/brand.ftl b/app/assets/branding/locale/brand.ftl
new file mode 100644
index 0000000000..896ddc864f
--- /dev/null
+++ b/app/assets/branding/locale/brand.ftl
@@ -0,0 +1,7 @@
+-brand-shorter-name = Zotero
+-brand-short-name = Zotero
+-brand-full-name = Zotero
+-brand-product-name = Zotero
+-vendor-short-name = Zotero
+-app-name = Zotero
+trademarkInfo = Zotero is a trademark of the Corporation for Digital Scholarship.
diff --git a/app/assets/branding/locale/brand.properties b/app/assets/branding/locale/brand.properties
new file mode 100644
index 0000000000..5adb572994
--- /dev/null
+++ b/app/assets/branding/locale/brand.properties
@@ -0,0 +1,3 @@
+brandShorterName=Zotero
+brandShortName=Zotero
+brandFullName=Zotero
diff --git a/app/assets/chrome.manifest b/app/assets/chrome.manifest
new file mode 100644
index 0000000000..3844885506
--- /dev/null
+++ b/app/assets/chrome.manifest
@@ -0,0 +1,3 @@
+locale branding en-US chrome/en-US/locale/branding/
+content branding chrome/branding/content/
+skin browser preferences chrome/skin/
diff --git a/app/assets/commandLineHandler.js b/app/assets/commandLineHandler.js
new file mode 100644
index 0000000000..931044f9ee
--- /dev/null
+++ b/app/assets/commandLineHandler.js
@@ -0,0 +1,102 @@
+let { CommandLineOptions, TestOptions } = ChromeUtils.importESModule("chrome://zotero/content/modules/commandLineOptions.mjs");
+
+// Only allow BrowserContentHandler to open a new window if this is the initial launch,
+// meaning our CLH isn't registered yet.
+if (cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
+ cmdLine.preventDefault = true;
+}
+
+// Force debug output to window
+if (cmdLine.handleFlag("ZoteroDebug", false)) {
+ CommandLineOptions.forceDebugLog = 2;
+}
+// Force debug output to text console
+else if (cmdLine.handleFlag("ZoteroDebugText", false)) {
+ CommandLineOptions.forceDebugLog = 1;
+}
+// Pressing Ctrl-C via the terminal is interpreted as a crash, and after three crashes
+// Firefox starts up in automatic safe mode (troubleshooting mode). To avoid this, we clear the crash
+// counter when using one of the debug-logging flags, which generally imply terminal usage.
+if (CommandLineOptions.forceDebugLog) {
+ Services.prefs.getBranch("toolkit.startup.").clearUserPref("recent_crashes");
+}
+
+CommandLineOptions.forceDataDir = cmdLine.handleFlagWithParam("datadir", false);
+// Set here, to be acted upon in xpcom/commandLineHandler.js
+CommandLineOptions.file = cmdLine.handleFlagWithParam("file", false);
+CommandLineOptions.url = cmdLine.handleFlagWithParam("url", false);
+if (CommandLineOptions.url) {
+ CommandLineOptions.url = cmdLine.resolveURI(CommandLineOptions.url);
+}
+
+var processTestOptions = false;
+if (cmdLine.handleFlag("ZoteroTest", false)) {
+ CommandLineOptions.test = true;
+ processTestOptions = true;
+}
+if (cmdLine.handleFlag("ZoteroAutomatedTest", false)) {
+ CommandLineOptions.automatedTest = true;
+}
+if (cmdLine.handleFlag("ZoteroSkipBundledFiles", false)) {
+ CommandLineOptions.skipBundledFiles = true;
+}
+
+if (processTestOptions) {
+ TestOptions.tests = cmdLine.handleFlagWithParam("test", false);
+ TestOptions.noquit = cmdLine.handleFlag("noquit", false);
+ TestOptions.makeTestData = cmdLine.handleFlag("makeTestData", false);
+ TestOptions.noquit = !TestOptions.makeTestData && this.noquit;
+ TestOptions.runTests = !TestOptions.makeTestData;
+ TestOptions.bail = cmdLine.handleFlag("bail", false);
+ TestOptions.startAt = cmdLine.handleFlagWithParam("startAtTestFile", false);
+ TestOptions.stopAt = cmdLine.handleFlagWithParam("stopAtTestFile", false);
+ TestOptions.grep = cmdLine.handleFlagWithParam("grep", false);
+ TestOptions.timeout = cmdLine.handleFlagWithParam("ZoteroTestTimeout", false);
+
+ Services.ww.openWindow(
+ null,
+ "chrome://zotero-unit/content/runtests.html",
+ "_blank",
+ "chrome,dialog=no,all",
+ Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray)
+ );
+ cmdLine.preventDefault = true;
+}
+
+if (cmdLine.handleFlag("debugger", false)) {
+ (async function () {
+ try {
+ let portOrPath = Services.prefs.getBranch('').getIntPref('devtools.debugger.remote-port');
+
+ const { DevToolsLoader } = ChromeUtils.import(
+ "resource://devtools/shared/loader/Loader.jsm"
+ );
+ const loader = new DevToolsLoader({
+ freshCompartment: true,
+ });
+ const { DevToolsServer } = loader.require("devtools/server/devtools-server");
+ const { SocketListener } = loader.require("devtools/shared/security/socket");
+
+ if (DevToolsServer.initialized) {
+ dump("Debugger server already initialized\n\n");
+ return;
+ }
+
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ DevToolsServer.allowChromeProcess = true;
+ const socketOptions = { portOrPath };
+ const listener = new SocketListener(DevToolsServer, socketOptions);
+ await listener.open();
+ if (!DevToolsServer.listeningSockets) {
+ throw new Error("No listening sockets");
+ }
+
+ dump(`Debugger server started on ${portOrPath}\n\n`);
+ }
+ catch (e) {
+ dump(e + "\n\n");
+ Components.utils.reportError(e);
+ }
+ })();
+}
diff --git a/app/assets/mac/chrome/skin/preferences/preferences.css b/app/assets/mac/chrome/skin/preferences/preferences.css
new file mode 100644
index 0000000000..f866b7e1f0
--- /dev/null
+++ b/app/assets/mac/chrome/skin/preferences/preferences.css
@@ -0,0 +1,242 @@
+/*
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is the Firefox Preferences System.
+#
+# The Initial Developer of the Original Code is
+# Ben Goodger.
+# Portions created by the Initial Developer are Copyright (C) 2005
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Ben Goodger
+# Kevin Gerich
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+*/
+
+.windowDialog {
+ padding: 12px;
+ font: -moz-dialog;
+}
+
+.paneSelector {
+ list-style-image: url("chrome://browser/skin/preferences/Options.png");
+}
+
+/* ----- GENERAL BUTTON ----- */
+
+radio[pane=paneGeneral],
+radio[pane=paneMain] {
+ -moz-image-region: rect(0px, 32px, 32px, 0px);
+}
+
+/* ----- TABS BUTTON ----- */
+
+radio[pane=paneTabs] {
+ -moz-image-region: rect(0px, 64px, 32px, 32px);
+}
+
+/* ----- CONTENT BUTTON ----- */
+
+radio[pane=paneContent] {
+ -moz-image-region: rect(0px, 96px, 32px, 64px);
+}
+
+/* ----- APPLICATIONS BUTTON ----- */
+
+radio[pane=paneApplications] {
+ -moz-image-region: rect(0px, 128px, 32px, 96px);
+}
+
+/* ----- PRIVACY BUTTON ----- */
+
+radio[pane=panePrivacy] {
+ -moz-image-region: rect(0px, 160px, 32px, 128px);
+}
+
+/* ----- SECURITY BUTTON ----- */
+
+radio[pane=paneSecurity] {
+ -moz-image-region: rect(0px, 192px, 32px, 160px);
+}
+
+/* ----- ADVANCED BUTTON ----- */
+
+radio[pane=paneAdvanced] {
+ -moz-image-region: rect(0px, 224px, 32px, 192px);
+}
+
+/* ----- SYNC BUTTON ----- */
+
+radio[pane=paneSync] {
+ list-style-image: url("chrome://browser/skin/preferences/Options-sync.png");
+}
+
+
+/* ----- APPLICATIONS PREFPANE ----- */
+#BrowserPreferences[animated="true"] #handlersView {
+ height: 25em;
+}
+
+#BrowserPreferences[animated="false"] #handlersView {
+ -moz-box-flex: 1;
+}
+
+description {
+ font: small-caption;
+ font-weight: normal;
+ line-height: 1.3em;
+ margin-bottom: 4px !important;
+}
+
+prefpane .groupbox-body {
+ -moz-appearance: none;
+ padding: 8px 4px 4px 4px;
+}
+
+#paneTabs > groupbox {
+ margin: 0;
+}
+
+#tabPrefsBox {
+ margin: 12px 4px;
+}
+
+prefpane .groupbox-title {
+ background: url("chrome://global/skin/50pct_transparent_grey.png") repeat-x bottom left;
+ margin-bottom: 4px;
+}
+
+tabpanels {
+ padding: 20px 7px 7px;
+}
+
+caption {
+ -moz-padding-start: 5px;
+ padding-top: 4px;
+ padding-bottom: 2px;
+}
+
+#paneMain description,
+#paneContent description,
+#paneAdvanced description,
+#paneSecurity description {
+ font: -moz-dialog;
+}
+
+#paneContent {
+ padding-top: 8px;
+}
+
+#paneContent row {
+ padding: 2px 4px;
+ -moz-box-align: center;
+}
+
+#popupPolicyRow,
+#enableSoftwareInstallRow,
+#enableImagesRow {
+ margin-bottom: 4px !important;
+ padding-bottom: 4px !important;
+ border-bottom: 1px solid #ccc;
+}
+
+#browserUseCurrent,
+#browserUseBookmark,
+#browserUseBlank {
+ margin-top: 10px;
+}
+
+#advancedPrefs {
+ margin: 0 8px;
+}
+
+#privacyPrefs {
+ padding: 0 4px;
+}
+
+#privacyPrefs > tabpanels {
+ padding: 18px 10px 10px;
+}
+
+#OCSPDialogPane {
+ font: message-box !important;
+}
+
+/**
+ * Privacy Pane
+ */
+
+/* styles for the link elements copied from .text-link in global.css */
+.inline-link {
+ color: -moz-nativehyperlinktext;
+ text-decoration: underline;
+}
+
+.inline-link:not(:focus) {
+ outline: 1px dotted transparent;
+}
+
+/**
+ * Update Preferences
+ */
+#autoInstallOptions {
+ -moz-margin-start: 20px;
+}
+
+.updateControls {
+ -moz-margin-start: 10px;
+}
+
+/**
+ * Clear Private Data
+ */
+#SanitizeDialogPane > groupbox {
+ margin-top: 0;
+}
+
+
+/* ----- SYNC PANE ----- */
+
+#syncDesc {
+ padding: 0 8em;
+}
+
+#accountCaptionImage {
+ list-style-image: url("chrome://mozapps/skin/profile/profileicon.png");
+}
+
+#syncAddDeviceLabel {
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+#syncEnginesList {
+ height: 10em;
+}
+
diff --git a/app/assets/multilocale.txt b/app/assets/multilocale.txt
new file mode 100644
index 0000000000..af1f405563
--- /dev/null
+++ b/app/assets/multilocale.txt
@@ -0,0 +1 @@
+en-US,ar,bg-BG,br,ca-AD,cs-CZ,da-DK,de,el-GR,en-AU,en-CA,en-GB,en-NZ,es-ES,et-EE,eu-ES,fa,fi-FI,fr-FR,gl-ES,hu-HU,id-ID,is-IS,it-IT,ja-JP,km,ko-KR,lt-LT,nb-NO,nl-NL,pl-PL,pt-BR,pt-PT,ro-RO,ru-RU,sk-SK,sl-SI,sr-RS,sv-SE,ta,th-TH,tr-TR,uk-UA,vi-VN,zh-CN,zh-TW
diff --git a/app/assets/prefs.js b/app/assets/prefs.js
new file mode 100644
index 0000000000..bf137177d2
--- /dev/null
+++ b/app/assets/prefs.js
@@ -0,0 +1,157 @@
+// We only want a single window, I think
+pref("toolkit.singletonWindowType", "navigator:browser");
+
+// For debugging purposes, show errors in console by default
+pref("javascript.options.showInConsole", true);
+
+// Don't retrieve unrequested links when performing standalone translation
+pref("network.prefetch-next", false);
+
+// Let operations run as long as necessary
+pref("dom.max_chrome_script_run_time", 0);
+
+// .dotm Word plugin VBA uses this to find the running Zotero instance
+pref("ui.window_class_override", "ZoteroWindowClass");
+
+pref("intl.locale.requested", '');
+pref("intl.regional_prefs.use_os_locales", false);
+
+// Fix error initializing login manager after this was changed in Firefox 57
+// Could also disable this with MOZ_LOADER_SHARE_GLOBAL, supposedly
+pref("jsloader.shareGlobal", false);
+
+// Needed due to https://bugzilla.mozilla.org/show_bug.cgi?id=1181977
+pref("browser.hiddenWindowChromeURL", "chrome://zotero/content/standalone/hiddenWindow.xhtml");
+// Use basicViewer for opening new DOM windows from content (for TinyMCE)
+pref("browser.chromeURL", "chrome://zotero/content/standalone/basicViewer.xhtml");
+// We need these to get the save dialog working with contentAreaUtils.js
+pref("browser.download.useDownloadDir", false);
+pref("browser.download.manager.showWhenStarting", false);
+pref("browser.download.folderList", 1);
+
+// Don't show add-on selection dialog
+pref("extensions.shownSelectionUI", true);
+pref("extensions.autoDisableScope", 11);
+
+pref("network.protocol-handler.expose-all", false);
+pref("network.protocol-handler.expose.zotero", true);
+pref("network.protocol-handler.expose.http", true);
+pref("network.protocol-handler.expose.https", true);
+
+// Never go offline
+pref("offline.autoDetect", false);
+pref("network.manage-offline-status", false);
+
+// Without this, we will throw up dialogs if asked to translate strange pages
+pref("browser.xul.error_pages.enabled", true);
+
+// Without this, scripts may decide to open popups
+pref("dom.disable_open_during_load", true);
+
+// Don't show security warning. The "warn_viewing_mixed" warning just lets the user know that some
+// page elements were loaded over an insecure connection. This doesn't matter if all we're doing is
+// scraping the page, since we don't provide any information to the site.
+pref("security.warn_viewing_mixed", false);
+
+// Preferences for add-on discovery
+pref("extensions.getAddons.cache.enabled", false);
+//pref("extensions.getAddons.maxResults", 15);
+//pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/%LOCALE%/%APP%/api/%API_VERSION%/search/guid:%IDS%?src=thunderbird&appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%");
+//pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/%APP%/search?q=%TERMS%");
+//pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/%APP%/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%?src=thunderbird");
+//pref("extensions.webservice.discoverURL", "https://www.zotero.org/support/plugins");
+
+// Check Windows certificate store for custom CAs
+pref("security.enterprise_roots.enabled", true);
+
+// Disable add-on signature checking with unbranded Firefox build
+pref("xpinstall.signatures.required", false);
+// Allow legacy extensions (though this might not be necessary)
+pref("extensions.legacy.enabled", true);
+// Allow installing XPIs from any host
+pref("xpinstall.whitelist.required", false);
+// Allow installing XPIs when using a custom CA
+pref("extensions.install.requireBuiltInCerts", false);
+pref("extensions.update.requireBuiltInCerts", false);
+
+// Don't connect to the Mozilla extensions blocklist
+pref("extensions.blocklist.enabled", false);
+// Avoid warning in console when opening Tools -> Add-ons
+pref("extensions.getAddons.link.url", "");
+
+// Disable places
+pref("places.history.enabled", false);
+
+// Probably not used, but prevent an error in the console
+pref("app.support.baseURL", "https://www.zotero.org/support/");
+
+// Disable Telemetry, Health Report, error reporting, and remote settings
+pref("toolkit.telemetry.unified", false);
+pref("toolkit.telemetry.enabled", false);
+pref("datareporting.policy.dataSubmissionEnabled", false);
+pref("toolkit.crashreporter.enabled", false);
+pref("extensions.remoteSettings.disabled", true);
+
+pref("extensions.update.url", "");
+
+// Don't try to load the "Get Add-ons" tab on first load of Add-ons window
+pref("extensions.ui.lastCategory", "addons://list/extension");
+
+// Not set on Windows in Firefox anymore since it's a per-installation pref,
+// but we override that in fetch_xulrunner
+pref("app.update.auto", true);
+
+// URL user can browse to manually if for some reason all update installation
+// attempts fail.
+pref("app.update.url.manual", "https://www.zotero.org/download");
+
+// A default value for the "More information about this update" link
+// supplied in the "An update is available" page of the update wizard.
+pref("app.update.url.details", "https://www.zotero.org/support/changelog");
+
+// Interval: Time between checks for a new version (in seconds)
+// default=1 day
+pref("app.update.interval", 86400);
+
+// The minimum delay in seconds for the timer to fire.
+// default=2 minutes
+pref("app.update.timerMinimumDelay", 120);
+
+// Whether or not we show a dialog box informing the user that the update was
+// successfully applied. This is off in Firefox by default since we show a
+// upgrade start page instead! Other apps may wish to show this UI, and supply
+// a whatsNewURL field in their brand.properties that contains a link to a page
+// which tells users what's new in this new update.
+
+// update channel for this build
+pref("app.update.channel", "default");
+
+// This should probably not be a preference that's used in toolkit....
+pref("browser.preferences.instantApply", false);
+
+// Allow elements to be displayed full-screen
+pref("full-screen-api.enabled", true);
+
+// Allow chrome access in DevTools
+// This enables the input field in the Browser Console tool
+pref("devtools.chrome.enabled", true);
+
+// Default mousewheel action with Alt/Option is History Back/Forward in Firefox
+// We don't have History navigation and users want to scroll the tree with Option
+// key held down
+pref("mousewheel.with_alt.action", 1);
+
+// Use the system print dialog instead of the new tab-based print dialog in Firefox
+pref("print.prefer_system_dialog", true);
+
+// Disable libvpx decoding/encoding
+pref("media.webm.enabled", false);
+pref("media.encoder.webm.enabled", false);
+pref("media.mediasource.webm.enabled", false);
+pref("media.mediasource.webm.audio.enabled", false);
+pref("media.mediasource.vp9.enabled", false);
+pref("media.ffvpx.enabled", false);
+pref("media.ffvpx.mp3.enabled", false);
+pref("media.rdd-vpx.enabled", false);
+pref("media.rdd-ffvpx.enabled", false);
+pref("media.utility-ffvpx.enabled", false);
diff --git a/app/assets/unix/skin/preferences/preferences.css b/app/assets/unix/skin/preferences/preferences.css
new file mode 100644
index 0000000000..3a19fbb185
--- /dev/null
+++ b/app/assets/unix/skin/preferences/preferences.css
@@ -0,0 +1,188 @@
+%if 0
+/*
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is the Firefox Preferences System.
+#
+# The Initial Developer of the Original Code is
+# Ben Goodger.
+# Portions created by the Initial Developer are Copyright (C) 2005
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Ben Goodger
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+*/
+%endif
+
+/* Global Styles */
+#BrowserPreferences radio[pane] {
+ list-style-image: url("chrome://browser/skin/preferences/Options.png");
+}
+
+radio[pane=paneMain] {
+ -moz-image-region: rect(0px, 32px, 32px, 0px)
+}
+
+radio[pane=paneTabs] {
+ -moz-image-region: rect(0px, 64px, 32px, 32px)
+}
+
+radio[pane=paneContent] {
+ -moz-image-region: rect(0px, 96px, 32px, 64px)
+}
+
+radio[pane=paneApplications] {
+ -moz-image-region: rect(0px, 128px, 32px, 96px)
+}
+
+radio[pane=panePrivacy] {
+ -moz-image-region: rect(0px, 160px, 32px, 128px)
+}
+
+radio[pane=paneSecurity] {
+ -moz-image-region: rect(0px, 192px, 32px, 160px)
+}
+
+radio[pane=paneAdvanced] {
+ -moz-image-region: rect(0px, 224px, 32px, 192px)
+}
+
+%ifdef MOZ_SERVICES_SYNC
+radio[pane=paneSync] {
+ list-style-image: url("chrome://browser/skin/preferences/Options-sync.png") !important;
+}
+%endif
+
+/* Applications Pane */
+#BrowserPreferences[animated="true"] #handlersView {
+ height: 25em;
+}
+
+#BrowserPreferences[animated="false"] #handlersView {
+ -moz-box-flex: 1;
+}
+
+/* Privacy Pane */
+
+/* styles for the link elements copied from .text-link in global.css */
+.inline-link {
+ color: -moz-nativehyperlinktext;
+ text-decoration: underline;
+}
+
+.inline-link:not(:focus) {
+ outline: 1px dotted transparent;
+}
+
+/* Modeless Window Dialogs */
+.windowDialog,
+.windowDialog prefpane {
+ padding: 0px;
+}
+
+.contentPane {
+ margin: 9px 8px 5px 8px;
+}
+
+.actionButtons {
+ margin: 0px 3px 6px 3px !important;
+}
+
+/* Cookies Manager */
+#cookiesChildren::-moz-tree-image(domainCol) {
+ width: 16px;
+ height: 16px;
+ margin: 0px 2px;
+ list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
+}
+
+#paneApplications {
+ margin-left: 4px;
+ margin-right: 4px;
+ padding-left: 0;
+ padding-right: 0;
+}
+
+#linksOpenInBox {
+ margin-top: 5px;
+}
+
+#paneAdvanced {
+ padding-bottom: 10px;
+}
+#advancedPrefs {
+ margin-left: 0;
+ margin-right: 0;
+}
+
+#cookiesChildren::-moz-tree-image(domainCol, container) {
+ list-style-image: url("moz-icon://stock/gtk-directory?size=menu");
+}
+
+#cookieInfoBox {
+ border: 1px solid ThreeDShadow;
+ border-radius: 0px;
+ margin: 4px;
+ padding: 0px;
+}
+
+/* bottom-most box containing a groupbox in a prefpane. Prevents the bottom
+ of the groupbox from being cutoff */
+.bottomBox {
+ padding-bottom: 4px;
+}
+
+/**
+ * Clear Private Data
+ */
+#SanitizeDialogPane > groupbox {
+ margin-top: 0;
+}
+
+%ifdef MOZ_SERVICES_SYNC
+/* Sync Pane */
+
+#syncDesc {
+ padding: 0 8em;
+}
+
+#accountCaptionImage {
+ list-style-image: url("chrome://mozapps/skin/profile/profileicon.png");
+}
+
+#syncAddDeviceLabel {
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+#syncEnginesList {
+ height: 10em;
+}
+
+%endif
diff --git a/app/assets/updater.ini b/app/assets/updater.ini
new file mode 100644
index 0000000000..e68c8edc2e
--- /dev/null
+++ b/app/assets/updater.ini
@@ -0,0 +1,4 @@
+; This file is in the UTF-8 encoding
+[Strings]
+Title=Zotero Update
+Info=Zotero is installing your updates and will start in a few moments…
\ No newline at end of file
diff --git a/app/assets/win/skin/preferences/preferences.css b/app/assets/win/skin/preferences/preferences.css
new file mode 100644
index 0000000000..d36cb7c285
--- /dev/null
+++ b/app/assets/win/skin/preferences/preferences.css
@@ -0,0 +1,178 @@
+/*
+# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is the Firefox Preferences System.
+#
+# The Initial Developer of the Original Code is
+# Ben Goodger.
+# Portions created by the Initial Developer are Copyright (C) 2005
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Ben Goodger
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+*/
+
+/* Global Styles */
+#BrowserPreferences radio[pane] {
+ list-style-image: url("chrome://browser/skin/preferences/Options.png");
+ padding: 5px 3px 1px;
+}
+
+radio[pane=paneMain] {
+ -moz-image-region: rect(0, 32px, 32px, 0);
+}
+
+radio[pane=paneTabs] {
+ -moz-image-region: rect(0, 64px, 32px, 32px);
+}
+
+radio[pane=paneContent] {
+ -moz-image-region: rect(0, 96px, 32px, 64px);
+}
+
+radio[pane=paneApplications] {
+ -moz-image-region: rect(0, 128px, 32px, 96px);
+}
+
+radio[pane=panePrivacy] {
+ -moz-image-region: rect(0, 160px, 32px, 128px);
+}
+
+radio[pane=paneSecurity] {
+ -moz-image-region: rect(0, 192px, 32px, 160px);
+}
+
+radio[pane=paneAdvanced] {
+ -moz-image-region: rect(0, 224px, 32px, 192px);
+}
+
+%ifdef MOZ_SERVICES_SYNC
+radio[pane=paneSync] {
+ list-style-image: url("chrome://browser/skin/preferences/Options-sync.png") !important;
+}
+%endif
+
+/* Applications Pane */
+#BrowserPreferences[animated="true"] #handlersView {
+ height: 25em;
+}
+
+#BrowserPreferences[animated="false"] #handlersView {
+ -moz-box-flex: 1;
+}
+
+/* Privacy Pane */
+
+/* styles for the link elements copied from .text-link in global.css */
+.inline-link {
+ color: -moz-nativehyperlinktext;
+ text-decoration: underline;
+}
+
+.inline-link:not(:focus) {
+ outline: 1px dotted transparent;
+}
+
+/* Modeless Window Dialogs */
+.windowDialog,
+.windowDialog prefpane {
+ padding: 0;
+}
+
+.contentPane {
+ margin: 9px 8px 5px;
+}
+
+.actionButtons {
+ margin: 0 3px 6px !important;
+}
+
+/* Cookies Manager */
+#cookiesChildren::-moz-tree-image(domainCol) {
+ width: 16px;
+ height: 16px;
+ margin: 0 2px;
+ list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png") !important;
+}
+
+#cookiesChildren::-moz-tree-image(domainCol, container) {
+ list-style-image: url("chrome://global/skin/icons/folder-item.png") !important;
+ -moz-image-region: rect(0, 32px, 16px, 16px);
+}
+
+#cookiesChildren::-moz-tree-image(domainCol, container, open) {
+ -moz-image-region: rect(16px, 32px, 32px, 16px);
+}
+
+#cookieInfoBox {
+ border: 1px solid ThreeDShadow;
+ border-radius: 0;
+ margin: 4px;
+ padding: 0;
+}
+
+/* Advanced Pane */
+
+/* Adding padding-bottom prevents the bottom of the tabpanel from being cutoff
+ when browser.preferences.animateFadeIn = true */
+#advancedPrefs {
+ padding-bottom: 8px;
+}
+
+/* bottom-most box containing a groupbox in a prefpane. Prevents the bottom
+ of the groupbox from being cutoff */
+.bottomBox {
+ padding-bottom: 4px;
+}
+
+%ifdef MOZ_SERVICES_SYNC
+/* Sync Pane */
+
+#syncDesc {
+ padding: 0 8em;
+}
+
+.syncGroupBox {
+ padding: 10px;
+}
+
+#accountCaptionImage {
+ list-style-image: url("chrome://mozapps/skin/profile/profileicon.png");
+}
+
+#syncAddDeviceLabel {
+ margin-top: 1em;
+ margin-bottom: 1em;
+ }
+
+#syncEnginesList {
+ height: 11em;
+}
+
+%endif
diff --git a/app/build.sh b/app/build.sh
new file mode 100755
index 0000000000..3de6bbabcb
--- /dev/null
+++ b/app/build.sh
@@ -0,0 +1,869 @@
+#!/bin/bash -e
+
+# 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 .
+
+CALLDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+. "$CALLDIR/config.sh"
+
+if [ "`uname`" = "Darwin" ]; then
+ MAC_NATIVE=1
+else
+ MAC_NATIVE=0
+fi
+if [ "`uname -o 2> /dev/null`" = "Cygwin" ]; then
+ WIN_NATIVE=1
+else
+ WIN_NATIVE=0
+fi
+
+function usage {
+ cat >&2 <&1
+ exit 1
+ fi
+}
+
+function abspath {
+ echo $(cd $(dirname $1); pwd)/$(basename $1);
+}
+
+function check_lfs_file {
+ if [ "$(head --bytes 5 "$1")" = "versi" ]; then
+ echo "$1 not checked out -- install Git LFS and run 'git lfs pull'" >&2
+ exit 1
+ fi
+}
+
+SOURCE_DIR=""
+ZIP_FILE=""
+BUILD_MAC=0
+BUILD_WIN=0
+BUILD_LINUX=0
+PACKAGE=1
+DEVTOOLS=0
+quick_build=0
+while getopts "d:f:p:c:tseq" opt; do
+ case $opt in
+ d)
+ SOURCE_DIR="$OPTARG"
+ ;;
+ f)
+ ZIP_FILE="$OPTARG"
+ ;;
+ p)
+ for i in `seq 0 1 $((${#OPTARG}-1))`
+ do
+ case ${OPTARG:i:1} in
+ m) BUILD_MAC=1;;
+ w) BUILD_WIN=1;;
+ l) BUILD_LINUX=1;;
+ *)
+ echo "$0: Invalid platform option ${OPTARG:i:1}"
+ usage
+ ;;
+ esac
+ done
+ ;;
+ c)
+ UPDATE_CHANNEL="$OPTARG"
+ ;;
+ t)
+ DEVTOOLS=1
+ ;;
+ e)
+ SIGN=1
+ ;;
+ s)
+ PACKAGE=0
+ ;;
+ q)
+ quick_build=1
+ ;;
+ *)
+ usage
+ ;;
+ esac
+ shift $((OPTIND-1)); OPTIND=1
+done
+
+function check_xulrunner_hash {
+ platform=$1
+
+ if [ $platform == "m" ]; then
+ platform_hash_file="hash-mac"
+ elif [ $platform == "w" ]; then
+ platform_hash_file="hash-win"
+ elif [ $platform == "l" ]; then
+ platform_hash_file="hash-linux"
+ else
+ echo "Platform parameter incorrect. Acceptable values: m/w/l"
+ exit 1
+ fi
+
+ if [ ! -e "$CALLDIR/xulrunner/$platform_hash_file" ]; then
+ echo "xulrunner not found -- downloading"
+ echo
+ $CALLDIR/scripts/fetch_xulrunner -p $platform
+ else
+ recalculated_xulrunner_hash=$("$CALLDIR/scripts/xulrunner_hash" -p $platform)
+ current_xulrunner_hash=$(< "$CALLDIR/xulrunner/$platform_hash_file")
+ if [ "$current_xulrunner_hash" != "$recalculated_xulrunner_hash" ]; then
+ echo "xulrunner hashes don't match -- redownloading"
+ echo
+ "$CALLDIR/scripts/fetch_xulrunner" -p $platform
+ current_xulrunner_hash=$(< "$CALLDIR/xulrunner/$platform_hash_file")
+ if [ "$current_xulrunner_hash" != "$recalculated_xulrunner_hash" ]; then
+ echo "xulrunner hashes don't match after running fetch_xulrunner!"
+ exit 1
+ fi
+ fi
+ fi
+}
+
+#Check if xulrunner and GECKO_VERSION for each platform match
+if [ $BUILD_MAC == 1 ]; then
+ check_xulrunner_hash m
+fi
+if [ $BUILD_WIN == 1 ]; then
+ check_xulrunner_hash w
+fi
+if [ $BUILD_LINUX == 1 ]; then
+ check_xulrunner_hash l
+fi
+
+
+
+# Require source dir or ZIP file
+if [[ -z "$SOURCE_DIR" ]] && [[ -z "$ZIP_FILE" ]]; then
+ usage
+elif [[ -n "$SOURCE_DIR" ]] && [[ -n "$ZIP_FILE" ]]; then
+ usage
+fi
+
+# Require at least one platform
+if [[ $BUILD_MAC == 0 ]] && [[ $BUILD_WIN == 0 ]] && [[ $BUILD_LINUX == 0 ]]; then
+ usage
+fi
+
+if [[ -z "${ZOTERO_TEST:-}" ]] || [[ $ZOTERO_TEST == "0" ]]; then
+ include_tests=0
+else
+ include_tests=1
+fi
+
+# Bundle devtools with dev builds
+if [ "$UPDATE_CHANNEL" == "beta" ] || [ "$UPDATE_CHANNEL" == "dev" ] || [ "$UPDATE_CHANNEL" == "test" ]; then
+ DEVTOOLS=1
+fi
+if [ -z "$UPDATE_CHANNEL" ]; then UPDATE_CHANNEL="default"; fi
+BUILD_ID=`date +%Y%m%d%H%M%S`
+
+# Paths to Gecko runtimes
+MAC_RUNTIME_PATH="$CALLDIR/xulrunner/Firefox.app"
+WIN_RUNTIME_PATH_PREFIX="$CALLDIR/xulrunner/firefox-"
+LINUX_RUNTIME_PATH_PREFIX="$CALLDIR/xulrunner/firefox-"
+
+base_dir="$BUILD_DIR/base"
+app_dir="$BUILD_DIR/base/app"
+omni_dir="$BUILD_DIR/base/app/omni"
+
+shopt -s extglob
+mkdir -p "$app_dir"
+rm -rf "$STAGE_DIR"
+mkdir "$STAGE_DIR"
+rm -rf "$DIST_DIR"
+mkdir "$DIST_DIR"
+
+# Save build id, which is needed for updates manifest
+echo $BUILD_ID > "$DIST_DIR/build_id"
+
+cd "$app_dir"
+
+# Copy 'browser' files from Firefox
+#
+# omni.ja is left uncompressed within the Firefox application files by fetch_xulrunner
+#
+# TEMP: Also extract .hyf hyphenation files from the outer (still compressed) omni.ja
+# This works around https://bugzilla.mozilla.org/show_bug.cgi?id=1772900
+set +e
+if [ $BUILD_MAC == 1 ]; then
+ cp -Rp "$MAC_RUNTIME_PATH"/Contents/Resources/browser/omni "$app_dir"
+ unzip -qj "$MAC_RUNTIME_PATH"/Contents/Resources/omni.ja "hyphenation/*" -d "$app_dir"/hyphenation/
+elif [ $BUILD_WIN == 1 ]; then
+ # Non-arch-specific files, so just use 64-bit version
+ cp -Rp "${WIN_RUNTIME_PATH_PREFIX}win-x64"/browser/omni "$app_dir"
+ unzip -qj "${WIN_RUNTIME_PATH_PREFIX}win-x64"/omni.ja "hyphenation/*" -d "$app_dir"/hyphenation/
+elif [ $BUILD_LINUX == 1 ]; then
+ # Non-arch-specific files, so just use 64-bit version
+ cp -Rp "${LINUX_RUNTIME_PATH_PREFIX}x86_64"/browser/omni "$app_dir"
+ unzip -qj "${LINUX_RUNTIME_PATH_PREFIX}x86_64"/omni.ja "hyphenation/*" -d "$app_dir"/hyphenation/
+fi
+set -e
+cd $omni_dir
+# Move some Firefox files that would be overwritten out of the way
+mv chrome.manifest chrome.manifest-fx
+mv defaults defaults-fx
+
+# Extract Zotero files
+if [ -n "$ZIP_FILE" ]; then
+ ZIP_FILE="`abspath $ZIP_FILE`"
+ echo "Building from $ZIP_FILE"
+ unzip -q $ZIP_FILE -d "$omni_dir"
+else
+ rsync_params=""
+ if [ $include_tests -eq 0 ]; then
+ rsync_params="--exclude /test"
+ fi
+ rsync -a $rsync_params "$SOURCE_DIR/" ./
+fi
+
+mv defaults defaults-z
+mv defaults-fx defaults
+prefs_file=defaults/preferences/zotero.js
+
+# Transfer Firefox prefs, omitting some with undesirable overrides from the base prefs
+#
+# - network.captive-portal-service.enabled
+# Disable the captive portal check against Mozilla servers
+# - extensions.systemAddon.update.url
+egrep -v '(network.captive-portal-service.enabled|extensions.systemAddon.update.url)' defaults/preferences/firefox.js > $prefs_file
+rm defaults/preferences/firefox.js
+
+# Combine app and "extension" Zotero prefs
+echo "" >> $prefs_file
+echo "#" >> $prefs_file
+echo "# Zotero app prefs" >> $prefs_file
+echo "#" >> $prefs_file
+echo "" >> $prefs_file
+cat "$CALLDIR/assets/prefs.js" >> $prefs_file
+echo "" >> $prefs_file
+echo "# Zotero extension prefs" >> $prefs_file
+echo "" >> $prefs_file
+cat defaults-z/preferences/zotero.js >> $prefs_file
+
+rm -rf defaults-z
+
+# Platform-specific prefs
+if [ $BUILD_MAC == 1 ]; then
+ perl -pi -e 's/pref\("browser\.preferences\.instantApply", false\);/pref\("browser\.preferences\.instantApply", true);/' $prefs_file
+ perl -pi -e 's/%GECKO_VERSION%/'"$GECKO_VERSION_MAC"'/g' $prefs_file
+ # Fix horizontal mousewheel scrolling (this is set to 4 in the Fx60 .app greprefs.js, but
+ # defaults to 1 in later versions of Firefox, and needs to be 1 to work on macOS)
+ echo 'pref("mousewheel.with_shift.action", 1);' >> $prefs_file
+elif [ $BUILD_WIN == 1 ]; then
+ perl -pi -e 's/%GECKO_VERSION%/'"$GECKO_VERSION_WIN"'/g' $prefs_file
+elif [ $BUILD_LINUX == 1 ]; then
+ # Modify platform-specific prefs
+ perl -pi -e 's/pref\("browser\.preferences\.instantApply", false\);/pref\("browser\.preferences\.instantApply", true);/' $prefs_file
+ perl -pi -e 's/%GECKO_VERSION%/'"$GECKO_VERSION_LINUX"'/g' $prefs_file
+fi
+
+# Clear list of built-in add-ons
+echo '{"dictionaries": {"en-US": "dictionaries/en-US.dic"}, "system": []}' > chrome/browser/content/browser/built_in_addons.json
+
+# chrome.manifest
+mv chrome.manifest zotero.manifest
+mv chrome.manifest-fx chrome.manifest
+# TEMP
+#echo "manifest zotero.manifest" >> "$base_dir/chrome.manifest"
+cat zotero.manifest >> chrome.manifest
+rm zotero.manifest
+
+# Update channel
+perl -pi -e 's/pref\("app\.update\.channel", "[^"]*"\);/pref\("app\.update\.channel", "'"$UPDATE_CHANNEL"'");/' $prefs_file
+echo -n "Channel: "
+grep app.update.channel $prefs_file
+echo
+
+# Add devtools prefs
+if [ $DEVTOOLS -eq 1 ]; then
+ echo >> $prefs_file
+ echo "// Dev Tools" >> $prefs_file
+ echo 'pref("devtools.debugger.remote-enabled", true);' >> $prefs_file
+ echo 'pref("devtools.debugger.remote-port", 6100);' >> $prefs_file
+ if [ $UPDATE_CHANNEL != "beta" ]; then
+ echo 'pref("devtools.debugger.prompt-connection", false);' >> $prefs_file
+ fi
+fi
+
+# 5.0.96.3 / 5.0.97-beta.37+ddc7be75c
+VERSION=`cat version`
+# 5.0.96 / 5.0.97
+VERSION_NUMERIC=`perl -ne 'print and last if s/^(\d+\.\d+(\.\d+)?).*/\1/;' version`
+if [ -z "$VERSION" ]; then
+ echo "Version number not found in version file"
+ exit 1
+fi
+rm version
+
+echo
+echo "Version: $VERSION"
+
+# Delete Mozilla signing info if present
+rm -rf META-INF
+
+# Copy branding
+cp -R "$CALLDIR"/assets/branding/locale/brand.{dtd,properties} chrome/en-US/locale/branding/
+
+# Copy browser localization .ftl files
+for locale in `ls chrome/locale/`; do
+ mkdir -p localization/$locale/branding
+ cp "$CALLDIR/assets/branding/locale/brand.ftl" localization/$locale/branding/brand.ftl
+
+ mkdir -p localization/$locale/toolkit/global
+ cp chrome/locale/$locale/zotero/mozilla/textActions.ftl localization/$locale/toolkit/global
+ cp chrome/locale/$locale/zotero/mozilla/wizard.ftl localization/$locale/toolkit/global
+
+ mkdir -p localization/$locale/browser
+ cp chrome/locale/$locale/zotero/mozilla/browserSets.ftl localization/$locale/browser
+ cp chrome/locale/$locale/zotero/mozilla/menubar.ftl localization/$locale/browser
+
+ mkdir -p localization/$locale/devtools/client
+ cp chrome/locale/$locale/zotero/mozilla/toolbox.ftl localization/$locale/devtools/client
+
+ # TEMP: Until we've created zotero.ftl in all locales
+ touch chrome/locale/$locale/zotero/zotero.ftl
+ cp chrome/locale/$locale/zotero/*.ftl localization/$locale/
+done
+
+# Add to chrome manifest
+echo "" >> chrome.manifest
+cat "$CALLDIR/assets/chrome.manifest" >> chrome.manifest
+
+replace_line 'handle: function bch_handle\(cmdLine\) {' 'handle: function bch_handle(cmdLine) {
+ \/\/ TEST_OPTIONS_PLACEHOLDER
+ ' modules/BrowserContentHandler.sys.mjs
+export CALLDIR && perl -pi -e 'BEGIN { local $/; open $fh, "$ENV{CALLDIR}/assets/commandLineHandler.js"; $replacement = <$fh>; close $fh; } s/\/\/ TEST_OPTIONS_PLACEHOLDER/$replacement/' modules/BrowserContentHandler.sys.mjs
+
+# Move test files to root directory
+if [ $include_tests -eq 1 ]; then
+ cat test/chrome.manifest >> chrome.manifest
+ rm test/chrome.manifest
+ cp -R test/tests "$base_dir/tests"
+fi
+
+# Copy platform-specific assets
+if [ $BUILD_MAC == 1 ]; then
+ rsync -a "$CALLDIR/assets/mac/" ./
+elif [ $BUILD_WIN == 1 ]; then
+ rsync -a "$CALLDIR/assets/win/" ./
+elif [ $BUILD_LINUX == 1 ]; then
+ rsync -a "$CALLDIR/assets/unix/" ./
+fi
+
+# Add word processor plug-ins
+echo >> chrome.manifest
+if [ $BUILD_MAC == 1 ]; then
+ pluginDir="$CALLDIR/modules/zotero-word-for-mac-integration"
+ mkdir -p "integration/word-for-mac"
+ cp -RH "$pluginDir/components" \
+ "$pluginDir/resource" \
+ "$pluginDir/chrome.manifest" \
+ "integration/word-for-mac"
+ echo -n "Word for Mac plugin version: "
+ cat "integration/word-for-mac/resource/version.txt"
+ echo
+ echo >> $prefs_file
+ cat "$CALLDIR/modules/zotero-word-for-mac-integration/defaults/preferences/zoteroMacWordIntegration.js" >> $prefs_file
+ echo >> $prefs_file
+
+ echo "manifest integration/word-for-mac/chrome.manifest" >> chrome.manifest
+
+elif [ $BUILD_WIN == 1 ]; then
+ pluginDir="$CALLDIR/modules/zotero-word-for-windows-integration"
+ mkdir -p "integration/word-for-windows"
+ cp -RH "$pluginDir/components" \
+ "$pluginDir/resource" \
+ "$pluginDir/chrome.manifest" \
+ "integration/word-for-windows"
+ echo -n "Word for Windows plugin version: "
+ cat "integration/word-for-windows/resource/version.txt"
+ echo
+ echo >> $prefs_file
+ cat "$CALLDIR/modules/zotero-word-for-windows-integration/defaults/preferences/zoteroWinWordIntegration.js" >> $prefs_file
+ echo >> $prefs_file
+
+ echo "manifest integration/word-for-windows/chrome.manifest" >> chrome.manifest
+fi
+# Libreoffice plugin for all platforms
+pluginDir="$CALLDIR/modules/zotero-libreoffice-integration"
+mkdir -p "integration/libreoffice"
+cp -RH "$pluginDir/chrome" \
+ "$pluginDir/components" \
+ "$pluginDir/resource" \
+ "$pluginDir/chrome.manifest" \
+ "integration/libreoffice"
+echo -n "LibreOffice plugin version: "
+cat "integration/libreoffice/resource/version.txt"
+echo
+echo >> $prefs_file
+cat "$CALLDIR/modules/zotero-libreoffice-integration/defaults/preferences/zoteroLibreOfficeIntegration.js" >> $prefs_file
+echo >> $prefs_file
+
+echo "manifest integration/libreoffice/chrome.manifest" >> chrome.manifest
+
+# Delete files that shouldn't be distributed
+find chrome -name .DS_Store -exec rm -f {} \;
+
+# Zip browser and Zotero files into omni.ja
+if [ $quick_build -eq 1 ]; then
+ # If quick build, don't compress or optimize
+ zip -qrXD omni.ja *
+else
+ zip -qr9XD omni.ja *
+ python3 "$CALLDIR/scripts/optimizejars.py" --optimize ./ ./ ./
+fi
+
+mv omni.ja ..
+cd "$CALLDIR"
+rm -rf "$omni_dir"
+
+# Copy updater.ini
+cp "$CALLDIR/assets/updater.ini" "$base_dir"
+
+# Adjust chrome.manifest
+#perl -pi -e 's^(chrome|resource)/^jar:zotero.jar\!/$1/^g' "$BUILD_DIR/zotero/chrome.manifest"
+
+# Copy application.ini and modify
+cp "$CALLDIR/assets/application.ini" "$app_dir/application.ini"
+perl -pi -e "s/\{\{VERSION}}/$VERSION/" "$app_dir/application.ini"
+perl -pi -e "s/\{\{BUILDID}}/$BUILD_ID/" "$app_dir/application.ini"
+
+# Remove unnecessary files
+find "$BUILD_DIR" -name .DS_Store -exec rm -f {} \;
+
+# Mac
+if [ $BUILD_MAC == 1 ]; then
+ echo 'Building Zotero.app'
+
+ # Set up directory structure
+ APPDIR="$STAGE_DIR/Zotero.app"
+ rm -rf "$APPDIR"
+ mkdir "$APPDIR"
+ chmod 755 "$APPDIR"
+ cp -r "$CALLDIR/mac/Contents" "$APPDIR"
+ CONTENTSDIR="$APPDIR/Contents"
+
+ # Merge relevant assets from Firefox
+ mkdir "$CONTENTSDIR/MacOS"
+ cp -r "$MAC_RUNTIME_PATH/Contents/MacOS/"!(firefox|firefox-bin|crashreporter.app|pingsender|updater.app) "$CONTENTSDIR/MacOS"
+ cp -r "$MAC_RUNTIME_PATH/Contents/Resources/"!(application.ini|browser|defaults|precomplete|removed-files|updater.ini|update-settings.ini|webapprt*|*.icns|*.lproj) "$CONTENTSDIR/Resources"
+
+ # Use our own launcher
+ check_lfs_file "$CALLDIR/mac/zotero.xz"
+ xz -d --stdout "$CALLDIR/mac/zotero.xz" > "$CONTENTSDIR/MacOS/zotero"
+ chmod 755 "$CONTENTSDIR/MacOS/zotero"
+
+ # TEMP: Custom version of XUL with some backported Mozilla bug fixes
+ cp "$MAC_RUNTIME_PATH/../MacOS/XUL" "$CONTENTSDIR/MacOS/"
+
+ # Use our own updater, because Mozilla's requires updates signed by Mozilla
+ cd "$CONTENTSDIR/MacOS"
+ check_lfs_file "$CALLDIR/mac/updater.tar.xz"
+ tar xf "$CALLDIR/mac/updater.tar.xz"
+
+ # Modify Info.plist
+ perl -pi -e "s/\{\{VERSION\}\}/$VERSION/" "$CONTENTSDIR/Info.plist"
+ perl -pi -e "s/\{\{VERSION_NUMERIC\}\}/$VERSION_NUMERIC/" "$CONTENTSDIR/Info.plist"
+ if [ $UPDATE_CHANNEL == "beta" ] || [ $UPDATE_CHANNEL == "dev" ] || [ $UPDATE_CHANNEL == "source" ]; then
+ perl -pi -e "s/org\.zotero\.zotero/org.zotero.zotero-$UPDATE_CHANNEL/" "$CONTENTSDIR/Info.plist"
+ fi
+ perl -pi -e "s/\{\{VERSION\}\}/$VERSION/" "$CONTENTSDIR/Info.plist"
+ # Needed for "monkeypatch" Windows builds:
+ # http://www.nntp.perl.org/group/perl.perl5.porters/2010/08/msg162834.html
+ rm -f "$CONTENTSDIR/Info.plist.bak"
+
+ echo
+ grep -B 1 org.zotero.zotero "$CONTENTSDIR/Info.plist"
+ echo
+ grep -A 1 CFBundleShortVersionString "$CONTENTSDIR/Info.plist"
+ echo
+ grep -A 1 CFBundleVersion "$CONTENTSDIR/Info.plist"
+ echo
+
+ # Copy app files
+ rsync -a "$base_dir/" "$CONTENTSDIR/Resources/"
+
+ # Add word processor plug-ins
+ mkdir "$CONTENTSDIR/Resources/integration"
+ cp -RH "$CALLDIR/modules/zotero-libreoffice-integration/install" "$CONTENTSDIR/Resources/integration/libreoffice"
+ cp -RH "$CALLDIR/modules/zotero-word-for-mac-integration/install" "$CONTENTSDIR/Resources/integration/word-for-mac"
+
+ # Delete extraneous files
+ find "$CONTENTSDIR" -depth -type d -name .git -exec rm -rf {} \;
+ find "$CONTENTSDIR" \( -name .DS_Store -or -name update.rdf \) -exec rm -f {} \;
+
+ # Copy over removed-files and make a precomplete file here since it needs to be stable for the
+ # signature. This is done in build_autocomplete.sh for other platforms.
+ cp "$CALLDIR/update-packaging/removed-files_mac" "$CONTENTSDIR/Resources/removed-files"
+ touch "$CONTENTSDIR/Resources/precomplete"
+
+ # Sign
+ if [ $SIGN == 1 ]; then
+ # Unlock keychain if a password is provided (necessary for building from a shell)
+ if [ -n "$KEYCHAIN_PASSWORD" ]; then
+ security -v unlock-keychain -p "$KEYCHAIN_PASSWORD" ~/Library/Keychains/$KEYCHAIN.keychain-db
+ fi
+ # Clear extended attributes, which can cause codesign to fail
+ /usr/bin/xattr -cr "$APPDIR"
+
+ # Sign app
+ entitlements_file="$CALLDIR/mac/entitlements.xml"
+ /usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" \
+ "$APPDIR/Contents/MacOS/XUL" \
+ "$APPDIR/Contents/MacOS/updater.app/Contents/MacOS/org.mozilla.updater"
+ find "$APPDIR/Contents" -name '*.dylib' -exec /usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" {} \;
+ find "$APPDIR/Contents" -name '*.app' -exec /usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" {} \;
+ /usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" "$APPDIR/Contents/MacOS/zotero"
+
+ # Bundle and sign Safari App Extension
+ #
+ # Even though it's signed by Xcode, we sign it again to make sure it matches the parent app signature
+ if [[ -n "$SAFARI_APPEX" ]] && [[ -d "$SAFARI_APPEX" ]]; then
+ echo
+ # Extract entitlements, which differ from parent app
+ /usr/bin/codesign -d --entitlements :"$BUILD_DIR/safari-entitlements.plist" $SAFARI_APPEX
+ mkdir "$APPDIR/Contents/PlugIns"
+ cp -R $SAFARI_APPEX "$APPDIR/Contents/PlugIns/ZoteroSafariExtension.appex"
+ # Add suffix to appex bundle identifier
+ if [ $UPDATE_CHANNEL == "beta" ] || [ $UPDATE_CHANNEL == "dev" ] || [ $UPDATE_CHANNEL == "source" ]; then
+ perl -pi -e "s/org\.zotero\.SafariExtensionApp\.SafariExtension/org.zotero.SafariExtensionApp.SafariExtension-$UPDATE_CHANNEL/" "$APPDIR/Contents/PlugIns/ZoteroSafariExtension.appex/Contents/Info.plist"
+ fi
+ find "$APPDIR/Contents/PlugIns/ZoteroSafariExtension.appex/Contents" -name '*.dylib' -exec /usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" {} \;
+ /usr/bin/codesign --force --options runtime --entitlements "$BUILD_DIR/safari-entitlements.plist" --sign "$DEVELOPER_ID" "$APPDIR/Contents/PlugIns/ZoteroSafariExtension.appex"
+ fi
+
+ # Sign final app package
+ echo
+ /usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" "$APPDIR"
+
+ # Verify app
+ /usr/bin/codesign --verify -vvvv "$APPDIR"
+ # Verify Safari App Extension
+ if [[ -n "$SAFARI_APPEX" ]] && [[ -d "$SAFARI_APPEX" ]]; then
+ echo
+ /usr/bin/codesign --verify -vvvv "$APPDIR/Contents/PlugIns/ZoteroSafariExtension.appex"
+ fi
+ fi
+
+ # Build and notarize disk image
+ if [ $PACKAGE == 1 ]; then
+ if [ $MAC_NATIVE == 1 ]; then
+ echo "Creating Mac installer"
+ dmg="$DIST_DIR/Zotero-$VERSION.dmg"
+ "$CALLDIR/mac/pkg-dmg" --source "$STAGE_DIR/Zotero.app" \
+ --target "$dmg" \
+ --sourcefile --volname Zotero --copy "$CALLDIR/mac/DSStore:/.DS_Store" \
+ --symlink /Applications:"/Drag Here to Install" > /dev/null
+
+ if [ "$UPDATE_CHANNEL" != "test" ]; then
+ # Upload disk image to Apple
+ "$CALLDIR/scripts/notarize_mac_app" "$dmg"
+ echo
+
+ # Staple notarization info to disk image
+ "$CALLDIR/scripts/notarization_stapler" "$dmg"
+
+ echo "Notarization complete"
+ else
+ echo "Test build -- skipping notarization"
+ fi
+ echo
+ else
+ echo 'Not building on Mac; creating Mac distribution as a zip file'
+ rm -f "$DIST_DIR/Zotero_mac.zip"
+ cd "$STAGE_DIR" && zip -rqX "$DIST_DIR/Zotero-${VERSION}_mac.zip" Zotero.app
+ fi
+ fi
+fi
+
+# Windows
+if [ $BUILD_WIN == 1 ]; then
+ echo "Building Windows common"
+
+ COMMON_APPDIR="$STAGE_DIR/Zotero_common"
+ mkdir "$COMMON_APPDIR"
+
+ # Package non-arch-specific components
+ if [ $PACKAGE -eq 1 ]; then
+ # Copy installer files
+ cp -r "$CALLDIR/win/installer" "$BUILD_DIR/win_installer"
+
+ perl -pi -e "s/\{\{VERSION}}/$VERSION/" "$BUILD_DIR/win_installer/defines.nsi"
+ mkdir "$COMMON_APPDIR/uninstall"
+
+ # Compress 7zSD.sfx
+ upx --best -o "`cygpath -w \"$BUILD_DIR/7zSD.sfx\"`" \
+ "`cygpath -w \"$CALLDIR/win/installer/7zstub/firefox/7zSD.sfx\"`" > /dev/null
+
+ fi
+
+ for arch in win32 win-x64 win-aarch64; do
+ echo "Building Zotero_$arch"
+
+ runtime_path="${WIN_RUNTIME_PATH_PREFIX}${arch}"
+
+ # Set up directory
+ APPDIR="$STAGE_DIR/Zotero_$arch"
+ mkdir "$APPDIR"
+
+ # Copy relevant assets from Firefox
+ cp -R "$runtime_path"/!(application.ini|browser|defaults|devtools-files|crashreporter*|firefox.exe|maintenanceservice*|precomplete|removed-files|uninstall|update*) "$APPDIR"
+
+ # Copy zotero.exe, which is built directly from Firefox source and then modified by
+ # ResourceHacker to add icons
+ check_lfs_file "$CALLDIR/win/zotero.exe.tar.xz"
+ tar xf "$CALLDIR/win/zotero.exe.tar.xz" --to-stdout zotero_$arch.exe > "$APPDIR/zotero.exe"
+
+ # Update .exe version number (only possible on Windows)
+ if [ $WIN_NATIVE == 1 ]; then
+ # FileVersion is limited to four integers, so it won't be properly updated for non-release
+ # builds (e.g., we'll show 5.0.97.0 for 5.0.97-beta.37). ProductVersion will be the full
+ # version string.
+ rcedit "`cygpath -w \"$APPDIR/zotero.exe\"`" \
+ --set-file-version "$VERSION_NUMERIC" \
+ --set-product-version "$VERSION"
+ fi
+
+ # Use our own updater, because Mozilla's requires updates signed by Mozilla
+ check_lfs_file "$CALLDIR/win/updater.exe.tar.xz"
+ tar xf "$CALLDIR/win/updater.exe.tar.xz" --to-stdout updater-$arch.exe > "$APPDIR/updater.exe"
+ cat "$CALLDIR/win/installer/updater_append.ini" >> "$APPDIR/updater.ini"
+
+ # Sign updater
+ if [ $SIGN -eq 1 ]; then
+ "$CALLDIR/win/codesign" "$APPDIR/updater.exe" "$SIGNATURE_DESC Updater"
+ fi
+
+ # Copy app files
+ rsync -a "$base_dir/" "$APPDIR/"
+ #mv "$APPDIR/app/application.ini" "$APPDIR/"
+
+ # Copy in common files
+ rsync -a "$COMMON_APPDIR/" "$APPDIR/"
+
+ # Add devtools
+ #if [ $DEVTOOLS -eq 1 ]; then
+ # # Create devtools.jar
+ # cd "$BUILD_DIR"
+ # mkdir -p devtools/locale
+ # cp -r "$runtime_path"/devtools-files/chrome/devtools/* devtools/
+ # cp -r "$runtime_path"/devtools-files/chrome/locale/* devtools/locale/
+ # cd devtools
+ # zip -r -q ../devtools.jar *
+ # cd ..
+ # rm -rf devtools
+ # mv devtools.jar "$APPDIR"
+ #
+ # cp "$runtime_path/devtools-files/components/interfaces.xpt" "$APPDIR/components/"
+ #fi
+
+ # Add word processor plug-ins
+ mkdir -p "$APPDIR/integration"
+ cp -RH "$CALLDIR/modules/zotero-libreoffice-integration/install" "$APPDIR/integration/libreoffice"
+ cp -RH "$CALLDIR/modules/zotero-word-for-windows-integration/install" "$APPDIR/integration/word-for-windows"
+ if [ $arch = 'win32' ]; then
+ rm "$APPDIR/integration/word-for-windows/libzoteroWinWordIntegration_x64.dll"
+ rm "$APPDIR/integration/word-for-windows/libzoteroWinWordIntegration_ARM64.dll"
+ elif [ $arch = 'win-x64' ]; then
+ mv "$APPDIR/integration/word-for-windows/libzoteroWinWordIntegration_x64.dll" \
+ "$APPDIR/integration/word-for-windows/libzoteroWinWordIntegration.dll"
+ rm "$APPDIR/integration/word-for-windows/libzoteroWinWordIntegration_ARM64.dll"
+ elif [ $arch = 'win-aarch64' ]; then
+ mv "$APPDIR/integration/word-for-windows/libzoteroWinWordIntegration_ARM64.dll" \
+ "$APPDIR/integration/word-for-windows/libzoteroWinWordIntegration.dll"
+ rm "$APPDIR/integration/word-for-windows/libzoteroWinWordIntegration_x64.dll"
+ fi
+
+ # Delete extraneous files
+ find "$APPDIR" -depth -type d -name .git -exec rm -rf {} \;
+ find "$APPDIR" \( -name .DS_Store -or -name '.git*' -or -name '.travis.yml' -or -name update.rdf -or -name '*.bak' \) -exec rm -f {} \;
+ find "$APPDIR" \( -name '*.exe' -or -name '*.dll' \) -exec chmod 755 {} \;
+
+ if [ $PACKAGE -eq 1 ]; then
+ if [ $WIN_NATIVE -eq 1 ]; then
+ echo "Creating Windows installer"
+ # Build uninstaller
+ if [ "$arch" = "win32" ]; then
+ "`cygpath -u \"${NSIS_DIR}makensis.exe\"`" /V1 "`cygpath -w \"$BUILD_DIR/win_installer/uninstaller.nsi\"`"
+ elif [ "$arch" = "win-x64" ]; then
+ "`cygpath -u \"${NSIS_DIR}makensis.exe\"`" /DHAVE_64BIT_OS /V1 "`cygpath -w \"$BUILD_DIR/win_installer/uninstaller.nsi\"`"
+ elif [ "$arch" = "win-aarch64" ]; then
+ # TODO
+ "`cygpath -u \"${NSIS_DIR}makensis.exe\"`" /DHAVE_64BIT_OS /V1 "`cygpath -w \"$BUILD_DIR/win_installer/uninstaller.nsi\"`"
+ fi
+
+ mv "$BUILD_DIR/win_installer/helper.exe" "$APPDIR/uninstall"
+
+ if [ $SIGN -eq 1 ]; then
+ "$CALLDIR/win/codesign" "$APPDIR/uninstall/helper.exe" "$SIGNATURE_DESC Uninstaller"
+ sleep $SIGNTOOL_DELAY
+ fi
+
+
+ if [ "$arch" = "win32" ]; then
+ INSTALLER_PATH="$DIST_DIR/Zotero-${VERSION}_win32_setup.exe"
+ elif [ "$arch" = "win-x64" ]; then
+ INSTALLER_PATH="$DIST_DIR/Zotero-${VERSION}_x64_setup.exe"
+ elif [ "$arch" = "win-aarch64" ]; then
+ INSTALLER_PATH="$DIST_DIR/Zotero-${VERSION}_aarch64_setup.exe"
+ fi
+
+ if [ $SIGN -eq 1 ]; then
+ "$CALLDIR/win/codesign" "$APPDIR/zotero.exe" "$SIGNATURE_DESC"
+ sleep $SIGNTOOL_DELAY
+ fi
+
+ # Stage installer
+ INSTALLER_STAGE_DIR="$BUILD_DIR/win_installer/staging"
+ rm -rf "$INSTALLER_STAGE_DIR"
+ mkdir "$INSTALLER_STAGE_DIR"
+ cp -r "$APPDIR" "$INSTALLER_STAGE_DIR/core"
+
+ # Build and sign setup.exe
+ if [ "$arch" = "win32" ]; then
+ "`cygpath -u \"${NSIS_DIR}makensis.exe\"`" /V1 "`cygpath -w \"$BUILD_DIR/win_installer/installer.nsi\"`"
+ elif [ "$arch" = "win-x64" ]; then
+ "`cygpath -u \"${NSIS_DIR}makensis.exe\"`" /DHAVE_64BIT_OS /V1 "`cygpath -w \"$BUILD_DIR/win_installer/installer.nsi\"`"
+ elif [ "$arch" = "win-aarch64" ]; then
+ # TODO
+ "`cygpath -u \"${NSIS_DIR}makensis.exe\"`" /DHAVE_64BIT_OS /V1 "`cygpath -w \"$BUILD_DIR/win_installer/installer.nsi\"`"
+ fi
+
+ mv "$BUILD_DIR/win_installer/setup.exe" "$INSTALLER_STAGE_DIR"
+
+ if [ $SIGN == 1 ]; then
+ "$CALLDIR/win/codesign" "$INSTALLER_STAGE_DIR/setup.exe" "$SIGNATURE_DESC Setup"
+ sleep $SIGNTOOL_DELAY
+ fi
+
+ # Compress application
+ cd "$INSTALLER_STAGE_DIR" && 7z a -r -t7z "`cygpath -w \"$BUILD_DIR/app_$arch.7z\"`" \
+ -mx -m0=BCJ2 -m1=LZMA:d24 -m2=LZMA:d19 -m3=LZMA:d19 -mb0:1 -mb0s1:2 -mb0s2:3 > /dev/null
+
+ # Combine 7zSD.sfx and app.tag into setup.exe
+ cat "$BUILD_DIR/7zSD.sfx" "$CALLDIR/win/installer/app.tag" \
+ "$BUILD_DIR/app_$arch.7z" > "$INSTALLER_PATH"
+
+ # Sign installer .exe
+ if [ $SIGN == 1 ]; then
+ "$CALLDIR/win/codesign" "$INSTALLER_PATH" "$SIGNATURE_DESC Installer"
+ fi
+
+ chmod 755 "$INSTALLER_PATH"
+ else
+ echo 'Not building on Windows; only building zip file'
+ fi
+ cd "$STAGE_DIR"
+ zip -rqX "$DIST_DIR/Zotero-${VERSION}_$arch.zip" Zotero_$arch
+ fi
+ done
+
+ rm -rf "$COMMON_APPDIR"
+fi
+
+# Linux
+if [ $BUILD_LINUX == 1 ]; then
+ # Skip 32-bit build in tests
+ if [[ "${ZOTERO_TEST:-}" = "1" ]] || [[ "${SKIP_32:-}" = "1" ]]; then
+ archs="x86_64"
+ else
+ archs="i686 x86_64"
+ fi
+
+ for arch in $archs; do
+ runtime_path="${LINUX_RUNTIME_PATH_PREFIX}${arch}"
+
+ # Set up directory
+ echo 'Building Zotero_linux-'$arch
+ APPDIR="$STAGE_DIR/Zotero_linux-$arch"
+ rm -rf "$APPDIR"
+ mkdir "$APPDIR"
+
+ # Merge relevant assets from Firefox
+ cp -r "$runtime_path/"!(application.ini|browser|defaults|devtools-files|crashreporter|crashreporter.ini|firefox|pingsender|precomplete|removed-files|run-mozilla.sh|update-settings.ini|updater|updater.ini) "$APPDIR"
+
+ # Use our own launcher that calls the original Firefox executable with -app
+ mv "$APPDIR"/firefox-bin "$APPDIR"/zotero-bin
+ cp "$CALLDIR/linux/zotero" "$APPDIR"/zotero
+
+ # Copy Ubuntu launcher files
+ cp "$CALLDIR/linux/zotero.desktop" "$APPDIR"
+ cp "$CALLDIR/linux/set_launcher_icon" "$APPDIR"
+
+ # Use our own updater, because Mozilla's requires updates signed by Mozilla
+ check_lfs_file "$CALLDIR/linux/updater.tar.xz"
+ tar xf "$CALLDIR/linux/updater.tar.xz" --to-stdout updater-$arch > "$APPDIR/updater"
+ chmod 755 "$APPDIR/updater"
+
+ # Copy app files
+ rsync -a "$base_dir/" "$APPDIR/"
+
+ # Add word processor plug-ins
+ mkdir "$APPDIR/integration"
+ cp -RH "$CALLDIR/modules/zotero-libreoffice-integration/install" "$APPDIR/integration/libreoffice"
+
+ # Copy icons
+ cp "$CALLDIR/linux/icons/icon32.png" "$APPDIR/icons/"
+ cp "$CALLDIR/linux/icons/icon64.png" "$APPDIR/icons/"
+ cp "$CALLDIR/linux/icons/icon128.png" "$APPDIR/icons/"
+ cp "$CALLDIR/linux/icons/symbolic.svg" "$APPDIR/icons/"
+
+ # Delete extraneous files
+ find "$APPDIR" -depth -type d -name .git -exec rm -rf {} \;
+ find "$APPDIR" \( -name .DS_Store -or -name update.rdf \) -exec rm -f {} \;
+
+ if [ $PACKAGE == 1 ]; then
+ # Create tar
+ rm -f "$DIST_DIR/Zotero-${VERSION}_linux-$arch.tar.bz2"
+ cd "$STAGE_DIR"
+ tar -cjf "$DIST_DIR/Zotero-${VERSION}_linux-$arch.tar.bz2" "Zotero_linux-$arch"
+ fi
+ done
+fi
+
+rm -rf $BUILD_DIR
diff --git a/app/config.sh b/app/config.sh
new file mode 100644
index 0000000000..e1c53dcf3d
--- /dev/null
+++ b/app/config.sh
@@ -0,0 +1,68 @@
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# Version of Gecko to build with
+GECKO_VERSION_MAC="115.9.1esr"
+GECKO_VERSION_LINUX="115.9.1esr"
+GECKO_VERSION_WIN="115.9.1esr"
+RUST_VERSION=1.69.0
+
+# URL prefix for custom builds of Firefox components
+custom_components_url="https://download.zotero.org/dev/firefox-components/"
+custom_components_hash_mac="bec8e3adebf8d5021f1f35fd2c65d752d4979839cbdd1ee1aa4b1d3d5ba0953b"
+
+APP_NAME="Zotero"
+APP_ID="zotero\@zotero.org"
+
+# Whether to sign builds
+SIGN=0
+
+# OS X Developer ID certificate information
+DEVELOPER_ID=F0F1FE48DB909B263AC51C8215374D87FDC12121
+# Keychain and keychain password, if not building via the GUI
+KEYCHAIN=""
+KEYCHAIN_PASSWORD=""
+NOTARIZATION_BUNDLE_ID=""
+NOTARIZATION_USER=""
+NOTARIZATION_TEAM_ID=""
+NOTARIZATION_PASSWORD=""
+
+# Paths for Windows installer build
+NSIS_DIR='C:\Program Files (x86)\NSIS\'
+
+SIGNTOOL_DELAY=5
+
+# Directory for unpacked binaries
+STAGE_DIR="$DIR/staging"
+# Directory for packed binaries
+DIST_DIR="$DIR/dist"
+
+SOURCE_REPO_URL="https://github.com/zotero/zotero"
+S3_BUCKET="zotero-download"
+S3_CI_ZIP_PATH="ci/client"
+S3_DIST_PATH="client"
+
+DEPLOY_HOST="deploy.zotero"
+DEPLOY_PATH="www/www-production/public/download/client/manifests"
+DEPLOY_CMD="ssh $DEPLOY_HOST update-site-files"
+
+BUILD_PLATFORMS=""
+NUM_INCREMENTALS=6
+
+if [ "`uname`" = "Darwin" ]; then
+ shopt -s expand_aliases
+fi
+
+if [ "`uname -o 2> /dev/null`" = "Cygwin" ]; then
+ export WIN_NATIVE=1
+else
+ export WIN_NATIVE=0
+fi
+
+# Make utilities (mar/mbsdiff) available in the path
+PATH="$DIR/xulrunner/bin:$PATH"
+
+if [ -f "$DIR/config-custom.sh" ]; then
+ . "$DIR/config-custom.sh"
+fi
+
+unset DIR
diff --git a/app/linux/icons/icon128.png b/app/linux/icons/icon128.png
new file mode 100644
index 0000000000..4a615089e4
Binary files /dev/null and b/app/linux/icons/icon128.png differ
diff --git a/app/linux/icons/icon32.png b/app/linux/icons/icon32.png
new file mode 100644
index 0000000000..21ad5ded22
Binary files /dev/null and b/app/linux/icons/icon32.png differ
diff --git a/app/linux/icons/icon64.png b/app/linux/icons/icon64.png
new file mode 100644
index 0000000000..c15b15697f
Binary files /dev/null and b/app/linux/icons/icon64.png differ
diff --git a/app/linux/icons/symbolic.svg b/app/linux/icons/symbolic.svg
new file mode 100644
index 0000000000..5885fdba06
--- /dev/null
+++ b/app/linux/icons/symbolic.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/linux/mozconfig b/app/linux/mozconfig
new file mode 100644
index 0000000000..8e7fa5cf88
--- /dev/null
+++ b/app/linux/mozconfig
@@ -0,0 +1,27 @@
+# Uncomment to build 32-bit
+#ac_add_options --target=i686
+
+ac_add_options --enable-bootstrap
+
+mk_add_options AUTOCLOBBER=1
+
+# These don't all affect the stub, but they can't hurt, and we'll want them if
+# we switch to custom XUL builds
+ac_add_options MOZ_ENABLE_JS_DUMP=1
+ac_add_options MOZ_ENABLE_FORKSERVER=
+ac_add_options MOZ_TELEMETRY_REPORTING=
+ac_add_options MOZ_DATA_REPORTING=
+ac_add_options --disable-tests
+ac_add_options --disable-debug
+ac_add_options --disable-debug-symbols
+ac_add_options --disable-webrtc
+ac_add_options --disable-eme
+
+export MOZILLA_OFFICIAL=1
+export RELEASE_OR_BETA=1
+MOZ_REQUIRE_SIGNING=
+
+ac_add_options --enable-official-branding
+
+# Build updater without MAR signature verification
+ac_add_options --disable-verify-mar
diff --git a/app/linux/set_launcher_icon b/app/linux/set_launcher_icon
new file mode 100755
index 0000000000..002377be85
--- /dev/null
+++ b/app/linux/set_launcher_icon
@@ -0,0 +1,13 @@
+#!/bin/bash -e
+
+#
+# Run this to update the launcher file with the current path to the application icon
+#
+
+APPDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+if [ -w "$APPDIR"/zotero.desktop ]; then
+ sed -i -e "s@^Icon=.*@Icon=$APPDIR/icons/icon128.png@" "$APPDIR"/zotero.desktop
+else
+ echo "$APPDIR"/zotero.desktop is not writable
+ exit 1
+fi
diff --git a/app/linux/updater.tar.xz b/app/linux/updater.tar.xz
new file mode 100644
index 0000000000..5df9e2da4c
--- /dev/null
+++ b/app/linux/updater.tar.xz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d7e9c81d2f86ec0f70d75107a022649292889807bc8b0b7eaf1bdfd5c1030bf1
+size 74516
diff --git a/app/linux/zotero b/app/linux/zotero
new file mode 100755
index 0000000000..fbaa031ce3
--- /dev/null
+++ b/app/linux/zotero
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# Increase open files limit
+#
+# Mozilla file functions (OS.File.move()/copy(), NetUtil.asyncFetch/asyncCopy()) can leave file
+# descriptors open for a few seconds (even with an explicit inputStream.close() in the case of
+# the latter), so a source installation that copies ~500 translators and styles (with fds for
+# source and target) can exceed the default 1024 limit.
+# Current hard-limit on Ubuntu 16.10 is 4096
+ulimit -n 4096
+
+# Allow profile downgrade for Zotero
+export MOZ_ALLOW_DOWNGRADE=1
+# Don't create dedicated profile (default-esr)
+export MOZ_LEGACY_PROFILES=1
+# Use wayland backend when available
+export MOZ_ENABLE_WAYLAND=1
+
+CALLDIR="$(dirname "$(readlink -f "$0")")"
+"$CALLDIR/zotero-bin" -app "$CALLDIR/app/application.ini" "$@"
diff --git a/app/linux/zotero.desktop b/app/linux/zotero.desktop
new file mode 100755
index 0000000000..705d0b3d46
--- /dev/null
+++ b/app/linux/zotero.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Name=Zotero
+Exec=bash -c "$(dirname $(realpath $(echo %k | sed -e 's/^file:\/\///')))/zotero -url %U"
+Icon=zotero.ico
+Type=Application
+Terminal=false
+Categories=Office;
+MimeType=text/plain;x-scheme-handler/zotero;application/x-research-info-systems;text/x-research-info-systems;text/ris;application/x-endnote-refer;application/x-inst-for-Scientific-info;application/mods+xml;application/rdf+xml;application/x-bibtex;text/x-bibtex;application/marc;application/vnd.citationstyles.style+xml
+X-GNOME-SingleWindow=true
diff --git a/app/mac/Contents/Info.plist b/app/mac/Contents/Info.plist
new file mode 100644
index 0000000000..95332c7775
--- /dev/null
+++ b/app/mac/Contents/Info.plist
@@ -0,0 +1,204 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleDocumentTypes
+
+
+
+ CFBundleTypeExtensions
+
+ ris
+
+
+ CFBundleTypeMIMETypes
+
+ application/x-research-info-systems
+ text/x-research-info-systems
+ text/ris
+ ris
+ application/x-endnote-refer
+
+ CFBundleTypeName
+ Research Information Systems Document
+ CFBundleTypeRole
+ Viewer
+
+
+ CFBundleTypeExtensions
+
+ ciw
+ isi
+
+
+ CFBundleTypeMIMETypes
+
+ application/x-inst-for-Scientific-info
+
+ CFBundleTypeName
+ ISI Common Export Format Document
+ CFBundleTypeRole
+ Viewer
+
+
+ CFBundleTypeExtensions
+
+ mods
+
+
+ CFBundleTypeMIMETypes
+
+ application/mods+xml
+
+ CFBundleTypeName
+ Metadata Object Description Schema Document
+ CFBundleTypeRole
+ Viewer
+
+
+ CFBundleTypeExtensions
+
+ rdf
+
+
+ CFBundleTypeMIMETypes
+
+ application/rdf+xml
+
+ CFBundleTypeName
+ Resource Description Framework Document
+ CFBundleTypeRole
+ Viewer
+
+
+ CFBundleTypeExtensions
+
+ bib
+ bibtex
+
+
+ CFBundleTypeMIMETypes
+
+ application/x-bibtex
+ text/x-bibtex
+
+ CFBundleTypeName
+ BibTeX Document
+ CFBundleTypeRole
+ Viewer
+
+
+ CFBundleTypeExtensions
+
+ mrc
+ marc
+
+
+ CFBundleTypeMIMETypes
+
+ application/marc
+
+ CFBundleTypeName
+ MARC Document
+ CFBundleTypeRole
+ Viewer
+
+
+
+
+ CFBundleTypeExtensions
+
+ csl
+ csl.txt
+
+
+ CFBundleTypeMIMETypes
+
+ application/vnd.citationstyles.style+xml
+
+ CFBundleTypeName
+ CSL Citation Style
+ CFBundleTypeRole
+ Viewer
+
+
+
+
+ CFBundleTypeExtensions
+
+ xml
+
+
+ CFBundleTypeName
+ XML Document
+ CFBundleTypeRole
+ Viewer
+
+
+ CFBundleTypeExtensions
+
+ txt
+
+
+ CFBundleTypeName
+ Text File
+ CFBundleTypeRole
+ Viewer
+
+
+ CFBundleExecutable
+ zotero
+ CFBundleGetInfoString
+ Zotero {{VERSION}}, © 2006-2018 Contributors
+ CFBundleIconFile
+ zotero
+ CFBundleIdentifier
+ org.zotero.zotero
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ Zotero
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ {{VERSION_NUMERIC}}
+ CFBundleSignature
+ ZOTR
+ CFBundleURLTypes
+
+
+
+ CFBundleURLName
+ zotero URL
+ CFBundleURLSchemes
+
+ zotero
+
+
+
+ CFBundleVersion
+ {{VERSION_NUMERIC}}
+ NSAppleScriptEnabled
+
+ CGDisableCoalescedUpdates
+
+ NSHighResolutionCapable
+
+ NSSupportsAutomaticGraphicsSwitching
+
+ LSMinimumSystemVersion
+ 10.9.0
+
+
diff --git a/app/mac/Contents/PkgInfo b/app/mac/Contents/PkgInfo
new file mode 100644
index 0000000000..7591ec70b1
--- /dev/null
+++ b/app/mac/Contents/PkgInfo
@@ -0,0 +1 @@
+APPLZOTR
\ No newline at end of file
diff --git a/app/mac/Contents/Resources/zotero.icns b/app/mac/Contents/Resources/zotero.icns
new file mode 100644
index 0000000000..bb374b2890
Binary files /dev/null and b/app/mac/Contents/Resources/zotero.icns differ
diff --git a/app/mac/DSStore b/app/mac/DSStore
new file mode 100644
index 0000000000..8f1597ec26
Binary files /dev/null and b/app/mac/DSStore differ
diff --git a/app/mac/build-and-unify b/app/mac/build-and-unify
new file mode 100755
index 0000000000..6d8f4b4256
--- /dev/null
+++ b/app/mac/build-and-unify
@@ -0,0 +1,66 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+. "$APP_ROOT_DIR/config.sh"
+
+fx_app_name=Firefox.app
+
+# Get Mozilla source directory from command line
+if [ -z "${1:-}" ]; then
+ echo "Usage: $0 /path/to/mozilla-unified" >&2
+ exit 1
+fi
+GECKO_PATH=$1
+mach=$GECKO_PATH/mach
+
+BUILD_DIR=`mktemp -d`
+function cleanup {
+ rm -rf $BUILD_DIR
+}
+trap cleanup EXIT
+
+set -x
+
+export MOZ_BUILD_DATE=`date "+%Y%m%d%H%M%S"`
+
+# Install required Rust version
+rustup toolchain install $RUST_VERSION
+rustup target add aarch64-apple-darwin
+rustup target add x86_64-apple-darwin
+rustup default $RUST_VERSION
+
+cp "$SCRIPT_DIR/mozconfig" "$GECKO_PATH"
+
+# Build Firefox for Intel and Apple Silicon
+export Z_ARCH=x64
+$mach build
+$mach package
+export Z_ARCH=aarch64
+$mach build
+$mach package
+
+cd $BUILD_DIR
+
+# Unify into Universal build
+# From https://searchfox.org/mozilla-central/rev/97c902e8f92b15dc63eb584bfc594ecb041242a4/taskcluster/scripts/misc/unify.sh
+for i in x86_64 aarch64; do
+ $mach python -m mozbuild.action.unpack_dmg "$GECKO_PATH"/obj-$i-apple-darwin/dist/*.dmg $i
+done
+mv x86_64 x64
+
+$mach python "$GECKO_PATH/toolkit/mozapps/installer/unify.py" x64/*.app aarch64/*.app
+
+cp x64/$fx_app_name/Contents/MacOS/firefox zotero
+xz zotero
+mv zotero.xz "$APP_ROOT_DIR/mac/zotero.xz"
+
+# TEMP: Copy a unified XUL for now
+cp x64/$fx_app_name/Contents/MacOS/XUL "$APP_ROOT_DIR/mac/XUL"
+
+# Uncomment to build updater
+#cd x64/$fx_app_name/Contents/MacOS/
+#"$APP_ROOT_DIR/mac/updater_renamer"
+#cat updater.app/Contents/Resources/English.lproj/InfoPlist.strings
+#tar cfvJ "$APP_ROOT_DIR/mac/updater.tar.xz" updater.app/
diff --git a/app/mac/entitlements.xml b/app/mac/entitlements.xml
new file mode 100644
index 0000000000..bcf47d2194
--- /dev/null
+++ b/app/mac/entitlements.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+
+ com.apple.security.cs.disable-library-validation
+
+
+
+ com.apple.security.device.audio-input
+
+
+
+ com.apple.security.device.camera
+
+
+
+ com.apple.security.personal-information.location
+
+
+
+ com.apple.security.smartcard
+
+
+ com.apple.security.automation.apple-events
+
+
diff --git a/app/mac/mozconfig b/app/mac/mozconfig
new file mode 100644
index 0000000000..1dde8c9923
--- /dev/null
+++ b/app/mac/mozconfig
@@ -0,0 +1,30 @@
+if [ "$Z_ARCH" == "x64" ]; then
+ ac_add_options --target=x86_64-apple-darwin
+elif [ "$Z_ARCH" == "aarch64" ]; then
+ ac_add_options --target=aarch64-apple-darwin
+fi
+ac_add_options --enable-bootstrap
+ac_add_options --with-macos-sdk=$HOME/tmp/MacOSX13.3.sdk
+
+mk_add_options AUTOCLOBBER=1
+
+# These don't all affect the stub, but they can't hurt, and we'll want them if
+# we switch to custom XUL builds
+ac_add_options MOZ_ENABLE_JS_DUMP=1
+ac_add_options MOZ_ENABLE_FORKSERVER=
+ac_add_options MOZ_TELEMETRY_REPORTING=
+ac_add_options MOZ_DATA_REPORTING=
+ac_add_options --disable-tests
+ac_add_options --disable-debug
+ac_add_options --disable-debug-symbols
+ac_add_options --disable-webrtc
+ac_add_options --disable-eme
+
+export MOZILLA_OFFICIAL=1
+export RELEASE_OR_BETA=1
+MOZ_REQUIRE_SIGNING=
+
+ac_add_options --enable-official-branding
+
+# Build updater without MAR signature verification
+ac_add_options --enable-unverified-updates
diff --git a/app/mac/mozilla-115.patch b/app/mac/mozilla-115.patch
new file mode 100644
index 0000000000..9caeccf0cc
--- /dev/null
+++ b/app/mac/mozilla-115.patch
@@ -0,0 +1,1508 @@
+diff --git a/browser/app/nsBrowserApp.cpp b/browser/app/nsBrowserApp.cpp
+--- a/browser/app/nsBrowserApp.cpp
++++ b/browser/app/nsBrowserApp.cpp
+@@ -154,19 +154,31 @@ static bool IsArg(const char* arg, const
+ #endif
+
+ return false;
+ }
+
+ Bootstrap::UniquePtr gBootstrap;
+
+ static int do_main(int argc, char* argv[], char* envp[]) {
++ // Allow profile downgrade for Zotero
++ setenv("MOZ_ALLOW_DOWNGRADE", "1", 1);
++ // Don't create dedicated profile (default-esr)
++ setenv("MOZ_LEGACY_PROFILES", "1", 1);
++
+ // Allow firefox.exe to launch XULRunner apps via -app
+ // Note that -app must be the *first* argument.
+- const char* appDataFile = getenv("XUL_APP_FILE");
++ UniqueFreePtr iniPath = BinaryPath::GetApplicationIni();
++ if (!iniPath) {
++ Output("Couldn't find application.ini.\n");
++ return 255;
++ }
++ char *appDataFile = iniPath.get();
++
++
+ if ((!appDataFile || !*appDataFile) && (argc > 1 && IsArg(argv[1], "app"))) {
+ if (argc == 2) {
+ Output("Incorrect number of arguments passed to -app");
+ return 255;
+ }
+ appDataFile = argv[2];
+
+ char appEnv[MAXPATHLEN];
+diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp
+--- a/layout/base/PresShell.cpp
++++ b/layout/base/PresShell.cpp
+@@ -11529,18 +11529,17 @@ static nsIWidget* GetPresContextContaine
+ return mainWidget;
+ }
+
+ static bool IsTopLevelWidget(nsIWidget* aWidget) {
+ using WindowType = mozilla::widget::WindowType;
+
+ auto windowType = aWidget->GetWindowType();
+ return windowType == WindowType::TopLevel ||
+- windowType == WindowType::Dialog || windowType == WindowType::Popup ||
+- windowType == WindowType::Sheet;
++ windowType == WindowType::Dialog || windowType == WindowType::Popup;
+ }
+
+ PresShell::WindowSizeConstraints PresShell::GetWindowSizeConstraints() {
+ nsSize minSize(0, 0);
+ nsSize maxSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE);
+ nsIFrame* rootFrame = FrameConstructor()->GetRootElementStyleFrame();
+ if (!rootFrame || !mPresContext) {
+ return {minSize, maxSize};
+diff --git a/layout/xul/nsXULTooltipListener.cpp b/layout/xul/nsXULTooltipListener.cpp
+--- a/layout/xul/nsXULTooltipListener.cpp
++++ b/layout/xul/nsXULTooltipListener.cpp
+@@ -365,53 +365,56 @@ nsresult nsXULTooltipListener::ShowToolt
+
+ // get the tooltip content designated for the target node
+ nsCOMPtr tooltipNode;
+ GetTooltipFor(sourceNode, getter_AddRefs(tooltipNode));
+ if (!tooltipNode || sourceNode == tooltipNode)
+ return NS_ERROR_FAILURE; // the target node doesn't need a tooltip
+
+ // set the node in the document that triggered the tooltip and show it
+- if (tooltipNode->GetComposedDoc() &&
+- nsContentUtils::IsChromeDoc(tooltipNode->GetComposedDoc())) {
+- // Make sure the target node is still attached to some document.
+- // It might have been deleted.
+- if (sourceNode->IsInComposedDoc()) {
+- if (!mIsSourceTree) {
+- mLastTreeRow = -1;
+- mLastTreeCol = nullptr;
+- }
++ // Make sure the document still has focus.
++ auto* doc = tooltipNode->GetComposedDoc();
++ if (!doc || !nsContentUtils::IsChromeDoc(doc) ||
++ !doc->HasFocus(IgnoreErrors())) {
++ return NS_OK;
++ }
++ // Make sure the target node is still attached to some document.
++ // It might have been deleted.
++ if (sourceNode->IsInComposedDoc()) {
++ if (!mIsSourceTree) {
++ mLastTreeRow = -1;
++ mLastTreeCol = nullptr;
++ }
+
+- mCurrentTooltip = do_GetWeakReference(tooltipNode);
+- LaunchTooltip();
+- mTargetNode = nullptr;
+-
+- nsCOMPtr currentTooltip = do_QueryReferent(mCurrentTooltip);
+- if (!currentTooltip) return NS_OK;
++ mCurrentTooltip = do_GetWeakReference(tooltipNode);
++ LaunchTooltip();
++ mTargetNode = nullptr;
+
+- // listen for popuphidden on the tooltip node, so that we can
+- // be sure DestroyPopup is called even if someone else closes the tooltip
+- currentTooltip->AddSystemEventListener(u"popuphiding"_ns, this, false,
+- false);
++ nsCOMPtr currentTooltip = do_QueryReferent(mCurrentTooltip);
++ if (!currentTooltip) return NS_OK;
++
++ // listen for popuphidden on the tooltip node, so that we can
++ // be sure DestroyPopup is called even if someone else closes the tooltip
++ currentTooltip->AddSystemEventListener(u"popuphiding"_ns, this, false,
++ false);
+
+- // listen for mousedown, mouseup, keydown, and mouse events at
+- // document level
+- if (Document* doc = sourceNode->GetComposedDoc()) {
+- // Probably, we should listen to untrusted events for hiding tooltips
+- // on content since tooltips might disturb something of web
+- // applications. If we don't specify the aWantsUntrusted of
+- // AddSystemEventListener(), the event target sets it to TRUE if the
+- // target is in content.
+- doc->AddSystemEventListener(u"wheel"_ns, this, true);
+- doc->AddSystemEventListener(u"mousedown"_ns, this, true);
+- doc->AddSystemEventListener(u"mouseup"_ns, this, true);
+- doc->AddSystemEventListener(u"keydown"_ns, this, true);
+- }
+- mSourceNode = nullptr;
++ // listen for mousedown, mouseup, keydown, and mouse events at
++ // document level
++ if (Document* doc = sourceNode->GetComposedDoc()) {
++ // Probably, we should listen to untrusted events for hiding tooltips
++ // on content since tooltips might disturb something of web
++ // applications. If we don't specify the aWantsUntrusted of
++ // AddSystemEventListener(), the event target sets it to TRUE if the
++ // target is in content.
++ doc->AddSystemEventListener(u"wheel"_ns, this, true);
++ doc->AddSystemEventListener(u"mousedown"_ns, this, true);
++ doc->AddSystemEventListener(u"mouseup"_ns, this, true);
++ doc->AddSystemEventListener(u"keydown"_ns, this, true);
+ }
++ mSourceNode = nullptr;
+ }
+
+ return NS_OK;
+ }
+
+ static void SetTitletipLabel(XULTreeElement* aTree, Element* aTooltip,
+ int32_t aRow, nsTreeColumn* aCol) {
+ nsCOMPtr view = aTree->GetView();
+diff --git a/python/mozbuild/mozbuild/action/unpack_dmg.py b/python/mozbuild/mozbuild/action/unpack_dmg.py
+--- a/python/mozbuild/mozbuild/action/unpack_dmg.py
++++ b/python/mozbuild/mozbuild/action/unpack_dmg.py
+@@ -34,18 +34,18 @@ def main(args):
+ options = parser.parse_args(args)
+
+ dmg_tool = bootstrap_toolchain("dmg/dmg")
+ hfs_tool = bootstrap_toolchain("dmg/hfsplus")
+
+ dmg.extract_dmg(
+ dmgfile=Path(options.dmgfile),
+ output=Path(options.outpath),
+- dmg_tool=Path(dmg_tool),
+- hfs_tool=Path(hfs_tool),
++ dmg_tool=_path_or_none(dmg_tool),
++ hfs_tool=_path_or_none(hfs_tool),
+ dsstore=_path_or_none(options.dsstore),
+ background=_path_or_none(options.background),
+ icon=_path_or_none(options.icon),
+ )
+ return 0
+
+
+ if __name__ == "__main__":
+diff --git a/widget/InitData.h b/widget/InitData.h
+--- a/widget/InitData.h
++++ b/widget/InitData.h
+@@ -12,17 +12,16 @@
+
+ namespace mozilla::widget {
+
+ // Window types
+ enum class WindowType : uint8_t {
+ TopLevel, // default top level window
+ Dialog, // top level window but usually handled differently
+ // by the OS
+- Sheet, // MacOSX sheet (special dialog class)
+ Popup, // used for combo boxes, etc
+ Child, // child windows (contained inside a window on the
+ // desktop (has no border))
+ Invisible, // windows that are invisible or offscreen
+ };
+
+ // Popup types for WindowType::Popup
+ enum class PopupType : uint8_t {
+diff --git a/widget/cocoa/moz.build b/widget/cocoa/moz.build
+--- a/widget/cocoa/moz.build
++++ b/widget/cocoa/moz.build
+@@ -6,22 +6,16 @@
+
+ with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+ SCHEDULES.exclusive = ["macosx"]
+
+ with Files("*TextInput*"):
+ BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling")
+
+-XPIDL_SOURCES += [
+- "nsPIWidgetCocoa.idl",
+-]
+-
+-XPIDL_MODULE = "widget_cocoa"
+-
+ EXPORTS += [
+ "CFTypeRefPtr.h",
+ "DesktopBackgroundImage.h",
+ "MediaHardwareKeysEventSourceMac.h",
+ "MediaHardwareKeysEventSourceMacMediaCenter.h",
+ "mozView.h",
+ "nsBidiKeyboard.h",
+ "nsChangeObserver.h",
+diff --git a/widget/cocoa/nsChildView.mm b/widget/cocoa/nsChildView.mm
+--- a/widget/cocoa/nsChildView.mm
++++ b/widget/cocoa/nsChildView.mm
+@@ -1678,29 +1678,23 @@ void nsChildView::UpdateThemeGeometries(
+
+ if (![[mView window] isKindOfClass:[ToolbarWindow class]]) return;
+
+ // Update unified toolbar height and sheet attachment position.
+ int32_t windowWidth = mBounds.width;
+ int32_t titlebarBottom = FindTitlebarBottom(aThemeGeometries, windowWidth);
+ int32_t unifiedToolbarBottom =
+ FindUnifiedToolbarBottom(aThemeGeometries, windowWidth, titlebarBottom);
+- int32_t toolboxBottom = FindFirstRectOfType(aThemeGeometries, eThemeGeometryTypeToolbox).YMost();
+
+ ToolbarWindow* win = (ToolbarWindow*)[mView window];
+ int32_t titlebarHeight =
+ [win drawsContentsIntoWindowFrame] ? 0 : CocoaPointsToDevPixels([win titlebarHeight]);
+ int32_t devUnifiedHeight = titlebarHeight + unifiedToolbarBottom;
+ [win setUnifiedToolbarHeight:DevPixelsToCocoaPoints(devUnifiedHeight)];
+
+- int32_t sheetPositionDevPx = std::max(toolboxBottom, unifiedToolbarBottom);
+- NSPoint sheetPositionView = {0, DevPixelsToCocoaPoints(sheetPositionDevPx)};
+- NSPoint sheetPositionWindow = [mView convertPoint:sheetPositionView toView:nil];
+- [win setSheetAttachmentPosition:sheetPositionWindow.y];
+-
+ // Update titlebar control offsets.
+ LayoutDeviceIntRect windowButtonRect =
+ FindFirstRectOfType(aThemeGeometries, eThemeGeometryTypeWindowButtons);
+ [win placeWindowButtons:[mView convertRect:DevPixelsToCocoaPoints(windowButtonRect) toView:nil]];
+ }
+
+ static Maybe ThemeGeometryTypeToVibrancyType(
+ nsITheme::ThemeGeometryType aThemeGeometryType) {
+@@ -4899,24 +4893,16 @@ BOOL ChildViewMouseTracker::WindowAccept
+ return WindowAcceptsEvent([aWindow parentWindow], aEvent, aView, aIsClickThrough);
+
+ case WindowType::TopLevel:
+ case WindowType::Dialog:
+ if ([aWindow attachedSheet]) return NO;
+
+ topLevelWindow = aWindow;
+ break;
+- case WindowType::Sheet: {
+- nsIWidget* parentWidget = windowWidget->GetSheetWindowParent();
+- if (!parentWidget) return YES;
+-
+- topLevelWindow = (NSWindow*)parentWidget->GetNativeData(NS_NATIVE_WINDOW);
+- break;
+- }
+-
+ default:
+ return YES;
+ }
+
+ if (!topLevelWindow || ([topLevelWindow isMainWindow] && !aIsClickThrough) ||
+ [aEvent type] == NSEventTypeOtherMouseDown ||
+ (([aEvent modifierFlags] & NSEventModifierFlagCommand) != 0 &&
+ [aEvent type] != NSEventTypeMouseMoved))
+diff --git a/widget/cocoa/nsCocoaWindow.h b/widget/cocoa/nsCocoaWindow.h
+--- a/widget/cocoa/nsCocoaWindow.h
++++ b/widget/cocoa/nsCocoaWindow.h
+@@ -7,17 +7,16 @@
+ #define nsCocoaWindow_h_
+
+ #undef DARWIN
+
+ #import
+
+ #include "mozilla/RefPtr.h"
+ #include "nsBaseWidget.h"
+-#include "nsPIWidgetCocoa.h"
+ #include "nsCocoaUtils.h"
+ #include "nsTouchBar.h"
+ #include
+ #include
+
+ class nsCocoaWindow;
+ class nsChildView;
+ class nsMenuBarX;
+@@ -191,60 +190,53 @@ typedef struct _nsCocoaWindowList {
+ // destroyed by updateTitlebarView.
+ MOZTitlebarView* mTitlebarView; // [STRONG]
+ // mFullscreenTitlebarTracker attaches an invisible rectangle to the system
+ // title bar. This allows us to detect when the title bar is showing in
+ // fullscreen.
+ FullscreenTitlebarTracker* mFullscreenTitlebarTracker;
+
+ CGFloat mUnifiedToolbarHeight;
+- CGFloat mSheetAttachmentPosition;
+ CGFloat mMenuBarHeight;
+ /* Store the height of the titlebar when this window is initialized. The
+ titlebarHeight getter returns 0 when in fullscreen, which is not useful in
+ some cases. */
+ CGFloat mInitialTitlebarHeight;
+ NSRect mWindowButtonsRect;
+ }
+ - (void)setUnifiedToolbarHeight:(CGFloat)aHeight;
+ - (CGFloat)unifiedToolbarHeight;
+ - (CGFloat)titlebarHeight;
+ - (NSRect)titlebarRect;
+ - (void)setTitlebarNeedsDisplay;
+ - (void)setDrawsContentsIntoWindowFrame:(BOOL)aState;
+-- (void)setSheetAttachmentPosition:(CGFloat)aY;
+-- (CGFloat)sheetAttachmentPosition;
+ - (void)placeWindowButtons:(NSRect)aRect;
+ - (NSRect)windowButtonsRect;
+ - (void)windowMainStateChanged;
+ @end
+
+-class nsCocoaWindow final : public nsBaseWidget, public nsPIWidgetCocoa {
++class nsCocoaWindow final : public nsBaseWidget {
+ private:
+ typedef nsBaseWidget Inherited;
+
+ public:
+ nsCocoaWindow();
+
+- NS_DECL_ISUPPORTS_INHERITED
+- NS_DECL_NSPIWIDGETCOCOA; // semicolon for clang-format bug 1629756
+-
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const DesktopIntRect& aRect, InitData* = nullptr) override;
+
+ [[nodiscard]] virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ InitData* = nullptr) override;
+
+ virtual void Destroy() override;
+
+ virtual void Show(bool aState) override;
+ virtual bool NeedsRecreateToReshow() override;
+
+- virtual nsIWidget* GetSheetWindowParent(void) override;
+ virtual void Enable(bool aState) override;
+ virtual bool IsEnabled() const override;
+ virtual void SetModal(bool aState) override;
+ virtual void SetFakeModal(bool aState) override;
+ virtual bool IsRunningAppModal() override;
+ virtual bool IsVisible() const override;
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override;
+@@ -393,47 +385,45 @@ class nsCocoaWindow final : public nsBas
+ virtual ~nsCocoaWindow();
+
+ nsresult CreateNativeWindow(const NSRect& aRect, BorderStyle aBorderStyle, bool aRectIsFrameRect,
+ bool aIsPrivateBrowsing);
+ nsresult CreatePopupContentView(const LayoutDeviceIntRect& aRect, InitData*);
+ void DestroyNativeWindow();
+ void UpdateBounds();
+ int32_t GetWorkspaceID();
++ void SendSetZLevelEvent();
+
+ void DoResize(double aX, double aY, double aWidth, double aHeight, bool aRepaint,
+ bool aConstrainToCurrentScreen);
+
+ void UpdateFullscreenState(bool aFullScreen, bool aNativeMode);
+ nsresult DoMakeFullScreen(bool aFullScreen, bool aUseSystemTransition);
+
+ virtual already_AddRefed AllocateChildPopupWidget() override {
+ return nsIWidget::CreateTopLevelWindow();
+ }
+
+ nsIWidget* mParent; // if we're a popup, this is our parent [WEAK]
+ nsIWidget* mAncestorLink; // link to traverse ancestors [WEAK]
+ BaseWindow* mWindow; // our cocoa window [STRONG]
+ WindowDelegate* mDelegate; // our delegate for processing window msgs [STRONG]
+ RefPtr mMenuBar;
+- NSWindow* mSheetWindowParent; // if this is a sheet, this is the NSWindow it's attached to
+ nsChildView* mPopupContentView; // if this is a popup, this is its content widget
+ // if this is a toplevel window, and there is any ongoing fullscreen
+ // transition, it is the animation object.
+ NSAnimation* mFullscreenTransitionAnimation;
+ mozilla::StyleWindowShadow mShadowStyle;
+
+ CGFloat mBackingScaleFactor;
+ CGFloat mAspectRatio;
+
+ WindowAnimationType mAnimationType;
+
+ bool mWindowMadeHere; // true if we created the window, false for embedding
+- bool mSheetNeedsShow; // if this is a sheet, are we waiting to be shown?
+- // this is used for sibling sheet contention only
+ nsSizeMode mSizeMode;
+ bool mInFullScreenMode;
+ // Whether we are currently using native fullscreen. It could be false because
+ // we are in the emulated fullscreen where we do not use the native fullscreen.
+ bool mInNativeFullScreenMode;
+
+ mozilla::Maybe mTransitionCurrent;
+ std::queue mTransitionsPending;
+diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm
+--- a/widget/cocoa/nsCocoaWindow.mm
++++ b/widget/cocoa/nsCocoaWindow.mm
+@@ -30,16 +30,17 @@
+ #include "nsMenuUtilsX.h"
+ #include "nsStyleConsts.h"
+ #include "nsNativeThemeColors.h"
+ #include "nsNativeThemeCocoa.h"
+ #include "nsChildView.h"
+ #include "nsCocoaFeatures.h"
+ #include "nsIScreenManager.h"
+ #include "nsIWidgetListener.h"
++#include "nsXULPopupManager.h"
+ #include "SDKDeclarations.h"
+ #include "VibrancyManager.h"
+ #include "nsPresContext.h"
+ #include "nsDocShell.h"
+
+ #include "gfxPlatform.h"
+ #include "qcms.h"
+
+@@ -99,28 +100,27 @@ typedef enum {
+ static NSString* const CGSSpaceIDKey = @"ManagedSpaceID";
+ static NSString* const CGSSpacesKey = @"Spaces";
+ extern CGSConnection _CGSDefaultConnection(void);
+ extern CGError CGSSetWindowTransform(CGSConnection cid, CGSWindow wid, CGAffineTransform transform);
+ }
+
+ #define NS_APPSHELLSERVICE_CONTRACTID "@mozilla.org/appshell/appShellService;1"
+
+-NS_IMPL_ISUPPORTS_INHERITED(nsCocoaWindow, Inherited, nsPIWidgetCocoa)
+-
+-// A note on testing to see if your object is a sheet...
+-// |mWindowType == WindowType::Sheet| is true if your gecko nsIWidget is a sheet
+-// widget - whether or not the sheet is showing. |[mWindow isSheet]| will return
+-// true *only when the sheet is actually showing*. Choose your test wisely.
+-
+ static void RollUpPopups(
+ nsIRollupListener::AllowAnimations aAllowAnimations = nsIRollupListener::AllowAnimations::Yes) {
++ if (RefPtr pm = nsXULPopupManager::GetInstance()) {
++ pm->RollupTooltips();
++ }
++
+ nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
+- NS_ENSURE_TRUE_VOID(rollupListener);
+-
++
++ if (!rollupListener) {
++ return;
++ }
+ if (rollupListener->RollupNativeMenu()) {
+ return;
+ }
+
+ nsCOMPtr rollupWidget = rollupListener->GetRollupWidget();
+ if (!rollupWidget) {
+ return;
+ }
+@@ -129,24 +129,22 @@ static void RollUpPopups(
+ rollupListener->Rollup(options);
+ }
+
+ nsCocoaWindow::nsCocoaWindow()
+ : mParent(nullptr),
+ mAncestorLink(nullptr),
+ mWindow(nil),
+ mDelegate(nil),
+- mSheetWindowParent(nil),
+ mPopupContentView(nil),
+ mFullscreenTransitionAnimation(nil),
+ mShadowStyle(StyleWindowShadow::Default),
+ mBackingScaleFactor(0.0),
+ mAnimationType(nsIWidget::eGenericWindowAnimation),
+ mWindowMadeHere(false),
+- mSheetNeedsShow(false),
+ mSizeMode(nsSizeMode_Normal),
+ mInFullScreenMode(false),
+ mInNativeFullScreenMode(false),
+ mIgnoreOcclusionCount(0),
+ mHasStartedNativeFullscreen(false),
+ mModal(false),
+ mFakeModal(false),
+ mIsAnimationSuppressed(false),
+@@ -431,25 +429,16 @@ nsresult nsCocoaWindow::CreateNativeWind
+ features |= NSWindowStyleMaskClosable;
+ }
+ }
+ break;
+ case WindowType::TopLevel:
+ case WindowType::Dialog:
+ features = WindowMaskForBorderStyle(aBorderStyle);
+ break;
+- case WindowType::Sheet:
+- if (mParent->GetWindowType() != WindowType::Invisible &&
+- aBorderStyle & BorderStyle::ResizeH) {
+- features = NSWindowStyleMaskResizable;
+- } else {
+- features = NSWindowStyleMaskMiniaturizable;
+- }
+- features |= NSWindowStyleMaskTitled;
+- break;
+ default:
+ NS_ERROR("Unhandled window type!");
+ return NS_ERROR_FAILURE;
+ }
+
+ NSRect contentRect;
+
+ if (aRectIsFrameRect) {
+@@ -650,24 +639,16 @@ void nsCocoaWindow::Destroy() {
+ if (mInNativeFullScreenMode) {
+ DestroyNativeWindow();
+ } else if (mWindow) {
+ nsCocoaUtils::HideOSChromeOnScreen(false);
+ }
+ }
+ }
+
+-nsIWidget* nsCocoaWindow::GetSheetWindowParent(void) {
+- if (mWindowType != WindowType::Sheet) return nullptr;
+- nsCocoaWindow* parent = static_cast(mParent);
+- while (parent && (parent->mWindowType == WindowType::Sheet))
+- parent = static_cast(parent->mParent);
+- return parent;
+-}
+-
+ void* nsCocoaWindow::GetNativeData(uint32_t aDataType) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ void* retVal = nullptr;
+
+ switch (aDataType) {
+ // to emulate how windows works, we always have to return a NSView
+ // for NS_NATIVE_WIDGET
+@@ -707,17 +688,17 @@ void* nsCocoaWindow::GetNativeData(uint3
+ return retVal;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nullptr);
+ }
+
+ bool nsCocoaWindow::IsVisible() const {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+- return (mWindow && ([mWindow isVisibleOrBeingShown] || mSheetNeedsShow));
++ return mWindow && mWindow.isVisibleOrBeingShown;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(false);
+ }
+
+ void nsCocoaWindow::SetModal(bool aState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) return;
+@@ -734,64 +715,60 @@ void nsCocoaWindow::SetModal(bool aState
+ ++gXULModalLevel;
+ // When a non-sheet window gets "set modal", make the window(s) that it
+ // appears over behave as they should. We can't rely on native methods to
+ // do this, for the following reason: The OS runs modal non-sheet windows
+ // in an event loop (using [NSApplication runModalForWindow:] or similar
+ // methods) that's incompatible with the modal event loop in AppWindow::
+ // ShowModal() (each of these event loops is "exclusive", and can't run at
+ // the same time as other (similar) event loops).
+- if (mWindowType != WindowType::Sheet) {
+- while (ancestor) {
+- if (ancestor->mNumModalDescendents++ == 0) {
+- NSWindow* aWindow = ancestor->GetCocoaWindow();
+- if (ancestor->mWindowType != WindowType::Invisible) {
+- [[aWindow standardWindowButton:NSWindowCloseButton] setEnabled:NO];
+- [[aWindow standardWindowButton:NSWindowMiniaturizeButton] setEnabled:NO];
+- [[aWindow standardWindowButton:NSWindowZoomButton] setEnabled:NO];
+- }
++ while (ancestor) {
++ if (ancestor->mNumModalDescendents++ == 0) {
++ NSWindow* aWindow = ancestor->GetCocoaWindow();
++ if (ancestor->mWindowType != WindowType::Invisible) {
++ [[aWindow standardWindowButton:NSWindowCloseButton] setEnabled:NO];
++ [[aWindow standardWindowButton:NSWindowMiniaturizeButton] setEnabled:NO];
++ [[aWindow standardWindowButton:NSWindowZoomButton] setEnabled:NO];
+ }
+- ancestor = static_cast(ancestor->mParent);
+ }
+- [mWindow setLevel:NSModalPanelWindowLevel];
+- nsCocoaWindowList* windowList = new nsCocoaWindowList;
+- if (windowList) {
+- windowList->window = this; // Don't ADDREF
+- windowList->prev = gGeckoAppModalWindowList;
+- gGeckoAppModalWindowList = windowList;
+- }
++ ancestor = static_cast(ancestor->mParent);
++ }
++ [mWindow setLevel:NSModalPanelWindowLevel];
++ nsCocoaWindowList* windowList = new nsCocoaWindowList;
++ if (windowList) {
++ windowList->window = this; // Don't ADDREF
++ windowList->prev = gGeckoAppModalWindowList;
++ gGeckoAppModalWindowList = windowList;
+ }
+ } else {
+ --gXULModalLevel;
+ NS_ASSERTION(gXULModalLevel >= 0, "Mismatched call to nsCocoaWindow::SetModal(false)!");
+- if (mWindowType != WindowType::Sheet) {
+- while (ancestor) {
+- if (--ancestor->mNumModalDescendents == 0) {
+- NSWindow* aWindow = ancestor->GetCocoaWindow();
+- if (ancestor->mWindowType != WindowType::Invisible) {
+- [[aWindow standardWindowButton:NSWindowCloseButton] setEnabled:YES];
+- [[aWindow standardWindowButton:NSWindowMiniaturizeButton] setEnabled:YES];
+- [[aWindow standardWindowButton:NSWindowZoomButton] setEnabled:YES];
+- }
++ while (ancestor) {
++ if (--ancestor->mNumModalDescendents == 0) {
++ NSWindow* aWindow = ancestor->GetCocoaWindow();
++ if (ancestor->mWindowType != WindowType::Invisible) {
++ [[aWindow standardWindowButton:NSWindowCloseButton] setEnabled:YES];
++ [[aWindow standardWindowButton:NSWindowMiniaturizeButton] setEnabled:YES];
++ [[aWindow standardWindowButton:NSWindowZoomButton] setEnabled:YES];
+ }
+- NS_ASSERTION(ancestor->mNumModalDescendents >= 0, "Widget hierarchy changed while modal!");
+- ancestor = static_cast(ancestor->mParent);
+ }
+- if (gGeckoAppModalWindowList) {
+- NS_ASSERTION(gGeckoAppModalWindowList->window == this,
+- "Widget hierarchy changed while modal!");
+- nsCocoaWindowList* saved = gGeckoAppModalWindowList;
+- gGeckoAppModalWindowList = gGeckoAppModalWindowList->prev;
+- delete saved; // "window" not ADDREFed
+- }
+- if (mWindowType == WindowType::Popup)
+- SetPopupWindowLevel();
+- else
+- [mWindow setLevel:NSNormalWindowLevel];
++ NS_ASSERTION(ancestor->mNumModalDescendents >= 0, "Widget hierarchy changed while modal!");
++ ancestor = static_cast(ancestor->mParent);
+ }
++ if (gGeckoAppModalWindowList) {
++ NS_ASSERTION(gGeckoAppModalWindowList->window == this,
++ "Widget hierarchy changed while modal!");
++ nsCocoaWindowList* saved = gGeckoAppModalWindowList;
++ gGeckoAppModalWindowList = gGeckoAppModalWindowList->prev;
++ delete saved; // "window" not ADDREFed
++ }
++ if (mWindowType == WindowType::Popup)
++ SetPopupWindowLevel();
++ else
++ [mWindow setLevel:NSNormalWindowLevel];
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ }
+
+ void nsCocoaWindow::SetFakeModal(bool aState) {
+ mFakeModal = aState;
+ SetModal(aState);
+@@ -800,32 +777,28 @@ void nsCocoaWindow::SetFakeModal(bool aS
+ bool nsCocoaWindow::IsRunningAppModal() { return [NSApp _isRunningAppModal]; }
+
+ // Hide or show this window
+ void nsCocoaWindow::Show(bool bState) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if (!mWindow) return;
+
+- if (!mSheetNeedsShow) {
+- // Early exit if our current visibility state is already the requested state.
+- if (bState == ([mWindow isVisible] || [mWindow isBeingShown])) {
+- return;
+- }
++ // Early exit if our current visibility state is already the requested state.
++ if (bState == ([mWindow isVisible] || [mWindow isBeingShown])) {
++ return;
+ }
+
+ [mWindow setBeingShown:bState];
+ if (bState && !mWasShown) {
+ mWasShown = true;
+ }
+
+- nsIWidget* parentWidget = mParent;
+- nsCOMPtr piParentWidget(do_QueryInterface(parentWidget));
+ NSWindow* nativeParentWindow =
+- (parentWidget) ? (NSWindow*)parentWidget->GetNativeData(NS_NATIVE_WINDOW) : nil;
++ mParent ? (NSWindow*)mParent->GetNativeData(NS_NATIVE_WINDOW) : nil;
+
+ if (bState && !mBounds.IsEmpty()) {
+ // If we had set the activationPolicy to accessory, then right now we won't
+ // have a dock icon. Make sure that we undo that and show a dock icon now that
+ // we're going to show a window.
+ if ([NSApp activationPolicy] != NSApplicationActivationPolicyRegular) {
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
+ PR_SetEnv("MOZ_APP_NO_DOCK=");
+@@ -838,87 +811,17 @@ void nsCocoaWindow::Show(bool bState) {
+ }
+ }
+
+ if (mPopupContentView) {
+ // Ensure our content view is visible. We never need to hide it.
+ mPopupContentView->Show(true);
+ }
+
+- if (mWindowType == WindowType::Sheet) {
+- // bail if no parent window (its basically what we do in Carbon)
+- if (!nativeParentWindow || !piParentWidget) return;
+-
+- NSWindow* topNonSheetWindow = nativeParentWindow;
+-
+- // If this sheet is the child of another sheet, hide the parent so that
+- // this sheet can be displayed. Leave the parent mSheetNeedsShow alone,
+- // that is only used to handle sibling sheet contention. The parent will
+- // return once there are no more child sheets.
+- bool parentIsSheet = false;
+- if (NS_SUCCEEDED(piParentWidget->GetIsSheet(&parentIsSheet)) && parentIsSheet) {
+- piParentWidget->GetSheetWindowParent(&topNonSheetWindow);
+-#ifdef MOZ_THUNDERBIRD
+- [NSApp endSheet:nativeParentWindow];
+-#else
+- [nativeParentWindow.sheetParent endSheet:nativeParentWindow];
+-#endif
+- }
+-
+- nsCOMPtr sheetShown;
+- if (NS_SUCCEEDED(piParentWidget->GetChildSheet(true, getter_AddRefs(sheetShown))) &&
+- (!sheetShown || sheetShown == this)) {
+- // If this sheet is already the sheet actually being shown, don't
+- // tell it to show again. Otherwise the number of calls to
+-#ifdef MOZ_THUNDERBIRD
+- // [NSApp beginSheet...] won't match up with [NSApp endSheet...].
+-#else
+- // [NSWindow beginSheet...] won't match up with [NSWindow endSheet...].
+-#endif
+- if (![mWindow isVisible]) {
+- mSheetNeedsShow = false;
+- mSheetWindowParent = topNonSheetWindow;
+-#ifdef MOZ_THUNDERBIRD
+- // Only set contextInfo if our parent isn't a sheet.
+- NSWindow* contextInfo = parentIsSheet ? nil : mSheetWindowParent;
+- [TopLevelWindowData deactivateInWindow:mSheetWindowParent];
+- [NSApp beginSheet:mWindow
+- modalForWindow:mSheetWindowParent
+- modalDelegate:mDelegate
+- didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
+- contextInfo:contextInfo];
+-#else
+- NSWindow* sheet = mWindow;
+- NSWindow* nonSheetParent = parentIsSheet ? nil : mSheetWindowParent;
+- [TopLevelWindowData deactivateInWindow:mSheetWindowParent];
+- [mSheetWindowParent beginSheet:sheet
+- completionHandler:^(NSModalResponse returnCode) {
+- // Note: 'nonSheetParent' (if it is set) is the window that is the parent
+- // of the sheet. If it's set, 'nonSheetParent' is always the top- level
+- // window, not another sheet itself. But 'nonSheetParent' is nil if our
+- // parent window is also a sheet -- in that case we shouldn't send the
+- // top-level window any activate events (because it's our parent window
+- // that needs to get these events, not the top-level window).
+- [TopLevelWindowData deactivateInWindow:sheet];
+- [sheet orderOut:nil];
+- if (nonSheetParent) {
+- [TopLevelWindowData activateInWindow:nonSheetParent];
+- }
+- }];
+-#endif
+- [TopLevelWindowData activateInWindow:mWindow];
+- SendSetZLevelEvent();
+- }
+- } else {
+- // A sibling of this sheet is active, don't show this sheet yet.
+- // When the active sheet hides, its brothers and sisters that have
+- // mSheetNeedsShow set will have their opportunities to display.
+- mSheetNeedsShow = true;
+- }
+- } else if (mWindowType == WindowType::Popup) {
++ if (mWindowType == WindowType::Popup) {
+ // For reasons that aren't yet clear, calls to [NSWindow orderFront:] or
+ // [NSWindow makeKeyAndOrderFront:] can sometimes trigger "Error (1000)
+ // creating CGSWindow", which in turn triggers an internal inconsistency
+ // NSException. These errors shouldn't be fatal. So we need to wrap
+ // calls to ...orderFront: in TRY blocks. See bmo bug 470864.
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ [[mWindow contentView] setNeedsDisplay:YES];
+ [mWindow orderFront:nil];
+@@ -974,120 +877,30 @@ void nsCocoaWindow::Show(bool bState) {
+ SendSetZLevelEvent();
+ }
+ } else {
+ // roll up any popups if a top-level window is going away
+ if (mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog) {
+ RollUpPopups();
+ }
+
+- // now get rid of the window/sheet
+- if (mWindowType == WindowType::Sheet) {
+- if (mSheetNeedsShow) {
+- // This is an attempt to hide a sheet that never had a chance to
+- // be shown. There's nothing to do other than make sure that it
+- // won't show.
+- mSheetNeedsShow = false;
+- } else {
+- // get sheet's parent *before* hiding the sheet (which breaks the linkage)
+- NSWindow* sheetParent = mSheetWindowParent;
+-
+- // hide the sheet
+-#ifdef MOZ_THUNDERBIRD
+- [NSApp endSheet:mWindow];
+-#else
+- [mSheetWindowParent endSheet:mWindow];
+-#endif
+- [TopLevelWindowData deactivateInWindow:mWindow];
+-
+- nsCOMPtr siblingSheetToShow;
+- bool parentIsSheet = false;
+-
+- if (nativeParentWindow && piParentWidget &&
+- NS_SUCCEEDED(
+- piParentWidget->GetChildSheet(false, getter_AddRefs(siblingSheetToShow))) &&
+- siblingSheetToShow) {
+- // First, give sibling sheets an opportunity to show.
+- siblingSheetToShow->Show(true);
+- } else if (nativeParentWindow && piParentWidget &&
+- NS_SUCCEEDED(piParentWidget->GetIsSheet(&parentIsSheet)) && parentIsSheet) {
+-#ifdef MOZ_THUNDERBIRD
+- // Only set contextInfo if the parent of the parent sheet we're about
+- // to restore isn't itself a sheet.
+- NSWindow* contextInfo = sheetParent;
+-#else
+- // Only set nonSheetGrandparent if the parent of the parent sheet we're about
+- // to restore isn't itself a sheet.
+- NSWindow* nonSheetGrandparent = sheetParent;
+-#endif
+- nsIWidget* grandparentWidget = nil;
+- if (NS_SUCCEEDED(piParentWidget->GetRealParent(&grandparentWidget)) &&
+- grandparentWidget) {
+- nsCOMPtr piGrandparentWidget(do_QueryInterface(grandparentWidget));
+- bool grandparentIsSheet = false;
+- if (piGrandparentWidget &&
+- NS_SUCCEEDED(piGrandparentWidget->GetIsSheet(&grandparentIsSheet)) &&
+- grandparentIsSheet) {
+-#ifdef MOZ_THUNDERBIRD
+- contextInfo = nil;
+-#else
+- nonSheetGrandparent = nil;
+-#endif
+- }
+- }
+- // If there are no sibling sheets, but the parent is a sheet, restore
+- // it. It wasn't sent any deactivate events when it was hidden, so
+- // don't call through Show, just let the OS put it back up.
+-#ifdef MOZ_THUNDERBIRD
+- [NSApp beginSheet:nativeParentWindow
+- modalForWindow:sheetParent
+- modalDelegate:[nativeParentWindow delegate]
+- didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
+- contextInfo:contextInfo];
+-#else
+- [nativeParentWindow beginSheet:sheetParent
+- completionHandler:^(NSModalResponse returnCode) {
+- // Note: 'nonSheetGrandparent' (if it is set) is the window that is the
+- // parent of sheetParent. If it's set, 'nonSheetGrandparent' is always the
+- // top-level window, not another sheet itself. But 'nonSheetGrandparent'
+- // is nil if our parent window is also a sheet -- in that case we shouldn't
+- // send the top-level window any activate events (because it's our parent
+- // window that needs to get these events, not the top-level window).
+- [TopLevelWindowData deactivateInWindow:sheetParent];
+- [sheetParent orderOut:nil];
+- if (nonSheetGrandparent) {
+- [TopLevelWindowData activateInWindow:nonSheetGrandparent];
+- }
+- }];
+-#endif
+- } else {
+- // Sheet, that was hard. No more siblings or parents, going back
+- // to a real window.
+- NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+- [sheetParent makeKeyAndOrderFront:nil];
+- NS_OBJC_END_TRY_IGNORE_BLOCK;
+- }
+- SendSetZLevelEvent();
+- }
+- } else {
+- // If the window is a popup window with a parent window we need to
+- // unhook it here before ordering it out. When you order out the child
+- // of a window it hides the parent window.
+- if (mWindowType == WindowType::Popup && nativeParentWindow)
+- [nativeParentWindow removeChildWindow:mWindow];
+-
+- [mWindow orderOut:nil];
+-
+- // If our popup window is a non-native context menu, tell the OS (and
+- // other programs) that a menu has closed.
+- if ([mWindow isKindOfClass:[PopupWindow class]] && [(PopupWindow*)mWindow isContextMenu]) {
+- [[NSDistributedNotificationCenter defaultCenter]
+- postNotificationName:@"com.apple.HIToolbox.endMenuTrackingNotification"
+- object:@"org.mozilla.gecko.PopupWindow"];
+- }
++ // If the window is a popup window with a parent window we need to
++ // unhook it here before ordering it out. When you order out the child
++ // of a window it hides the parent window.
++ if (mWindowType == WindowType::Popup && nativeParentWindow)
++ [nativeParentWindow removeChildWindow:mWindow];
++
++ [mWindow orderOut:nil];
++
++ // If our popup window is a non-native context menu, tell the OS (and
++ // other programs) that a menu has closed.
++ if ([mWindow isKindOfClass:[PopupWindow class]] && [(PopupWindow*)mWindow isContextMenu]) {
++ [[NSDistributedNotificationCenter defaultCenter]
++ postNotificationName:@"com.apple.HIToolbox.endMenuTrackingNotification"
++ object:@"org.mozilla.gecko.PopupWindow"];
+ }
+ }
+
+ [mWindow setBeingShown:NO];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ }
+
+@@ -2136,59 +1949,22 @@ void nsCocoaWindow::Invalidate(const Lay
+ // a drop, to a drag enter/leave, or a drag over event. The actual event
+ // is passed in |aMessage| and is passed along to our event hanlder so Gecko
+ // knows about it.
+ bool nsCocoaWindow::DragEvent(unsigned int aMessage, mozilla::gfx::Point aMouseGlobal,
+ UInt16 aKeyModifiers) {
+ return false;
+ }
+
+-NS_IMETHODIMP nsCocoaWindow::SendSetZLevelEvent() {
+- nsWindowZ placement = nsWindowZTop;
+- nsCOMPtr actualBelow;
+- if (mWidgetListener)
++void nsCocoaWindow::SendSetZLevelEvent() {
++ if (mWidgetListener) {
++ nsWindowZ placement = nsWindowZTop;
++ nsCOMPtr actualBelow;
+ mWidgetListener->ZLevelChanged(true, &placement, nullptr, getter_AddRefs(actualBelow));
+- return NS_OK;
+-}
+-
+-NS_IMETHODIMP nsCocoaWindow::GetChildSheet(bool aShown, nsIWidget** _retval) {
+- nsIWidget* child = GetFirstChild();
+-
+- while (child) {
+- if (child->GetWindowType() == WindowType::Sheet) {
+- // if it's a sheet, it must be an nsCocoaWindow
+- nsCocoaWindow* cocoaWindow = static_cast(child);
+- if (cocoaWindow->mWindow && ((aShown && [cocoaWindow->mWindow isVisible]) ||
+- (!aShown && cocoaWindow->mSheetNeedsShow))) {
+- nsCOMPtr widget = cocoaWindow;
+- widget.forget(_retval);
+- return NS_OK;
+- }
+- }
+- child = child->GetNextSibling();
+ }
+-
+- *_retval = nullptr;
+-
+- return NS_OK;
+-}
+-
+-NS_IMETHODIMP nsCocoaWindow::GetRealParent(nsIWidget** parent) {
+- *parent = mParent;
+- return NS_OK;
+-}
+-
+-NS_IMETHODIMP nsCocoaWindow::GetIsSheet(bool* isSheet) {
+- mWindowType == WindowType::Sheet ? * isSheet = true : * isSheet = false;
+- return NS_OK;
+-}
+-
+-NS_IMETHODIMP nsCocoaWindow::GetSheetWindowParent(NSWindow** sheetWindowParent) {
+- *sheetWindowParent = mSheetWindowParent;
+- return NS_OK;
+ }
+
+ // Invokes callback and ProcessEvent methods on Event Listener object
+ nsresult nsCocoaWindow::DispatchEvent(WidgetGUIEvent* event, nsEventStatus& aStatus) {
+ aStatus = nsEventStatus_eIgnore;
+
+ nsCOMPtr kungFuDeathGrip(event->mWidget);
+ mozilla::Unused << kungFuDeathGrip; // Not used within this function
+@@ -3030,19 +2806,17 @@ void nsCocoaWindow::CocoaWindowDidResize
+
+ - (void)windowDidBecomeKey:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ RollUpPopups();
+ ChildViewMouseTracker::ReEvaluateMouseEnterState();
+
+ NSWindow* window = [aNotification object];
+- if ([window isSheet]) [WindowDelegate paintMenubarForWindow:window];
+-
+- nsChildView* mainChildView =
++ auto* mainChildView =
+ static_cast([[(BaseWindow*)window mainChildView] widget]);
+ if (mainChildView) {
+ if (mainChildView->GetInputContext().IsPasswordEditor()) {
+ TextInputHandler::EnableSecureEventInput();
+ } else {
+ TextInputHandler::EnsureSecureEventInputDisabled();
+ }
+ }
+@@ -3051,22 +2825,16 @@ void nsCocoaWindow::CocoaWindowDidResize
+ }
+
+ - (void)windowDidResignKey:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ RollUpPopups(nsIRollupListener::AllowAnimations::No);
+
+ ChildViewMouseTracker::ReEvaluateMouseEnterState();
+-
+- // If a sheet just resigned key then we should paint the menu bar
+- // for whatever window is now main.
+- NSWindow* window = [aNotification object];
+- if ([window isSheet]) [WindowDelegate paintMenubarForWindow:[NSApp mainWindow]];
+-
+ TextInputHandler::EnsureSecureEventInputDisabled();
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ }
+
+ - (void)windowWillMove:(NSNotification*)aNotification {
+ RollUpPopups();
+ }
+@@ -3105,42 +2873,16 @@ void nsCocoaWindow::CocoaWindowDidResize
+
+ - (BOOL)windowShouldZoom:(NSWindow*)window toFrame:(NSRect)proposedFrame {
+ if (!mHasEverBeenZoomed && [window isZoomed]) return NO; // See bug 429954.
+
+ mHasEverBeenZoomed = YES;
+ return YES;
+ }
+
+-- (NSRect)window:(NSWindow*)window willPositionSheet:(NSWindow*)sheet usingRect:(NSRect)rect {
+- if ([window isKindOfClass:[ToolbarWindow class]]) {
+- rect.origin.y = [(ToolbarWindow*)window sheetAttachmentPosition];
+- }
+- return rect;
+-}
+-
+-#ifdef MOZ_THUNDERBIRD
+-- (void)didEndSheet:(NSWindow*)sheet returnCode:(int)returnCode contextInfo:(void*)contextInfo {
+- NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+-
+- // Note: 'contextInfo' (if it is set) is the window that is the parent of
+- // the sheet. The value of contextInfo is determined in
+- // nsCocoaWindow::Show(). If it's set, 'contextInfo' is always the top-
+- // level window, not another sheet itself. But 'contextInfo' is nil if
+- // our parent window is also a sheet -- in that case we shouldn't send
+- // the top-level window any activate events (because it's our parent
+- // window that needs to get these events, not the top-level window).
+- [TopLevelWindowData deactivateInWindow:sheet];
+- [sheet orderOut:self];
+- if (contextInfo) [TopLevelWindowData activateInWindow:(NSWindow*)contextInfo];
+-
+- NS_OBJC_END_TRY_ABORT_BLOCK;
+-}
+-#endif
+-
+ - (void)windowDidChangeBackingProperties:(NSNotification*)aNotification {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ NSWindow* window = (NSWindow*)[aNotification object];
+
+ if ([window respondsToSelector:@selector(backingScaleFactor)]) {
+ CGFloat oldFactor =
+ [[[aNotification userInfo] objectForKey:@"NSBackingPropertyOldScaleFactorKey"] doubleValue];
+@@ -3787,17 +3529,16 @@ static const NSString* kStateWantsTitleD
+ NSRect contentRect = frameRect;
+
+ if ((self = [super initWithContentRect:contentRect
+ styleMask:aStyle
+ backing:aBufferingType
+ defer:aFlag])) {
+ mTitlebarView = nil;
+ mUnifiedToolbarHeight = 22.0f;
+- mSheetAttachmentPosition = aChildViewRect.size.height;
+ mWindowButtonsRect = NSZeroRect;
+ mInitialTitlebarHeight = [self titlebarHeight];
+
+ [self setTitlebarAppearsTransparent:YES];
+ #if !defined(MAC_OS_VERSION_11_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_11_0
+ if (nsCocoaFeatures::OnBigSurOrLater()) {
+ #else
+ if (@available(macOS 11.0, *)) {
+@@ -4014,24 +3755,16 @@ static bool ShouldShiftByMenubarHeightIn
+ [self updateTitlebarView];
+ }
+
+ - (void)setWantsTitleDrawn:(BOOL)aDrawTitle {
+ [super setWantsTitleDrawn:aDrawTitle];
+ [self setTitlebarNeedsDisplay];
+ }
+
+-- (void)setSheetAttachmentPosition:(CGFloat)aY {
+- mSheetAttachmentPosition = aY;
+-}
+-
+-- (CGFloat)sheetAttachmentPosition {
+- return mSheetAttachmentPosition;
+-}
+-
+ - (void)placeWindowButtons:(NSRect)aRect {
+ if (!NSEqualRects(mWindowButtonsRect, aRect)) {
+ mWindowButtonsRect = aRect;
+ [self reflowTitlebarElements];
+ }
+ }
+
+ - (NSRect)windowButtonsRect {
+diff --git a/widget/cocoa/nsMenuX.h b/widget/cocoa/nsMenuX.h
+--- a/widget/cocoa/nsMenuX.h
++++ b/widget/cocoa/nsMenuX.h
+@@ -181,17 +181,18 @@ class nsMenuX final : public nsMenuParen
+ already_AddRefed GetMenuPopupContent();
+ void WillInsertChild(const MenuChild& aChild);
+ void WillRemoveChild(const MenuChild& aChild);
+ void AddMenuChild(MenuChild&& aChild);
+ void InsertMenuChild(MenuChild&& aChild);
+ void RemoveMenuChild(const MenuChild& aChild);
+ mozilla::Maybe CreateMenuChild(nsIContent* aContent);
+ RefPtr CreateMenuItem(nsIContent* aMenuItemContent);
+- GeckoNSMenu* CreateMenuWithGeckoString(nsString& aMenuTitle);
++ GeckoNSMenu* CreateMenuWithGeckoString(nsString& aMenuTitle,
++ bool aShowServices);
+ void DidFirePopupShowing();
+
+ // Find the index at which aChild needs to be inserted into mMenuChildren such that mMenuChildren
+ // remains in correct content order, i.e. the order in mMenuChildren is the same as the order of
+ // the DOM children of our .
+ size_t FindInsertionIndex(const MenuChild& aChild);
+
+ // Calculates the index at which aChild's NSMenuItem should be inserted into our NSMenu.
+diff --git a/widget/cocoa/nsMenuX.mm b/widget/cocoa/nsMenuX.mm
+--- a/widget/cocoa/nsMenuX.mm
++++ b/widget/cocoa/nsMenuX.mm
+@@ -89,20 +89,24 @@ nsMenuX::nsMenuX(nsMenuParentX* aParent,
+
+ mMenuDelegate = [[MenuDelegate alloc] initWithGeckoMenu:this];
+ mMenuDelegate.menuIsInMenubar = mMenuGroupOwner->GetMenuBar() != nullptr;
+
+ if (!nsMenuBarX::sNativeEventTarget) {
+ nsMenuBarX::sNativeEventTarget = [[NativeMenuItemTarget alloc] init];
+ }
+
++ bool shouldShowServices = false;
+ if (mContent->IsElement()) {
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, mLabel);
++
++ shouldShowServices =
++ mContent->AsElement()->HasAttr(nsGkAtoms::showservicesmenu);
+ }
+- mNativeMenu = CreateMenuWithGeckoString(mLabel);
++ mNativeMenu = CreateMenuWithGeckoString(mLabel, shouldShowServices);
+
+ // register this menu to be notified when changes are made to our content object
+ NS_ASSERTION(mMenuGroupOwner, "No menu owner given, must have one");
+ mMenuGroupOwner->RegisterForContentChanges(mContent, this);
+
+ mVisible = !nsMenuUtilsX::NodeIsHiddenOrCollapsed(mContent);
+
+ NSString* newCocoaLabelString = nsMenuUtilsX::GetTruncatedCocoaLabel(mLabel);
+@@ -773,28 +777,33 @@ nsresult nsMenuX::SetEnabled(bool aIsEna
+ }
+
+ nsresult nsMenuX::GetEnabled(bool* aIsEnabled) {
+ NS_ENSURE_ARG_POINTER(aIsEnabled);
+ *aIsEnabled = mIsEnabled;
+ return NS_OK;
+ }
+
+-GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& aMenuTitle) {
++GeckoNSMenu* nsMenuX::CreateMenuWithGeckoString(nsString& aMenuTitle,
++ bool aShowServices) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
+
+ NSString* title = [NSString stringWithCharacters:(UniChar*)aMenuTitle.get()
+ length:aMenuTitle.Length()];
+ GeckoNSMenu* myMenu = [[GeckoNSMenu alloc] initWithTitle:title];
+ myMenu.delegate = mMenuDelegate;
+
+ // We don't want this menu to auto-enable menu items because then Cocoa
+ // overrides our decisions and things get incorrectly enabled/disabled.
+ myMenu.autoenablesItems = NO;
+
++ // Only show "Services", "Autofill" and similar entries provided by macOS
++ // if our caller wants them:
++ myMenu.allowsContextMenuPlugIns = aShowServices;
++
+ // we used to install Carbon event handlers here, but since NSMenu* doesn't
+ // create its underlying MenuRef until just before display, we delay until
+ // that happens. Now we install the event handlers when Cocoa notifies
+ // us that a menu is about to display - see the Cocoa MenuDelegate class.
+
+ return myMenu;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK;
+diff --git a/widget/cocoa/nsNativeThemeCocoa.mm b/widget/cocoa/nsNativeThemeCocoa.mm
+--- a/widget/cocoa/nsNativeThemeCocoa.mm
++++ b/widget/cocoa/nsNativeThemeCocoa.mm
+@@ -387,19 +387,18 @@ static BOOL FrameIsInActiveWindow(nsIFra
+ if ([win isSheet]) {
+ return [win isKeyWindow];
+ }
+ return [win isMainWindow] && ![win attachedSheet];
+ }
+
+ // Toolbar controls and content controls respond to different window
+ // activeness states.
+-static BOOL IsActive(nsIFrame* aFrame, BOOL aIsToolbarControl) {
+- if (aIsToolbarControl) return [NativeWindowForFrame(aFrame) isMainWindow];
+- return FrameIsInActiveWindow(aFrame);
++static BOOL IsActiveToolbarControl(nsIFrame* aFrame) {
++ return NativeWindowForFrame(aFrame).isMainWindow;
+ }
+
+ static bool IsInSourceList(nsIFrame* aFrame) {
+ for (nsIFrame* frame = aFrame->GetParent(); frame;
+ frame = nsLayoutUtils::GetCrossDocParentFrameInProcess(frame)) {
+ if (frame->StyleDisplay()->EffectiveAppearance() == StyleAppearance::MozMacSourceList) {
+ return true;
+ }
+@@ -2397,17 +2396,17 @@ Maybe ns
+ return Some(WidgetInfo::Toolbar(isMain));
+ }
+
+ case StyleAppearance::MozWindowTitlebar: {
+ return Nothing();
+ }
+
+ case StyleAppearance::Statusbar:
+- return Some(WidgetInfo::StatusBar(IsActive(aFrame, YES)));
++ return Some(WidgetInfo::StatusBar(IsActiveToolbarControl(aFrame)));
+
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist: {
+ ControlParams controlParams = ComputeControlParams(aFrame, elementState);
+ controlParams.pressed = IsOpenButton(aFrame);
+ DropdownParams params;
+ params.controlParams = controlParams;
+ params.pullsDown = false;
+diff --git a/widget/cocoa/nsPIWidgetCocoa.idl b/widget/cocoa/nsPIWidgetCocoa.idl
+--- a/widget/cocoa/nsPIWidgetCocoa.idl
++++ b/widget/cocoa/nsPIWidgetCocoa.idl
+@@ -1,37 +0,0 @@
+-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+-/* This Source Code Form is subject to the terms of the Mozilla Public
+- * License, v. 2.0. If a copy of the MPL was not distributed with this
+- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+-
+-#include "nsISupports.idl"
+-
+-interface nsIWidget;
+-
+-[ptr] native NSWindowPtr(NSWindow);
+-
+-//
+-// nsPIWidgetCocoa
+-//
+-// A private interface (unfrozen, private to the widget implementation) that
+-// gives us access to some extra features on a widget/window.
+-//
+-[uuid(f75ff69e-3a51-419e-bd29-042f804bc2ed)]
+-interface nsPIWidgetCocoa : nsISupports
+-{
+- void SendSetZLevelEvent();
+-
+- // Find the displayed child sheet (if aShown) or a child sheet that
+- // wants to be displayed (if !aShown)
+- nsIWidget GetChildSheet(in boolean aShown);
+-
+- // Get the parent widget (if any) StandardCreate() was called with.
+- nsIWidget GetRealParent();
+-
+- // If the object implementing this interface is a sheet, this will return the
+- // native NSWindow it is attached to
+- readonly attribute NSWindowPtr sheetWindowParent;
+-
+- // True if window is a sheet
+- readonly attribute boolean isSheet;
+-
+-}; // nsPIWidgetCocoa
+diff --git a/widget/cocoa/nsWindowMap.mm b/widget/cocoa/nsWindowMap.mm
+--- a/widget/cocoa/nsWindowMap.mm
++++ b/widget/cocoa/nsWindowMap.mm
+@@ -160,17 +160,17 @@
+ // only child widgets (nsChildView objects)). (The notification is sent
+ // to windowBecameKey: or windowBecameMain: below.)
+ //
+ // For use with clients that (like Firefox) do use top-level widgets (and
+ // have NSWindow delegates of class WindowDelegate).
+ + (void)activateInWindow:(NSWindow*)aWindow {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+- WindowDelegate* delegate = (WindowDelegate*)[aWindow delegate];
++ WindowDelegate* delegate = (WindowDelegate*)aWindow.delegate;
+ if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) return;
+
+ if ([delegate toplevelActiveState]) return;
+ [delegate sendToplevelActivateEvents];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+ }
+
+diff --git a/widget/headless/HeadlessWidget.cpp b/widget/headless/HeadlessWidget.cpp
+--- a/widget/headless/HeadlessWidget.cpp
++++ b/widget/headless/HeadlessWidget.cpp
+@@ -155,20 +155,19 @@ void HeadlessWidget::GetCompositorWidget
+ mozilla::widget::CompositorWidgetInitData* aInitData) {
+ *aInitData =
+ mozilla::widget::HeadlessCompositorWidgetInitData(GetClientSize());
+ }
+
+ nsIWidget* HeadlessWidget::GetTopLevelWidget() { return mTopLevel; }
+
+ void HeadlessWidget::RaiseWindow() {
+- MOZ_ASSERT(mWindowType == WindowType::TopLevel ||
+- mWindowType == WindowType::Dialog ||
+- mWindowType == WindowType::Sheet,
+- "Raising a non-toplevel window.");
++ MOZ_ASSERT(
++ mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog,
++ "Raising a non-toplevel window.");
+
+ // Do nothing if this is the currently active window.
+ RefPtr activeWindow = GetActiveWindow();
+ if (activeWindow == this) {
+ return;
+ }
+
+ // Raise the window to the top of the stack.
+@@ -199,17 +198,17 @@ void HeadlessWidget::Show(bool aState) {
+
+ LOG(("HeadlessWidget::Show [%p] state %d\n", (void*)this, aState));
+
+ // Top-level window and dialogs are activated/raised when shown.
+ // NB: alwaysontop windows are generally used for peripheral indicators,
+ // so we don't focus them by default.
+ if (aState && !mAlwaysOnTop &&
+ (mWindowType == WindowType::TopLevel ||
+- mWindowType == WindowType::Dialog || mWindowType == WindowType::Sheet)) {
++ mWindowType == WindowType::Dialog)) {
+ RaiseWindow();
+ }
+
+ ApplySizeModeSideEffects();
+ }
+
+ bool HeadlessWidget::IsVisible() const { return mVisible; }
+
+diff --git a/xpcom/build/BinaryPath.h b/xpcom/build/BinaryPath.h
+--- a/xpcom/build/BinaryPath.h
++++ b/xpcom/build/BinaryPath.h
+@@ -128,16 +128,56 @@ class BinaryPath {
+ } else {
+ rv = NS_ERROR_FAILURE;
+ }
+
+ CFRelease(executableURL);
+ return rv;
+ }
+
++ static nsresult GetApplicationIni(char aResult[MAXPATHLEN])
++ {
++ // Works even if we're not bundled.
++ CFBundleRef appBundle = CFBundleGetMainBundle();
++ if (!appBundle) {
++ return NS_ERROR_FAILURE;
++ }
++
++ CFURLRef iniURL = CFBundleCopyResourceURL(appBundle, CFSTR("application.ini"),
++ NULL, CFSTR("app"));
++ if (!iniURL) {
++ return NS_ERROR_FAILURE;
++ }
++
++ nsresult rv;
++ if (CFURLGetFileSystemRepresentation(iniURL, false, (UInt8*)aResult,
++ MAXPATHLEN)) {
++ // Sanitize path in case the app was launched from Terminal via
++ // './firefox' for example.
++ size_t readPos = 0;
++ size_t writePos = 0;
++ while (aResult[readPos] != '\0') {
++ if (aResult[readPos] == '.' && aResult[readPos + 1] == '/') {
++ readPos += 2;
++ } else {
++ aResult[writePos] = aResult[readPos];
++ readPos++;
++ writePos++;
++ }
++ }
++ aResult[writePos] = '\0';
++ rv = NS_OK;
++ } else {
++ rv = NS_ERROR_FAILURE;
++ }
++
++ CFRelease(iniURL);
++ return rv;
++ }
++
+ #elif defined(ANDROID)
+ static nsresult Get(char aResult[MAXPATHLEN]) {
+ // On Android, we use the MOZ_ANDROID_LIBDIR variable that is set by the
+ // Java bootstrap code.
+ const char* libDir = getenv("MOZ_ANDROID_LIBDIR");
+ if (!libDir) {
+ return NS_ERROR_FAILURE;
+ }
+@@ -283,16 +323,29 @@ class BinaryPath {
+ if (NS_FAILED(Get(path))) {
+ return nullptr;
+ }
+ UniqueFreePtr result;
+ result.reset(strdup(path));
+ return result;
+ }
+
++#if defined(XP_MACOSX)
++ static UniqueFreePtr GetApplicationIni()
++ {
++ char path[MAXPATHLEN];
++ if (NS_FAILED(GetApplicationIni(path))) {
++ return nullptr;
++ }
++ UniqueFreePtr result;
++ result.reset(strdup(path));
++ return result;
++ }
++#endif
++
+ #ifdef MOZILLA_INTERNAL_API
+ static nsresult GetFile(nsIFile** aResult) {
+ nsCOMPtr lf;
+ # ifdef XP_WIN
+ wchar_t exePath[MAXPATHLEN];
+ nsresult rv = GetW(exePath);
+ # else
+ char exePath[MAXPATHLEN];
+diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py
+--- a/xpcom/ds/StaticAtoms.py
++++ b/xpcom/ds/StaticAtoms.py
+@@ -1141,16 +1141,17 @@ STATIC_ATOMS = [
+ Atom("self", "self"),
+ Atom("seltype", "seltype"),
+ Atom("setcookie", "set-cookie"),
+ Atom("setter", "setter"),
+ Atom("shadow", "shadow"),
+ Atom("shape", "shape"),
+ Atom("show", "show"),
+ Atom("showcaret", "showcaret"),
++ Atom("showservicesmenu", "showservicesmenu"),
+ Atom("sibling", "sibling"),
+ Atom("simple", "simple"),
+ Atom("simp_chinese_formal", "simp-chinese-formal"),
+ Atom("simp_chinese_informal", "simp-chinese-informal"),
+ Atom("single", "single"),
+ Atom("size", "size"),
+ Atom("sizes", "sizes"),
+ Atom("sizemode", "sizemode"),
+diff --git a/xpfe/appshell/nsAppShellService.cpp b/xpfe/appshell/nsAppShellService.cpp
+--- a/xpfe/appshell/nsAppShellService.cpp
++++ b/xpfe/appshell/nsAppShellService.cpp
+@@ -590,32 +590,16 @@ nsresult nsAppShellService::JustCreateTo
+ nsIWebBrowserChrome::CHROME_TITLEBAR |
+ nsIWebBrowserChrome::CHROME_STATUSBAR;
+ if (widgetInitData.mWindowType == widget::WindowType::Dialog &&
+ ((aChromeMask & pipMask) == pipMask) && !(aChromeMask & barMask)) {
+ widgetInitData.mPIPWindow = true;
+ }
+ #endif
+
+-#ifdef XP_MACOSX
+- // Mac OS X sheet support
+- // Adding CHROME_OPENAS_CHROME to sheetMask makes modal windows opened from
+- // nsGlobalWindow::ShowModalDialog() be dialogs (not sheets), while modal
+- // windows opened from nsPromptService::DoDialog() still are sheets. This
+- // fixes bmo bug 395465 (see nsCocoaWindow::StandardCreate() and
+- // nsCocoaWindow::SetModal()).
+- uint32_t sheetMask = nsIWebBrowserChrome::CHROME_OPENAS_DIALOG |
+- nsIWebBrowserChrome::CHROME_MODAL |
+- nsIWebBrowserChrome::CHROME_OPENAS_CHROME;
+- if (parent && (parent != mHiddenWindow) &&
+- ((aChromeMask & sheetMask) == sheetMask)) {
+- widgetInitData.mWindowType = widget::WindowType::Sheet;
+- }
+-#endif
+-
+ #if defined(XP_WIN)
+ if (widgetInitData.mWindowType == widget::WindowType::TopLevel ||
+ widgetInitData.mWindowType == widget::WindowType::Dialog)
+ widgetInitData.mClipChildren = true;
+ #endif
+
+ // note default chrome overrides other OS chrome settings, but
+ // not internal chrome
diff --git a/app/mac/pkg-dmg b/app/mac/pkg-dmg
new file mode 100755
index 0000000000..f84ba962ca
--- /dev/null
+++ b/app/mac/pkg-dmg
@@ -0,0 +1,1520 @@
+#!/usr/bin/perl
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is pkg-dmg, a Mac OS X disk image (.dmg) packager
+#
+# The Initial Developer of the Original Code is
+# Mark Mentovai .
+# Portions created by the Initial Developer are Copyright (C) 2005
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+use strict;
+use warnings;
+
+=pod
+
+=head1 NAME
+
+B - Mac OS X disk image (.dmg) packager
+
+=head1 SYNOPSIS
+
+B
+B<--source> I
+B<--target> I
+[B<--format> I]
+[B<--volname> I]
+[B<--tempdir> I]
+[B<--mkdir> I]
+[B<--copy> I[:I]]
+[B<--symlink> I[:I]]
+[B<--license> I]
+[B<--resource> I]
+[B<--icon> I]
+[B<--attribute> I:I[:I...]
+[B<--idme>]
+[B<--sourcefile>]
+[B<--verbosity> I]
+[B<--dry-run>]
+
+=head1 DESCRIPTION
+
+I takes a directory identified by I and transforms
+it into a disk image stored as I. The disk image will
+occupy the least space possible for its format, or the least space that the
+authors have been able to figure out how to achieve.
+
+=head1 OPTIONS
+
+=over 5
+
+==item B<--source> I
+
+Identifies the directory that will be packaged up. This directory is not
+touched, a copy will be made in a temporary directory for staging purposes.
+See B<--tempdir>.
+
+==item B<--target> I
+
+The disk image to create. If it exists and is not in use, it will be
+overwritten. If I already contains a suitable extension,
+it will be used unmodified. If no extension is present, or the extension
+is incorrect for the selected format, the proper extension will be added.
+See B<--format>.
+
+==item B<--format> I
+
+The format to create the disk image in. Valid values for I are:
+ - UDZO - zlib-compressed, read-only; extension I<.dmg>
+ - UDBZ - bzip2-compressed, read-only; extension I<.dmg>;
+ create and use on 10.4 ("Tiger") and later only
+ - UDRW - read-write; extension I<.dmg>
+ - UDSP - read-write, sparse; extension I<.sparseimage>
+
+UDBZ is the default format.
+
+See L for a description of these formats.
+
+=item B<--volname> I
+
+The name of the volume in the disk image. If not specified, I
+defaults to the name of the source directory from B<--source>.
+
+=item B<--tempdir> I
+
+A temporary directory to stage intermediate files in. I must
+have enough space available to accommodate twice the size of the files
+being packaged. If not specified, defaults to the same directory that
+the I is to be placed in. B will remove any
+temporary files it places in I.
+
+=item B<--mkdir> I
+
+Specifies a directory that should be created in the disk image.
+I and any ancestor directories will be created. This is
+useful in conjunction with B<--copy>, when copying files to directories
+that may not exist in I. B<--mkdir> may appear multiple
+times.
+
+=item B<--copy> I[:I]
+
+Additional files to copy into the disk image. If I is
+specified, I is copied to the location I identifies,
+otherwise, I is copied to the root of the new volume. B<--copy>
+provides a way to package up a I by adding files to it
+without modifying the original I. B<--copy> may appear
+multiple times.
+
+This option is useful for adding .DS_Store files and window backgrounds
+to disk images.
+
+=item B<--symlink> I[:I]
+
+Like B<--copy>, but allows symlinks to point out of the volume. Empty symlink
+destinations are interpreted as "like the source path, but inside the dmg"
+
+This option is useful for adding symlinks to external resources,
+e.g. to /Applications.
+
+=item B<--license> I
+
+A plain text file containing a license agreement to be displayed before
+the disk image is mounted. English is the only supported language. To
+include license agreements in other languages, in multiple languages,
+or to use formatted text, prepare a resource and use L<--resource>.
+
+=item B<--resource> I
+
+A resource file to merge into I. If I is UDZO or
+UDBZ, the disk image will be flattened to a single-fork file that contains
+the resource but may be freely transferred without any special encodings.
+I must be in a format suitable for L. See L for a
+description of the format, and L for a discussion on flattened
+disk images. B<--resource> may appear multiple times.
+
+This option is useful for adding license agreements and other messages
+to disk images.
+
+=item B<--icon> I
+
+Specifies an I file that will be used as the icon for the root of
+the volume. This file will be copied to the new volume and the custom
+icon attribute will be set on the root folder.
+
+=item B<--attribute> I:I[:I...]
+
+Sets the attributes of I to the attribute list in I. See
+L
+
+=item B<--idme>
+
+Enable IDME to make the disk image "Internet-enabled." The first time
+the image is mounted, if IDME processing is enabled on the system, the
+contents of the image will be copied out of the image and the image will
+be placed in the trash with IDME disabled.
+
+=item B<--sourcefile>
+
+If this option is present, I is treated as a file, and is
+placed as a file within the volume's root folder. Without this option,
+I is treated as the volume root itself.
+
+=item B<--verbosity> I
+
+Adjusts the level of loudness of B. The possible values for
+I are:
+ 0 - Only error messages are displayed.
+ 1 - Print error messages and command invocations.
+ 2 - Print everything, including command output.
+
+The default I is 2.
+
+=item B<--dry-run>
+
+When specified, the commands that would be executed are printed, without
+actually executing them. When commands depend on the output of previous
+commands, dummy values are displayed.
+
+=back
+
+=head1 NON-OPTIONS
+
+=over 5
+
+=item
+
+Resource forks aren't copied.
+
+=item
+
+The root folder of the created volume is designated as the folder
+to open when the volume is mounted. See L.
+
+=item
+
+All files in the volume are set to be world-readable, only writable
+by the owner, and world-executable when appropriate. All other
+permissions bits are cleared.
+
+=item
+
+When possible, disk images are created without any partition tables. This
+is what L refers to as I<-layout NONE>, and saves a handful of
+kilobytes. The alternative, I, contains a partition table that
+is not terribly handy on disk images that are not intended to represent any
+physical disk.
+
+=item
+
+Read-write images are created with journaling off. Any read-write image
+created by this tool is expected to be transient, and the goal of this tool
+is to create images which consume a minimum of space.
+
+=back
+
+=head1 EXAMPLE
+
+pkg-dmg --source /Applications/DeerPark.app --target ~/DeerPark.dmg
+ --sourcefile --volname DeerPark --icon ~/DeerPark.icns
+ --mkdir /.background
+ --copy DeerParkBackground.png:/.background/background.png
+ --copy DeerParkDSStore:/.DS_Store
+ --symlink /Applications:"/Drag to here"
+
+=head1 REQUIREMENTS
+
+I has been tested with Mac OS X releases 10.2 ("Jaguar")
+through 10.4 ("Tiger"). Certain adjustments to behavior are made
+depending on the host system's release. Mac OS X 10.3 ("Panther") or
+later are recommended.
+
+=head1 LICENSE
+
+MPL 1.1/GPL 2.0/LGPL 2.1. Your choice.
+
+=head1 AUTHOR
+
+Mark Mentovai
+
+=head1 SEE ALSO
+
+L, L, L, L, L,
+L, L
+
+=cut
+
+use Fcntl;
+use POSIX;
+use Getopt::Long;
+
+sub argumentEscape(@);
+sub cleanupDie($);
+sub command(@);
+sub commandInternal($@);
+sub commandInternalVerbosity($$@);
+sub commandOutput(@);
+sub commandOutputVerbosity($@);
+sub commandVerbosity($@);
+sub copyFiles($@);
+sub diskImageMaker($$$$$$$$);
+sub giveExtension($$);
+sub hdidMountImage($@);
+sub isFormatCompressed($);
+sub licenseMaker($$);
+sub pathSplit($);
+sub setAttributes($@);
+sub trapSignal($);
+sub usage();
+
+# Variables used as globals
+my(@gCleanup, %gConfig, $gDarwinMajor, $gDryRun, $gVerbosity);
+
+# Use the commands by name if they're expected to be in the user's
+# $PATH (/bin:/sbin:/usr/bin:/usr/sbin). Otherwise, go by absolute
+# path. These may be overridden with --config.
+%gConfig = ('cmd_bless' => 'bless',
+ 'cmd_chmod' => 'chmod',
+ 'cmd_diskutil' => 'diskutil',
+ 'cmd_du' => 'du',
+ 'cmd_hdid' => 'hdid',
+ 'cmd_hdiutil' => 'hdiutil',
+ 'cmd_mkdir' => 'mkdir',
+ 'cmd_mktemp' => 'mktemp',
+ 'cmd_Rez' => '/Developer/Tools/Rez',
+ 'cmd_rm' => 'rm',
+ 'cmd_rsync' => 'rsync',
+ 'cmd_SetFile' => '/Developer/Tools/SetFile',
+
+ # create_directly indicates whether hdiutil create supports
+ # -srcfolder and -srcdevice. It does on >= 10.3 (Panther).
+ # This is fixed up for earlier systems below. If false,
+ # hdiutil create is used to create empty disk images that
+ # are manually filled.
+ 'create_directly' => 1,
+
+ # If hdiutil attach -mountpoint exists, use it to avoid
+ # mounting disk images in the default /Volumes. This reduces
+ # the likelihood that someone will notice a mounted image and
+ # interfere with it. Only available on >= 10.3 (Panther),
+ # fixed up for earlier systems below.
+ #
+ # This is presently turned off for all systems, because there
+ # is an infrequent synchronization problem during ejection.
+ # diskutil eject might return before the image is actually
+ # unmounted. If pkg-dmg then attempts to clean up its
+ # temporary directory, it could remove items from a read-write
+ # disk image or attempt to remove items from a read-only disk
+ # image (or a read-only item from a read-write image) and fail,
+ # causing pkg-dmg to abort. This problem is experienced
+ # under Tiger, which appears to eject asynchronously where
+ # previous systems treated it as a synchronous operation.
+ # Using hdiutil attach -mountpoint didn't always keep images
+ # from showing up on the desktop anyway.
+ 'hdiutil_mountpoint' => 0,
+
+ # hdiutil makehybrid results in optimized disk images that
+ # consume less space and mount more quickly. Use it when
+ # it's available, but that's only on >= 10.3 (Panther).
+ # If false, hdiutil create is used instead. Fixed up for
+ # earlier systems below.
+ 'makehybrid' => 1,
+
+ # hdiutil create doesn't allow specifying a folder to open
+ # at volume mount time, so those images are mounted and
+ # their root folders made holy with bless -openfolder. But
+ # only on >= 10.3 (Panther). Earlier systems are out of luck.
+ # Even on Panther, bless refuses to run unless root.
+ # Fixed up below.
+ 'openfolder_bless' => 1,
+
+ # It's possible to save a few more kilobytes by including the
+ # partition only without any partition table in the image.
+ # This is a good idea on any system, so turn this option off.
+ #
+ # Except it's buggy. "-layout NONE" seems to be creating
+ # disk images with more data than just the partition table
+ # stripped out. You might wind up losing the end of the
+ # filesystem - the last file (or several) might be incomplete.
+ 'partition_table' => 1,
+
+ # To create a partition table-less image from something
+ # created by makehybrid, the hybrid image needs to be
+ # mounted and a new image made from the device associated
+ # with the relevant partition. This requires >= 10.4
+ # (Tiger), presumably because earlier systems have
+ # problems creating images from devices themselves attached
+ # to images. If this is false, makehybrid images will
+ # have partition tables, regardless of the partition_table
+ # setting. Fixed up for earlier systems below.
+ 'recursive_access' => 1);
+
+# --verbosity
+$gVerbosity = 2;
+
+# --dry-run
+$gDryRun = 0;
+
+# %gConfig fix-ups based on features and bugs present in certain releases.
+my($ignore, $uname_r, $uname_s);
+($uname_s, $ignore, $uname_r, $ignore, $ignore) = POSIX::uname();
+if($uname_s eq 'Darwin') {
+ ($gDarwinMajor, $ignore) = split(/\./, $uname_r, 2);
+
+ # $major is the Darwin major release, which for our purposes, is 4 higher
+ # than the interesting digit in a Mac OS X release.
+ if($gDarwinMajor <= 6) {
+ # <= 10.2 (Jaguar)
+ # hdiutil create does not support -srcfolder or -srcdevice
+ $gConfig{'create_directly'} = 0;
+ # hdiutil attach does not support -mountpoint
+ $gConfig{'hdiutil_mountpoint'} = 0;
+ # hdiutil mkhybrid does not exist
+ $gConfig{'makehybrid'} = 0;
+ }
+ if($gDarwinMajor <= 7) {
+ # <= 10.3 (Panther)
+ # Can't mount a disk image and then make a disk image from the device
+ $gConfig{'recursive_access'} = 0;
+ # bless does not support -openfolder on 10.2 (Jaguar) and must run
+ # as root under 10.3 (Panther)
+ $gConfig{'openfolder_bless'} = 0;
+ }
+}
+else {
+ # If it's not Mac OS X, just assume all of those good features are
+ # available. They're not, but things will fail long before they
+ # have a chance to make a difference.
+ #
+ # Now, if someone wanted to document some of these private formats...
+ print STDERR ($0.": warning, not running on Mac OS X, ".
+ "this could be interesting.\n");
+}
+
+# Non-global variables used in Getopt
+my(@attributes, @copyFiles, @createSymlinks, $iconFile, $idme, $licenseFile,
+ @makeDirs, $outputFormat, @resourceFiles, $sourceFile, $sourceFolder,
+ $targetImage, $tempDir, $volumeName);
+
+# --format
+$outputFormat = 'UDBZ';
+
+# --idme
+$idme = 0;
+
+# --sourcefile
+$sourceFile = 0;
+
+# Leaving this might screw up the Apple tools.
+delete $ENV{'NEXT_ROOT'};
+
+# This script can get pretty messy, so trap a few signals.
+$SIG{'INT'} = \&trapSignal;
+$SIG{'HUP'} = \&trapSignal;
+$SIG{'TERM'} = \&trapSignal;
+
+Getopt::Long::Configure('pass_through');
+GetOptions('source=s' => \$sourceFolder,
+ 'target=s' => \$targetImage,
+ 'volname=s' => \$volumeName,
+ 'format=s' => \$outputFormat,
+ 'tempdir=s' => \$tempDir,
+ 'mkdir=s' => \@makeDirs,
+ 'copy=s' => \@copyFiles,
+ 'symlink=s' => \@createSymlinks,
+ 'license=s' => \$licenseFile,
+ 'resource=s' => \@resourceFiles,
+ 'icon=s' => \$iconFile,
+ 'attribute=s' => \@attributes,
+ 'idme' => \$idme,
+ 'sourcefile' => \$sourceFile,
+ 'verbosity=i' => \$gVerbosity,
+ 'dry-run' => \$gDryRun,
+ 'config=s' => \%gConfig); # "hidden" option not in usage()
+
+if(@ARGV) {
+ # All arguments are parsed by Getopt
+ usage();
+ exit(1);
+}
+
+if($gVerbosity<0 || $gVerbosity>2) {
+ usage();
+ exit(1);
+}
+
+if(!defined($sourceFolder) || $sourceFolder eq '' ||
+ !defined($targetImage) || $targetImage eq '') {
+ # --source and --target are required arguments
+ usage();
+ exit(1);
+}
+
+# Make sure $sourceFolder doesn't contain trailing slashes. It messes with
+# rsync.
+while(substr($sourceFolder, -1) eq '/') {
+ chop($sourceFolder);
+}
+
+if(!defined($volumeName)) {
+ # Default volumeName is the name of the source directory.
+ my(@components);
+ @components = pathSplit($sourceFolder);
+ $volumeName = pop(@components);
+}
+
+my(@tempDirComponents, $targetImageFilename);
+@tempDirComponents = pathSplit($targetImage);
+$targetImageFilename = pop(@tempDirComponents);
+
+if(defined($tempDir)) {
+ @tempDirComponents = pathSplit($tempDir);
+}
+else {
+ # Default tempDir is the same directory as what is specified for
+ # targetImage
+ $tempDir = join('/', @tempDirComponents);
+}
+
+# Ensure that the path of the target image has a suitable extension. If
+# it didn't, hdiutil would add one, and we wouldn't be able to find the
+# file.
+#
+# Note that $targetImageFilename is not being reset. This is because it's
+# used to build other names below, and we don't need to be adding all sorts
+# of extra unnecessary extensions to the name.
+my($originalTargetImage, $requiredExtension);
+$originalTargetImage = $targetImage;
+if($outputFormat eq 'UDSP') {
+ $requiredExtension = '.sparseimage';
+}
+else {
+ $requiredExtension = '.dmg';
+}
+$targetImage = giveExtension($originalTargetImage, $requiredExtension);
+
+if($targetImage ne $originalTargetImage) {
+ print STDERR ($0.": warning: target image extension is being added\n");
+ print STDERR (' The new filename is '.
+ giveExtension($targetImageFilename,$requiredExtension)."\n");
+}
+
+# Make a temporary directory in $tempDir for our own nefarious purposes.
+my(@output, $tempSubdir, $tempSubdirTemplate);
+$tempSubdirTemplate=join('/', @tempDirComponents,
+ 'pkg-dmg.'.$$.'.XXXXXXXX');
+if(!(@output = commandOutput($gConfig{'cmd_mktemp'}, '-d',
+ $tempSubdirTemplate)) || $#output != 0) {
+ cleanupDie('mktemp failed');
+}
+
+if($gDryRun) {
+ (@output)=($tempSubdirTemplate);
+}
+
+($tempSubdir) = @output;
+
+push(@gCleanup,
+ sub {commandVerbosity(0, $gConfig{'cmd_rm'}, '-rf', $tempSubdir);});
+
+my($tempMount, $tempRoot, @tempsToMake);
+$tempRoot = $tempSubdir.'/stage';
+$tempMount = $tempSubdir.'/mount';
+push(@tempsToMake, $tempRoot);
+if($gConfig{'hdiutil_mountpoint'}) {
+ push(@tempsToMake, $tempMount);
+}
+
+if(command($gConfig{'cmd_mkdir'}, @tempsToMake) != 0) {
+ cleanupDie('mkdir tempRoot/tempMount failed');
+}
+
+# This cleanup object is not strictly necessary, because $tempRoot is inside
+# of $tempSubdir, but the rest of the script relies on this object being
+# on the cleanup stack and expects to remove it.
+push(@gCleanup,
+ sub {commandVerbosity(0, $gConfig{'cmd_rm'}, '-rf', $tempRoot);});
+
+# If $sourceFile is true, it means that $sourceFolder is to be treated as
+# a file and placed as a file within the volume root, as opposed to being
+# treated as the volume root itself. rsync will do this by default, if no
+# trailing '/' is present. With a trailing '/', $sourceFolder becomes
+# $tempRoot, instead of becoming an entry in $tempRoot.
+if(command($gConfig{'cmd_rsync'}, '-a', '--copy-unsafe-links',
+ $sourceFolder.($sourceFile?'':'/'),$tempRoot) != 0) {
+ cleanupDie('rsync failed');
+}
+
+if(@makeDirs) {
+ my($makeDir, @tempDirsToMake);
+ foreach $makeDir (@makeDirs) {
+ if($makeDir =~ /^\//) {
+ push(@tempDirsToMake, $tempRoot.$makeDir);
+ }
+ else {
+ push(@tempDirsToMake, $tempRoot.'/'.$makeDir);
+ }
+ }
+ if(command($gConfig{'cmd_mkdir'}, '-p', @tempDirsToMake) != 0) {
+ cleanupDie('mkdir failed');
+ }
+}
+
+# copy files and/or create symlinks
+copyFiles($tempRoot, 'copy', @copyFiles);
+copyFiles($tempRoot, 'symlink', @createSymlinks);
+
+if($gConfig{'create_directly'}) {
+ # If create_directly is false, the contents will be rsynced into a
+ # disk image and they would lose their attributes.
+ setAttributes($tempRoot, @attributes);
+}
+
+if(defined($iconFile)) {
+ if(command($gConfig{'cmd_rsync'}, '-a', '--copy-unsafe-links', $iconFile,
+ $tempRoot.'/.VolumeIcon.icns') != 0) {
+ cleanupDie('rsync failed for volume icon');
+ }
+
+ # It's pointless to set the attributes of the root when diskutil create
+ # -srcfolder is being used. In that case, the attributes will be set
+ # later, after the image is already created.
+ if(isFormatCompressed($outputFormat) &&
+ (command($gConfig{'cmd_SetFile'}, '-a', 'C', $tempRoot) != 0)) {
+ cleanupDie('SetFile failed');
+ }
+}
+
+if(command($gConfig{'cmd_chmod'}, '-R', 'a+rX,a-st,u+w,go-w',
+ $tempRoot) != 0) {
+ cleanupDie('chmod failed');
+}
+
+my($unflattenable);
+if(isFormatCompressed($outputFormat)) {
+ $unflattenable = 1;
+}
+else {
+ $unflattenable = 0;
+}
+
+diskImageMaker($tempRoot, $targetImage, $outputFormat, $volumeName,
+ $tempSubdir, $tempMount, $targetImageFilename, defined($iconFile));
+
+if(defined($licenseFile) && $licenseFile ne '') {
+ my($licenseResource);
+ $licenseResource = $tempSubdir.'/license.r';
+ if(!licenseMaker($licenseFile, $licenseResource)) {
+ cleanupDie('licenseMaker failed');
+ }
+ push(@resourceFiles, $licenseResource);
+ # Don't add a cleanup object because licenseResource is in tempSubdir.
+}
+
+if(@resourceFiles) {
+ # Add resources, such as a license agreement.
+
+ # Only unflatten read-only and compressed images. It's not supported
+ # on other image times.
+ if($unflattenable &&
+ (command($gConfig{'cmd_hdiutil'}, 'unflatten', $targetImage)) != 0) {
+ cleanupDie('hdiutil unflatten failed');
+ }
+ # Don't push flatten onto the cleanup stack. If we fail now, we'll be
+ # removing $targetImage anyway.
+
+ # Type definitions come from Carbon.r.
+ if(command($gConfig{'cmd_Rez'}, 'Carbon.r', @resourceFiles, '-a', '-o',
+ $targetImage) != 0) {
+ cleanupDie('Rez failed');
+ }
+
+ # Flatten. This merges the resource fork into the data fork, so no
+ # special encoding is needed to transfer the file.
+ if($unflattenable &&
+ (command($gConfig{'cmd_hdiutil'}, 'flatten', $targetImage)) != 0) {
+ cleanupDie('hdiutil flatten failed');
+ }
+}
+
+# $tempSubdir is no longer needed. It's buried on the stack below the
+# rm of the fresh image file. Splice in this fashion is equivalent to
+# pop-save, pop, push-save.
+splice(@gCleanup, -2, 1);
+# No need to remove licenseResource separately, it's in tempSubdir.
+if(command($gConfig{'cmd_rm'}, '-rf', $tempSubdir) != 0) {
+ cleanupDie('rm -rf tempSubdir failed');
+}
+
+if($idme) {
+ if(command($gConfig{'cmd_hdiutil'}, 'internet-enable', '-yes',
+ $targetImage) != 0) {
+ cleanupDie('hdiutil internet-enable failed');
+ }
+}
+
+# Done.
+
+exit(0);
+
+# argumentEscape(@arguments)
+#
+# Takes a list of @arguments and makes them shell-safe.
+sub argumentEscape(@) {
+ my(@arguments);
+ @arguments = @_;
+ my($argument, @argumentsOut);
+ foreach $argument (@arguments) {
+ $argument =~ s%([^A-Za-z0-9_\-/.=+,])%\\$1%g;
+ push(@argumentsOut, $argument);
+ }
+ return @argumentsOut;
+}
+
+# cleanupDie($message)
+#
+# Displays $message as an error message, and then runs through the
+# @gCleanup stack, performing any cleanup operations needed before
+# exiting. Does not return, exits with exit status 1.
+sub cleanupDie($) {
+ my($message);
+ ($message) = @_;
+ print STDERR ($0.': '.$message.(@gCleanup?' (cleaning up)':'')."\n");
+ while(@gCleanup) {
+ my($subroutine);
+ $subroutine = pop(@gCleanup);
+ &$subroutine;
+ }
+ exit(1);
+}
+
+# command(@arguments)
+#
+# Runs the specified command at the verbosity level defined by $gVerbosity.
+# Returns nonzero on failure, returning the exit status if appropriate.
+# Discards command output.
+sub command(@) {
+ my(@arguments);
+ @arguments = @_;
+ return commandVerbosity($gVerbosity,@arguments);
+}
+
+# commandInternal($command, @arguments)
+#
+# Runs the specified internal command at the verbosity level defined by
+# $gVerbosity.
+# Returns zero(!) on failure, because commandInternal is supposed to be a
+# direct replacement for the Perl system call wrappers, which, unlike shell
+# commands and C equivalent system calls, return true (instead of 0) to
+# indicate success.
+sub commandInternal($@) {
+ my(@arguments, $command);
+ ($command, @arguments) = @_;
+ return commandInternalVerbosity($gVerbosity, $command, @arguments);
+}
+
+# commandInternalVerbosity($verbosity, $command, @arguments)
+#
+# Run an internal command, printing a bogus command invocation message if
+# $verbosity is true.
+#
+# If $command is unlink:
+# Removes the files specified by @arguments. Wraps unlink.
+#
+# If $command is symlink:
+# Creates the symlink specified by @arguments. Wraps symlink.
+sub commandInternalVerbosity($$@) {
+ my(@arguments, $command, $verbosity);
+ ($verbosity, $command, @arguments) = @_;
+ if($command eq 'unlink') {
+ if($verbosity || $gDryRun) {
+ print(join(' ', 'rm', '-f', argumentEscape(@arguments))."\n");
+ }
+ if($gDryRun) {
+ return $#arguments+1;
+ }
+ return unlink(@arguments);
+ }
+ elsif($command eq 'symlink') {
+ if($verbosity || $gDryRun) {
+ print(join(' ', 'ln', '-s', argumentEscape(@arguments))."\n");
+ }
+ if($gDryRun) {
+ return 1;
+ }
+ my($source, $target);
+ ($source, $target) = @arguments;
+ return symlink($source, $target);
+ }
+}
+
+# commandOutput(@arguments)
+#
+# Runs the specified command at the verbosity level defined by $gVerbosity.
+# Output is returned in an array of lines. undef is returned on failure.
+# The exit status is available in $?.
+sub commandOutput(@) {
+ my(@arguments);
+ @arguments = @_;
+ return commandOutputVerbosity($gVerbosity, @arguments);
+}
+
+# commandOutputVerbosity($verbosity, @arguments)
+#
+# Runs the specified command at the verbosity level defined by the
+# $verbosity argument. Output is returned in an array of lines. undef is
+# returned on failure. The exit status is available in $?.
+#
+# If an error occurs in fork or exec, an error message is printed to
+# stderr and undef is returned.
+#
+# If $verbosity is 0, the command invocation is not printed, and its
+# stdout is not echoed back to stdout.
+#
+# If $verbosity is 1, the command invocation is printed.
+#
+# If $verbosity is 2, the command invocation is printed and the output
+# from stdout is echoed back to stdout.
+#
+# Regardless of $verbosity, stderr is left connected.
+sub commandOutputVerbosity($@) {
+ my(@arguments, $verbosity);
+ ($verbosity, @arguments) = @_;
+ my($pid);
+ if($verbosity || $gDryRun) {
+ print(join(' ', argumentEscape(@arguments))."\n");
+ }
+ if($gDryRun) {
+ return(1);
+ }
+ if (!defined($pid = open(*COMMAND, '-|'))) {
+ printf STDERR ($0.': fork: '.$!."\n");
+ return undef;
+ }
+ elsif ($pid) {
+ # parent
+ my(@lines);
+ while(!eof(*COMMAND)) {
+ my($line);
+ chop($line = );
+ if($verbosity > 1) {
+ print($line."\n");
+ }
+ push(@lines, $line);
+ }
+ close(*COMMAND);
+ if ($? == -1) {
+ printf STDERR ($0.': fork: '.$!."\n");
+ return undef;
+ }
+ elsif ($? & 127) {
+ printf STDERR ($0.': exited on signal '.($? & 127).
+ ($? & 128 ? ', core dumped' : '')."\n");
+ return undef;
+ }
+ return @lines;
+ }
+ else {
+ # child; this form of exec is immune to shell games
+ if(!exec {$arguments[0]} (@arguments)) {
+ printf STDERR ($0.': exec: '.$!."\n");
+ exit(-1);
+ }
+ }
+}
+
+# commandVerbosity($verbosity, @arguments)
+#
+# Runs the specified command at the verbosity level defined by the
+# $verbosity argument. Returns nonzero on failure, returning the exit
+# status if appropriate. Discards command output.
+sub commandVerbosity($@) {
+ my(@arguments, $verbosity);
+ ($verbosity, @arguments) = @_;
+ if(!defined(commandOutputVerbosity($verbosity, @arguments))) {
+ return -1;
+ }
+ return $?;
+}
+
+# copyFiles($tempRoot, $method, @arguments)
+#
+# Copies files or create symlinks in the disk image.
+# See --copy and --symlink descriptions for details.
+# If $method is 'copy', @arguments are interpreted as source:target, if $method
+# is 'symlink', @arguments are interpreted as symlink:target.
+sub copyFiles($@) {
+ my(@fileList, $method, $tempRoot);
+ ($tempRoot, $method, @fileList) = @_;
+ my($file, $isSymlink);
+ $isSymlink = ($method eq 'symlink');
+ foreach $file (@fileList) {
+ my($source, $target);
+ ($source, $target) = split(/:/, $file);
+ if(!defined($target) and $isSymlink) {
+ # empty symlink targets would result in an invalid target and fail,
+ # but they shall be interpreted as "like source path, but inside dmg"
+ $target = $source;
+ }
+ if(!defined($target)) {
+ $target = $tempRoot;
+ }
+ elsif($target =~ /^\//) {
+ $target = $tempRoot.$target;
+ }
+ else {
+ $target = $tempRoot.'/'.$target;
+ }
+
+ my($success);
+ if($isSymlink) {
+ $success = commandInternal('symlink', $source, $target);
+ }
+ else {
+ $success = !command($gConfig{'cmd_rsync'}, '-a', '--copy-unsafe-links',
+ $source, $target);
+ }
+ if(!$success) {
+ cleanupDie('copyFiles failed for method '.$method);
+ }
+ }
+}
+
+# diskImageMaker($source, $destination, $format, $name, $tempDir, $tempMount,
+# $baseName, $setRootIcon)
+#
+# Creates a disk image in $destination of format $format corresponding to the
+# source directory $source. $name is the volume name. $tempDir is a good
+# place to write temporary files, which should be empty (aside from the other
+# things that this script might create there, like stage and mount).
+# $tempMount is a mount point for temporary disk images. $baseName is the
+# name of the disk image, and is presently unused. $setRootIcon is true if
+# a volume icon was added to the staged $source and indicates that the
+# custom volume icon bit on the volume root needs to be set.
+sub diskImageMaker($$$$$$$$) {
+ my($baseName, $destination, $format, $name, $setRootIcon, $source,
+ $tempDir, $tempMount);
+ ($source, $destination, $format, $name, $tempDir, $tempMount,
+ $baseName, $setRootIcon) = @_;
+ if(isFormatCompressed($format)) {
+ my($uncompressedImage);
+
+ if($gConfig{'makehybrid'}) {
+ my($hybridImage);
+ $hybridImage = giveExtension($tempDir.'/hybrid', '.dmg');
+
+ if(command($gConfig{'cmd_hdiutil'}, 'makehybrid', '-hfs',
+ '-hfs-volume-name', $name, '-hfs-openfolder', $source, '-ov',
+ $source, '-o', $hybridImage) != 0) {
+ cleanupDie('hdiutil makehybrid failed');
+ }
+
+ $uncompressedImage = $hybridImage;
+
+ # $source is no longer needed and will be removed before anything
+ # else can fail. splice in this form is the same as pop/push.
+ splice(@gCleanup, -1, 1,
+ sub {commandInternalVerbosity(0, 'unlink', $hybridImage);});
+
+ if(command($gConfig{'cmd_rm'}, '-rf', $source) != 0) {
+ cleanupDie('rm -rf failed');
+ }
+
+ if(!$gConfig{'partition_table'} && $gConfig{'recursive_access'}) {
+ # Even if we do want to create disk images without partition tables,
+ # it's impossible unless recursive_access is set.
+ my($rootDevice, $partitionDevice, $partitionMountPoint);
+
+ if(!(($rootDevice, $partitionDevice, $partitionMountPoint) =
+ hdidMountImage($tempMount, '-readonly', $hybridImage))) {
+ cleanupDie('hdid mount failed');
+ }
+
+ push(@gCleanup, sub {commandVerbosity(0,
+ $gConfig{'cmd_diskutil'}, 'eject', $rootDevice);});
+
+ my($udrwImage);
+ $udrwImage = giveExtension($tempDir.'/udrw', '.dmg');
+
+ if(command($gConfig{'cmd_hdiutil'}, 'create', '-format', 'UDRW',
+ '-ov', '-srcdevice', $partitionDevice, $udrwImage) != 0) {
+ cleanupDie('hdiutil create failed');
+ }
+
+ $uncompressedImage = $udrwImage;
+
+ # Going to eject before anything else can fail. Get the eject off
+ # the stack.
+ pop(@gCleanup);
+
+ # $hybridImage will be removed soon, but until then, it needs to
+ # stay on the cleanup stack. It needs to wait until after
+ # ejection. $udrwImage is staying around. Make it appear as
+ # though it's been done before $hybridImage.
+ #
+ # splice in this form is the same as popping one element to
+ # @tempCleanup and pushing the subroutine.
+ my(@tempCleanup);
+ @tempCleanup = splice(@gCleanup, -1, 1,
+ sub {commandInternalVerbosity(0, 'unlink', $udrwImage);});
+ push(@gCleanup, @tempCleanup);
+
+ if(command($gConfig{'cmd_diskutil'}, 'eject', $rootDevice) != 0) {
+ cleanupDie('diskutil eject failed');
+ }
+
+ # Pop unlink of $uncompressedImage
+ pop(@gCleanup);
+
+ if(commandInternal('unlink', $hybridImage) != 1) {
+ cleanupDie('unlink hybridImage failed: '.$!);
+ }
+ }
+ }
+ else {
+ # makehybrid is not available, fall back to making a UDRW and
+ # converting to a compressed image. It ought to be possible to
+ # create a compressed image directly, but those come out far too
+ # large (journaling?) and need to be read-write to fix up the
+ # volume icon anyway. Luckily, we can take advantage of a single
+ # call back into this function.
+ my($udrwImage);
+ $udrwImage = giveExtension($tempDir.'/udrw', '.dmg');
+
+ diskImageMaker($source, $udrwImage, 'UDRW', $name, $tempDir,
+ $tempMount, $baseName, $setRootIcon);
+
+ # The call back into diskImageMaker already removed $source.
+
+ $uncompressedImage = $udrwImage;
+ }
+
+ # The uncompressed disk image is now in its final form. Compress it.
+ # Jaguar doesn't support hdiutil convert -ov, but it always allows
+ # overwriting.
+ # bzip2-compressed UDBZ images can only be created and mounted on 10.4
+ # and later. The bzip2-level imagekey is only effective when creating
+ # images in 10.5. In 10.4, bzip2-level is harmlessly ignored, and the
+ # default value of 1 is always used.
+ if(command($gConfig{'cmd_hdiutil'}, 'convert', '-format', $format,
+ '-imagekey', ($format eq 'UDBZ' ? 'bzip2-level=9' : 'zlib-level=9'),
+ (defined($gDarwinMajor) && $gDarwinMajor <= 6 ? () : ('-ov')),
+ $uncompressedImage, '-o', $destination) != 0) {
+ cleanupDie('hdiutil convert failed');
+ }
+
+ # $uncompressedImage is going to be unlinked before anything else can
+ # fail. splice in this form is the same as pop/push.
+ splice(@gCleanup, -1, 1,
+ sub {commandInternalVerbosity(0, 'unlink', $destination);});
+
+ if(commandInternal('unlink', $uncompressedImage) != 1) {
+ cleanupDie('unlink uncompressedImage failed: '.$!);
+ }
+
+ # At this point, the only thing that the compressed block has added to
+ # the cleanup stack is the removal of $destination. $source has already
+ # been removed, and its cleanup entry has been removed as well.
+ }
+ elsif($format eq 'UDRW' || $format eq 'UDSP') {
+ my(@extraArguments);
+ if(!$gConfig{'partition_table'}) {
+ @extraArguments = ('-layout', 'NONE');
+ }
+
+ if($gConfig{'create_directly'}) {
+ # Use -fs HFS+ to suppress the journal.
+ if(command($gConfig{'cmd_hdiutil'}, 'create', '-format', $format,
+ @extraArguments, '-fs', 'HFS+', '-volname', $name,
+ '-ov', '-srcfolder', $source, $destination) != 0) {
+ cleanupDie('hdiutil create failed');
+ }
+
+ # $source is no longer needed and will be removed before anything
+ # else can fail. splice in this form is the same as pop/push.
+ splice(@gCleanup, -1, 1,
+ sub {commandInternalVerbosity(0, 'unlink', $destination);});
+
+ if(command($gConfig{'cmd_rm'}, '-rf', $source) != 0) {
+ cleanupDie('rm -rf failed');
+ }
+ }
+ else {
+ # hdiutil create does not support -srcfolder or -srcdevice, it only
+ # knows how to create blank images. Figure out how large an image
+ # is needed, create it, and fill it. This is needed for Jaguar.
+
+ # Use native block size for hdiutil create -sectors.
+ delete $ENV{'BLOCKSIZE'};
+
+ my(@duOutput, $ignore, $sizeBlocks, $sizeOverhead, $sizeTotal, $type);
+ if(!(@output = commandOutput($gConfig{'cmd_du'}, '-s', $tempRoot)) ||
+ $? != 0) {
+ cleanupDie('du failed');
+ }
+ ($sizeBlocks, $ignore) = split(' ', $output[0], 2);
+
+ # The filesystem itself takes up 152 blocks of its own blocks for the
+ # filesystem up to 8192 blocks, plus 64 blocks for every additional
+ # 4096 blocks or portion thereof.
+ $sizeOverhead = 152 + 64 * POSIX::ceil(
+ (($sizeBlocks - 8192) > 0) ? (($sizeBlocks - 8192) / (4096 - 64)) : 0);
+
+ # The number of blocks must be divisible by 8.
+ my($mod);
+ if($mod = ($sizeOverhead % 8)) {
+ $sizeOverhead += 8 - $mod;
+ }
+
+ # sectors is taken as the size of a disk, not a filesystem, so the
+ # partition table eats into it.
+ if($gConfig{'partition_table'}) {
+ $sizeOverhead += 80;
+ }
+
+ # That was hard. Leave some breathing room anyway. Use 1024 sectors
+ # (512kB). These read-write images wouldn't be useful if they didn't
+ # have at least a little free space.
+ $sizeTotal = $sizeBlocks + $sizeOverhead + 1024;
+
+ # Minimum sizes - these numbers are larger on Jaguar than on later
+ # systems. Just use the Jaguar numbers, since it's unlikely to wind
+ # up here on any other release.
+ if($gConfig{'partition_table'} && $sizeTotal < 8272) {
+ $sizeTotal = 8272;
+ }
+ if(!$gConfig{'partition_table'} && $sizeTotal < 8192) {
+ $sizeTotal = 8192;
+ }
+
+ # hdiutil create without -srcfolder or -srcdevice will not accept
+ # -format. It uses -type. Fortunately, the two supported formats
+ # here map directly to the only two supported types.
+ if ($format eq 'UDSP') {
+ $type = 'SPARSE';
+ }
+ else {
+ $type = 'UDIF';
+ }
+
+ if(command($gConfig{'cmd_hdiutil'}, 'create', '-type', $type,
+ @extraArguments, '-fs', 'HFS+', '-volname', $name,
+ '-ov', '-sectors', $sizeTotal, $destination) != 0) {
+ cleanupDie('hdiutil create failed');
+ }
+
+ push(@gCleanup,
+ sub {commandInternalVerbosity(0, 'unlink', $destination);});
+
+ # The rsync will occur shortly.
+ }
+
+ my($mounted, $rootDevice, $partitionDevice, $partitionMountPoint);
+
+ $mounted=0;
+ if(!$gConfig{'create_directly'} || $gConfig{'openfolder_bless'} ||
+ $setRootIcon) {
+ # The disk image only needs to be mounted if:
+ # create_directly is false, because the content needs to be copied
+ # openfolder_bless is true, because bless -openfolder needs to run
+ # setRootIcon is true, because the root needs its attributes set.
+ if(!(($rootDevice, $partitionDevice, $partitionMountPoint) =
+ hdidMountImage($tempMount, $destination))) {
+ cleanupDie('hdid mount failed');
+ }
+
+ $mounted=1;
+
+ push(@gCleanup, sub {commandVerbosity(0,
+ $gConfig{'cmd_diskutil'}, 'eject', $rootDevice);});
+ }
+
+ if(!$gConfig{'create_directly'}) {
+ # Couldn't create and copy directly in one fell swoop. Now that
+ # the volume is mounted, copy the files. --copy-unsafe-links is
+ # unnecessary since it was used to copy everything to the staging
+ # area. There can be no more unsafe links.
+ if(command($gConfig{'cmd_rsync'}, '-a',
+ $source.'/',$partitionMountPoint) != 0) {
+ cleanupDie('rsync to new volume failed');
+ }
+
+ # We need to get the rm -rf of $source off the stack, because it's
+ # being cleaned up here. There are two items now on top of it:
+ # removing the target image and, above that, ejecting it. Splice it
+ # out.
+ my(@tempCleanup);
+ @tempCleanup = splice(@gCleanup, -2);
+ # The next splice is the same as popping once and pushing @tempCleanup.
+ splice(@gCleanup, -1, 1, @tempCleanup);
+
+ if(command($gConfig{'cmd_rm'}, '-rf', $source) != 0) {
+ cleanupDie('rm -rf failed');
+ }
+ }
+
+ if($gConfig{'openfolder_bless'}) {
+ # On Tiger, the bless docs say to use --openfolder, but only
+ # --openfolder is accepted on Panther. Tiger takes it with a single
+ # dash too. Jaguar is out of luck.
+ if(command($gConfig{'cmd_bless'}, '-openfolder',
+ $partitionMountPoint) != 0) {
+ cleanupDie('bless failed');
+ }
+ }
+
+ setAttributes($partitionMountPoint, @attributes);
+
+ if($setRootIcon) {
+ # When "hdiutil create -srcfolder" is used, the root folder's
+ # attributes are not copied to the new volume. Fix up.
+
+ if(command($gConfig{'cmd_SetFile'}, '-a', 'C',
+ $partitionMountPoint) != 0) {
+ cleanupDie('SetFile failed');
+ }
+ }
+
+ if($mounted) {
+ # Pop diskutil eject
+ pop(@gCleanup);
+
+ if(command($gConfig{'cmd_diskutil'}, 'eject', $rootDevice) != 0) {
+ cleanupDie('diskutil eject failed');
+ }
+ }
+
+ # End of UDRW/UDSP section. At this point, $source has been removed
+ # and its cleanup entry has been removed from the stack.
+ }
+ else {
+ cleanupDie('unrecognized format');
+ print STDERR ($0.": unrecognized format\n");
+ exit(1);
+ }
+}
+
+# giveExtension($file, $extension)
+#
+# If $file does not end in $extension, $extension is added. The new
+# filename is returned.
+sub giveExtension($$) {
+ my($extension, $file);
+ ($file, $extension) = @_;
+ if(substr($file, -length($extension)) ne $extension) {
+ return $file.$extension;
+ }
+ return $file;
+}
+
+# hdidMountImage($mountPoint, @arguments)
+#
+# Runs the hdid command with arguments specified by @arguments.
+# @arguments may be a single-element array containing the name of the
+# disk image to mount. Returns a three-element array, with elements
+# corresponding to:
+# - The root device of the mounted image, suitable for ejection
+# - The device corresponding to the mounted partition
+# - The mounted partition's mount point
+#
+# If running on a system that supports easy mounting at points outside
+# of the default /Volumes with hdiutil attach, it is used instead of hdid,
+# and $mountPoint is used as the mount point.
+#
+# The root device will differ from the partition device when the disk
+# image contains a partition table, otherwise, they will be identical.
+#
+# If hdid fails, undef is returned.
+sub hdidMountImage($@) {
+ my(@arguments, @command, $mountPoint);
+ ($mountPoint, @arguments) = @_;
+ my(@output);
+
+ if($gConfig{'hdiutil_mountpoint'}) {
+ @command=($gConfig{'cmd_hdiutil'}, 'attach', @arguments,
+ '-mountpoint', $mountPoint);
+ }
+ else {
+ @command=($gConfig{'cmd_hdid'}, @arguments);
+ }
+
+ if(!(@output = commandOutput(@command)) ||
+ $? != 0) {
+ return undef;
+ }
+
+ if($gDryRun) {
+ return('/dev/diskX','/dev/diskXsY','/Volumes/'.$volumeName);
+ }
+
+ my($line, $restOfLine, $rootDevice);
+
+ foreach $line (@output) {
+ my($device, $mountpoint);
+ if($line !~ /^\/dev\//) {
+ # Consider only lines that correspond to /dev entries
+ next;
+ }
+ ($device, $restOfLine) = split(' ', $line, 2);
+
+ if(!defined($rootDevice) || $rootDevice eq '') {
+ # If this is the first device seen, it's the root device to be
+ # used for ejection. Keep it.
+ $rootDevice = $device;
+ }
+
+ if($restOfLine =~ /(\/.*)/) {
+ # The first partition with a mount point is the interesting one. It's
+ # usually Apple_HFS and usually the last one in the list, but beware of
+ # the possibility of other filesystem types and the Apple_Free partition.
+ # If the disk image contains no partition table, the partition will not
+ # have a type, so look for the mount point by looking for a slash.
+ $mountpoint = $1;
+ return($rootDevice, $device, $mountpoint);
+ }
+ }
+
+ # No mount point? This is bad. If there's a root device, eject it.
+ if(defined($rootDevice) && $rootDevice ne '') {
+ # Failing anyway, so don't care about failure
+ commandVerbosity(0, $gConfig{'cmd_diskutil'}, 'eject', $rootDevice);
+ }
+
+ return undef;
+}
+
+# isFormatCompressed($format)
+#
+# Returns true if $format corresponds to a compressed disk image format.
+# Returns false otherwise.
+sub isFormatCompressed($) {
+ my($format);
+ ($format) = @_;
+ return $format eq 'UDZO' || $format eq 'UDBZ';
+}
+
+# licenseMaker($text, $resource)
+#
+# Takes a plain text file at path $text and creates a license agreement
+# resource containing the text at path $license. English-only, and
+# no special formatting. This is the bare-bones stuff. For more
+# intricate license agreements, create your own resource.
+#
+# ftp://ftp.apple.com/developer/Development_Kits/SLAs_for_UDIFs_1.0.dmg
+sub licenseMaker($$) {
+ my($resource, $text);
+ ($text, $resource) = @_;
+ if(!sysopen(*TEXT, $text, O_RDONLY)) {
+ print STDERR ($0.': licenseMaker: sysopen text: '.$!."\n");
+ return 0;
+ }
+ if(!sysopen(*RESOURCE, $resource, O_WRONLY|O_CREAT|O_EXCL)) {
+ print STDERR ($0.': licenseMaker: sysopen resource: '.$!."\n");
+ return 0;
+ }
+ print RESOURCE << '__EOT__';
+// See /System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework/Headers/Script.h for language IDs.
+data 'LPic' (5000) {
+ // Default language ID, 0 = English
+ $"0000"
+ // Number of entries in list
+ $"0001"
+
+ // Entry 1
+ // Language ID, 0 = English
+ $"0000"
+ // Resource ID, 0 = STR#/TEXT/styl 5000
+ $"0000"
+ // Multibyte language, 0 = no
+ $"0000"
+};
+
+resource 'STR#' (5000, "English") {
+ {
+ // Language (unused?) = English
+ "English",
+ // Agree
+ "Agree",
+ // Disagree
+ "Disagree",
+__EOT__
+ # This stuff needs double-quotes for interpolations to work.
+ print RESOURCE (" // Print, ellipsis is 0xC9\n");
+ print RESOURCE (" \"Print\xc9\",\n");
+ print RESOURCE (" // Save As, ellipsis is 0xC9\n");
+ print RESOURCE (" \"Save As\xc9\",\n");
+ print RESOURCE (' // Descriptive text, curly quotes are 0xD2 and 0xD3'.
+ "\n");
+ print RESOURCE (' "If you agree to the terms of this license '.
+ "agreement, click \xd2Agree\xd3 to access the software. If you ".
+ "do not agree, press \xd2Disagree.\xd3\"\n");
+print RESOURCE << '__EOT__';
+ };
+};
+
+// Beware of 1024(?) byte (character?) line length limitation. Split up long
+// lines.
+// If straight quotes are used ("), remember to escape them (\").
+// Newline is \n, to leave a blank line, use two of them.
+// 0xD2 and 0xD3 are curly double-quotes ("), 0xD4 and 0xD5 are curly
+// single quotes ('), 0xD5 is also the apostrophe.
+data 'TEXT' (5000, "English") {
+__EOT__
+
+ while(!eof(*TEXT)) {
+ my($line);
+ chop($line = );
+
+ while(defined($line)) {
+ my($chunk);
+
+ # Rez doesn't care for lines longer than (1024?) characters. Split
+ # at less than half of that limit, in case everything needs to be
+ # backwhacked.
+ if(length($line)>500) {
+ $chunk = substr($line, 0, 500);
+ $line = substr($line, 500);
+ }
+ else {
+ $chunk = $line;
+ $line = undef;
+ }
+
+ if(length($chunk) > 0) {
+ # Unsafe characters are the double-quote (") and backslash (\), escape
+ # them with backslashes.
+ $chunk =~ s/(["\\])/\\$1/g;
+
+ print RESOURCE ' "'.$chunk.'"'."\n";
+ }
+ }
+ print RESOURCE ' "\n"'."\n";
+ }
+ close(*TEXT);
+
+ print RESOURCE << '__EOT__';
+};
+
+data 'styl' (5000, "English") {
+ // Number of styles following = 1
+ $"0001"
+
+ // Style 1. This is used to display the first two lines in bold text.
+ // Start character = 0
+ $"0000 0000"
+ // Height = 16
+ $"0010"
+ // Ascent = 12
+ $"000C"
+ // Font family = 1024 (Lucida Grande)
+ $"0400"
+ // Style bitfield, 0x1=bold 0x2=italic 0x4=underline 0x8=outline
+ // 0x10=shadow 0x20=condensed 0x40=extended
+ $"00"
+ // Style, unused?
+ $"02"
+ // Size = 12 point
+ $"000C"
+ // Color, RGB
+ $"0000 0000 0000"
+};
+__EOT__
+ close(*RESOURCE);
+
+ return 1;
+}
+
+# pathSplit($pathname)
+#
+# Splits $pathname into an array of path components.
+sub pathSplit($) {
+ my($pathname);
+ ($pathname) = @_;
+ return split(/\//, $pathname);
+}
+
+# setAttributes($root, @attributeList)
+#
+# @attributeList is an array, each element of which must be in the form
+# :. is a list of attributes, per SetFile. is a file
+# which is taken as relative to $root (even if it appears as an absolute
+# path.) SetFile is called to set the attributes on each file in
+# @attributeList.
+sub setAttributes($@) {
+ my(@attributes, $root);
+ ($root, @attributes) = @_;
+ my($attribute);
+ foreach $attribute (@attributes) {
+ my($attrList, $file, @fileList, @fixedFileList);
+ ($attrList, @fileList) = split(/:/, $attribute);
+ if(!defined($attrList) || !@fileList) {
+ cleanupDie('--attribute requires :');
+ }
+ @fixedFileList=();
+ foreach $file (@fileList) {
+ if($file =~ /^\//) {
+ push(@fixedFileList, $root.$file);
+ }
+ else {
+ push(@fixedFileList, $root.'/'.$file);
+ }
+ }
+ if(command($gConfig{'cmd_SetFile'}, '-a', $attrList, @fixedFileList)) {
+ cleanupDie('SetFile failed to set attributes');
+ }
+ }
+ return;
+}
+
+sub trapSignal($) {
+ my($signalName);
+ ($signalName) = @_;
+ cleanupDie('exiting on SIG'.$signalName);
+}
+
+sub usage() {
+ print STDERR (
+"usage: pkg-dmg --source \n".
+" --target \n".
+" [--format ] (default: UDZO)\n".
+" [--volname ] (default: same name as source)\n".
+" [--tempdir ] (default: same dir as target)\n".
+" [--mkdir ] (make directory in image)\n".
+" [--copy [:]] (extra files to add)\n".
+" [--symlink [:]] (extra symlinks to add)\n".
+" [--license ] (plain text license agreement)\n".
+" [--resource ] (flat .r files to merge)\n".
+" [--icon ] (volume icon)\n".
+" [--attribute :] (set file attributes)\n".
+" [--idme] (make Internet-enabled image)\n".
+" [--sourcefile] (treat --source as a file)\n".
+" [--verbosity ] (0, 1, 2; default=2)\n".
+" [--dry-run] (print what would be done)\n");
+ return;
+}
diff --git a/app/mac/updater.tar.xz b/app/mac/updater.tar.xz
new file mode 100644
index 0000000000..1451d049cc
--- /dev/null
+++ b/app/mac/updater.tar.xz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53642619a815ae1c848dfb6f33e1e528db74edc39b3a692b6f312cace640c437
+size 92884
diff --git a/app/mac/updater_renamer b/app/mac/updater_renamer
new file mode 100755
index 0000000000..52d5b42eae
--- /dev/null
+++ b/app/mac/updater_renamer
@@ -0,0 +1,10 @@
+#!/usr/bin/env python3
+
+# Convert localized app name from "Firefox Software Update" to "Zotero Software Update"
+#
+# plist is UTF-16
+f='updater.app/Contents/Resources/English.lproj/InfoPlist.strings'
+with open(f, 'r', encoding='utf-16') as inputfile:
+ newText = inputfile.read().replace('Firefox', 'Zotero')
+with open(f, 'w', encoding='utf-16') as outputfile:
+ outputfile.write(newText)
diff --git a/app/mac/zotero.xz b/app/mac/zotero.xz
new file mode 100755
index 0000000000..68b5691df2
Binary files /dev/null and b/app/mac/zotero.xz differ
diff --git a/app/modules/zotero-libreoffice-integration b/app/modules/zotero-libreoffice-integration
new file mode 160000
index 0000000000..e9be47895c
--- /dev/null
+++ b/app/modules/zotero-libreoffice-integration
@@ -0,0 +1 @@
+Subproject commit e9be47895c97e6a61a39b830a7347d9a76c480e5
diff --git a/app/modules/zotero-word-for-mac-integration b/app/modules/zotero-word-for-mac-integration
new file mode 160000
index 0000000000..43f8efffa4
--- /dev/null
+++ b/app/modules/zotero-word-for-mac-integration
@@ -0,0 +1 @@
+Subproject commit 43f8efffa447e542b6804b8698570b00045a7588
diff --git a/app/modules/zotero-word-for-windows-integration b/app/modules/zotero-word-for-windows-integration
new file mode 160000
index 0000000000..9ce2a32f2c
--- /dev/null
+++ b/app/modules/zotero-word-for-windows-integration
@@ -0,0 +1 @@
+Subproject commit 9ce2a32f2cc6b2f387524bb23dc6a25d584d7c28
diff --git a/app/scripts/6.0_release_build_and_deploy b/app/scripts/6.0_release_build_and_deploy
new file mode 100755
index 0000000000..d3f647946f
--- /dev/null
+++ b/app/scripts/6.0_release_build_and_deploy
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -euo pipefail
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
+. "$APP_ROOT_DIR/config.sh"
+
+CHANNEL="release"
+BRANCH="6.0"
+
+cd "$SCRIPT_DIR"
+./check_requirements
+
+hash=`./get_repo_branch_hash $BRANCH`
+source_dir=`./get_commit_files $hash`
+build_dir=`mktemp -d`
+
+function cleanup {
+ rm -rf "$source_dir"
+ rm -rf "$build_dir"
+}
+trap cleanup EXIT
+
+./prepare_build -s "$source_dir" -o "$build_dir" -c $CHANNEL -m $hash
+./build_and_deploy -d "$build_dir" -p $BUILD_PLATFORMS -c $CHANNEL
diff --git a/app/scripts/7.0_beta_build_and_deploy b/app/scripts/7.0_beta_build_and_deploy
new file mode 100755
index 0000000000..079d67fe79
--- /dev/null
+++ b/app/scripts/7.0_beta_build_and_deploy
@@ -0,0 +1,30 @@
+#!/bin/bash
+set -euo pipefail
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
+. "$APP_ROOT_DIR/config.sh"
+
+CHANNEL="beta"
+BRANCH="main"
+export SAFARI_APPEX="$ROOT_DIR/../safari-app-extension-builds/beta/ZoteroSafariExtension.appex"
+
+# Set Safari extension LSMinimumSystemVersion to Mojave for betas
+perl -pi -e 's/11\.0<\/string>/10.14<\/string>/' "$SAFARI_APPEX"/Contents/Info.plist
+
+cd "$SCRIPT_DIR"
+./check_requirements
+
+hash=`./get_repo_branch_hash $BRANCH`
+source_dir=`./get_commit_files $hash`
+build_dir=`mktemp -d`
+
+function cleanup {
+ rm -rf "$source_dir"
+ rm -rf "$build_dir"
+}
+trap cleanup EXIT
+
+./prepare_build -s "$source_dir" -o "$build_dir" -c $CHANNEL -m $hash
+./build_and_deploy -d "$build_dir" -p $BUILD_PLATFORMS -c $CHANNEL
diff --git a/app/scripts/7.0_dev_build_and_deploy b/app/scripts/7.0_dev_build_and_deploy
new file mode 100755
index 0000000000..7e9cca3006
--- /dev/null
+++ b/app/scripts/7.0_dev_build_and_deploy
@@ -0,0 +1,27 @@
+#!/bin/bash
+set -euo pipefail
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
+. "$APP_ROOT_DIR/config.sh"
+
+CHANNEL="dev"
+BRANCH="main"
+export SAFARI_APPEX="$ROOT_DIR/../safari-app-extension-builds/dev/ZoteroSafariExtension.appex"
+
+cd "$SCRIPT_DIR"
+./check_requirements
+
+hash=`./get_repo_branch_hash $BRANCH`
+source_dir=`./get_commit_files $hash`
+build_dir=`mktemp -d`
+
+function cleanup {
+ rm -rf "$source_dir"
+ rm -rf "$build_dir"
+}
+trap cleanup EXIT
+
+./prepare_build -s "$source_dir" -o "$build_dir" -c $CHANNEL -m $hash
+./build_and_deploy -d "$build_dir" -p $BUILD_PLATFORMS -c $CHANNEL -i 1
diff --git a/app/scripts/7.0_test_build_and_deploy b/app/scripts/7.0_test_build_and_deploy
new file mode 100755
index 0000000000..a2864e80bb
--- /dev/null
+++ b/app/scripts/7.0_test_build_and_deploy
@@ -0,0 +1,27 @@
+#!/bin/bash
+set -euo pipefail
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
+. "$APP_ROOT_DIR/config.sh"
+
+CHANNEL="test"
+BRANCH="main"
+export SAFARI_APPEX="$ROOT_DIR/../safari-app-extension-builds/test/ZoteroSafariExtension.appex"
+
+cd "$SCRIPT_DIR"
+./check_requirements
+
+hash=`./get_repo_branch_hash $BRANCH`
+source_dir=`./get_commit_files $hash`
+build_dir=`mktemp -d`
+
+function cleanup {
+ rm -rf "$source_dir"
+ rm -rf "$build_dir"
+}
+trap cleanup EXIT
+
+./prepare_build -s "$source_dir" -o "$build_dir" -c $CHANNEL -m $hash
+./build_and_deploy -d "$build_dir" -p $BUILD_PLATFORMS -c $CHANNEL -i 1
diff --git a/app/scripts/add_omni_file b/app/scripts/add_omni_file
new file mode 100755
index 0000000000..cc309e14da
--- /dev/null
+++ b/app/scripts/add_omni_file
@@ -0,0 +1,52 @@
+#!/bin/bash
+set -euo pipefail
+
+#
+# Zip a file directly into app/omni.ja in staging/
+#
+# Zip paths are relative to the current directory, so this should be run from
+# the client build/ directory
+#
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+. "$ROOT_DIR/config.sh"
+
+function usage {
+ cat >&2 <&2 <&2
+ exit 1
+ ;;
+ esac
+done
+
+if [ $REBUILD -eq 1 ]; then
+ PARAMS=""
+ if [ $DEBUGGER -eq 1 ]; then
+ PARAMS="-t"
+ fi
+
+ # Check if build watch is running
+ # If not, run now
+ if ! ps u | grep js-build/build.js | grep -v grep > /dev/null; then
+ echo "Running JS build process"
+ echo
+ cd $ROOT_DIR
+ # TEMP: --openssl-legacy-provider avoids a build error in pdf.js
+ NODE_OPTIONS=--openssl-legacy-provider npm run build
+ echo
+ fi
+
+ "$SCRIPT_DIR/dir_build" -q $PARAMS
+
+ if [ "`uname`" = "Darwin" ]; then
+ # Sign the Word dylib so it works on Apple Silicon
+ "$SCRIPT_DIR/codesign_local" "$APP_ROOT_DIR/staging/Zotero.app"
+ fi
+fi
+
+PARAMS=""
+if [ $SKIP_BUNDLED_FILES -eq 1 ]; then
+ PARAMS="$PARAMS -ZoteroSkipBundledFiles"
+fi
+if [ $DEBUGGER -eq 1 ]; then
+ PARAMS="$PARAMS -debugger"
+fi
+
+if [ "`uname`" = "Darwin" ]; then
+ command="Zotero.app/Contents/MacOS/zotero"
+elif [ "`uname`" = "Linux" ]; then
+ command="Zotero_linux-x86_64/zotero"
+elif [ "`uname -o 2> /dev/null`" = "Cygwin" ]; then
+ command="Zotero_win-x64/zotero.exe"
+else
+ echo "Unknown platform" >&2
+ exit 1
+fi
+
+"$APP_ROOT_DIR/staging/$command" $profile -ZoteroDebugText -jsconsole -purgecaches $PARAMS "$@"
diff --git a/app/scripts/check_requirements b/app/scripts/check_requirements
new file mode 100755
index 0000000000..a42c67548e
--- /dev/null
+++ b/app/scripts/check_requirements
@@ -0,0 +1,161 @@
+#!/bin/bash
+set -uo pipefail
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+ROOT_DIR="$(dirname "$APP_ROOT_DIR")"
+. "$APP_ROOT_DIR/config.sh"
+. "$SCRIPT_DIR/bootstrap.sh"
+
+cd "$ROOT_DIR"
+
+platform=`get_current_platform`
+
+FAIL_CMD='echo -e \033[31;1mFAIL\033[0m'
+FAIL_CMD_INLINE='echo -n -e \033[31;1mFAIL\033[0m'
+FAILED=0
+
+hdr_start=`tput smul`
+hdr_stop=`tput rmul`
+
+echo "${hdr_start}Checking build requirements:${hdr_stop}"
+echo
+
+echo -n "Checking for Node.js: "
+which node || { $FAIL_CMD_INLINE; FAILED=1; }
+node_version=$(node -v | cut -c 2-)
+echo -n "Checking Node version: $node_version"
+if [ $(echo $node_version | cut -d '.' -f1 ) -ge "18" ]; then
+ echo
+else
+ echo -n " -- must be 18 or later "
+ { $FAIL_CMD; FAILED=1; }
+fi
+
+echo -n "Checking for Git LFS: "
+which git-lfs || { $FAIL_CMD_INLINE; FAILED=1; }
+
+echo "Checking for Git LFS checkouts: "
+lfs_files=$(git lfs ls-files -n 2> /dev/null) || { $FAIL_CMD; FAILED=1; }
+for lfs_file in $lfs_files; do
+ echo -n " - $lfs_file: "
+ file_type=$(file -b $lfs_file)
+ echo -n $file_type" "
+ if [ -z "$(echo -n $file_type | grep XZ)" ]; then
+ { $FAIL_CMD_INLINE; FAILED=1; echo " -- run 'git lfs pull'"; }
+ fi
+ echo
+done
+
+echo -n "Checking for perl: "
+which perl || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for python3: "
+which python3 || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for curl: "
+which curl || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for wget: "
+which wget || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for zip: "
+which zip || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for unzip: "
+which unzip || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for xz: "
+which xz || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for awk: "
+which awk || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for rsync: "
+which rsync || { $FAIL_CMD; FAILED=1; }
+
+if [ $platform = "w" ]; then
+ echo -n "Checking for 7z: "
+ which 7z || { $FAIL_CMD; FAILED=1; }
+
+ echo -n "Checking for rcedit: "
+ which rcedit || { $FAIL_CMD; FAILED=1; echo " -- Install with fetch_rcedit"; }
+fi
+
+if [ $platform = "w" ]; then
+ echo
+ echo "${hdr_start}Checking Windows packaging requirements:${hdr_stop}"
+ echo
+
+ echo -n "Checking for upx: "
+ which upx || { $FAIL_CMD; FAILED=1; }
+
+ echo -n "Checking for uuidgen: "
+ which uuidgen || { $FAIL_CMD; FAILED=1; }
+
+ echo -n "Checking for code-signing script: "
+ if [ -x "$APP_ROOT_DIR/win/codesign" ]; then
+ echo "$APP_ROOT_DIR/win/codesign"
+ else
+ $FAIL_CMD
+ FAILED=1
+ fi
+
+ echo -n "Checking for Unicode NSIS: "
+ if [ -x "`cygpath -u \"${NSIS_DIR}makensis.exe\"`" ]; then
+ echo "`cygpath -u \"${NSIS_DIR}makensis.exe\"`"
+ else
+ $FAIL_CMD
+ FAILED=1
+ fi
+
+ plugin_path=$(cd "$NSIS_DIR\\Plugins\\x86-unicode" && pwd)
+ plugins="AppAssocReg ApplicationID InvokeShellVerb ShellLink UAC"
+ echo "Checking for NSIS plugins in $plugin_path"
+ for i in $plugins; do
+ echo -n " $i.dll: "
+ if [ -f "$plugin_path/$i.dll" ]; then
+ echo OK
+ else
+ $FAIL_CMD
+ FAILED=1
+ fi
+ done
+fi
+
+if [ $platform = "m" ]; then
+ echo
+ echo "${hdr_start}Checking Mac packaging requirements:${hdr_stop}"
+ echo
+
+ echo -n "Checking for codesign: "
+ which /usr/bin/codesign || { $FAIL_CMD; FAILED=1; }
+fi
+
+echo
+echo "${hdr_start}Checking distribution requirements:${hdr_stop}"
+echo
+
+echo -n "Checking for Mozilla ARchive (MAR) tool: "
+which mar || { $FAIL_CMD; FAILED=1; echo " -- Install with fetch_mar_tools"; }
+
+echo -n "Checking for mbsdiff: "
+which mbsdiff || { $FAIL_CMD; FAILED=1; echo " -- Install with fetch_mar_tools"; }
+
+echo -n "Checking for rsync: "
+which rsync || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for sha512sum/shasum: "
+which sha512sum 2>/dev/null || which shasum 2>/dev/null || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for AWS CLI: "
+which aws || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for AWS S3 access: "
+aws s3 ls $S3_BUCKET/$S3_DIST_PATH | sed 's/^[[:blank:]]*//' || { $FAIL_CMD; FAILED=1; }
+
+echo -n "Checking for deploy host directory access: "
+ssh $DEPLOY_HOST ls -d $DEPLOY_PATH || { $FAIL_CMD; FAILED=1; }
+
+
+exit $FAILED
diff --git a/app/scripts/codesign_local b/app/scripts/codesign_local
new file mode 100755
index 0000000000..cba7af2b17
--- /dev/null
+++ b/app/scripts/codesign_local
@@ -0,0 +1,40 @@
+#!/bin/bash
+set -euo pipefail
+
+# Perform ad-hoc code signing of Zotero.app for local usage
+#
+# Currently we sign only the Word dylib, since that's necessary for Zotero developers to work on
+# Word integration on Apple Silicon. If we discover other problems, we can uncomment some of the
+# other lines. If you're making a custom build, you can modify this file to sign the entire build
+# instead of just the bare minimum needed for development.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+. "$ROOT_DIR/config.sh"
+
+if [ -z "${1:-}" ]; then
+ echo "Usage: $0 path/to/staging/Zotero.app"
+ exit 1
+fi
+
+APPDIR=$1
+DEVELOPER_ID="-"
+
+entitlements_file="$ROOT_DIR/mac/entitlements.xml"
+#/usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" \
+# "$APPDIR/Contents/MacOS/XUL" \
+# "$APPDIR/Contents/MacOS/updater.app/Contents/MacOS/org.mozilla.updater"
+#find "$APPDIR/Contents" -name '*.dylib' -exec /usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" {} \;
+#find "$APPDIR/Contents" -name '*.app' -exec /usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" {} \;
+#/usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" "$APPDIR/Contents/MacOS/zotero"
+
+# Skip signing of Safari extension, since it's not present for local builds
+
+# Sign final app package
+#echo
+#/usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" "$APPDIR"
+
+# Verify app
+#/usr/bin/codesign --verify -vvvv "$APPDIR"
+
+find "$APPDIR/Contents" -name 'libZoteroWordIntegration.dylib' -exec /usr/bin/codesign --force --options runtime --entitlements "$entitlements_file" --sign "$DEVELOPER_ID" {} \;
diff --git a/app/scripts/dir_build b/app/scripts/dir_build
new file mode 100755
index 0000000000..0dac8807c0
--- /dev/null
+++ b/app/scripts/dir_build
@@ -0,0 +1,88 @@
+#!/bin/bash
+set -euo pipefail
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
+. "$APP_ROOT_DIR/config.sh"
+
+function usage {
+ cat >&2 <&2
+ exit 1
+ ;;
+ esac
+done
+
+if [[ -z $PLATFORM ]]; then
+ if [ "`uname`" = "Darwin" ]; then
+ PLATFORM="m"
+ elif [ "`uname`" = "Linux" ]; then
+ PLATFORM="l"
+
+ # If platform not given explicitly, skip 32-bit build if 64-bit system
+ if [ "$(arch)" = "x86_64" ]; then
+ export SKIP_32=1
+ fi
+ elif [ "`uname -o 2> /dev/null`" = "Cygwin" ]; then
+ PLATFORM="w"
+ fi
+fi
+
+CHANNEL="source"
+
+PARAMS=""
+if [ $DEVTOOLS -eq 1 ]; then
+ PARAMS+=" -t"
+fi
+if [ $quick_build -eq 1 ]; then
+ PARAMS+=" -q"
+fi
+
+hash=`git -C "$ROOT_DIR" rev-parse --short HEAD`
+
+build_dir=`mktemp -d`
+function cleanup {
+ rm -rf $build_dir
+}
+trap cleanup EXIT
+
+"$SCRIPT_DIR/prepare_build" -s "$ROOT_DIR/build" -o "$build_dir" -c $CHANNEL -m $hash
+"$APP_ROOT_DIR/build.sh" -d "$build_dir" -p $PLATFORM -c $CHANNEL -s $PARAMS
+
+echo Done
diff --git a/app/scripts/fetch_mar_tools b/app/scripts/fetch_mar_tools
new file mode 100755
index 0000000000..24f836bebc
--- /dev/null
+++ b/app/scripts/fetch_mar_tools
@@ -0,0 +1,22 @@
+#!/bin/bash
+set -euo pipefail
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+
+cd "$APP_ROOT_DIR"
+
+mkdir -p "xulrunner/bin"
+if [ "`uname`" = "Darwin" ]; then
+ # Mozilla has Linux executables where the Mac files should be, so supply our own Mac builds
+ curl -o "xulrunner/bin/mar" https://zotero-download.s3.us-east-1.amazonaws.com/tools/mac/102.11.0esr/mar
+ curl -o "xulrunner/bin/mbsdiff" https://zotero-download.s3.us-east-1.amazonaws.com/tools/mac/102.11.0esr/mbsdiff
+elif [ "`uname -o 2> /dev/null`" = "Cygwin" ]; then
+ # Mozilla doesn't seem to provide Windows versions via its file server anymore, so supply our own
+ curl -o "xulrunner/bin/mar.exe" https://zotero-download.s3.us-east-1.amazonaws.com/tools/win/102.11.0esr/mar.exe
+ curl -o "xulrunner/bin/mbsdiff.exe" https://zotero-download.s3.us-east-1.amazonaws.com/tools/win/102.11.0esr/mbsdiff.exe
+else
+ curl -o "xulrunner/bin/mar" https://ftp.mozilla.org/pub/firefox/nightly/2022/05/2022-05-30-09-39-43-mozilla-central/mar-tools/linux64/mar
+ curl -o "xulrunner/bin/mbsdiff" https://ftp.mozilla.org/pub/firefox/nightly/2022/05/2022-05-30-09-39-43-mozilla-central/mar-tools/linux64/mbsdiff
+fi
+chmod 755 xulrunner/bin/mar xulrunner/bin/mbsdiff
diff --git a/app/scripts/fetch_rcedit b/app/scripts/fetch_rcedit
new file mode 100755
index 0000000000..89ac62bd10
--- /dev/null
+++ b/app/scripts/fetch_rcedit
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -euo pipefail
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+
+cd "$ROOT_DIR"
+
+mkdir -p "xulrunner/bin"
+curl -L -o "xulrunner/bin/rcedit.exe" https://github.com/electron/rcedit/releases/download/v1.1.1/rcedit-x86.exe
+chmod 755 xulrunner/bin/rcedit
diff --git a/app/scripts/fetch_xulrunner b/app/scripts/fetch_xulrunner
new file mode 100755
index 0000000000..3cf5d44b97
--- /dev/null
+++ b/app/scripts/fetch_xulrunner
@@ -0,0 +1,571 @@
+#!/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 "$file" -- 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 {
+ 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.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_SERVICES_HEALTHREPORT:' 'MOZ_SERVICES_HEALTHREPORT: false \&\&' modules/AppConstants.sys.mjs
+ replace_line 'MOZ_TELEMETRY_REPORTING:' 'MOZ_TELEMETRY_REPORTING: false \&\&' modules/AppConstants.sys.mjs
+ replace_line 'MOZ_TELEMETRY_ON_BY_DEFAULT:' 'MOZ_TELEMETRY_ON_BY_DEFAULT: 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
+
+ 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
+
+ # Do not trigger LoginManager event that logs an error on autocomplete submission
+ remove_line 'DOMInputPasswordAdded: \{\},' modules/ActorManagerParent.sys.mjs
+
+ # 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
+ rm modules/FxAccounts*
+ # Causes a startup error -- try an empty file or a shim instead?
+ #rm modules/Telemetry*
+ rm modules/URLDecorationAnnotationsService.sys.mjs
+ 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.sys.mjs
+
+ # 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.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.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
+
+ # 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.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.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
+
+ # 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.userPermissions\) \{' '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.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.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 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"] + 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: -moz-nativehyperlinktext; }' >> $file
+ echo '.addon-detail-row-homepage .text-link:hover { text-decoration: underline; }' >> $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
+
+ # 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>' \
+ '