Compare commits

..

No commits in common. "main" and "7.0.3" have entirely different histories.
main ... 7.0.3

1939 changed files with 100009 additions and 241380 deletions

17
.eslintignore Normal file
View file

@ -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/*

63
.eslintrc Normal file
View file

@ -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": {}
}

9
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -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.

View file

@ -1,364 +1,88 @@
name: CI
on:
push:
pull_request:
workflow_dispatch:
schedule:
# Monthly, to catch runner image and OS changes
- cron: '0 6 1 * *'
on: [push, pull_request]
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
# Decide whether to run the network-filesystem and Windows tests: always for manual and
# scheduled runs, and otherwise only when the changes touch the Gecko version, the files
# responsible for network-filesystem database handling, or (for Windows) the files that
# make up the Windows build
changes:
name: Detect changes
build:
name: Build, Upload, Test
runs-on: ubuntu-latest
if: >
github.event_name != 'pull_request'
|| github.event.pull_request.head.repo.full_name != github.repository
outputs:
network: ${{ steps.check.outputs.network }}
windows: ${{ steps.check.outputs.windows }}
steps:
- name: Check for network-filesystem-related changes
id: check
env:
EVENT: ${{ github.event_name }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
BEFORE: ${{ github.event.before }}
BASE: ${{ github.event.pull_request.base.sha }}
run: |
network=false
windows=false
if [[ "$EVENT" == "workflow_dispatch" || "$EVENT" == "schedule" ]]; then
network=true
windows=true
else
base="$BASE"
[[ -z "$base" ]] && base="$BEFORE"
git init -q repo && cd repo
git remote add origin "https://github.com/$REPO"
git fetch -q --depth 1 origin "$SHA"
# Run the tests if the base of the change can't be determined (e.g., a new
# branch or an unfetchable pre-force-push commit)
if [[ -z "$base" || "$base" == 0000000000000000000000000000000000000000 ]] \
|| ! git fetch -q --depth 1 origin "$base" 2>/dev/null; then
network=true
windows=true
else
files=$(git diff --name-only "$base" "$SHA")
if grep -qE '^(app/config\.sh|chrome/content/zotero/xpcom/(db|file)\.js|\.github/workflows/ci\.yml)$' <<< "$files"; then
network=true
windows=true
fi
if grep -qE '^app/(win/|build\.sh$|scripts/(dir_build|fetch_xulrunner|prepare_build)$)' <<< "$files"; then
windows=true
fi
fi
fi
echo "network=$network" >> "$GITHUB_OUTPUT"
echo "windows=$windows" >> "$GITHUB_OUTPUT"
test:
name: Test (${{ matrix.shard }})
runs-on: ubuntu-latest
timeout-minutes: 30
needs: changes
# Same-repo PRs are covered by the push run, so run pull_request jobs only for forks
if: >
github.event_name != 'pull_request'
|| github.event.pull_request.head.repo.full_name != github.repository
strategy:
matrix:
# Numbered shards split the full suite. 'smb' and 'nfs' run the db and file tests
# with the test data directory on a loopback CIFS or NFS mount, so that connection
# initialization itself runs against a network filesystem, which is where
# network-filesystem failures (startup crashes and hangs) occur. The network shards
# run only when the 'changes' job detects relevant changes.
shard: ${{ fromJSON(needs.changes.outputs.network == 'true'
&& '["1", "2", "3", "4", "smb", "nfs"]'
|| '["1", "2", "3", "4"]') }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
lfs: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 24
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-24-${{ hashFiles('package-lock.json') }}
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 document-worker Webpack: https://stackoverflow.com/a/69746937
# Currently necessary for pdf-worker Webpack: https://stackoverflow.com/a/69746937
env:
NODE_OPTIONS: --openssl-legacy-provider
# Create deployment ZIP from the build output, then replace build/ with the
# unzipped contents so that tests run against the same artifact that gets
# deployed. This catches problems that only manifest after a zip round-trip
# (e.g., resolved symlinks, missing files).
- name: Create deployment ZIP
- 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 *
zip -r ../build-zip/$GITHUB_SHA.zip *
cd ..
- name: Replace build with ZIP contents
run: |
rm -rf build
mkdir build
cd build
unzip ../build.zip
- name: Set up SMB share
if: matrix.shard == 'smb'
timeout-minutes: 10
run: |
sudo apt install -y --no-install-recommends samba cifs-utils
sudo mkdir -p /srv/zotero-smb /mnt/zotero-smb
sudo chown $(whoami) /srv/zotero-smb
sudo tee -a /etc/samba/smb.conf > /dev/null <<EOF
[ztest]
path = /srv/zotero-smb
read only = no
EOF
(echo smbtestpass; echo smbtestpass) | sudo smbpasswd -s -a $(whoami)
sudo systemctl restart smbd
sudo mount -t cifs //127.0.0.1/ztest /mnt/zotero-smb \
-o user=$(whoami),pass=smbtestpass,vers=3.0,uid=$(id -u),gid=$(id -g)
- name: Set up NFS share
if: matrix.shard == 'nfs'
timeout-minutes: 10
run: |
sudo apt install -y --no-install-recommends nfs-kernel-server
sudo mkdir -p /srv/zotero-nfs /mnt/zotero-nfs
sudo chown $(whoami) /srv/zotero-nfs
echo "/srv/zotero-nfs localhost(rw,no_subtree_check,insecure)" | sudo tee -a /etc/exports
sudo exportfs -ra
sudo mount -t nfs4 localhost:/srv/zotero-nfs /mnt/zotero-nfs
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
if: matrix.shard != 'smb' && matrix.shard != 'nfs'
run: xvfb-run test/runtests.sh -f -r 3 -p ${{ matrix.shard }}/4
# The step timeout turns a startup hang into a failure
- name: Run tests on network share
if: matrix.shard == 'smb' || matrix.shard == 'nfs'
timeout-minutes: 10
run: TMPDIR=/mnt/zotero-${{ matrix.shard }} xvfb-run test/runtests.sh -f -r 3 -b db file
# Run the db and file tests on macOS with the test data directory on a loopback NFS
# mount. On macOS, SQLite selects locking methods without shared-memory support on
# network filesystems, which is what the network-filesystem database handling has to
# detect and handle, so this runs the real code path that crashed on SMB/NFS/sshfs
# data directories.
test-mac:
name: Test (macOS NFS)
runs-on: macos-15
timeout-minutes: 30
needs: changes
if: >
needs.changes.outputs.network == 'true'
&& (github.event_name != 'pull_request'
|| github.event.pull_request.head.repo.full_name != github.repository)
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
lfs: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 24
- name: Cache xulrunner
id: xulrunner-cache
uses: actions/cache@v4
with:
path: app/xulrunner/Firefox.app
key: xulrunner-mac-${{ 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 m
- name: Cache Node modules
id: node-cache
uses: actions/cache@v4
with:
path: node_modules
key: node-modules-mac-24-${{ 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 document-worker Webpack: https://stackoverflow.com/a/69746937
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Set up NFS share
run: |
sudo mkdir -p /private/var/zotero-nfs
sudo chown $(whoami) /private/var/zotero-nfs
echo "/private/var/zotero-nfs -mapall=$(whoami) localhost" | sudo tee /etc/exports
sudo nfsd enable || sudo nfsd start
sleep 2
showmount -e localhost
mkdir -p "$HOME/zotero-nfs"
sudo mount_nfs -o vers=3 localhost:/private/var/zotero-nfs "$HOME/zotero-nfs"
# The step timeout turns a startup hang into a failure
- name: Run tests on network share
timeout-minutes: 10
run: TMPDIR="$HOME/zotero-nfs" test/runtests.sh -f -r 3 -b db file
# Boot the Windows build -- which uses custom-built Firefox components (xul.dll) rather
# than stock Mozilla binaries -- and run the db and file tests. A Gecko bump commit
# updates the custom-component hashes in config.sh along with the version, so this
# tests each new set of custom components.
test-win:
name: Test (Windows ${{ matrix.arch }})
runs-on: ${{ matrix.os }}
timeout-minutes: 40
needs: changes
if: >
needs.changes.outputs.windows == 'true'
&& (github.event_name != 'pull_request'
|| github.event.pull_request.head.repo.full_name != github.repository)
strategy:
fail-fast: false
matrix:
include:
- arch: x64
os: windows-latest
- arch: arm64
os: windows-11-arm
defaults:
run:
shell: bash
steps:
# The repository contains symlinks, which Git checks out as plain files on Windows
# by default, breaking the build
- name: Enable symlinks
run: git config --global core.symlinks true
- uses: actions/checkout@v4
with:
submodules: recursive
lfs: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 24
# Git Bash has unzip but not zip, which fetch_xulrunner needs to repack omni.ja
- name: Install zip
run: choco install zip --no-progress -y
- name: Cache xulrunner
id: xulrunner-cache
uses: actions/cache@v4
with:
path: app/xulrunner/firefox-win-${{ matrix.arch }}
key: xulrunner-win-${{ matrix.arch }}-${{ 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 w -a ${{ matrix.arch }}
- name: Cache Node modules
id: node-cache
uses: actions/cache@v4
with:
path: node_modules
key: node-modules-win-24-${{ hashFiles('package-lock.json') }}
- name: Install Node modules
if: steps.node-cache.outputs.cache-hit != 'true'
run: npm install
# Committed symlinks point into node_modules, which doesn't exist at checkout time,
# so Git creates the directory ones with the wrong symlink type on Windows. Recreate
# them now that the targets exist.
- name: Recreate symlinks
run: |
git ls-files -s | awk '$1 == 120000 { print $4 }' | xargs rm
git checkout -- .
- name: Build Zotero
run: npm run build
# Currently necessary for document-worker Webpack: https://stackoverflow.com/a/69746937
env:
NODE_OPTIONS: --openssl-legacy-provider
# The step timeout turns a startup hang into a failure
- name: Run tests
timeout-minutes: 15
run: test/runtests.sh -f -r 3 -b -x app/staging/Zotero_win-${{ matrix.arch }}/zotero.exe db file
utilities:
name: Utilities Tests
runs-on: ubuntu-latest
if: >
github.event_name != 'pull_request'
|| github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 24
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-24-${{ hashFiles('chrome/content/zotero/xpcom/utilities/package-lock.json') }}
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'
@ -367,73 +91,3 @@ jobs:
- name: Run utilities tests
run: |
npm test --prefix chrome/content/zotero/xpcom/utilities -- -j resource/schema/global/schema.json
# Build deployment ZIPs from main, version branches (e.g., 9.0, 10.0), and *-hotfix branches
deploy:
name: Build, Upload
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.repository == 'zotero/zotero'
steps:
- name: Check if deployment branch
id: check-deploy
if: env.ACT != 'true'
run: |
branch="${GITHUB_REF#refs/heads/}"
if [[ "$branch" == "main" || "$branch" =~ ^[0-9]+\.[0-9]+$ || "$branch" == *-hotfix ]]; then
echo "deploy=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v4
if: steps.check-deploy.outputs.deploy == 'true'
with:
submodules: recursive
lfs: true
- name: Install Node
if: steps.check-deploy.outputs.deploy == 'true'
uses: actions/setup-node@v4
with:
node-version: 24
- name: Cache Node modules
id: node-cache
if: steps.check-deploy.outputs.deploy == 'true'
uses: actions/cache@v4
with:
path: node_modules
key: node-modules-24-${{ hashFiles('package-lock.json') }}
- name: Install Node modules
if: steps.check-deploy.outputs.deploy == 'true' && steps.node-cache.outputs.cache-hit != 'true'
run: npm install
- name: Build Zotero
if: steps.check-deploy.outputs.deploy == 'true'
run: npm run build
# Currently necessary for document-worker Webpack: https://stackoverflow.com/a/69746937
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Create deployment ZIP
if: steps.check-deploy.outputs.deploy == 'true'
run: |
cd build
zip -r ../build.zip *
cd ..
- uses: ruby/setup-ruby@v1
if: steps.check-deploy.outputs.deploy == 'true'
with:
ruby-version: '3.3'
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Upload deployment ZIP
if: steps.check-deploy.outputs.deploy == 'true'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
mkdir build-zip
cp build.zip build-zip/$GITHUB_SHA.zip
gem install --no-document dpl -v '>= 2.0'
dpl s3 --bucket zotero-download --local_dir build-zip --upload_dir ci/client --acl public_read

8
.gitmodules vendored
View file

@ -18,6 +18,10 @@
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
@ -30,8 +34,8 @@
url = https://github.com/zotero/reader.git
branch = master
[submodule "pdf-worker"]
path = document-worker
url = https://github.com/zotero/document-worker.git
path = pdf-worker
url = https://github.com/zotero/pdf-worker.git
branch = master
[submodule "note-editor"]
path = note-editor

110
CLAUDE.md
View file

@ -1,110 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Zotero is a Firefox-based desktop research management application. It runs as a XUL/XHTML application on a Mozilla platform (not Electron, not a web app). The UI uses a mix of XUL/XHTML, custom elements (Web Components), and React components.
## Build & Development Commands
```bash
npm install # Install dependencies
npm run build # Full build (Babel transpile JS/JSX, compile SCSS, browserify, symlink)
npm run clean-build # Clean build output and rebuild
```
### Running Tests
Tests run inside a built Zotero application instance via `test/runtests.sh`:
```bash
test/runtests.sh # Run all tests
test/runtests.sh item # Run a single test file (omit "Test" and .js extension)
test/runtests.sh item collections # Run multiple test files
test/runtests.sh -g "pattern" # Run only tests matching a grep pattern
test/runtests.sh -f # Stop after first failure
test/runtests.sh -s item # Start at a specific test file
test/runtests.sh -e item # End at a specific test file
test/runtests.sh -c # Open JS console, don't quit on completion
test/runtests.sh -b # Skip bundled translator/style installation (for faster startup when not necessary)
test/runtests.sh -d 5 # Enable debug logging (level 1-5; default is 3)
```
Tests use Mocha + Chai (assert style) + Sinon. Test files live in `test/tests/` and follow the naming convention `<module>Test.js`. Test helpers are in `test/content/support.js`.
The test runner automatically triggers `npm run build` if the watch process isn't running, so it's not necessary to manually run a build before running tests.
Tips:
- Pass flags before test names (e.g., `test/runtests.sh -f item`).
- `-f` should almost always be used, since test failures can cause later spurious test failures.
- To view debug logging for a specific test when running multiple tests, add `Zotero.Debug.init(1)` at the beginning of the test and run with `-d 5`.
### Linting
```bash
npx eslint <file> # Lint a specific file
```
ESLint v9 flat config is in `eslint.config.mjs`. Uses `@zotero/eslint-config`, Babel parser, and React plugin. Currently, the code base is not fully linted, so only worry about issues with new or modified code lines.
## Architecture
### Core Layers
- **XPCOM modules** (`chrome/content/zotero/xpcom/`) -- Core business logic. Loaded sequentially by `chrome/content/zotero/zotero.mjs`. All modules attach to the global `Zotero` namespace (e.g., `Zotero.Items`, `Zotero.Sync.Runner`).
- **Data model** (`chrome/content/zotero/xpcom/data/`) -- ORM-like classes for database entities. `Zotero.DataObject` is the base class; `Zotero.Item`, `Zotero.Collection`, `Zotero.Search`, `Zotero.Library`, etc. extend it. Plural classes (`Zotero.Items`, `Zotero.Collections`) manage object caches and queries. Data objects use an async `saveTx()` pattern for persistence.
- **Database** (`chrome/content/zotero/xpcom/db.js`) -- SQLite via Mozilla's mozStorage API. Accessed through `Zotero.DB.queryAsync()`, `Zotero.DB.executeTransaction()`.
- **Sync system** (`chrome/content/zotero/xpcom/sync/`) -- `syncRunner.js` orchestrates sync. `syncEngine.js` handles object-level sync logic. `syncAPIClient.js` communicates with the Zotero API server. `syncLocal.js` manages local sync state. Similar `storage*` files for file syncing, along with zfs.js (Zotero Storage) and webdav.js (WebDAV).
- **HTTP server** (`chrome/content/zotero/xpcom/server/`) -- Local HTTP server for browser connector integration and local API.
### UI Layers
- **Main window**: `chrome/content/zotero/zoteroPane.xhtml` + `zoteroPane.js`
- **Custom elements** (`chrome/content/zotero/elements/`) -- XUL custom elements inheriting from `XULElementBase` (defined in `elements/base.js`), which provides lifecycle helpers (`init()`, `destroy()`, `content` getter for templates). Registered via `customElements.define()`.
- **React components** (`chrome/content/zotero/components/`) -- Used for complex interactive UI (tag selector, virtualized table, item tree, collection tree). Major tree views are `itemTree.jsx` and `collectionTree.jsx`.
- **SCSS styles** (`scss/`) -- Compiled to CSS. Platform-specific overrides in `scss/mac/`, `scss/win/`, `scss/linux/`.
- **Localization** -- Fluent (`.ftl` files) in `chrome/locale/en-US/zotero/`. Accessed via `data-l10n-id` attributes or `Zotero.getString()` for legacy `.properties` strings. All new strings must be added to `.ftl` files -- do not add new strings to `.properties` or `.dtd` files.
### Submodules
Several features are developed in separate repos and included as Git submodules:
- `reader/` -- PDF/EPUB/snapshot reader with annotations
- `note-editor/` -- Rich text note editor
- `document-worker/` -- PDF processing (extraction, manipulation)
- `translators/` -- 760+ web translators for importing metadata from websites
- `styles/` -- CSL citation styles
- `chrome/content/zotero/xpcom/utilities/` -- Shared utility library
- `chrome/content/zotero/xpcom/translate/` -- Translation framework
- `resource/SingleFile/` -- Web page archiving
### Build System (`js-build/`)
Custom Node.js build system (not Webpack). `js-build/config.js` defines what gets built:
- **JS/JSX files** in `chrome/`, `components/`, `defaults/`, `test/` are transpiled via Babel (React JSX, CommonJS modules)
- **SCSS files** in `scss/` and `chrome/skin/` are compiled with Dart Sass
- **Browserify** bundles select npm packages (react-select, sinon, url) for use in the XUL environment
- **Symlinks** connect source files and submodules into the `build/` directory
- The built app reads from `build/`, not directly from source
## Code Style
- Tabs for indentation, not spaces
- Use `let` instead of `const` except for true scalar constants (e.g., `const MAX_SECONDS = 5 * 60;`)
- No cuddled braces (opening brace on same line, but `else`/`catch`/etc. on their own line)
- Use two hyphens `--` in comments, not an em dash
- Indent blank lines to match surrounding indentation level
- Objects attach to the `Zotero` global namespace rather than using ES module exports
- Async code uses `async`/`await` throughout
- Mozilla/XPCOM APIs are available globally: `Cc`, `Ci`, `Cu`, `Cr`, `Services`, `ChromeUtils`, `IOUtils`, `PathUtils`
## Translators
See `translators/CLAUDE.md` for guidelines. Key rules: **never** generate translators from scratch, **never** generate UUIDs, **never** generate test cases -- all of these must be done through Zotero's Scaffold tool.

View file

@ -3,12 +3,12 @@ Vendor=Zotero
Name=Zotero
Version={{VERSION}}
BuildID={{BUILDID}}
Copyright=Copyright (c) 2006-2025 Contributors
Copyright=Copyright (c) 2006-2022 Contributors
ID=zotero@zotero.org
[Gecko]
MinVersion=153.0
MaxVersion=153.99.*
MinVersion=115.0
MaxVersion=115.99.*
[XRE]
EnableExtensionManager=1

View file

@ -4,5 +4,4 @@
-brand-product-name = Zotero
-vendor-short-name = Zotero
-app-name = Zotero
-subscription-name = Zotero Storage
trademarkInfo = Zotero is a trademark of the Corporation for Digital Scholarship.

View file

@ -50,10 +50,8 @@ if (processTestOptions) {
TestOptions.bail = cmdLine.handleFlag("bail", false);
TestOptions.startAt = cmdLine.handleFlagWithParam("startAtTestFile", false);
TestOptions.stopAt = cmdLine.handleFlagWithParam("stopAtTestFile", false);
TestOptions.shard = cmdLine.handleFlagWithParam("shard", false);
TestOptions.grep = cmdLine.handleFlagWithParam("grep", false);
TestOptions.timeout = cmdLine.handleFlagWithParam("ZoteroTestTimeout", false);
TestOptions.retries = cmdLine.handleFlagWithParam("retries", false) || 0;
Services.ww.openWindow(
null,
@ -70,8 +68,8 @@ if (cmdLine.handleFlag("debugger", false)) {
try {
let portOrPath = Services.prefs.getBranch('').getIntPref('devtools.debugger.remote-port');
const { DevToolsLoader } = ChromeUtils.importESModule(
"resource://devtools/shared/loader/Loader.sys.mjs"
const { DevToolsLoader } = ChromeUtils.import(
"resource://devtools/shared/loader/Loader.jsm"
);
const loader = new DevToolsLoader({
freshCompartment: true,

View file

@ -194,7 +194,7 @@ caption {
/* styles for the link elements copied from .text-link in global.css */
.inline-link {
color: LinkText;
color: -moz-nativehyperlinktext;
text-decoration: underline;
}

View file

@ -6,10 +6,6 @@ pref("javascript.options.showInConsole", true);
// Don't retrieve unrequested links when performing standalone translation
pref("network.prefetch-next", false);
// Don't make DNS requests for links in snapshots
pref("network.dns.disablePrefetch", true);
// Don't open TCP connection when clicking on a link in a snapshot
pref("network.http.speculative-parallel-limit", 0);
// Let operations run as long as necessary
pref("dom.max_chrome_script_run_time", 0);
@ -54,14 +50,6 @@ pref("dom.disable_open_during_load", true);
// scraping the page, since we don't provide any information to the site.
pref("security.warn_viewing_mixed", false);
// Temporarily allow plugins to load their own scripts from jar:file: URIs with loadSubScript()
// without passing allowUnsafeURL (Mozilla bug 1974213). This will be disabled once plugins have
// had time to update.
pref("security.allow_unsafe_subscript_loads", true);
// We do need synchronous XHR
pref("network.xhr.block_sync_system_requests", false);
// Preferences for add-on discovery
pref("extensions.getAddons.cache.enabled", false);
//pref("extensions.getAddons.maxResults", 15);
@ -167,6 +155,3 @@ pref("media.ffvpx.mp3.enabled", false);
pref("media.rdd-vpx.enabled", false);
pref("media.rdd-ffvpx.enabled", false);
pref("media.utility-ffvpx.enabled", false);
// Allow collectionTree scrolling when Control is highlighting collections on win
pref("mousewheel.with_control.action", 1);

View file

@ -92,7 +92,7 @@ radio[pane=paneSync] {
/* styles for the link elements copied from .text-link in global.css */
.inline-link {
color: LinkText;
color: -moz-nativehyperlinktext;
text-decoration: underline;
}

View file

@ -91,7 +91,7 @@ radio[pane=paneSync] {
/* styles for the link elements copied from .text-link in global.css */
.inline-link {
color: LinkText;
color: -moz-nativehyperlinktext;
text-decoration: underline;
}

View file

@ -20,7 +20,6 @@
CALLDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
. "$CALLDIR/config.sh"
. "$CALLDIR/scripts/utils.sh"
if [ "`uname`" = "Darwin" ]; then
MAC_NATIVE=1
@ -35,16 +34,12 @@ fi
function usage {
cat >&2 <<DONE
Usage: $0 [-d DIR] [-f FILE] -p PLATFORMS [-a ARCH] [-c CHANNEL] [options]
Usage: $0 [-d DIR] [-f FILE] -p PLATFORMS [-c CHANNEL] [-d]
Options
-d DIR build directory to build from (from build_xpi; cannot be used with -f)
-f FILE ZIP file to build from (cannot be used with -d)
-t add devtools
-p PLATFORMS build for platforms PLATFORMS (m=Mac, w=Windows, l=Linux)
-a ARCH architecture to build (arm64, x64, win32)
* Ignored for Mac (always universal)
* If omitted on Windows/Linux, all standard archs are built
-c CHANNEL use update channel CHANNEL
-e enforce signing
-s don't package; only build binaries in staging/ directory
@ -59,14 +54,16 @@ function cleanup {
}
trap cleanup EXIT
# Copy the contents of a directory into another directory, which may already exist,
# using rsync if it's available (not on Windows)
function copy_dir {
if command -v rsync > /dev/null; then
rsync -a "$1/" "$2/"
function replace_line {
pattern=$1
replacement=$2
file=$3
if egrep -q "$pattern" "$file"; then
perl -pi -e "s/$pattern/$replacement/" "$file"
else
mkdir -p "$2"
cp -a "$1/." "$2/"
echo "$pattern" not found in "$file" -- aborting 2>&1
exit 1
fi
}
@ -89,8 +86,7 @@ BUILD_LINUX=0
PACKAGE=1
DEVTOOLS=0
quick_build=0
arch=""
while getopts "d:f:p:a:c:tseq" opt; do
while getopts "d:f:p:c:tseq" opt; do
case $opt in
d)
SOURCE_DIR="$OPTARG"
@ -99,21 +95,19 @@ while getopts "d:f:p:a:c:tseq" opt; do
ZIP_FILE="$OPTARG"
;;
p)
for (( i=0; i<${#OPTARG}; i++ )); do
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 ;;
m) BUILD_MAC=1;;
w) BUILD_WIN=1;;
l) BUILD_LINUX=1;;
*)
echo "$0: Invalid platform option ${OPTARG:i:1}" >&2
echo "$0: Invalid platform option ${OPTARG:i:1}"
usage
;;
esac
done
;;
a)
arch="$OPTARG"
;;
c)
UPDATE_CHANNEL="$OPTARG"
;;
@ -136,56 +130,31 @@ while getopts "d:f:p:a:c:tseq" opt; do
shift $((OPTIND-1)); OPTIND=1
done
xul_hash_file() {
local _platform=$1
local _arch=$2
case $_platform in
m)
echo "hash-mac";;
w)
[[ $_arch == x64 ]] && _arch="win-x64"
[[ $_arch == arm64 ]] && _arch="win-arm64"
echo "hash-${_arch}";;
l)
[[ $_arch == x64 ]] && _arch="x86_64"
echo "hash-linux-${_arch}";;
*)
echo "Invalid platform '$_platform'" >&2
exit 1
esac
}
function check_xulrunner_hash {
local _platform=$1
local _arch=$2
local platform_hash_file=$(xul_hash_file "$_platform" "${_arch:-universal}")
# TEMP: If previous Windows hash file without architecture is present, force a refresh,
# because the Firefox folder might have been changed without the new hash file being
# updated, resulting in a broken build (e.g, fx140 using fx128). We'll want to keep this
# as long as we're switching back to <=fx128. This isn't an issue on macOS because there's
# no architecture and isn't an issue on Linux because the runtime folder names changed.
if [[ $_platform = 'w' ]] && [[ -e "$CALLDIR/xulrunner/hash-win" ]]; then
echo "Found old hash-win -- forcing redownload"
rm "$CALLDIR"/xulrunner/hash-win*
fi
if [ ! -e "$CALLDIR/xulrunner/$platform_hash_file" ]; then
echo "$platform_hash_file not found -- downloading"
echo
$CALLDIR/scripts/fetch_xulrunner -p $_platform ${_arch:+-a $_arch}
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
recalculated_xulrunner_hash=$("$CALLDIR/scripts/xulrunner_hash" -p $_platform ${_arch:+-a $_arch})
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
if [ "$current_xulrunner_hash" != "$recalculated_xulrunner_hash" ]; then
echo "xulrunner hashes don't match -- redownloading"
echo
"$CALLDIR/scripts/fetch_xulrunner" -p $_platform ${_arch:+-a $_arch}
"$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!"
@ -195,36 +164,15 @@ function check_xulrunner_hash {
fi
}
# Derive platform flags from host if none provided
if [[ $BUILD_MAC == 0 ]] && [[ $BUILD_WIN == 0 ]] && [[ $BUILD_LINUX == 0 ]]; then
case "$(uname -s)" in
Darwin) BUILD_MAC=1 ;;
Linux) BUILD_LINUX=1 ;;
CYGWIN*|MINGW*) BUILD_WIN=1 ;;
esac
fi
# Check xulrunner hashes for requested builds
#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
if [[ -n $arch ]]; then
check_xulrunner_hash w $(get_canonical_arch w $arch)
else
for _a in win-x64 win-arm64 win32; do
check_xulrunner_hash w "$_a"
done
fi
check_xulrunner_hash w
fi
if [ $BUILD_LINUX == 1 ]; then
if [[ -n $arch ]]; then
check_xulrunner_hash l $(get_canonical_arch l $arch)
else
for _a in x64 arm64; do
check_xulrunner_hash l "$_a"
done
fi
check_xulrunner_hash l
fi
@ -257,7 +205,7 @@ 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-linux-"
LINUX_RUNTIME_PATH_PREFIX="$CALLDIR/xulrunner/firefox-"
base_dir="$BUILD_DIR/base"
app_dir="$BUILD_DIR/base/app"
@ -285,96 +233,17 @@ 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
# These are non-arch-specific files, so just find a runtime we fetched:
# - If the caller specified -a, that arch is guaranteed present.
# - Otherwise prefer win-x64 → win-arm64 → win32, falling back to the first
# directory that exists under xulrunner/.
if [[ -n $arch ]]; then
omni_arch=$(get_canonical_arch w $arch)
else
for cand in win-x64 win-arm64 win32; do
[[ -d "${WIN_RUNTIME_PATH_PREFIX}${cand}" ]] && { omni_arch="$cand"; break; }
done
fi
cp -Rp "${WIN_RUNTIME_PATH_PREFIX}${omni_arch}"/browser/omni "$app_dir"
unzip -qj "${WIN_RUNTIME_PATH_PREFIX}${omni_arch}"/omni.ja "hyphenation/*" \
-d "$app_dir"/hyphenation/
elif [[ $BUILD_LINUX == 1 ]]; then
# Same as above
if [[ -n $arch ]]; then
omni_arch=$(get_canonical_arch l $arch)
else
for cand in x86_64 arm64; do
[[ -d "${LINUX_RUNTIME_PATH_PREFIX}${cand}" ]] && { omni_arch="$cand"; break; }
done
fi
cp -Rp "${LINUX_RUNTIME_PATH_PREFIX}${omni_arch}"/browser/omni "$app_dir"
unzip -qj "${LINUX_RUNTIME_PATH_PREFIX}${omni_arch}"/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
# Remove Firefox files that we don't need
rm actors/AboutLogins{Parent,Child}.sys.mjs
rm actors/AboutMessagePreview{Parent,Child}.sys.mjs
rm actors/AboutNewTab{Parent,Child}.sys.mjs
rm actors/AboutPrivateBrowsing{Parent,Child}.sys.mjs
rm actors/AboutProtections{Parent,Child}.sys.mjs
rm actors/AboutReader{Parent,Child}.sys.mjs
rm actors/AboutTabCrashed{Parent,Child}.sys.mjs
rm actors/AboutWelcome{Parent,Child}.sys.mjs
rm actors/ASRouter{Parent,Child}.sys.mjs
rm actors/BackupUI{Parent,Child}.sys.mjs
rm actors/BlockedSite{Parent,Child}.sys.mjs
rm actors/BrowserProcessChild.sys.mjs
rm actors/BrowserTabChild.sys.mjs
rm actors/ClickHandler{Parent,Child}.sys.mjs
rm actors/ContentSearch{Parent,Child}.sys.mjs
rm actors/DecoderDoctor{Parent,Child}.sys.mjs
rm actors/DOMFullscreen{Parent,Child}.sys.mjs
rm actors/EncryptedMedia{Parent,Child}.sys.mjs
rm actors/FormValidation{Parent,Child}.sys.mjs
rm actors/LinkHandler{Parent,Child}.sys.mjs
rm actors/MigrationWizard{Parent,Child}.sys.mjs
rm actors/PageData{Parent,Child}.sys.mjs # Not our PageData actor
rm actors/PageInfoChild.sys.mjs
rm actors/PageStyle{Parent,Child}.sys.mjs
rm actors/Plugin{Parent,Child}.sys.mjs # GMP plugins, which we remove
rm actors/PointerLock{Parent,Child}.sys.mjs
#rm actors/RFPHelper{Parent,Child}.sys.mjs
rm actors/ScreenshotsComponentChild.sys.mjs
#rm actors/SearchSERPTelemetry{Parent,Child}.sys.mjs
#rm actors/ShoppingSidebar{Parent,Child}.sys.mjs
rm actors/SpeechDispatcher{Parent,Child}.sys.mjs
rm actors/WebRTC{Parent,Child}.sys.mjs
# Keep some essential files from chrome/browser/
mkdir chrome/browser-fx
browser_keep=(
content/browser/license.html
# For web-ext
content/browser/parent/ext-browser.js
# For spellchecking
content/browser/built_in_addons.json
# Statically imported by BackupService, which SelectableProfileService pulls in when it's
# instantiated as a command-line handler at startup
content/browser/backup/backup-constants.mjs
)
for file in "${browser_keep[@]}"; do
mkdir -p "$(dirname "chrome/browser-fx/$file")"
mv "chrome/browser/$file" "chrome/browser-fx/$file"
done
# Delete everything else
rm -r chrome/browser
# Move essential files back
mv chrome/browser-fx chrome/browser
#rm modules/SearchSERPTelemetry.sys.mjs
# Move some Firefox files that would be overwritten out of the way
mv chrome.manifest chrome.manifest-fx
mv defaults defaults-fx
@ -385,18 +254,11 @@ if [ -n "$ZIP_FILE" ]; then
echo "Building from $ZIP_FILE"
unzip -q $ZIP_FILE -d "$omni_dir"
else
if command -v rsync > /dev/null; then
rsync_params=""
if [ $include_tests -eq 0 ]; then
rsync_params="--exclude /test"
fi
rsync -a $rsync_params "$SOURCE_DIR/" ./
else
copy_dir "$SOURCE_DIR" .
if [ $include_tests -eq 0 ]; then
rm -rf ./test
fi
rsync_params=""
if [ $include_tests -eq 0 ]; then
rsync_params="--exclude /test"
fi
rsync -a $rsync_params "$SOURCE_DIR/" ./
fi
mv defaults defaults-z
@ -408,7 +270,7 @@ prefs_file=defaults/preferences/zotero.js
# - network.captive-portal-service.enabled
# Disable the captive portal check against Mozilla servers
# - extensions.systemAddon.update.url
grep -E -v '(network.captive-portal-service.enabled|extensions.systemAddon.update.url)' defaults/preferences/firefox.js > $prefs_file
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
@ -422,8 +284,6 @@ echo "" >> $prefs_file
echo "# Zotero extension prefs" >> $prefs_file
echo "" >> $prefs_file
cat defaults-z/preferences/zotero.js >> $prefs_file
# Babel strips the trailing newline, so anything appended below would share a line
echo "" >> $prefs_file
rm -rf defaults-z
@ -445,7 +305,7 @@ elif [ $BUILD_LINUX == 1 ]; then
perl -pi -e 's/pref\("general\.autoScroll", false\);/pref\("general.autoScroll", true);/' $prefs_file
fi
# Clear list of built-in add-ons, but leave the en-US dictionary
# 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
@ -476,58 +336,6 @@ if [ $DEVTOOLS -eq 1 ]; then
replace_line 'let command = Services.dirsvc.get\("XREExeF", Ci.nsIFile\).path;' \
'let command = Services.dirsvc.get("XREExeF", Ci.nsIFile).path; command = command.replace("zotero-bin", "zotero");' \
chrome/devtools/modules/devtools/client/framework/browser-toolbox/Launcher.sys.mjs
# Fix source display breaking when switching windows in the Browser Toolbox.
# When the user switches which chrome window is being inspected (via the frame
# picker), the thread actor's _onWindowReady handler clears all debuggees and
# source actors, then re-adds debuggees. But it never calls addAllSources() to
# re-emit the existing scripts — it only does that for BFCache navigations.
# Since the scripts already exist (they're not new), onNewScript won't fire,
# so the debugger's source list stays empty. Fix: also call addAllSources()
# after frame switching, just like BFCache.
replace_line '_onWindowReady\(\{ isTopLevel, isBFCache \}\)' \
'_onWindowReady({ isTopLevel, isBFCache, isFrameSwitching })' \
chrome/devtools/modules/devtools/server/actors/thread.js
replace_line 'if \(isBFCache\) {' \
'if (isBFCache || isFrameSwitching) {' \
chrome/devtools/modules/devtools/server/actors/thread.js
# Suppress "Failed to get the active browserId" error when profile recording starts
replace_line 'console\.error\(' \
'if (false) console.error(' \
chrome/devtools/modules/devtools/shared/performance-new/recording-utils.sys.mjs
# Stub out profile favicon lookup since we don't have PlacesUtils
replace_line 'return getPageFavicons\(pageUrls\);' \
'return pageUrls ? pageUrls.map(() => null) : [];' \
chrome/devtools/modules/devtools/client/performance-new/shared/background.sys.mjs
# Use our profilerViewer.xhtml to display profiles.
# We can't use basicViewer because that loads Zotero scripts, and profiling
# runs in the Browser Toolbox child process, where a second Zotero instance
# would collide with the locked database.
replace_line 'const contentBrowser = await new Promise\(resolveOnContentBrowserCreated =>' \
'if (typeof win.openWebLinkIn !== "function") {
const ww = Cc["\@mozilla.org\/embedcomp\/window-watcher;1"].getService(
Ci.nsIWindowWatcher
);
const arg = { uri: urlToLoad };
arg.wrappedJSObject = arg;
const viewerWin = ww.openWindow(
null,
"chrome:\/\/zotero\/content\/standalone\/profilerViewer.xhtml",
null,
"chrome,dialog=no,resizable,centerscreen,scrollbars",
arg
);
return await new Promise(resolve => {
viewerWin.addEventListener("load", () => {
resolve(viewerWin.document.getElementById("content"));
}, { once: true });
});
}
const contentBrowser = await new Promise(resolveOnContentBrowserCreated =>' \
chrome/devtools/modules/devtools/client/performance-new/shared/browser.js
fi
# 5.0.96.3 / 5.0.97-beta.37+ddc7be75c
@ -555,7 +363,6 @@ for locale in `ls chrome/locale/`; do
cp "$CALLDIR/assets/branding/locale/brand.ftl" localization/$locale/branding/brand.ftl
mkdir -p localization/$locale/toolkit/global
cp chrome/locale/$locale/zotero/mozilla/arrowscrollbox.ftl 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
@ -580,9 +387,6 @@ replace_line 'handle: function bch_handle\(cmdLine\) {' 'handle: function bch_ha
' 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
# Don't try to initialize Places, since we've removed its files
#replace_line 'this._placesInitialized = true;' 'if (true) return; this._placesInitialized = true;' components/places/PlacesBrowserStartup.sys.mjs
# Move test files to root directory
if [ $include_tests -eq 1 ]; then
cat test/chrome.manifest >> chrome.manifest
@ -592,11 +396,11 @@ fi
# Copy platform-specific assets
if [ $BUILD_MAC == 1 ]; then
copy_dir "$CALLDIR/assets/mac" .
rsync -a "$CALLDIR/assets/mac/" ./
elif [ $BUILD_WIN == 1 ]; then
copy_dir "$CALLDIR/assets/win" .
rsync -a "$CALLDIR/assets/win/" ./
elif [ $BUILD_LINUX == 1 ]; then
copy_dir "$CALLDIR/assets/unix" .
rsync -a "$CALLDIR/assets/unix/" ./
fi
# Add word processor plug-ins
@ -694,25 +498,16 @@ if [ $BUILD_MAC == 1 ]; then
# Merge relevant assets from Firefox
mkdir "$CONTENTSDIR/MacOS"
cp -r "$MAC_RUNTIME_PATH/Contents/MacOS/"!(firefox|firefox-bin|crashhelper|crashreporter.app|minidump-analyzer|nmhproxy|pingsender|updater.app) "$CONTENTSDIR/MacOS"
cp -r "$MAC_RUNTIME_PATH/Contents/Resources/"!(application.ini|Assets.car|browser|defaults|precomplete|removed-files|updater.ini|update-settings.ini|webapprt*|*.icns|*.lproj) "$CONTENTSDIR/Resources"
# Add our custom ChannelPrefs.framework and change channel if not a source build
mkdir "$CONTENTSDIR/Frameworks"
tar xvf "$CALLDIR/mac/ChannelPrefs.framework.tar.xz" -C "$CONTENTSDIR/Frameworks"
if [ "$UPDATE_CHANNEL" != "source" ]; then
"$CALLDIR/mac/set-channel-prefs-channel" "$CONTENTSDIR/Frameworks/ChannelPrefs.framework/ChannelPrefs" source $UPDATE_CHANNEL
fi
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"
# Overlay custom Firefox components (e.g., XUL or libmozglue.dylib)
if [ -n "$custom_components_hash_mac" ]; then
cp -R "$MAC_RUNTIME_PATH/../MacOS/." "$CONTENTSDIR/MacOS/"
fi
# 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"
@ -739,7 +534,7 @@ if [ $BUILD_MAC == 1 ]; then
echo
# Copy app files
copy_dir "$base_dir" "$CONTENTSDIR/Resources"
rsync -a "$base_dir/" "$CONTENTSDIR/Resources/"
# Add word processor plug-ins
mkdir "$CONTENTSDIR/Resources/integration"
@ -750,66 +545,12 @@ if [ $BUILD_MAC == 1 ]; then
find "$CONTENTSDIR" -depth -type d -name .git -exec rm -rf {} \;
find "$CONTENTSDIR" \( -name .DS_Store -or -name update.rdf \) -exec rm -f {} \;
# Add Safari extensions -- this depends on signing but needs to be done before generating
# Add Safari App Extension -- this depends on signing but needs to be done before generating
# the precomplete file
#
# $SAFARI_APPEX is a stub appex built from the safari-web-extension wrapper project. The web
# extension itself comes from $SAFARI_EXT_RESOURCES (a zotero-connectors build/safari
# directory), which replaces the stub's placeholder resources here before signing.
#
# $SAFARI_APP_EXTENSION is an optional prebuilt legacy Safari App Extension, embedded
# alongside the web extension for Safari versions that can't load Developer ID web
# extensions (supported in Safari 18.4 and later). On Safari versions that can load the
# web extension, the SFSafariAppExtensionBundleIdentifiersToReplace key causes it to
# replace the App Extension.
if [[ $SIGN == 1 ]] && [[ -n "$SAFARI_APPEX" ]] && [[ -d "$SAFARI_APPEX" ]]; then
if [[ -z "${SAFARI_EXT_RESOURCES:-}" ]] || [[ ! -f "$SAFARI_EXT_RESOURCES/manifest.json" ]]; then
echo "SAFARI_EXT_RESOURCES doesn't contain a web extension -- aborting" 2>&1
exit 1
fi
bundle_identifier=$(/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$APPDIR/Contents/Info.plist")
mkdir "$APPDIR/Contents/PlugIns"
webext_appex="$APPDIR/Contents/PlugIns/ZoteroSafariWebExtension.appex"
cp -R "$SAFARI_APPEX" "$webext_appex"
appex_resources="$webext_appex/Contents/Resources"
rm -rf "$appex_resources"
mkdir "$appex_resources"
cp -R "$SAFARI_EXT_RESOURCES/." "$appex_resources/"
# Show the connector version in Safari
connector_version=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1]))['version'])" "$appex_resources/manifest.json")
if [[ $connector_version == *999* ]] && [ "$UPDATE_CHANNEL" != "test" ]; then
echo "Placeholder connector version $connector_version not allowed for '$UPDATE_CHANNEL' channel -- aborting" 2>&1
exit 1
fi
/usr/libexec/PlistBuddy -c "Set CFBundleShortVersionString $connector_version" \
"$webext_appex/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set CFBundleVersion $connector_version" \
"$webext_appex/Contents/Info.plist"
# Give the appex the same bundle identifier prefix as the parent app
/usr/libexec/PlistBuddy -c "Set CFBundleIdentifier $bundle_identifier.SafariWebExtension" \
"$webext_appex/Contents/Info.plist"
# Replace the legacy App Extension on Safari versions that can load the web extension
/usr/libexec/PlistBuddy -c "Add :NSExtension:SFSafariAppExtensionBundleIdentifiersToReplace array" \
"$webext_appex/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Add :NSExtension:SFSafariAppExtensionBundleIdentifiersToReplace:0 string $bundle_identifier.SafariExtension" \
"$webext_appex/Contents/Info.plist"
# Add legacy Safari App Extension
if [[ -n "${SAFARI_APP_EXTENSION:-}" ]]; then
if [[ ! -d "$SAFARI_APP_EXTENSION" ]]; then
echo "SAFARI_APP_EXTENSION not found at $SAFARI_APP_EXTENSION -- aborting" 2>&1
exit 1
fi
appext_appex="$APPDIR/Contents/PlugIns/ZoteroSafariExtension.appex"
cp -R "$SAFARI_APP_EXTENSION" "$appext_appex"
rm -rf "$appext_appex/Contents/Resources/safari/test"
/usr/libexec/PlistBuddy -c "Set CFBundleIdentifier $bundle_identifier.SafariExtension" \
"$appext_appex/Contents/Info.plist"
fi
cp -R $SAFARI_APPEX "$APPDIR/Contents/PlugIns/ZoteroSafariExtension.appex"
rm -rf "$APPDIR/Contents/PlugIns/ZoteroSafariExtension.appex/Contents/Resources/safari/test/"
fi
# Copy over removed-files and make a precomplete file
@ -831,50 +572,30 @@ if [ $BUILD_MAC == 1 ]; then
# Clear extended attributes, which can cause codesign to fail
/usr/bin/xattr -cr "$APPDIR"
# Sign libraries and updater without entitlements
/usr/bin/codesign --force --options runtime --sign "$DEVELOPER_ID" \
"$APPDIR/Contents/MacOS/XUL" \
"$APPDIR/Contents/MacOS/updater.app/Contents/Frameworks/UpdateSettings.framework" \
"$APPDIR/Contents/MacOS/updater.app" \
"$APPDIR/Contents/Frameworks/ChannelPrefs.framework"
find "$APPDIR/Contents" -name '*.dylib' -print0 | xargs -0 /usr/bin/codesign --force --options runtime --sign "$DEVELOPER_ID"
# Sign bundled apps and main app bundle
# Sign app
entitlements_file="$CALLDIR/mac/entitlements.xml"
find "$APPDIR/Contents" -name '*.app' -not -name "updater.app" -print0 | xargs -0 /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/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"
# Sign .jnilib (Java native shared library) within LibreOffice extension, since notarization
# started failing without this. The .jnilib is within a .jar within the .oxt, so we have to
# extract both, sign the libraries, and then update each ZIP.
# Sign Safari App Extension
#
# TODO: Remove this block once the plugin ships a jna.jar without the macOS native
# libraries, which are never loaded (JNA is used only on Windows)
pushd "$BUILD_DIR"
mkdir libreoffice-repack
cd libreoffice-repack
unzip -q "$APPDIR/Contents/Resources/integration/libreoffice/Zotero_LibreOffice_Integration.oxt" external_jars/jna.jar
unzip -q external_jars/jna.jar 'com/sun/jna/darwin*/libjnidispatch.jnilib'
/usr/bin/codesign --force --options runtime --sign "$DEVELOPER_ID" com/sun/jna/darwin*/libjnidispatch.jnilib
zip -u external_jars/jna.jar com/sun/jna/darwin*/libjnidispatch.jnilib
zip -u "$APPDIR/Contents/Resources/integration/libreoffice/Zotero_LibreOffice_Integration.oxt" external_jars/jna.jar
cd ..
rm -rf libreoffice-repack
popd
# Sign Safari extensions
#
# Even though they're signed by Xcode, we sign them again to make sure they match the parent app signature
for appex in "$APPDIR"/Contents/PlugIns/*.appex; do
if [ ! -d "$appex" ]; then
continue
fi
# Even though it's signed by Xcode, we sign it again to make sure it matches the parent app signature
if [ -d "$APPDIR/Contents/PlugIns/ZoteroSafariExtension.appex" ]; then
echo
# Extract entitlements, which differ from parent app
/usr/bin/codesign -d --entitlements "$BUILD_DIR/safari-entitlements.plist" --xml "$appex"
/usr/bin/codesign -d --entitlements "$BUILD_DIR/safari-entitlements.plist" --xml "$SAFARI_APPEX"
find "$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" "$appex"
done
# 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
@ -882,21 +603,11 @@ if [ $BUILD_MAC == 1 ]; then
# Verify app
/usr/bin/codesign --verify -vvvv "$APPDIR"
# Verify Safari extensions
for appex in "$APPDIR"/Contents/PlugIns/*.appex; do
if [ ! -d "$appex" ]; then
continue
fi
# Verify Safari App Extension
if [[ -n "$SAFARI_APPEX" ]] && [[ -d "$SAFARI_APPEX" ]]; then
echo
/usr/bin/codesign --verify -vvvv "$appex"
done
elif [ -n "$custom_components_hash_mac" ]; then
# Mozilla's helper apps (plugin-container, GPU helper, etc.) are signed with Mozilla's Team ID
# and the hardened runtime, so without our signature they can't load custom components such as
# libmozglue.dylib and fail to launch. Re-sign them ad hoc, which removes both. (Signed builds
# re-sign everything with our Developer ID above.)
find "$APPDIR/Contents/MacOS" -maxdepth 1 -name '*.app' -not -name "updater.app" -print0 \
| xargs -0 /usr/bin/codesign --force --sign -
/usr/bin/codesign --verify -vvvv "$APPDIR/Contents/PlugIns/ZoteroSafariExtension.appex"
fi
fi
# Build and notarize disk image
@ -951,15 +662,10 @@ if [ $BUILD_WIN == 1 ]; then
fi
if [[ -n $arch ]]; then
archs=("$(get_canonical_arch w $arch)");
else
archs=(win-x64 win-arm64 win32);
fi
for arch in "${archs[@]}"; do
for arch in win32 win-x64 win-arm64; do
echo "Building Zotero_$arch"
runtime_path="${WIN_RUNTIME_PATH_PREFIX}$arch"
runtime_path="${WIN_RUNTIME_PATH_PREFIX}${arch}"
# Set up directory
APPDIR="$STAGE_DIR/Zotero_$arch"
@ -969,7 +675,7 @@ if [ $BUILD_WIN == 1 ]; then
#
# 'i686' is a huge directory containing x86 versions of xul.dll and other files in
# Firefox ARM64 builds for use with the EME DRM plugins
cp -R "$runtime_path"/!(application.ini|browser|crashhelper.exe|crashreporter*|default-browser-agent.exe|defaultagent*|defaults|desktop-launcher|devtools-files|firefox*|i686|maintenanceservice*|minidump-analyzer.exe|nmhproxy.exe|pingsender.exe|private_browsing*|precomplete|removed-files|uninstall|update*) "$APPDIR"
cp -R "$runtime_path"/!(application.ini|browser|crashreporter*|default-browser-agent.exe|defaultagent*|defaults|devtools-files|firefox*|i686|maintenanceservice*|minidump-analyzer.exe|pingsender.exe|private_browsing*|precomplete|removed-files|uninstall|update*) "$APPDIR"
# Copy zotero.exe, which is built directly from Firefox source and then modified by
# ResourceHacker to add icons
@ -993,12 +699,17 @@ if [ $BUILD_WIN == 1 ]; then
--set-product-version "$VERSION"
fi
# Sign updater
if [ $SIGN -eq 1 ]; then
"$CALLDIR/win/codesign" "$APPDIR/updater.exe" "$SIGNATURE_DESC Updater"
fi
# Copy app files
copy_dir "$base_dir" "$APPDIR"
rsync -a "$base_dir/" "$APPDIR/"
#mv "$APPDIR/app/application.ini" "$APPDIR/"
# Copy in common files
copy_dir "$COMMON_APPDIR" "$APPDIR"
rsync -a "$COMMON_APPDIR/" "$APPDIR/"
cat "$CALLDIR/win/installer/updater_append.ini" >> "$APPDIR/updater.ini"
@ -1040,17 +751,6 @@ if [ $BUILD_WIN == 1 ]; then
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 ]] && [[ $SIGN -eq 1 ]]; then
"$CALLDIR/win/codesign" "$APPDIR/zotero.exe" "$SIGNATURE_DESC"
sleep $SIGNTOOL_DELAY
"$CALLDIR/win/codesign" "$APPDIR/updater.exe" "$SIGNATURE_DESC Updater"
sleep $SIGNTOOL_DELAY
# Re-sign modified xul.dll
"$CALLDIR/win/codesign" "$APPDIR/xul.dll" "$SIGNATURE_DESC"
sleep $SIGNTOOL_DELAY
"$CALLDIR/win/codesign" "$APPDIR/integration/word-for-windows/libzoteroWinWordIntegration.dll" "$SIGNATURE_DESC Word Plugin"
fi
# Copy over removed-files and make a precomplete file
pushd "$APPDIR"
cp "$CALLDIR/update-packaging/removed-files_$arch" removed-files
@ -1080,6 +780,7 @@ if [ $BUILD_WIN == 1 ]; then
sleep $SIGNTOOL_DELAY
fi
if [ "$arch" = "win32" ]; then
INSTALLER_PATH="$DIST_DIR/Zotero-${VERSION}_win32_setup.exe"
elif [ "$arch" = "win-x64" ]; then
@ -1088,6 +789,11 @@ if [ $BUILD_WIN == 1 ]; then
INSTALLER_PATH="$DIST_DIR/Zotero-${VERSION}_arm64_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"
@ -1137,14 +843,14 @@ fi
# Linux
if [ $BUILD_LINUX == 1 ]; then
if [[ -n $arch ]]; then
archs=("$(get_canonical_arch l $arch)")
# Skip 32-bit build in tests
if [[ "${ZOTERO_TEST:-}" = "1" ]] || [[ "${SKIP_32:-}" = "1" ]]; then
archs="x86_64"
else
archs=(x64 arm64)
archs="i686 x86_64"
fi
for arch in "${archs[@]}"; do
[[ $arch == x64 ]] && arch="x86_64"
for arch in $archs; do
runtime_path="${LINUX_RUNTIME_PATH_PREFIX}${arch}"
# Set up directory
@ -1154,7 +860,7 @@ if [ $BUILD_LINUX == 1 ]; then
mkdir "$APPDIR"
# Merge relevant assets from Firefox
cp -r "$runtime_path/"!(application.ini|browser|crashhelper|crashreporter|crashreporter.ini|defaults|devtools-files|firefox|pingsender|precomplete|removed-files|run-mozilla.sh|update-settings.ini|updater|updater.ini) "$APPDIR"
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
@ -1170,14 +876,12 @@ if [ $BUILD_LINUX == 1 ]; then
chmod 755 "$APPDIR/updater"
# Copy app files
copy_dir "$base_dir" "$APPDIR"
rsync -a "$base_dir/" "$APPDIR/"
# Add word processor plug-ins
mkdir "$APPDIR/integration"
cp -RH "$CALLDIR/modules/zotero-libreoffice-integration/install" "$APPDIR/integration/libreoffice"
# Firefox includes an 'icons' folder with updater.png, but some Linux distro builds omit it
mkdir -p "$APPDIR/icons"
# Copy icons
cp "$CALLDIR/linux/icons/icon32.png" "$APPDIR/icons/"
cp "$CALLDIR/linux/icons/icon64.png" "$APPDIR/icons/"
@ -1200,9 +904,9 @@ if [ $BUILD_LINUX == 1 ]; then
if [ $PACKAGE == 1 ]; then
# Create tar
rm -f "$DIST_DIR/Zotero-${VERSION}_linux-$arch.tar.xz"
rm -f "$DIST_DIR/Zotero-${VERSION}_linux-$arch.tar.bz2"
cd "$STAGE_DIR"
tar -cJf "$DIST_DIR/Zotero-${VERSION}_linux-$arch.tar.xz" "Zotero_linux-$arch"
tar -cjf "$DIST_DIR/Zotero-${VERSION}_linux-$arch.tar.bz2" "Zotero_linux-$arch"
fi
done
fi

View file

@ -1,17 +1,14 @@
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Version of Gecko to build with
GECKO_VERSION_MAC="153.3.0esr"
GECKO_VERSION_LINUX="153.3.0esr"
GECKO_VERSION_WIN="153.3.0esr"
RUST_VERSION=1.94.0
GECKO_VERSION_MAC="115.9.1esr"
GECKO_VERSION_LINUX="115.9.1esr"
GECKO_VERSION_WIN="115.14.0esr"
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="6b586ed383a480fed9d9786ad18a6e6d90b140739e5e4491096df291ec887571"
custom_components_hash_win_x64="db7c9556bd0f9e36120069391ce66cc199ed1ec4e2fdd3b931f71e35b4586d2b"
custom_components_hash_win_arm64="fd0f44b38a471009ce1cdd926f7f48be43ed773dad350c62383b8a7235da8711"
custom_components_hash_win32="694944349fdfd91296f272a68ac5d03183f02d47fdaa7ba9f2b1c74747652aa2"
custom_components_hash_mac="bec8e3adebf8d5021f1f35fd2c65d752d4979839cbdd1ee1aa4b1d3d5ba0953b"
APP_NAME="Zotero"
APP_ID="zotero\@zotero.org"
@ -28,9 +25,6 @@ NOTARIZATION_BUNDLE_ID=""
NOTARIZATION_USER=""
NOTARIZATION_TEAM_ID=""
NOTARIZATION_PASSWORD=""
# Name of a notarytool keychain profile (see `xcrun notarytool store-credentials`), e.g., for an
# App Store Connect API key -- used instead of the Apple ID settings above if set
NOTARIZATION_PROFILE=""
# Paths for Windows installer build
NSIS_DIR='C:\Program Files (x86)\NSIS\'
@ -48,7 +42,8 @@ S3_CI_ZIP_PATH="ci/client"
S3_DIST_PATH="client"
DEPLOY_HOST="deploy.zotero"
DEPLOY_PATH="www/www-production/public/download/client"
DEPLOY_PATH="www/www-production/public/download/client/manifests"
DEPLOY_CMD="ssh $DEPLOY_HOST update-site-files"
BUILD_PLATFORMS=""
NUM_INCREMENTALS=6

View file

@ -1,44 +0,0 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
. "$APP_ROOT_DIR/config.sh"
if [ -z "${1:-}" ]; then
echo "Usage: $0 x64|arm64" >&2
exit 1
fi
gecko_path=~/src/firefox
arch=$1
if [ ! -d "$gecko_path" ]; then
echo "$gecko_path is not a directory" >&2
exit 1
fi
# Install required Rust version
rustup toolchain install $RUST_VERSION
if [ $arch = "x64" ]; then
rust_target=x86_64
elif [ $arch = "arm64" ]; then
rust_target=aarch64
else
echo "Unknown architecture $arch" >&2
exit 1
fi
rustup target add $rust_target-unknown-linux-gnu
rm "$gecko_path/mozconfig"
touch "$gecko_path/mozconfig"
if [ $arch == "arm64" ]; then
echo "ac_add_options --target=aarch64-linux-gnu" >> "$gecko_path/mozconfig"
fi
cat "$SCRIPT_DIR/mozconfig" >> "$gecko_path/mozconfig"
cd "$gecko_path"
./mach build
./mach package

View file

@ -1,3 +1,6 @@
# Uncomment to build 32-bit
#ac_add_options --target=i686
ac_add_options --enable-bootstrap
mk_add_options AUTOCLOBBER=1
@ -12,8 +15,7 @@ ac_add_options --disable-tests
ac_add_options --disable-debug
ac_add_options --disable-debug-symbols
ac_add_options --disable-webrtc
# This causes aarch64 build of 140.0esr to fail
#ac_add_options --disable-eme
ac_add_options --disable-eme
export MOZILLA_OFFICIAL=1
export RELEASE_OR_BETA=1
@ -22,4 +24,4 @@ MOZ_REQUIRE_SIGNING=
ac_add_options --enable-official-branding
# Build updater without MAR signature verification
ac_add_options --enable-unverified-updates
ac_add_options --disable-verify-mar

View file

@ -1,43 +0,0 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
if [ $# -ne 1 ]; then
echo "Usage: $0 <firefox-version>"
exit 1
fi
version="$1"
for arch in x86_64 aarch64; do
package="firefox-${version}.en-US.linux-${arch}.tar.xz"
if [ $arch = 'aarch64' ]; then
arch="arm64"
fi
if [ ! -f "$package" ]; then
echo "Error: Package not found: $package"
exit 1
fi
echo "Extracting updater from $package..."
tar --extract --xz --file="$package" --to-stdout "firefox/updater" > "updater-$arch"
# Add missing RUNPATH to custom fx140 builds, which is missing for unclear reasons.
# Without this, the updater fails looking for libmozsqlite3.so.
patchelf --set-rpath '$ORIGIN' "updater-$arch"
done
echo "Creating updater.tar.xz..."
if [ `uname` = "Darwin" ]; then
tar=gtar
else
tar=tar
fi
$tar --owner=0 --group=0 --numeric-owner --no-xattrs --mode=0755 -cJvf updater.tar.xz updater-x86_64 updater-arm64
rm updater-x86_64 updater-arm64
echo "Done: updater.tar.xz created"

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a1f368bb36408d61d4b8f9dda561738686e626513346e138d55a0ef58055a6fc
size 123776
oid sha256:d7e9c81d2f86ec0f70d75107a022649292889807bc8b0b7eaf1bdfd5c1030bf1
size 74516

View file

@ -161,8 +161,8 @@
<string>zotero</string>
<key>CFBundleGetInfoString</key>
<string>Zotero {{VERSION}}, © 2006-2018 Contributors</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIconFile</key>
<string>zotero</string>
<key>CFBundleIdentifier</key>
<string>org.zotero.zotero</string>
<key>CFBundleInfoDictionaryVersion</key>

Binary file not shown.

View file

@ -9,10 +9,10 @@ fx_app_name=Firefox.app
# Get Mozilla source directory from command line
if [ -z "${1:-}" ]; then
echo "Usage: $0 /path/to/firefox" >&2
echo "Usage: $0 /path/to/mozilla-unified" >&2
exit 1
fi
GECKO_PATH="$(cd "$1" && pwd)"
GECKO_PATH=$1
mach=$GECKO_PATH/mach
BUILD_DIR=`mktemp -d`
@ -27,7 +27,8 @@ export MOZ_BUILD_DATE=`date "+%Y%m%d%H%M%S"`
# Install required Rust version
rustup toolchain install $RUST_VERSION
rustup target add --toolchain $RUST_VERSION aarch64-apple-darwin x86_64-apple-darwin
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
rustup default $RUST_VERSION
cp "$SCRIPT_DIR/mozconfig" "$GECKO_PATH"
@ -55,36 +56,8 @@ cp x64/$fx_app_name/Contents/MacOS/firefox zotero
xz zotero
mv zotero.xz "$APP_ROOT_DIR/mac/zotero.xz"
# Package custom Firefox components for fetch_xulrunner. Add XUL to this list if needed.
# Upload the archive to ${custom_components_url}mac/ and set custom_components_hash_mac
# in config.sh to the hash printed below.
custom_components=(libmozglue.dylib)
mkdir MacOS
for component in "${custom_components[@]}"; do
cp "x64/$fx_app_name/Contents/MacOS/$component" MacOS/
done
tar cfJ MacOS.tar.xz MacOS
components_hash=$(shasum -a 256 MacOS.tar.xz | cut -d ' ' -f 1)
components_file="$APP_ROOT_DIR/mac/$components_hash.tar.xz"
mv MacOS.tar.xz "$components_file"
echo "Custom Firefox components: $components_file"
echo "custom_components_hash_mac=\"$components_hash\""
# Save a copy of ChannelPrefs.framework
#
# Frameworks need to be signed, and we can't sign source builds, so store a signed copy with
# 'default' replaced with 'source'. Deployed builds will then replace 'source' with
# 'release'/'beta'/'dev'.
rm -rf "$APP_ROOT_DIR/mac/ChannelPrefs.framework"
cp -R x64/$fx_app_name/Contents/Frameworks/ChannelPrefs.framework "$APP_ROOT_DIR/mac/ChannelPrefs.framework"
pushd "$APP_ROOT_DIR/mac"
./set-channel-prefs-channel ChannelPrefs.framework/ChannelPrefs default source
security -v unlock-keychain -p "$KEYCHAIN_PASSWORD" ~/Library/Keychains/$KEYCHAIN.keychain-db
/usr/bin/codesign --force --options runtime --sign "$DEVELOPER_ID" ChannelPrefs.framework
/usr/bin/codesign -vvvv ChannelPrefs.framework
tar cfvJ ChannelPrefs.framework.tar.xz ChannelPrefs.framework
rm -rf ChannelPrefs.framework
popd
# 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/

View file

@ -4,7 +4,7 @@ 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/src/MacOSX26.5.sdk
ac_add_options --with-macos-sdk=$HOME/tmp/MacOSX13.3.sdk
mk_add_options AUTOCLOBBER=1
@ -24,11 +24,6 @@ export MOZILLA_OFFICIAL=1
export RELEASE_OR_BETA=1
MOZ_REQUIRE_SIGNING=
# Official builds require source info, which Mozilla only detects automatically from
# Mercurial checkouts
export MOZ_SOURCE_REPO=https://github.com/mozilla-firefox/firefox
export MOZ_SOURCE_CHANGESET=$(git -C "$topsrcdir" rev-parse HEAD)
ac_add_options --enable-official-branding
# Build updater without MAR signature verification

View file

@ -1,242 +0,0 @@
diff --git a/browser/app/nsBrowserApp.cpp b/browser/app/nsBrowserApp.cpp
--- a/browser/app/nsBrowserApp.cpp
+++ b/browser/app/nsBrowserApp.cpp
@@ -193,19 +193,31 @@ static bool HasFlag(int argc, char* argv[], const char* s) {
}
return false;
}
#endif
constinit 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 <application.ini>
// Note that -app must be the *first* argument.
- const char* appDataFile = getenv("XUL_APP_FILE");
+ UniqueFreePtr<char> iniPath = BinaryPath::GetApplicationIni();
+ if (!iniPath) {
+ Output("Couldn't find application.ini.\n");
+ return 255;
+ }
+ char *appDataFile = iniPath.get();
+
+
if ((!appDataFile || !*appDataFile) && (argc > 1 && IsFlag(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/xpcom/build/BinaryPath.h b/xpcom/build/BinaryPath.h
--- a/xpcom/build/BinaryPath.h
+++ b/xpcom/build/BinaryPath.h
@@ -126,16 +126,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;
}
@@ -281,16 +321,29 @@ class BinaryPath {
if (NS_FAILED(Get(path))) {
return nullptr;
}
UniqueFreePtr<char> result;
result.reset(strdup(path));
return result;
}
+#if defined(XP_MACOSX)
+ static UniqueFreePtr<char> GetApplicationIni()
+ {
+ char path[MAXPATHLEN];
+ if (NS_FAILED(GetApplicationIni(path))) {
+ return nullptr;
+ }
+ UniqueFreePtr<char> result;
+ result.reset(strdup(path));
+ return result;
+ }
+#endif
+
#ifdef MOZILLA_INTERNAL_API
static nsresult GetFile(nsIFile** aResult) {
nsCOMPtr<nsIFile> lf;
# ifdef XP_WIN
wchar_t exePath[MAXPATHLEN];
nsresult rv = GetW(exePath);
# else
char exePath[MAXPATHLEN];
diff --git a/mozglue/build/AudioQueueSanitizer.cpp b/mozglue/build/AudioQueueSanitizer.cpp
new file mode 100644
index 000000000000..9efa3255c472
--- /dev/null
+++ b/mozglue/build/AudioQueueSanitizer.cpp
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+// Work around a use-after-free in the macOS 27 system speech synthesizer.
+//
+// When an utterance finishes, the AudioQueue player in the private
+// TextToSpeech framework (used by both NSSpeechSynthesizer and
+// AVSpeechSynthesizer) enqueues a short de-click ramp that starts from a
+// sample read out of a buffer it has already freed. With the system
+// allocator, which zeroes freed memory, that sample is 0 and the ramp is
+// silent. mozjemalloc poisons freed memory with 0xE5 instead, which reads
+// as -1.36e23 in float32, so every utterance ends with a ~10 ms full-scale
+// burst.
+//
+// We can't fix the framework, so we interpose AudioQueueEnqueueBuffer and
+// zero any float samples that are non-finite or far outside the nominal
+// [-1, 1] range before they reach CoreAudio. Real audio never gets close
+// to kMaxSampleMagnitude, so this only ever affects garbage.
+//
+// dyld only honors __interpose tuples in images loaded at launch, which is
+// why this needs to be in mozglue rather than in XUL (loaded with dlopen)
+// alongside OSXSpeechSynthesizerService.
+
+#include <AudioToolbox/AudioToolbox.h>
+#include <math.h>
+
+namespace {
+
+constexpr float kMaxSampleMagnitude = 16.0f;
+
+void SanitizeBuffer(AudioQueueRef aQueue, AudioQueueBufferRef aBuffer) {
+ AudioStreamBasicDescription format;
+ UInt32 size = sizeof(format);
+ if (AudioQueueGetProperty(aQueue, kAudioQueueProperty_StreamDescription,
+ &format, &size) != noErr) {
+ return;
+ }
+ if (format.mFormatID != kAudioFormatLinearPCM ||
+ !(format.mFormatFlags & kAudioFormatFlagIsFloat) ||
+ format.mBitsPerChannel != 32) {
+ return;
+ }
+
+ float* samples = static_cast<float*>(aBuffer->mAudioData);
+ UInt32 count = aBuffer->mAudioDataByteSize / sizeof(float);
+ for (UInt32 i = 0; i < count; i++) {
+ // Written so that NaN also fails the comparison.
+ if (!(fabsf(samples[i]) <= kMaxSampleMagnitude)) {
+ samples[i] = 0.0f;
+ }
+ }
+}
+
+OSStatus SanitizingEnqueueBuffer(
+ AudioQueueRef aQueue, AudioQueueBufferRef aBuffer, UInt32 aNumPackets,
+ const AudioStreamPacketDescription* aPacketDescs) {
+ SanitizeBuffer(aQueue, aBuffer);
+ return AudioQueueEnqueueBuffer(aQueue, aBuffer, aNumPackets, aPacketDescs);
+}
+
+OSStatus SanitizingEnqueueBufferWithParameters(
+ AudioQueueRef aQueue, AudioQueueBufferRef aBuffer, UInt32 aNumPackets,
+ const AudioStreamPacketDescription* aPacketDescs, UInt32 aTrimFrames,
+ UInt32 aTrimFramesAtEnd, UInt32 aNumParamValues,
+ const AudioQueueParameterEvent* aParamValues,
+ const AudioTimeStamp* aStartTime, AudioTimeStamp* aActualStartTime) {
+ SanitizeBuffer(aQueue, aBuffer);
+ return AudioQueueEnqueueBufferWithParameters(
+ aQueue, aBuffer, aNumPackets, aPacketDescs, aTrimFrames,
+ aTrimFramesAtEnd, aNumParamValues, aParamValues, aStartTime,
+ aActualStartTime);
+}
+
+struct Interpose {
+ const void* mReplacement;
+ const void* mReplacee;
+};
+
+__attribute__((used, section("__DATA,__interpose"))) const Interpose
+ kInterposes[] = {
+ {reinterpret_cast<const void*>(&SanitizingEnqueueBuffer),
+ reinterpret_cast<const void*>(&AudioQueueEnqueueBuffer)},
+ {reinterpret_cast<const void*>(&SanitizingEnqueueBufferWithParameters),
+ reinterpret_cast<const void*>(
+ &AudioQueueEnqueueBufferWithParameters)},
+};
+
+} // namespace
diff --git a/mozglue/build/moz.build b/mozglue/build/moz.build
index f8f01e999e54..111fe5af1b0d 100644
--- a/mozglue/build/moz.build
+++ b/mozglue/build/moz.build
@@ -41,6 +41,14 @@ if CONFIG["OS_TARGET"] == "WINNT":
"winmm.dll",
]
+if CONFIG["OS_TARGET"] == "Darwin" and FORCE_SHARED_LIB:
+ SOURCES += [
+ "AudioQueueSanitizer.cpp",
+ ]
+ OS_LIBS += [
+ "-framework AudioToolbox",
+ ]
+
if CONFIG["MOZ_WIDGET_TOOLKIT"]:
if CONFIG["MOZ_MEMORY"] and FORCE_SHARED_LIB:
pass

View file

@ -303,7 +303,6 @@ my(@gCleanup, %gConfig, $gDarwinMajor, $gDryRun, $gVerbosity);
'cmd_chmod' => 'chmod',
'cmd_diskutil' => 'diskutil',
'cmd_du' => 'du',
'cmd_find' => 'find',
'cmd_hdid' => 'hdid',
'cmd_hdiutil' => 'hdiutil',
'cmd_mkdir' => 'mkdir',
@ -312,7 +311,6 @@ my(@gCleanup, %gConfig, $gDarwinMajor, $gDryRun, $gVerbosity);
'cmd_rm' => 'rm',
'cmd_rsync' => 'rsync',
'cmd_SetFile' => '/Developer/Tools/SetFile',
'cmd_xattr' => 'xattr',
# create_directly indicates whether hdiutil create supports
# -srcfolder and -srcdevice. It does on >= 10.3 (Panther).
@ -998,65 +996,6 @@ sub diskImageMaker($$$$$$$$) {
cleanupDie('unlink hybridImage failed: '.$!);
}
}
# hdiutil makehybrid gives every file on the image non-empty
# Finder info (it sets an icon location), which appears as a
# com.apple.FinderInfo extended attribute that "codesign
# --verify --strict" rejects as detritus and that can cause
# Safari to silently ignore a bundled web extension (observed
# with Safari 17.6 on macOS 12; the exact trigger conditions
# are unknown). Mount a read-write copy of the image and strip
# the attributes before compressing. Note that Finder flags set
# with --attribute are stored in the same Finder info, so if
# --attribute is ever used, the affected files would need to be
# excluded from the strip.
if($uncompressedImage eq $hybridImage) {
my($udrwImage);
$udrwImage = giveExtension($tempDir.'/udrw', '.dmg');
if(command($gConfig{'cmd_hdiutil'}, 'convert', '-format', 'UDRW',
'-ov', $hybridImage, '-o', $udrwImage) != 0) {
cleanupDie('hdiutil convert to UDRW failed');
}
push(@gCleanup,
sub {commandInternalVerbosity(0, 'unlink', $udrwImage);});
$uncompressedImage = $udrwImage;
# $hybridImage is no longer needed. Remove it and its cleanup
# entry, which is below the entry for $udrwImage.
my(@tempCleanup) = splice(@gCleanup, -2);
push(@gCleanup, $tempCleanup[1]);
if(commandInternal('unlink', $hybridImage) != 1) {
cleanupDie('unlink hybridImage failed: '.$!);
}
}
my($rootDevice, $partitionDevice, $partitionMountPoint);
if(!(($rootDevice, $partitionDevice, $partitionMountPoint) =
hdidMountImage($tempMount, $uncompressedImage))) {
cleanupDie('hdid mount failed');
}
push(@gCleanup, sub {commandVerbosity(0,
$gConfig{'cmd_diskutil'}, 'eject', $rootDevice);});
if(command($gConfig{'cmd_find'}, $partitionMountPoint,
'-mindepth', '1', '-not', '-type', 'l',
'-xattrname', 'com.apple.FinderInfo',
'-exec', $gConfig{'cmd_xattr'}, '-d', 'com.apple.FinderInfo',
'{}', '+') != 0) {
cleanupDie('stripping com.apple.FinderInfo failed');
}
# Pop diskutil eject
pop(@gCleanup);
if(command($gConfig{'cmd_diskutil'}, 'eject', $rootDevice) != 0) {
cleanupDie('diskutil eject failed');
}
}
else {
# makehybrid is not available, fall back to making a UDRW and

View file

@ -1,69 +0,0 @@
#!/bin/bash
# Replace channel in ChannelPrefs, with appropriate null-byte padding
#
# ChannelPrefs.framework needs to be signed, which we can't do during normal source builds, so we
# use this to replace 'default' in our custom framework with 'source\0' and store that, and then
# when making real builds we replace 'source\0' with 'release'/'beta'/'dev'/'test' (padded to 7 chars)
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
. "$APP_ROOT_DIR/config.sh"
if [ -z "${1:-}" ] || [ -z "${2:-}" ] || [ -z "${3:-}" ]; then
echo "Usage: $0 /path/to/ChannelPrefs.framework/ChannelPrefs from_channel to_channel" >&2
exit 1
fi
binary=$1
from_channel=$2
to_channel=$3
# `strings` has a 4-character minimum by default, but we need 3 for 'dev'
# `-arch all` scans all slices of the universal binary rather than just the host architecture
strings_cmd="strings -n 3 -arch all"
if [ ${#to_channel} -gt 7 ]; then
echo "Channel length cannot exceed 7 characters -- aborting" >&2
exit 1
fi
if [ $($strings_cmd "$binary" | grep -c "$from_channel") -ne 2 ] ; then
echo "$from_channel not found twice in ChannelPrefs" >&2
echo >&2
$strings_cmd "$binary" >&2
exit 1
fi
original_size=$(wc -c "$binary" | awk '{print $1}')
perl -pi -e '
binmode(STDIN);
binmode(STDOUT);
use bytes;
my @channels = qw(release beta dev test source); # List of valid channels
# Channels from Bash
my $from_channel = "'"$from_channel"'";
my $to_channel = "'"$to_channel"'";
if (grep { $_ eq $to_channel } @channels) {
# Pad channels with null bytes
$from_channel .= "\0" x (7 - length($from_channel));
$to_channel .= "\0" x (7 - length($to_channel));
} else {
die "Unknown update channel: $to_channel\n";
}
s/$from_channel/$to_channel/g;
' "$binary"
new_size=$(wc -c "$binary" | awk '{print $1}')
if [ $original_size -ne $new_size ]; then
echo "File size of ChannelPrefs has changed -- aborting" >&2
exit 1
fi
if [ $($strings_cmd "$binary" | grep -c "$to_channel") -ne 2 ] ; then
echo "$to_channel not found twice in ChannelPrefs" >&2
$strings_cmd "$binary" >&2
exit 1
fi
$strings_cmd "$binary"

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:361730d7cea854b38c6968143555c5f5fe251e1c95f265a6133514ee72ea2c53
size 98904
oid sha256:53642619a815ae1c848dfb6f33e1e528db74edc39b3a692b6f312cace640c437
size 92884

Binary file not shown.

@ -1 +1 @@
Subproject commit 2e78b1010ad49b4dde70ec9f0094b34a7b6f8965
Subproject commit 55a2de7e79f2d92c26c7a7e1ac4c63178b95121f

@ -1 +1 @@
Subproject commit 300886d1984de7a0006d7a48310baa0c8c99bea0
Subproject commit de8ab22db748bef42f3730c20b9da1c0563de6c2

@ -1 +1 @@
Subproject commit c0aa6e4bef039d94e17e81cb28b1fe9170c45b96
Subproject commit 13a6cc157397ed76728806e1c9a23e29f45892bb

View file

@ -1,64 +0,0 @@
#!/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="10.0"
BUILD_ONLY=0
if [ "`uname`" = "Darwin" ]; then
export SAFARI_APPEX="$ROOT_DIR/../safari-web-extension-builds/ZoteroSafariExtension.appex"
export SAFARI_EXT_RESOURCES="$ROOT_DIR/../safari-web-extension-builds/release/safari"
export SAFARI_APP_EXTENSION="$ROOT_DIR/../safari-app-extension-builds/release/ZoteroSafariExtension.appex"
if [ ! -d "$SAFARI_APPEX" ]; then
echo "Safari extension stub not found at $SAFARI_APPEX -- aborting" >&2
exit 1
fi
if [ ! -f "$SAFARI_EXT_RESOURCES/manifest.json" ]; then
echo "Safari web extension not found in $SAFARI_EXT_RESOURCES -- aborting" >&2
exit 1
fi
if [ ! -d "$SAFARI_APP_EXTENSION" ]; then
echo "Safari App Extension not found at $SAFARI_APP_EXTENSION -- aborting" >&2
exit 1
fi
fi
while getopts "b" opt; do
case $opt in
b)
BUILD_ONLY=1
;;
esac
shift $((OPTIND-1)); OPTIND=1
done
cd "$SCRIPT_DIR"
./check_requirements
hash=`./get_repo_branch_hash $BRANCH`
./check_app_matches_commit $hash
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
VERSION="`cat \"$build_dir/version\"`"
./build_for_deploy -d "$build_dir" -p $BUILD_PLATFORMS -c $CHANNEL
if [ $BUILD_ONLY -eq 1 ]; then
echo
echo "Build only -- skipping deploy."
echo "To deploy, run on the deploy server:"
echo " $DEPLOY_PATH/deploy $CHANNEL $VERSION $BUILD_PLATFORMS"
else
ssh $DEPLOY_HOST "$DEPLOY_PATH/deploy" $CHANNEL "$VERSION" $BUILD_PLATFORMS
fi

View file

@ -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/<string>11\.0<\/string>/<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

View file

@ -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

View file

@ -0,0 +1,29 @@
#!/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="main"
if [ "`uname`" = "Darwin" ]; then
export SAFARI_APPEX="$ROOT_DIR/../safari-app-extension-builds/release/ZoteroSafariExtension.appex"
fi
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

View file

@ -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

View file

@ -37,14 +37,11 @@ win_path="$STAGE_DIR/Zotero_win-x64"
linux_path="$STAGE_DIR/Zotero_linux-x86_64"
added=0
build_id=$(date +%Y%m%d%H%M%S)
for path in "$mac_path" "$win_path" "$linux_path"; do
if [ -d "$path" ]; then
echo "$path/app/omni.ja"
echo "Updating $(basename $(dirname $(dirname $path)))"
zip "$path/app/omni.ja" $files
# Bump BuildID so that startup caches are invalidated
perl -pi -e "s/^BuildID=.*/BuildID=$build_id/" "$path/app/application.ini"
added=1
fi
done

View file

@ -4,7 +4,6 @@ Update a builds manifest with info on a given build
"""
import argparse
import os
import re
import sys
import shutil
import json
@ -26,7 +25,7 @@ def main():
try:
file = args.file
version = args.version
short_version = re.match(r'\d+\.\d+', version).group(0)
short_version = version[0:3]
# Back up JSON file
shutil.copy2(file, file + '.bak')

View file

@ -1,64 +0,0 @@
#!/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"
BUILD_ONLY=0
if [ "`uname`" = "Darwin" ]; then
export SAFARI_APPEX="$ROOT_DIR/../safari-web-extension-builds/ZoteroSafariExtension.appex"
export SAFARI_EXT_RESOURCES="$ROOT_DIR/../safari-web-extension-builds/beta/safari"
export SAFARI_APP_EXTENSION="$ROOT_DIR/../safari-app-extension-builds/beta/ZoteroSafariExtension.appex"
if [ ! -d "$SAFARI_APPEX" ]; then
echo "Safari extension stub not found at $SAFARI_APPEX -- aborting" >&2
exit 1
fi
if [ ! -f "$SAFARI_EXT_RESOURCES/manifest.json" ]; then
echo "Safari web extension not found in $SAFARI_EXT_RESOURCES -- aborting" >&2
exit 1
fi
if [ ! -d "$SAFARI_APP_EXTENSION" ]; then
echo "Safari App Extension not found at $SAFARI_APP_EXTENSION -- aborting" >&2
exit 1
fi
fi
while getopts "b" opt; do
case $opt in
b)
BUILD_ONLY=1
;;
esac
shift $((OPTIND-1)); OPTIND=1
done
cd "$SCRIPT_DIR"
./check_requirements
hash=`./get_repo_branch_hash $BRANCH`
./check_app_matches_commit $hash
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
VERSION="`cat \"$build_dir/version\"`"
./build_for_deploy -d "$build_dir" -p $BUILD_PLATFORMS -c $CHANNEL
if [ $BUILD_ONLY -eq 1 ]; then
echo
echo "Build only -- skipping deploy."
echo "To deploy, run on the deploy server:"
echo " $DEPLOY_PATH/deploy $CHANNEL $VERSION $BUILD_PLATFORMS"
else
ssh $DEPLOY_HOST "$DEPLOY_PATH/deploy" $CHANNEL "$VERSION" $BUILD_PLATFORMS
fi

14
app/scripts/bootstrap.sh Normal file
View file

@ -0,0 +1,14 @@
get_current_platform() {
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
echo l
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo m
elif [[ "$OSTYPE" == "cygwin" ]]; then
echo w
elif [[ "$OSTYPE" == "msys" ]]; then
echo w
# Unknown, so probably Unix-y
else
echo l
fi
}

View file

@ -1,7 +1,6 @@
#!/bin/bash
#
# Builds Zotero and uploads to S3 and deploy server, but does not make the build live.
# To make the build live, run the deploy script on the deploy server (or via SSH).
# Builds and deploys Zotero with full and incremental updates
#
set -euo pipefail
@ -61,8 +60,8 @@ if [ -z "$VERSION" ]; then
exit 1
fi
# Enforce signing unless a test build or macOS, which doesn't start otherwise
if [[ "$CHANNEL" != "test" || $PLATFORMS = "m" ]]; then
# Enforce signing unless a test build
if [ "$CHANNEL" != "test" ]; then
SIGN="-e"
else
SIGN=""
@ -108,23 +107,13 @@ do
usage
;;
esac
echo
echo "Getting $platform_name incrementals"
INCREMENTALS="`\"$SCRIPT_DIR/manage_incrementals\" -c $CHANNEL -p ${PLATFORMS:i:1} -n $NUM_INCREMENTALS`"
# Fetch pinned incrementals from deploy server and merge
PINNED_FILE="$DEPLOY_PATH/manifests/$CHANNEL/pinned-incrementals-$platform"
PINNED=""
if ssh $DEPLOY_HOST "test -f $PINNED_FILE"; then
PINNED="`ssh $DEPLOY_HOST cat $PINNED_FILE`"
fi
# Combine and deduplicate
INCREMENTALS="`echo -e "$INCREMENTALS\n$PINNED" | sort -u | grep -v '^$' || true`"
echo "$INCREMENTALS"
echo
if [ -n "$INCREMENTALS" ]; then
for from in $INCREMENTALS; do
echo "Building incremental update for $platform_name from $from to $VERSION"
@ -137,31 +126,46 @@ done
# Upload builds to S3
"$SCRIPT_DIR/upload_builds" $CHANNEL $VERSION
# Upload file lists and build info for each platform
channel_deploy_path="$DEPLOY_PATH/manifests/$CHANNEL"
# Upload file lists for each platform
channel_deploy_path="$DEPLOY_PATH/$CHANNEL"
mkdir "$TEMP_DIR/version_info"
chmod g+ws "$TEMP_DIR/version_info"
cp "$DIST_DIR"/files-* "$TEMP_DIR/version_info"
chmod g+w "$TEMP_DIR"/version_info/files-*
# Generate build-{os}.json for each platform
SHORT_VERSION=$(echo "$VERSION" | grep -oE '^[0-9]+\.[0-9]+')
DETAILS_URL="https://www.zotero.org/support/${SHORT_VERSION}_changelog"
rsync -rv "$TEMP_DIR/version_info/" $DEPLOY_HOST:"$channel_deploy_path/$VERSION/"
# Download updates JSON for each platform, update it, and reupload it
for i in `seq 0 1 $((${#PLATFORMS}-1))`
do
case ${PLATFORMS:i:1} in
m) os_name="mac" ;;
w) os_name="win" ;;
l) os_name="linux" ;;
m)
architectures="mac"
;;
w)
architectures="win32 win-x64 win-arm64"
;;
l)
architectures="linux-i686 linux-x86_64"
;;
esac
printf '{\n "buildID": "%s",\n "detailsURL": "%s"\n}\n' "$BUILD_ID" "$DETAILS_URL" \
> "$TEMP_DIR/version_info/build-${os_name}.json"
chmod g+w "$TEMP_DIR/version_info/build-${os_name}.json"
for arch in $architectures;
do
jsonfile="updates-$arch.json"
scp $DEPLOY_HOST:"$channel_deploy_path/$jsonfile" "$TEMP_DIR/$jsonfile"
"$SCRIPT_DIR/add_version_info" -f "$TEMP_DIR/$jsonfile" -v $VERSION -b $BUILD_ID
scp "$TEMP_DIR/$jsonfile" $DEPLOY_HOST:"$channel_deploy_path/$jsonfile"
done
done
rsync -rv "$TEMP_DIR/version_info/" $DEPLOY_HOST:"$channel_deploy_path/$VERSION/"
# Add version to incremental lists
echo
for i in `seq 0 1 $((${#PLATFORMS}-1))`
do
"$SCRIPT_DIR/manage_incrementals" -c $CHANNEL -p ${PLATFORMS:i:1} -a $VERSION
done
$DEPLOY_CMD
rm -rf "$STAGE_DIR"/*
echo
echo "Build $VERSION uploaded successfully."
echo "To deploy, run on the deploy server:"
echo " ./deploy $CHANNEL $VERSION $PLATFORMS"

View file

@ -5,28 +5,17 @@ APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
ROOT_DIR="$(dirname $APP_ROOT_DIR)"
# Set ZOTERO_PROFILE environment variable to choose profile
profile_args=()
if [ -n "${ZOTERO_PROFILE:-}" ]; then
profile_args=(-p "$ZOTERO_PROFILE")
profile="-p $ZOTERO_PROFILE"
fi
FORCE_FULL=0
NO_REBUILD=0
REBUILD=0
SKIP_BUNDLED_FILES=0
DEBUGGER=0
while getopts "rfnbd" opt; do
while getopts "rbd" opt; do
case $opt in
r)
# Deprecated -- rebuilding is now the default
echo "-r is deprecated -- rebuilding is now the default (use -n to skip)" >&2
;;
f)
FORCE_FULL=1
;;
n)
NO_REBUILD=1
REBUILD=1
;;
b)
@ -38,6 +27,7 @@ while getopts "rfnbd" opt; do
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
@ -46,15 +36,12 @@ done
# Remove options from $@
shift $((OPTIND-1))
if [ $NO_REBUILD -eq 0 ]; then
if [ $REBUILD -eq 1 ]; then
PARAMS=""
if [ $DEBUGGER -eq 1 ]; then
PARAMS="-t"
fi
if [ $FORCE_FULL -eq 1 ]; then
PARAMS="$PARAMS -f"
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
@ -62,11 +49,16 @@ if [ $NO_REBUILD -eq 0 ]; then
echo
cd $ROOT_DIR
# TEMP: --openssl-legacy-provider avoids a build error in pdf.js
NODE_OPTIONS=--openssl-legacy-provider node js-build/build.js
NODE_OPTIONS=--openssl-legacy-provider npm run build
echo
fi
"$SCRIPT_DIR/dir_build" $PARAMS
"$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=""
@ -82,11 +74,7 @@ fi
if [ "`uname`" = "Darwin" ]; then
command="Zotero.app/Contents/MacOS/zotero"
elif [ "`uname`" = "Linux" ]; then
if [[ "`uname -m`" = "aarch64" ]]; then
command="Zotero_linux-arm64/zotero"
else
command="Zotero_linux-x86_64/zotero"
fi
command="Zotero_linux-x86_64/zotero"
elif [ "`uname -o 2> /dev/null`" = "Cygwin" ]; then
command="Zotero_win-x64/zotero.exe"
else
@ -94,5 +82,4 @@ else
exit 1
fi
echo
"$APP_ROOT_DIR/staging/$command" "${profile_args[@]}" -ZoteroDebugText $PARAMS "$@"
"$APP_ROOT_DIR/staging/$command" $profile -ZoteroDebugText -purgecaches $PARAMS "$@"

View file

@ -1,37 +0,0 @@
#!/bin/bash
set -euo pipefail
# Check that app/ in this checkout matches app/ in the commit being built.
#
# The build-and-deploy scripts run the build scripts and config from the local
# checkout but build source files from the tip of the remote branch, so a stale
# checkout can silently build with the wrong Gecko version or omni patches.
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
if [ -z "${1:-}" ]; then
echo "Usage: $0 commit-hash" >&2
exit 1
fi
hash=$1
cd "$ROOT_DIR"
if ! git cat-file -e "$hash^{commit}" 2>/dev/null; then
git fetch -q origin
fi
if ! git cat-file -e "$hash^{commit}" 2>/dev/null; then
echo "Commit $hash not found locally after fetching -- aborting" >&2
exit 1
fi
if ! git diff --quiet HEAD "$hash" -- app/; then
echo >&2
echo "app/ files in this checkout differ from app/ in $hash:" >&2
echo >&2
git --no-pager diff --stat HEAD "$hash" -- app/ >&2
echo >&2
echo "Update this checkout (e.g., 'git pull') and try again" >&2
exit 1
fi

View file

@ -5,7 +5,7 @@ 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/utils.sh"
. "$SCRIPT_DIR/bootstrap.sh"
cd "$ROOT_DIR"
@ -42,7 +42,7 @@ for lfs_file in $lfs_files; do
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 install; git lfs pull'"; }
{ $FAIL_CMD_INLINE; FAILED=1; echo " -- run 'git lfs pull'"; }
fi
echo
done
@ -56,6 +56,9 @@ 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; }
@ -107,7 +110,7 @@ if [ $platform = "w" ]; then
fi
plugin_path=$(cd "$NSIS_DIR\\Plugins\\x86-unicode" && pwd)
plugins="AppAssocReg ApplicationID InvokeShellVerb nsProcess ShellLink UAC"
plugins="AppAssocReg ApplicationID InvokeShellVerb ShellLink UAC"
echo "Checking for NSIS plugins in $plugin_path"
for i in $plugins; do
echo -n " $i.dll: "

View file

@ -1,41 +0,0 @@
#!/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"
if [ "`uname`" = "Darwin" ]; then
export SAFARI_APPEX="$ROOT_DIR/../safari-web-extension-builds/ZoteroSafariExtension.appex"
export SAFARI_EXT_RESOURCES="$ROOT_DIR/../safari-web-extension-builds/dev/safari"
if [ ! -d "$SAFARI_APPEX" ]; then
echo "Safari extension stub not found at $SAFARI_APPEX -- aborting" >&2
exit 1
fi
if [ ! -f "$SAFARI_EXT_RESOURCES/manifest.json" ]; then
echo "Safari web extension not found in $SAFARI_EXT_RESOURCES -- aborting" >&2
exit 1
fi
fi
cd "$SCRIPT_DIR"
./check_requirements
hash=`./get_repo_branch_hash $BRANCH`
./check_app_matches_commit $hash
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
VERSION="`cat \"$build_dir/version\"`"
./build_for_deploy -d "$build_dir" -p $BUILD_PLATFORMS -c $CHANNEL -i 1
ssh $DEPLOY_HOST "$DEPLOY_PATH/deploy" $CHANNEL "$VERSION" $BUILD_PLATFORMS

View file

@ -5,171 +5,84 @@ 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"
. "$SCRIPT_DIR/utils.sh"
function usage {
cat >&2 <<DONE
Usage: $0 [options]
If -p is omitted, platform (and, for Windows/Linux, architecture) default to the host system.
Architecture is ignored for Mac builds, which are always shipped as universal binaries.
Usage: $0 -p platforms
Options
-p PLATFORM Platform to build (m=Mac, w=Windows, l=Linux)
-a ARCH Target architecture (arm64, x64, win32) — **Windows/Linux only**
-t add devtools
-f force a full rebuild instead of updating the staged build in place
-p PLATFORMS Platforms to build (m=Mac, w=Windows, l=Linux)
-t add devtools
-q quick build (skip compression and other optional steps for faster restarts during development)
DONE
exit 1
}
platform=""
arch=""
devtools=0
force_full=0
while getopts "tp:a:f" opt; do
DEVTOOLS=0
PLATFORM=""
quick_build=0
while getopts "tp:q" opt; do
case $opt in
t)
devtools=1
DEVTOOLS=1
;;
p)
for (( i=0; i<${#OPTARG}; i++ )); do
for i in `seq 0 1 $((${#OPTARG}-1))`
do
case ${OPTARG:i:1} in
m) platform="m";;
w) platform="w";;
l) platform="l";;
m) PLATFORM="m";;
w) PLATFORM="w";;
l) PLATFORM="l";;
*)
echo "$0: Invalid platform option ${OPTARG:i:1}" >&2
echo "$0: Invalid platform option ${OPTARG:i:1}"
usage
;;
esac
done
;;
a)
arch="$OPTARG"
;;
f)
force_full=1
q)
quick_build=1
;;
\?)
usage
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
# Derive platform from host if not provided
if [[ -z $platform ]]; then
case "$(uname -s)" in
Darwin) platform="m" ;;
Linux) platform="l" ;;
CYGWIN*|MINGW*) platform="w" ;;
esac
fi
# Handle architecture rules
if [[ $platform = "m" ]]; then
# No arch for Mac — ignore anything provided
if [[ -n $arch ]]; then
echo "Ignoring -a $arch for Mac build (universal binary)" >&2
fi
arch=""
else
# Windows / Linux: derive arch if not supplied
if [[ -z $arch ]]; then
# An x64 build of Git for Windows runs emulated on Windows on ARM, where it
# reports x86_64 from `uname -m`, while `uname -s` reports the native
# architecture
if [[ $platform = "w" && "$(uname -s)" == *ARM64* ]]; then
arch="arm64"
else
case "$(uname -m)" in
arm64|aarch64) arch="arm64" ;;
x86_64) arch="x64" ;;
esac
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"
# Remove stale symlinks left behind in build/ when source files are deleted,
# which would otherwise break the build
if [ -d "$ROOT_DIR/build" ]; then
# With -L, -type l matches only broken symlinks
stale_links=$(find -L "$ROOT_DIR/build" -type l -print)
if [ -n "$stale_links" ]; then
echo "Removing stale symlinks from build/:" >&2
echo "$stale_links" >&2
while IFS= read -r link; do
rm "$link"
done <<< "$stale_links"
fi
PARAMS=""
if [ $DEVTOOLS -eq 1 ]; then
PARAMS+=" -t"
fi
if [ $quick_build -eq 1 ]; then
PARAMS+=" -q"
fi
# Try to update the staged build in place instead of doing a full rebuild
if [[ $force_full -eq 0 ]]; then
incr_cmd=("$SCRIPT_DIR/incremental_update" -p "$platform")
[[ -n $arch ]] && incr_cmd+=( -a "$arch" )
[[ $devtools -eq 1 ]] && incr_cmd+=( -t )
set +e
"${incr_cmd[@]}"
incr_status=$?
set -e
if [ $incr_status -ne 2 ]; then
exit $incr_status
fi
fi
hash=`git -C "$ROOT_DIR" rev-parse --short HEAD`
hash=$(git -C "$ROOT_DIR" rev-parse --short HEAD)
build_dir=$(mktemp -d)
manifest_tmp=""
cleanup() {
rm -rf "$build_dir"
if [ -n "$manifest_tmp" ]; then
rm -f "$manifest_tmp"
fi
build_dir=`mktemp -d`
function cleanup {
rm -rf $build_dir
}
trap cleanup EXIT
# Snapshot the state of build/ and app/ before the build for later incremental
# updates
app_hash=""
if [ -d "$ROOT_DIR/build" ]; then
manifest_tmp=$(mktemp)
generate_build_manifest "$ROOT_DIR/build" > "$manifest_tmp"
app_hash=$(generate_app_hash "$APP_ROOT_DIR")
fi
"$SCRIPT_DIR/prepare_build" -s "$ROOT_DIR/build" -o "$build_dir" -c "$CHANNEL" -m "$hash"
build_cmd=("$APP_ROOT_DIR/build.sh" -d "$build_dir" -p "$platform" -c "$CHANNEL" -s -q)
[[ -n $arch ]] && build_cmd+=( -a "$arch" )
[[ $devtools -eq 1 ]] && build_cmd+=( -t )
"${build_cmd[@]}"
# Save manifest for future incremental updates
if [ -n "$manifest_tmp" ]; then
if [[ -z "${ZOTERO_TEST:-}" ]] || [[ "${ZOTERO_TEST:-}" == "0" ]]; then
include_tests=0
else
include_tests=1
fi
{
echo "#format=2"
echo "#include_tests=$include_tests"
echo "#devtools=$devtools"
echo "#app_hash=$app_hash"
cat "$manifest_tmp"
} > "$STAGE_DIR/.build-manifest"
fi
# Ad-hoc-sign the Word dylib so it works on Apple Silicon. This only needs to
# happen when the staged build is fully rebuilt -- incremental updates don't
# touch the dylib.
if [[ $platform = "m" ]] && [[ "$(uname -s)" = "Darwin" ]]; then
"$SCRIPT_DIR/codesign_local" "$STAGE_DIR/Zotero.app"
fi
"$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

View file

@ -22,21 +22,13 @@ set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
. "$APP_ROOT_DIR/config.sh"
. "$SCRIPT_DIR/utils.sh"
cd "$APP_ROOT_DIR"
usage() {
cat >&2 <<'DONE'
Usage: fetch_xulrunner.sh -p <m|w|l> [-a ARCH]
Required
-p PLATFORM m (Mac), w (Windows), l (Linux)
Optional / Conditional
-a ARCH - Mac: disallowed (builds are universal)
- Windows: x64 | arm64 | win32
- Linux: x64 (or x86_64) | arm64
If omitted on Windows/Linux, all standard archs are fetched.
function usage {
cat >&2 <<DONE
Usage: $0 -p platforms [-s]
Options
-p PLATFORMS Platforms to build (m=Mac, w=Windows, l=Linux)
DONE
exit 1
}
@ -44,20 +36,22 @@ DONE
BUILD_MAC=0
BUILD_WIN=0
BUILD_LINUX=0
arch=""
while getopts 'p:a:' opt; do
while getopts "p:s" opt; do
case $opt in
p)
for (( i=0; i<${#OPTARG}; i++ )); do
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 "Invalid platform: ${OPTARG:i:1}"; echo; usage;;
*)
echo "$0: Invalid platform option ${OPTARG:i:1}"
usage
;;
esac
done;;
a) arch="$OPTARG";;
*) usage;;
done
;;
esac
shift $((OPTIND-1)); OPTIND=1
done
@ -67,30 +61,31 @@ if [[ $BUILD_MAC == 0 ]] && [[ $BUILD_WIN == 0 ]] && [[ $BUILD_LINUX == 0 ]]; th
usage
fi
# Sanity‑check arch flag vs platform selections
if [[ $BUILD_MAC == 1 ]]; then
[[ -n $arch ]] && { echo "-a is not allowed for Mac" >&2; echo; usage; }
fi
if [[ $BUILD_WIN == 1 || $BUILD_LINUX == 1 ]]; then
# arch optional; if provided validate & map aliases now
if [[ -n $arch ]]; then
if [[ $BUILD_WIN == 1 && $BUILD_LINUX == 1 ]]; then
echo "-a cannot be used when building both Windows and Linux simultaneously" >&2; echo; usage
fi
if [[ $BUILD_WIN == 1 ]]; then
case $arch in
x64) arch="win-x64" ;;
arm64) arch="win-arm64" ;;
win32|win-x64|win-arm64) ;;
*) echo "Invalid Windows arch: $arch" >&2; echo; usage;;
esac
fi
if [[ $BUILD_LINUX == 1 ]]; then
[[ $arch == x64 ]] && arch="x86_64"
case $arch in x86_64|arm64) ;; *) echo "Invalid Linux arch: $arch" >&2; echo; usage;; esac
fi
function replace_line {
pattern=$1
replacement=$2
file=$3
if egrep -q "$pattern" "$file"; then
perl -pi -e "s/$pattern/$replacement/" "$file"
else
echo "$pattern" not found in "$file" -- aborting 2>&1
exit 1
fi
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}/')
@ -120,32 +115,16 @@ function modify_omni {
unzip omni.ja
rm omni.ja
rm -r chrome/pdfjs
rm -r modules/narrate
#rm -r modules/translation
rm actors/AboutTranslations{Parent,Child}.sys.mjs
rm actors/CookieBanner{Parent,Child}.sys.mjs
rm actors/PictureInPictureChild.sys.mjs
rm actors/ThumbnailsChild.sys.mjs
rm actors/Translations{Engine,}{Parent,Child}.sys.mjs
rm actors/AudioPlayback{Parent,Child}.sys.mjs
replace_line 'BROWSER_CHROME_URL:.+' 'BROWSER_CHROME_URL: "chrome:\/\/zotero\/content\/zoteroPane.xhtml",' modules/AppConstants.sys.mjs
# Used by OSKeyStore as the master-key label, visible in macOS Keychain Access.
# Verify that OSKeyStore still derives the label from MOZ_APP_BASENAME, so a
# future Mozilla change to a hardcoded string doesn't silently rebrand the
# keychain entry back to "Firefox Encrypted Storage".
replace_line 'MOZ_APP_BASENAME: "Firefox"' 'MOZ_APP_BASENAME: "Zotero"' modules/AppConstants.sys.mjs
check_line 'STORE_LABEL: AppConstants\.MOZ_APP_BASENAME \+ " Encrypted Storage"' modules/OSKeyStore.sys.mjs
# https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html
#
# It's not clear that most of these do anything anymore when not compiled in, but just in case
replace_line 'MOZ_REQUIRE_SIGNING:' 'MOZ_REQUIRE_SIGNING: false \&\&' modules/AppConstants.sys.mjs
replace_line 'MOZ_DATA_REPORTING:' 'MOZ_DATA_REPORTING: false \&\&' modules/AppConstants.sys.mjs
replace_line 'MOZ_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
@ -169,20 +148,6 @@ function modify_omni {
# Prevent error during network requests
replace_line 'async lazyInit\(\) \{' 'async lazyInit() { if (this.features) return false;' modules/UrlClassifierExceptionListService.sys.mjs
# Silence CrashManager "Could not get children of [...]/Crash Reports/events" spam
replace_line '\/\/ Missing events folders are allowed' '\/\/ Missing events folders are allowed (silence warning)
if (ex) return entries;' modules/CrashManager.sys.mjs
# Several services pull in services-settings (which we strip) and throw
# at profile-after-change. Remove their #init() methods to prevent errors.
replace_line 'async #init\(\) \{' 'async #init() {} async _unused_init() {' modules/EssentialDomainsRemoteSettings.sys.mjs
replace_line 'async #init\(\) \{' 'async #init() {} async _unused_init() {' modules/SimpleURIUnknownSchemesRemoteObserver.sys.mjs
# RemotePermissionService uses a top-level import, so even loading the
# module throws
replace_line 'import \{ RemoteSettings \} from "resource:\/\/services-settings\/remote-settings.sys.mjs";' \
'const RemoteSettings = () => ({ on() {}, get: async () => [] });' \
modules/RemotePermissionService.sys.mjs
replace_line 'pref\("network.captive-portal-service.enabled".+' 'pref("network.captive-portal-service.enabled", false);' greprefs.js
replace_line 'pref\("network.connectivity-service.enabled".+' 'pref("network.connectivity-service.enabled", false);' greprefs.js
@ -199,21 +164,8 @@ function modify_omni {
# Disable unwanted components
remove_line '(RemoteSettings|services-|telemetry|Telemetry|URLDecorationAnnotationsService)' components/components.manifest
file="modules/ActorManagerParent.sys.mjs"
# Remove deleted actors
# Entries in the JSWINDOWACTORS/JSPROCESSACTORS literals:
remove_between '^ AudioPlayback: \{' '^ },' $file
remove_between '^ CookieBanner: \{' '^ },' $file
remove_between '^ Thumbnails: \{' '^ },' $file
remove_between '^ Translations: \{' '^ },' $file
remove_between '^ TranslationsEngine: \{' '^ },' $file
# Entries added conditionally after the literals:
remove_between '^ JSWINDOWACTORS\.AboutTranslations = \{' '^ \};' $file
remove_between '^ JSWINDOWACTORS\.PictureInPictureLauncher = \{' '^ \};' $file
remove_between '^ JSWINDOWACTORS\.PictureInPictureToggle = \{' '^ \};' $file
remove_between '^ JSWINDOWACTORS\.PictureInPicture = \{' '^ \};' $file
# Do not trigger LoginManager event that logs an error on autocomplete submission
remove_line 'DOMInputPasswordAdded: \{\},' $file
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
@ -224,7 +176,6 @@ function modify_omni {
pushd modules
rm Bookmarks*
rm FxAccounts*
echo '' > FingerprintingWebCompatService.sys.mjs
echo '' > PartitioningExceptionListService.sys.mjs
rm Places*
echo '' > PlacesExpiration.sys.mjs
@ -239,29 +190,6 @@ function modify_omni {
rm -rf default-theme
popd
# fcntl() is variadic, but js-ctypes declares it with a fixed third argument, which violates
# the ABI on every platform. It's harmless on Linux, where variadic and fixed integer
# arguments are passed in the same registers, but Apple's arm64 ABI passes variadic
# arguments on the stack, so the flag goes into a register the callee never reads and
# fcntl() takes it from an uninitialized stack slot instead. F_SETFD can therefore set
# FD_CLOFORK (bit 1, added in macOS 26) rather than FD_CLOEXEC, closing the pipe in the
# forked child and breaking Subprocess spawns, and F_SETFL can silently fail to set
# O_NONBLOCK. Since the value comes from leftover stack contents, this varies by build and
# call path instead of failing consistently -- don't assume it's fixed just because a given
# build works. This change should stay until this is fixed upstream.
if [[ $platform != win* ]]; then
replace_line 'ctypes\.int \/\* \.\.\. \*\/,' '"...",' modules/subprocess/subprocess_shared_unix.js
replace_line 'libc\.fcntl\(fds\[0\], LIBC\.F_SETFD, LIBC\.FD_CLOEXEC\);' 'libc.fcntl(fds[0], LIBC.F_SETFD, ctypes.int(LIBC.FD_CLOEXEC));' modules/subprocess/subprocess_unix.worker.js
replace_line 'libc\.fcntl\(fds\[1\], LIBC\.F_SETFD, LIBC\.FD_CLOEXEC\);' 'libc.fcntl(fds[1], LIBC.F_SETFD, ctypes.int(LIBC.FD_CLOEXEC));' modules/subprocess/subprocess_unix.worker.js
replace_line 'libc\.fcntl\(fds\[1\], LIBC\.F_SETFL, LIBC\.O_NONBLOCK\);' 'libc.fcntl(fds[1], LIBC.F_SETFL, ctypes.int(LIBC.O_NONBLOCK));' modules/subprocess/subprocess_unix.worker.js
replace_line 'libc\.fcntl\(fds\[0\], LIBC\.F_SETFL, LIBC\.O_NONBLOCK\);' 'libc.fcntl(fds[0], LIBC.F_SETFL, ctypes.int(LIBC.O_NONBLOCK));' modules/subprocess/subprocess_unix.sys.mjs
replace_line 'libc\.fcntl\(fds\[0\], LIBC\.F_SETFD, LIBC\.FD_CLOEXEC\);' 'libc.fcntl(fds[0], LIBC.F_SETFD, ctypes.int(LIBC.FD_CLOEXEC));' modules/subprocess/subprocess_unix.sys.mjs
replace_line 'libc\.fcntl\(fds\[1\], LIBC\.F_SETFD, LIBC\.FD_CLOEXEC\);' 'libc.fcntl(fds[1], LIBC.F_SETFD, ctypes.int(LIBC.FD_CLOEXEC));' modules/subprocess/subprocess_unix.sys.mjs
fi
# This file loads remote-settings.sys.mjs, but if it's empty Zotero crashes
replace_line 'async #init\(\) \{' 'async #init() { if (true) { return; }' modules/URLQueryStrippingListService.sys.mjs
# Clear most WebExtension manifest properties
replace_line 'manifest = normalized.value;' 'manifest = normalized.value;
if (this.type == "extension") {
@ -281,35 +209,21 @@ function modify_omni {
manifest.web_accessible_resources = undefined;
manifest.experiment_apis = {};
}' modules/Extension.sys.mjs
# Use applications.zotero instead of applications.gecko
replace_line 'let bss = manifest.applications\?.gecko' 'let bss = manifest.applications?.zotero' modules/addons/XPIInstall.sys.mjs
replace_line '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.sys.mjs
# Set strictCompatibility to false in beta/dev/source builds to skip strict_max_version check
replace_line 'addon.strictCompatibility = true;' \
'let version = Services.appinfo.version;
addon.strictCompatibility = !version.includes("-beta") && !version.includes("-dev") && !version.includes("SOURCE");' modules/addons/XPIInstall.sys.mjs
# In beta/dev/source builds, ignore the persisted strictCompatibility flag for already-installed
# plugins, so plugins installed on a stable build (where the flag was set to true) aren't marked
# incompatible after upgrading. The manifest isn't re-parsed on app upgrade, so the flag set at
# install time sticks around.
replace_line '!this\.addon\.strictCompatibility' \
'(!this.addon.strictCompatibility || Services.appinfo.version.includes("-beta") || Services.appinfo.version.includes("-dev") || Services.appinfo.version.includes("SOURCE"))' modules/addons/XPIInstall.sys.mjs
replace_line '!this\.strictCompatibility &&' \
'(!this.strictCompatibility || Services.appinfo.version.includes("-beta") || Services.appinfo.version.includes("-dev") || Services.appinfo.version.includes("SOURCE")) &&' modules/addons/XPIDatabase.sys.mjs
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.sys.mjs
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.sys.mjs
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
@ -320,22 +234,13 @@ function modify_omni {
replace_line "id: TOOLKIT_ID," "id: '$APP_ID'," modules/addons/AddonUpdateChecker.sys.mjs
replace_line 'lazy.AddonManagerPrivate.webExtensionsMinPlatformVersion' '"7.0"' modules/addons/AddonUpdateChecker.sys.mjs
replace_line 'result.targetApplications.push' 'false && result.targetApplications.push' modules/addons/AddonUpdateChecker.sys.mjs
# In beta/dev/source builds, don't set strictCompatibility on parsed update entries so that
# AddonUpdateChecker doesn't reject an available update whose maxVersion is lower than the
# current (non-stable) app version.
replace_line 'result\.strictCompatibility = appEntry\.maxVersion != "\*";' \
'let appVer = Services.appinfo.version; result.strictCompatibility = appEntry.maxVersion != "*" && !appVer.includes("-beta") && !appVer.includes("-dev") && !appVer.includes("SOURCE");' modules/addons/AddonUpdateChecker.sys.mjs
# Set uninstall flag to true if the update config has it set
replace_line 'targetApplications: \[appEntry\],' \
'targetApplications: \[appEntry\],
uninstall: getProperty(update, "uninstall", "boolean"),' modules/addons/AddonUpdateChecker.sys.mjs
# Prevent automatic installation of default theme
# (chrome/toolkit/content/mozapps/extensions/default-theme, removed above)
replace_line 'this.maybeInstallBuiltinAddon\(' 'if (false) this.maybeInstallBuiltinAddon(' modules/addons/XPIProvider.sys.mjs
replace_line 'this.maybeInstallBuiltinAddon\(' 'if (false) this.maybeInstallBuiltinAddon(' modules/addons/XPIProvider.jsm
# Don't log a warning when checking 'distribution/extensions' for addons
replace_line 'logger.warn\(`Checking' 'logger.debug(`Checking' modules/addons/XPIProvider.sys.mjs
replace_line 'logger.warn\(`Checking' 'logger.debug(`Checking' modules/addons/XPIProvider.jsm
# Allow addon installation by bypassing confirmation dialogs. If we want a confirmation dialog,
# we need to either add gXPInstallObserver from browser-addons.js [1][2] or provide our own with
@ -344,7 +249,7 @@ function modify_omni {
# [1] https://searchfox.org/mozilla-esr102/rev/5a6d529652045050c5cdedc0558238949b113741/browser/base/content/browser.js#1902-1923
# [2] https://searchfox.org/mozilla-esr102/rev/5a6d529652045050c5cdedc0558238949b113741/browser/base/content/browser-addons.js#201
# [3] https://searchfox.org/mozilla-esr102/rev/5a6d529652045050c5cdedc0558238949b113741/toolkit/mozapps/extensions/AddonManager.sys.mjs#3114-3124
replace_line 'if \(info.addon.installPermissions\) \{' 'if (false) {' modules/AddonManager.sys.mjs
replace_line '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
@ -358,35 +263,9 @@ function modify_omni {
replace_line 'listener\[aMethod\].apply\(listener, aArgs\);' \
'let maybePromise = listener[aMethod].apply(listener, aArgs);
if (maybePromise && maybePromise.then) await maybePromise;' modules/AddonManager.sys.mjs
replace_line 'AddonManagerPrivate.callAddonListeners' 'await AddonManagerPrivate.callAddonListeners' modules/addons/XPIInstall.sys.mjs
replace_line 'let uninstall = \(\) => \{' 'let uninstall = async () => {' modules/addons/XPIInstall.sys.mjs
replace_line 'cancelUninstallAddon\(aAddon\)' 'async cancelUninstallAddon(aAddon)' modules/addons/XPIInstall.sys.mjs
# Check for `uninstall` flag in update config and uninstall the addon if set
replace_line 'if \(update && !this.addon.location.locked\) \{' \
'if (update && !this.addon.location.locked) {
if (update.uninstall) {
let aId = this.addon.id;
logger.debug(`AddonUpdateChecker: uninstall addon \${aId} with uninstall flag`);
let addon = await AddonManager.getAddonByID(aId);
if (addon && (addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
try {
addon.uninstall();
} catch (err) {
logger.error(`AddonUpdateChecker: error uninstalling addon \${aId} with uninstall flag`, err);
}
sendUpdateAvailableMessages(this, null);
return;
}
else {
logger.error(`AddonUpdateChecker: addon \${aId} with uninstall flag not found or cannot be uninstalled`);
sendUpdateAvailableMessages(this, null);
return;
}
}
' modules/addons/XPIInstall.sys.mjs
# Prevent login manager from trying to load FxAccountsCommon.sys.mjs
replace_line 'return login.hostname == lazy.FXA_PWDMGR_HOST;' 'return false;' modules/storage-json.sys.mjs
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
@ -415,67 +294,48 @@ function modify_omni {
file="chrome/toolkit/content/mozapps/extensions/aboutaddons.css"
echo >> $file
# Hide the search bar and the whole sidebar, since we only ever show plugins and the main pane
# is already headed "Manage Your Plugins"
echo '.main-search, #sidebar { display: none; }' >> $file
# Center the content in the window now that it isn't offset by the sidebar
echo '#full { grid-template-columns: 1fr; }' >> $file
echo '#content { max-width: calc(var(--page-main-content-width) + var(--main-margin-start)); margin-inline: auto; }' >> $file
# 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 "Run on sites with restrictions" row and helper message
echo '.addon-detail-row-quarantined-domains { display: none; }' >> $file
echo '.addon-detail-help-row:has([data-l10n-id="addon-detail-quarantined-domains-help"]) { display: none; }' >> $file
# Hide "Report" button from plugin menu
echo 'addon-card addon-options hr:first-of-type { display: none }' >> $file
echo '[data-l10n-id="report-addon-button"] { display: none }' >> $file
# Hide "Debug Addons" and "Manage Extension Shortcuts"
echo 'panel-item[action="debug-addons"], panel-item[action="reset-update-states"] + hr, panel-item[action="manage-shortcuts"] { display: none }' >> $file
# Show cursor feedback on plugin homepage links
echo '.addon-detail-row-homepage .text-link { cursor: pointer; color: LinkText; }' >> $file
echo '.addon-detail-row-homepage .text-link { 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-utils.mjs"
file="chrome/toolkit/content/mozapps/extensions/aboutaddons.js"
# Hide unsigned-addon warning
replace_line 'export function isUnsignedWarningMessageDisabled\(\) \{' \
'export function isUnsignedWarningMessageDisabled() {if (true) return true;' $file
# Use our own localized string for blocked plugin notification
replace_line 'details-notification-hard-blocked-\$\{typeSuffix\}' 'plugins-blocked-plugin' $file
# Hide Recommendations tab in sidebar and recommendations in main pane
replace_line 'function isDiscoverEnabled\(\) \{' 'function isDiscoverEnabled() {return false;' $file
file="chrome/toolkit/content/mozapps/extensions/components/addon-details.mjs"
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
# 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 target="_blank" dir="ltr"><\/a>' \
'<label target="_blank" class="text-link" dir="ltr"><\/label>' $file
file="chrome/toolkit/content/mozapps/extensions/components/addon-card.mjs"
# Use our own strings for the removal prompt
replace_line 'let \{ BrowserAddonUI \} = windowRoot.window;' '' $file
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;' chrome/toolkit/content/mozapps/extensions/components/addon-list.mjs
# Add zotero.ftl to the addons page for our own localized strings
replace_line 'href="toolkit\/about\/aboutAddons.ftl"' 'href="toolkit\/about\/aboutAddons.ftl" \/>\n <link rel="localization" href="zotero.ftl"' \
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 target="_blank" dir="ltr"><\/a>' \
'<label target="_blank" class="text-link" dir="ltr"><\/label>' \
chrome/toolkit/content/mozapps/extensions/aboutaddons.html
# TODO: Use our own strings, but for now fix the bundled English ones
replace_line 'Manage Your Extensions' 'Manage Your Plugins' localization/en-US/toolkit/about/aboutAddons.ftl
replace_line '= Extensions' '= Plugins' localization/en-US/toolkit/about/aboutAddons.ftl
replace_line 'Add-on' 'Plugin' localization/en-US/toolkit/about/aboutAddons.ftl
replace_line 'add-ons' 'plugins' localization/en-US/toolkit/about/aboutAddons.ftl
replace_line 'Select add-on to install' 'Select plugin to install' localization/en-US/toolkit/about/aboutAddons.ftl
# Hide Recommendations tab in sidebar and recommendations in main pane
replace_line 'function isDiscoverEnabled\(\) \{' 'function isDiscoverEnabled() {return false;' chrome/toolkit/content/mozapps/extensions/aboutaddonsCommon.js
replace_line 'pref\("extensions.htmlaboutaddons.recommendations.enabled".+' 'pref("extensions.htmlaboutaddons.recommendations.enabled", false);' greprefs.js
# Hide Report option
@ -490,23 +350,20 @@ function modify_omni {
replace_line 'id=\"commonDialogWindow\"' \
'id=\"commonDialogWindow\" class=\"zotero-dialog\" drawintitlebar-platforms=\"mac\"' \
chrome/toolkit/content/global/commonDialog.xhtml
# commonDialog.css link is split across multiple lines, so we have to do a weird substitution,
# so check the one-line global.css to make sure the format hasn't changed
check_line '<html:link rel="stylesheet" href="chrome:\/\/global\/skin\/global.css" \/>' chrome/toolkit/content/global/commonDialog.xhtml
replace_line 'chrome:\/\/global\/skin\/commonDialog.css"' \
'chrome:\/\/global\/skin\/commonDialog.css"\/>
<html:link rel="stylesheet" href="chrome:\/\/zotero-platform\/content\/zotero.css"' \
replace_line '<\?xml-stylesheet href=\"chrome:\/\/global\/skin\/commonDialog.css\" type=\"text\/css\"\?>' \
'<\?xml-stylesheet href=\"chrome:\/\/global\/skin\/commonDialog.css\" type=\"text\/css\"\?>
<\?xml-stylesheet href=\"chrome:\/\/zotero-platform\/content\/zotero.css\" type=\"text\/css\"\?>' \
chrome/toolkit/content/global/commonDialog.xhtml
# commonDialog.xhtml can be opened via Services.prompt in the Profile Manager, so check for a profile first
replace_line 'function commonDialogOnLoad\(\) \{' \
'function commonDialogOnLoad() {
replace_line '<script>' \
'<script>
try {
Services.dirsvc.get(\"ProfD\", Ci.nsIFile);
Services.scriptloader.loadSubScript(\"chrome:\/\/zotero\/content\/include.js\", this);
Services.scriptloader.loadSubScript(\"chrome:\/\/zotero\/content\/titlebar.js\", this);
Services.scriptloader.loadSubScript(\"chrome:\/\/zotero\/content\/customElements.js\", this);
} catch (e) {}' \
chrome/toolkit/content/global/commonDialog.js
chrome/toolkit/content/global/commonDialog.xhtml
# Use native checkbox instead of Firefox-themed version in prompt dialogs
replace_line '<xul:checkbox' '<xul:checkbox native=\"true\"' chrome/toolkit/content/global/commonDialog.xhtml
@ -520,42 +377,12 @@ function modify_omni {
popupset = document.createXULElement("popupset");
document.documentElement.appendChild(popupset);
}
popupset.appendChild(this._autoScrollPopup);' chrome/toolkit/content/global/elements/browser-custom-element.mjs
# Add the dropmarker for toolbarbuttons with wantdropmarker, which Firefox no longer creates
replace_line 'this.appendChild\(this.constructor.fragment.cloneNode\(true\)\);' \
'this.appendChild(this.constructor.fragment.cloneNode(true));
if (this.hasAttribute("wantdropmarker")) {
let dropmarker = document.createXULElement("dropmarker");
dropmarker.setAttribute("type", "menu");
dropmarker.className = "toolbarbutton-menu-dropmarker";
this.appendChild(dropmarker);
}' chrome/toolkit/content/global/elements/toolbarbutton.js
# Let clicks on the dropmarker pass through to the toolbarbutton, which only opens its menu
# when it's the original event target, as xul.css did when Firefox created the dropmarker
echo '.toolbarbutton-menu-dropmarker { pointer-events: none; }' >> chrome/toolkit/content/global/xul.css
# Remove non-native text input styles
remove_between 'html\|input\:where\(' '^}' chrome/toolkit/skin/classic/global/global-shared.css
# Prevent window dragging on scrollbars
echo 'scrollbar { -moz-window-dragging: no-drag; }' >> chrome/toolkit/res/scrollbars.css
# Provide a mechanism for non-UA stylesheets to override the overflow property
# of a text control's anonymous text node child
echo '::placeholder, ::-moz-text-control-editing-root, ::-moz-text-control-preview {
overflow: var(--moz-text-control-overflow);
}' >> chrome/toolkit/res/forms.css
# Size an autocomplete popup to the input that opened it, or to an ancestor that designates
# itself as the width container. Firefox sizes the popup to its content. (For creator inputs.)
replace_line '\/\/ invalidate\(\) depends on the width attribute' \
'let widthElement = aElement.closest(".autocomplete-popup-width-container") || aElement;
this.style.setProperty("--panel-width", Math.max(widthElement.getBoundingClientRect().width, 100) + "px");
\/\/ invalidate() depends on the width attribute' \
chrome/toolkit/content/global/elements/autocomplete-popup.js
popupset.appendChild(this._autoScrollPopup);' chrome/toolkit/content/global/elements/browser-custom-element.js
# Show Services menu on input fields
# (JS part of https://hg.mozilla.org/mozilla-central/rev/defc298879844981e7c1817f13e3fc2caf84f148 for input boxes)
replace_line '<menupopup class="textbox-contextmenu">' '<menupopup class="textbox-contextmenu" showservicesmenu="true">' chrome/toolkit/content/global/elements/moz-input-box.js
zip -qr9XD omni.ja *
mv omni.ja ..
cd ..
@ -601,14 +428,12 @@ if [ $BUILD_MAC == 1 ]; then
echo "Using Firefox $GECKO_VERSION.app.zip"
unzip "Firefox $GECKO_VERSION.app.zip"
else
curl -fo Firefox.dmg "$DOWNLOAD_URL/mac/en-US/Firefox%20$GECKO_VERSION.dmg"
curl -o Firefox.dmg "$DOWNLOAD_URL/mac/en-US/Firefox%20$GECKO_VERSION.dmg"
set +e
hdiutil detach -quiet /Volumes/Firefox 2>/dev/null
set -e
hdiutil attach -quiet Firefox.dmg
cp -a /Volumes/Firefox/Firefox.app .
# Store local copy of unmodified app
zip -r "Firefox $GECKO_VERSION.app.zip" Firefox.app
hdiutil detach -quiet /Volumes/Firefox
fi
@ -617,42 +442,27 @@ if [ $BUILD_MAC == 1 ]; then
echo
rm -rf MacOS
if [ -n "$custom_components_hash_mac" ]; then
tarfile="Firefox $GECKO_VERSION MacOS.tar.xz"
if [[ "$(uname)" = "Darwin" ]]; then
shasum="shasum -a 256"
if [ -e "Firefox $GECKO_VERSION MacOS.zip" ]; then
echo "Using Firefox $GECKO_VERSION MacOS.zip"
unzip "Firefox $GECKO_VERSION MacOS.zip"
else
shasum=sha256sum
fi
download_components() {
echo "Downloading custom Firefox components"
echo
curl -fo "$tarfile" "${custom_components_url}mac/$custom_components_hash_mac.tar.xz"
}
verify_components_hash() {
echo "$custom_components_hash_mac $tarfile" | $shasum -c -
}
if [ ! -e "$tarfile" ]; then
download_components
fi
# Verify hash
echo
if ! verify_components_hash; then
echo "SHA hash mismatch. Redownloading custom Firefox components…"
rm -f "$tarfile"
download_components
echo
if ! verify_components_hash; then
echo "Hash still doesn't match. Aborting."
exit 1
curl -o MacOS.tar.xz "${custom_components_url}mac/$custom_components_hash_mac.tar.xz"
# Verify hash
if [[ "`uname`" = "Darwin" ]]; then
shasum=shasum
else
shasum=sha256sum
fi
echo
echo "$custom_components_hash_mac MacOS.tar.xz" | $shasum -c -
echo
tar xvf MacOS.tar.xz
fi
echo
tar xvf "$tarfile"
echo
fi
pushd Firefox.app/Contents/Resources
@ -670,8 +480,13 @@ if [ $BUILD_MAC == 1 ]; then
exit 1
fi
rm -f Firefox.dmg
if [ ! -e "Firefox $GECKO_VERSION.app.zip" ]; then
rm "Firefox.dmg"
fi
#if [ ! -e "Firefox $GECKO_VERSION MacOS.zip" ]; then
# rm "MacOS.zip"
#fi
echo $("$SCRIPT_DIR/xulrunner_hash" -p m) > hash-mac
fi
@ -679,32 +494,23 @@ if [ $BUILD_WIN == 1 ]; then
GECKO_VERSION="$GECKO_VERSION_WIN"
DOWNLOAD_URL="https://ftp.mozilla.org/pub/firefox/releases/$GECKO_VERSION"
if [[ -n $arch ]]; then
arch_list=("$(get_canonical_arch w $arch)")
else
arch_list=(win-x64 win-arm64 win32)
fi
for arch in "${arch_list[@]}"; do
for arch in win32 win-x64 win-arm64; do
xdir=firefox-$arch
rm -rf $xdir
mkdir $xdir
archived_file="Firefox Setup $GECKO_VERSION-$arch.exe"
if [ -e "$archived_file" ]; then
echo "Using $archived_file"
cp "$archived_file" "Firefox%20Setup%20$GECKO_VERSION.exe"
if [ -e "Firefox Setup $GECKO_VERSION-$arch.exe" ]; then
echo "Using Firefox Setup $GECKO_VERSION-$arch.exe"
cp "Firefox Setup $GECKO_VERSION-$arch.exe" "Firefox%20Setup%20$GECKO_VERSION.exe"
else
if [ $arch = "win-x64" ]; then
moz_arch=win64
curl -O "$DOWNLOAD_URL/win64/en-US/Firefox%20Setup%20$GECKO_VERSION.exe"
elif [ $arch = "win-arm64" ]; then
moz_arch=win64-aarch64
curl -O "$DOWNLOAD_URL/win64-aarch64/en-US/Firefox%20Setup%20$GECKO_VERSION.exe"
else
moz_arch=$arch
curl -O "$DOWNLOAD_URL/$arch/en-US/Firefox%20Setup%20$GECKO_VERSION.exe"
fi
curl -fO "$DOWNLOAD_URL/$moz_arch/en-US/Firefox%20Setup%20$GECKO_VERSION.exe"
# Store local copy of unmodified app
cp "Firefox%20Setup%20$GECKO_VERSION.exe" "$archived_file"
fi
7z x Firefox%20Setup%20$GECKO_VERSION.exe -o$xdir 'core/*'
@ -712,88 +518,29 @@ if [ $BUILD_WIN == 1 ]; then
rm -rf $xdir
mv $xdir-core $xdir
# Download custom components, as an architecture-specific ZIP containing xul.dll, etc.
echo
hash_arch_name="${arch/-/_}"
arch_var_name="custom_components_hash_${hash_arch_name}"
hash=${!arch_var_name:-}
if [ -n "$hash" ]; then
zipfile="Firefox $GECKO_VERSION-$arch Components.zip"
# ZIP filename doesn't include "esr" or "win-"
url="${custom_components_url}win/${GECKO_VERSION//esr}-${arch#win-}-$hash.zip"
if [[ "$(uname)" = "Darwin" ]]; then
shasum="shasum -a 256"
else
shasum=sha256sum
fi
download_zip() {
echo "Downloading custom Firefox components"
echo
echo $url
curl -fo "$zipfile" "$url"
}
verify_hash() {
echo "$hash $zipfile" | $shasum -c -
}
# Download if file doesn't exist
if [ ! -e "$zipfile" ]; then
download_zip
fi
echo
if ! verify_hash; then
echo "SHA hash mismatch. Redownloading ZIP…"
rm -f "$zipfile"
download_zip
echo
if ! verify_hash; then
echo "Hash still doesn't match. Aborting."
exit 1
fi
fi
echo
unzip -o "$zipfile" -d "$xdir"
echo
fi
pushd $xdir
#
# NOTE: Below is disabled, since the full UTF-16 string no longer appears directly in the DLL
# -- it's now truncated after "e594e8d1" for x64/win32 and doesn't appear at all for ARM64
#
# Replace "Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38" with "Zotero" for C:\ProgramData directory
#
# Mozilla uses a UUID in the path because they previously used just "Mozilla" and needed to
# recreate the folder with correct permissions for security reasons, but we never had a folder in
# ProgramData, so we can just create it as "Zotero". Instead of using a custom xul.dll, just
# replace the hard-coded string with "Zotero" and add a bunch of NULs.
#
#from=$(get_utf16_chars "Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38")
#to=$(get_utf16_chars "Zotero" 38)
#perl -pe "s/$from/$to/" < xul.dll > xul.dll.new
#mv xul.dll.new xul.dll
from=$(get_utf16_chars "Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38")
to=$(get_utf16_chars "Zotero" 38)
perl -pe "s/$from/$to/" < xul.dll > xul.dll.new
mv xul.dll.new xul.dll
# Check for UTF-16 "Zotero" in DLL
#
# (The macOS strings command doesn't have an encoding option, so skip on macOS. You need to
# build on Windows to make an installer, so a Windows build from macOS likely isn't being
# deployed.)
#
#if [[ "`uname`" != "Darwin" ]] && [[ -z "$(strings -e l xul.dll | grep -m 1 Zotero)" ]]; then
# echo '"Zotero" not found in xul.dll after replacement'
# exit 1
#fi
# We re-sign DLL in build.sh, but presumably this file, which is for the Widevine CDM,
# isn't vaid anymore
rm xul.dll.sig
# (The macOS strings command doesn't have an encoding option, so skip on macOS. We could
# require binutils, which has GNU strings, but you need to build on Windows to make an
# installer, so a Windows build from macOS likely isn't being deployed, and there's no
# reason the Perl command above should fail anyway.)
if [[ "`uname`" != "Darwin" ]] && [[ -z "$(strings -e l xul.dll | grep -m 1 Zotero)" ]]; then
echo '"Zotero" not found in xul.dll after replacement'
exit 1
fi
modify_omni $arch
# Disable skeleton UI window
@ -806,49 +553,41 @@ if [ $BUILD_WIN == 1 ]; then
rm "Firefox%20Setup%20$GECKO_VERSION.exe"
echo
echo
echo "$( "$SCRIPT_DIR/xulrunner_hash" -p w -a "$arch" )" > "hash-$arch"
done
echo $("$SCRIPT_DIR/xulrunner_hash" -p w) > hash-win
fi
if [ $BUILD_LINUX == 1 ]; then
GECKO_VERSION="$GECKO_VERSION_LINUX"
DOWNLOAD_URL="https://ftp.mozilla.org/pub/firefox/releases/$GECKO_VERSION"
if [[ -n $arch ]]; then
arch_list=("$arch")
# Include 32-bit build if not in CI
if [[ "${CI:-}" = "1" ]] || [[ "${SKIP_32:-}" = "1" ]]; then
arches="x86_64"
else
arch_list=(x86_64 arm64)
arches="i686 x86_64"
fi
for arch in "${arch_list[@]}"; do
xdir="firefox-linux-$arch"
for arch in $arches; do
xdir="firefox-$arch"
rm -rf $xdir
archived_file="firefox-$GECKO_VERSION-$arch.tar.xz"
archived_file="firefox-$GECKO_VERSION-$arch.tar.bz2"
if [ -e "$archived_file" ]; then
echo "Using $archived_file"
cp "$archived_file" "firefox-$GECKO_VERSION.tar.xz"
cp "$archived_file" "firefox-$GECKO_VERSION.tar.bz2"
else
if [ $arch = "arm64" ]; then
moz_arch=aarch64
else
moz_arch=$arch
fi
curl -fO "$DOWNLOAD_URL/linux-$moz_arch/en-US/firefox-$GECKO_VERSION.tar.xz"
# Store local copy of unmodified app
cp "firefox-$GECKO_VERSION.tar.xz" "$archived_file"
curl -O "$DOWNLOAD_URL/linux-$arch/en-US/firefox-$GECKO_VERSION.tar.bz2"
fi
tar xvf firefox-$GECKO_VERSION.tar.xz
mv firefox $xdir
tar xvf firefox-$GECKO_VERSION.tar.bz2
mv firefox firefox-$arch
pushd $xdir
pushd firefox-$arch
modify_omni $arch
popd
rm "firefox-$GECKO_VERSION.tar.xz"
echo "$( $SCRIPT_DIR/xulrunner_hash -p l -a "$arch" )" > "hash-linux-${arch}"
echo $($SCRIPT_DIR/xulrunner_hash -p l) > hash-linux
rm "firefox-$GECKO_VERSION.tar.bz2"
done
fi

View file

@ -14,7 +14,7 @@ hash="$1"
tmpdir=`mktemp -d`
cd $tmpdir
curl -sS -f -L -o build.zip "https://$S3_BUCKET.s3.amazonaws.com/$S3_CI_ZIP_PATH/$hash.zip" >&2 \
wget -O build.zip "https://$S3_BUCKET.s3.amazonaws.com/$S3_CI_ZIP_PATH/$hash.zip" >&2 \
|| (echo "ZIP file not found for commit '$hash'" && exit 1)
unzip build.zip >&2
rm build.zip

View file

@ -1,322 +0,0 @@
#!/bin/bash
set -euo pipefail
#
# Incrementally update a staged build in staging/ by zipping changed files from
# the client build/ directory directly into app/omni.ja, based on a manifest
# saved by dir_build after the last full build.
#
# Only files that are copied into omni.ja unmodified by build.sh can be updated
# this way. If any other file changed (or a file was removed), a full rebuild
# is required.
#
# Exit codes:
# 0 -- staged build was updated (or nothing changed)
# 2 -- a full rebuild is required
#
# This is normally run automatically by dir_build, not directly.
#
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/utils.sh"
function usage {
cat >&2 <<DONE
Usage: $0 -p PLATFORM [-a ARCH] [-t]
Options
-p PLATFORM platform of the staged build to update (m=Mac, w=Windows, l=Linux)
-a ARCH target architecture (Windows/Linux only)
-t devtools requested
DONE
exit 1
}
platform=""
arch=""
devtools=0
while getopts "p:a:t" opt; do
case $opt in
p)
platform="$OPTARG"
;;
a)
arch="$OPTARG"
;;
t)
devtools=1
;;
*)
usage
;;
esac
done
if [ -z "$platform" ]; then
usage
fi
BUILD_SRC_DIR="$ROOT_DIR/build"
MANIFEST_FILE="$STAGE_DIR/.build-manifest"
function fall_back {
echo "$1 -- performing full build" >&2
exit 2
}
if [ ! -d "$BUILD_SRC_DIR" ]; then
fall_back "No build/ directory"
fi
if [ ! -f "$MANIFEST_FILE" ]; then
fall_back "No manifest from previous build"
fi
if [[ "$(sed -n 's/^#format=//p' "$MANIFEST_FILE")" != "2" ]]; then
fall_back "Unsupported manifest format"
fi
# Make sure a staged build exists for the requested platform
case $platform in
m)
app_dir="$STAGE_DIR/Zotero.app/Contents/Resources"
;;
w)
app_dir="$STAGE_DIR/Zotero_$(get_canonical_arch w "$arch")"
;;
l)
app_dir="$STAGE_DIR/Zotero_linux-$(get_canonical_arch l "$arch")"
;;
*)
fall_back "Unknown platform '$platform'"
;;
esac
if [ ! -f "$app_dir/app/omni.ja" ]; then
fall_back "No staged build for requested platform"
fi
# If the previous build didn't include tests or devtools and they're requested
# now, files are missing from the staged build
manifest_tests=$(sed -n 's/^#include_tests=//p' "$MANIFEST_FILE")
manifest_tests=${manifest_tests:-0}
manifest_devtools=$(sed -n 's/^#devtools=//p' "$MANIFEST_FILE")
manifest_devtools=${manifest_devtools:-0}
if [[ -z "${ZOTERO_TEST:-}" ]] || [[ "${ZOTERO_TEST:-}" == "0" ]]; then
want_tests=0
else
want_tests=1
fi
if [[ $want_tests -eq 1 ]] && [[ $manifest_tests != "1" ]]; then
fall_back "Tests requested but not included in staged build"
fi
if [[ $devtools -eq 1 ]] && [[ $manifest_devtools != "1" ]]; then
fall_back "Devtools requested but not included in staged build"
fi
# If anything changed in app/ (build scripts, assets, bundled modules, Firefox
# runtime), a full rebuild is required
manifest_app_hash=$(sed -n 's/^#app_hash=//p' "$MANIFEST_FILE")
app_hash=$(generate_app_hash "$APP_ROOT_DIR")
if [[ -z "$manifest_app_hash" ]] || [[ "$app_hash" != "$manifest_app_hash" ]]; then
fall_back "Files in app/ changed"
fi
file_list=$(mktemp)
new_stats=$(mktemp)
carried=$(mktemp)
candidates=$(mktemp)
removed=$(mktemp)
cand_entries=$(mktemp)
trap 'rm -f "$file_list" "$new_stats" "$carried" "$candidates" "$removed" "$cand_entries"' EXIT
cd "$BUILD_SRC_DIR"
# Quickly find files that may have changed based on size/mtime, so that only
# those have to be hashed. Unmodified manifest entries are carried over,
# new/modified files become candidates, and files no longer present are
# considered removed.
build_manifest_file_list > "$file_list"
stat_file_list < "$file_list" > "$new_stats"
awk -F'\t' -v carried="$carried" -v candidates="$candidates" -v removed="$removed" '
NR==FNR {
if (/^#/) next
hash[$3] = $1
stat[$3] = $2
next
}
{
if (!($2 in stat)) {
print > candidates
}
else {
if ($1 == stat[$2]) {
print hash[$2] "\t" $0 > carried
}
else {
print > candidates
}
delete stat[$2]
}
}
END {
for (path in stat) print path > removed
}
' "$MANIFEST_FILE" "$new_stats"
# Hash the candidates and compare to the previous hashes to find real changes
# -- A = added, C = changed, R = removed
if [ -s "$candidates" ]; then
paste \
<(cut -f2 "$candidates" | sed 's,^,./,' | tr '\n' '\0' | hash_file_list | cut -f1) \
"$candidates" \
> "$cand_entries"
fi
changes=$(
if [ -s "$cand_entries" ]; then
awk -F'\t' '
NR==FNR {
if (/^#/) next
old[$3] = $1
next
}
{
if (!($3 in old)) print "A\t" $3
else if (old[$3] != $1) print "C\t" $3
}
' "$MANIFEST_FILE" "$cand_entries"
fi
if [ -s "$removed" ]; then
awk '{print "R\t" $0}' "$removed"
fi
)
# Make sure all changed files can be zipped directly into omni.ja, and skip
# ones that aren't included in the staged build
zip_files=()
ftl_files=()
test_copy_files=()
while IFS=$'\t' read -r status path; do
if [ -z "$status" ]; then
continue
fi
if [ "$status" == "R" ]; then
case "$path" in
test/*)
# Ignore removed test files if tests weren't included
if [[ $manifest_tests != "1" ]]; then
continue
fi
;;
esac
fall_back "$path was removed"
fi
case "$path" in
# Copied into localization/ subdirectories by build.sh
chrome/locale/*/zotero/mozilla/*)
fall_back "Can't update $path in place"
;;
# Pruned by prepare_build
chrome/content/zotero/locale/csl/*)
fall_back "Can't update $path in place"
;;
# Copied to localization/<locale>/ in addition to the chrome path
chrome/locale/*/zotero/*.ftl)
zip_files+=("$path")
ftl_files+=("$path")
;;
chrome/*|components/*|resource/*)
zip_files+=("$path")
;;
# Appended to the main chrome.manifest by build.sh
test/chrome.manifest)
fall_back "Can't update $path in place"
;;
test/*)
# If tests weren't included in the staged build, ignore changes
# to test files
if [[ $manifest_tests == "1" ]]; then
zip_files+=("$path")
case "$path" in
# Also copied to tests/ at the top level of the app
test/tests/*)
test_copy_files+=("$path")
;;
esac
fi
;;
*)
fall_back "Can't update $path in place"
;;
esac
done <<< "$changes"
# Find all staged omni.ja files to update (multiple can exist -- e.g., all
# Windows architectures)
omni_files=()
while IFS= read -r file; do
omni_files+=("$file")
done < <(find "$STAGE_DIR" -path '*/app/omni.ja')
if [ ${#zip_files[@]} -gt 0 ]; then
echo "Changed files:"
for path in "${zip_files[@]}"; do
echo " $path"
done
cd "$BUILD_SRC_DIR"
for omni_file in "${omni_files[@]}"; do
echo "Updating ${omni_file#"$STAGE_DIR"/}"
zip -qX "$omni_file" "${zip_files[@]}"
done
# Copy .ftl files to their localization/ paths
if [ ${#ftl_files[@]} -gt 0 ]; then
l10n_dir=$(mktemp -d)
for path in "${ftl_files[@]}"; do
locale=${path#chrome/locale/}
locale=${locale%%/*}
mkdir -p "$l10n_dir/localization/$locale"
cp -L "$path" "$l10n_dir/localization/$locale/$(basename "$path")"
done
pushd "$l10n_dir" > /dev/null
for omni_file in "${omni_files[@]}"; do
zip -qrX "$omni_file" localization
done
popd > /dev/null
rm -rf "$l10n_dir"
fi
# Copy test files to tests/ directories
if [ ${#test_copy_files[@]} -gt 0 ]; then
for path in "${test_copy_files[@]}"; do
for omni_file in "${omni_files[@]}"; do
dest="${omni_file%/app/omni.ja}/tests/${path#test/tests/}"
mkdir -p "$(dirname "$dest")"
cp -L "$path" "$dest"
done
done
fi
# Bump BuildID so that startup caches are invalidated even without
# -purgecaches
build_id=$(date +%Y%m%d%H%M%S)
for omni_file in "${omni_files[@]}"; do
perl -pi -e "s/^BuildID=.*/BuildID=$build_id/" "$(dirname "$omni_file")/application.ini"
done
echo "Updated staged build with ${#zip_files[@]} changed file(s)"
elif [ -n "$changes" ]; then
echo "No staged files changed"
else
echo "No changes since last build"
fi
{
echo "#format=2"
echo "#include_tests=$manifest_tests"
echo "#devtools=$manifest_devtools"
echo "#app_hash=$app_hash"
LC_ALL=C sort -t$'\t' -k3 "$carried" "$cand_entries"
} > "$MANIFEST_FILE.tmp"
mv "$MANIFEST_FILE.tmp" "$MANIFEST_FILE"

View file

@ -64,19 +64,22 @@ if [[ "$VERSION" ]] && [[ "$NUM_VERSIONS" ]]; then
usage
fi
INCR_FILENAME="incrementals-$PLATFORM"
REMOTE_PATH="$DEPLOY_PATH/manifests/$CHANNEL/$INCR_FILENAME"
INCR_FILENAME="incrementals-$CHANNEL-$PLATFORM"
S3_URL="s3://$S3_BUCKET/$S3_DIST_PATH/$CHANNEL/incrementals-$PLATFORM"
INCR_PATH="$DIST_DIR/$INCR_FILENAME"
if [ "`uname -o 2> /dev/null`" = "Cygwin" ]; then
INCR_PATH=$(cygpath -w "$INCR_PATH")
fi
mkdir -p "$DIST_DIR"
scp $DEPLOY_HOST:"$REMOTE_PATH" "$INCR_PATH" >&2
aws s3 cp $S3_URL "$INCR_PATH" >&2
# Add version to file and reupload
if [ "$VERSION" ]; then
echo "Adding $VERSION to incrementals-$PLATFORM"
echo $VERSION >> "$INCR_PATH"
scp "$INCR_PATH" $DEPLOY_HOST:"$REMOTE_PATH"
aws s3 cp "$INCR_PATH" $S3_URL
# Show last n versions
elif [ "$NUM_VERSIONS" -gt 0 ]; then
# TEMP: Don't include 6.0 versions

View file

@ -4,7 +4,6 @@ set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
. "$ROOT_DIR/config.sh"
. "$SCRIPT_DIR/utils.sh"
function usage {
echo "Usage: $0 id"
@ -16,7 +15,5 @@ if [[ -z "$id" ]]; then
usage
fi
prepare_notary_auth
xcrun notarytool log "$id" "${notary_auth[@]}" notary_log.json
xcrun notarytool log "$id" --apple-id "$NOTARIZATION_USER" --team-id "$NOTARIZATION_TEAM_ID" --password "$NOTARIZATION_PASSWORD" notary_log.json
cat notary_log.json

View file

@ -4,7 +4,6 @@ set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
. "$ROOT_DIR/config.sh"
. "$SCRIPT_DIR/utils.sh"
function usage {
echo "Usage: $0 file"
@ -16,7 +15,5 @@ if [[ -z "$file" ]]; then
usage
fi
prepare_notary_auth
echo "Uploading ${file##*/} to Apple for notarization" >&2
xcrun notarytool submit $file "${notary_auth[@]}" --wait
xcrun notarytool submit $file --apple-id "$NOTARIZATION_USER" --team-id "$NOTARIZATION_TEAM_ID" --password="$NOTARIZATION_PASSWORD" --wait

View file

@ -71,42 +71,34 @@ def main():
tmp_src_dir = os.path.join(tmp_dir, 'zotero')
# Export a clean copy of the source tree -- with Python on Windows, where rsync,
# if present at all, doesn't handle Windows-style paths
if os.name == 'nt' or not shutil.which('rsync'):
shutil.copytree(
'.',
tmp_src_dir,
ignore=shutil.ignore_patterns('.*', '#*', 'package.json', 'package-lock.json')
)
else:
subprocess.check_call([
'rsync',
'-aL',
# Exclude hidden files
'--exclude', '.*',
'--exclude', '#*',
'--exclude', 'package.json',
'--exclude', 'package-lock.json',
'.' + os.sep,
tmp_src_dir + os.sep
])
# Export a clean copy of the source tree
subprocess.check_call([
'rsync',
'-aL',
# Exclude hidden files
'--exclude', '.*',
'--exclude', '#*',
'--exclude', 'package.json',
'--exclude', 'package-lock.json',
'.' + os.sep,
tmp_src_dir + os.sep
])
# Make sure the copy worked
# Make sure rsync worked
d = os.path.join(tmp_src_dir, 'chrome')
if not os.path.isdir(d):
raise FileNotFoundError(d + " not found")
# Delete CSL locale support files
csl_dir = os.path.normpath(tmp_src_dir + '/chrome/content/zotero/locale/csl/')
for root, dirs, files in os.walk(csl_dir, topdown=False):
for name in files:
if not name.endswith('.xml') and name != 'locales.json':
os.remove(os.path.join(root, name))
for name in dirs:
d = os.path.join(root, name)
if not os.listdir(d):
os.rmdir(d)
subprocess.check_call([
'find',
os.path.normpath(tmp_src_dir + '/chrome/content/zotero/locale/csl/'),
'-mindepth', '1',
'!', '-name', '*.xml',
'!', '-name', 'locales.json',
#'-print',
'-delete'
])
# Delete styles build script
os.remove(os.path.join(tmp_src_dir, 'styles', 'update'))

View file

@ -1,46 +0,0 @@
#!/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"
if [ "`uname`" = "Darwin" ]; then
export SAFARI_APPEX="$ROOT_DIR/../safari-web-extension-builds/ZoteroSafariExtension.appex"
export SAFARI_EXT_RESOURCES="$ROOT_DIR/../safari-web-extension-builds/test/safari"
export SAFARI_APP_EXTENSION="$ROOT_DIR/../safari-app-extension-builds/test/ZoteroSafariExtension.appex"
if [ ! -d "$SAFARI_APPEX" ]; then
echo "Safari extension stub not found at $SAFARI_APPEX -- aborting" >&2
exit 1
fi
if [ ! -f "$SAFARI_EXT_RESOURCES/manifest.json" ]; then
echo "Safari web extension not found in $SAFARI_EXT_RESOURCES -- aborting" >&2
exit 1
fi
if [ ! -d "$SAFARI_APP_EXTENSION" ]; then
echo "Safari App Extension not found at $SAFARI_APP_EXTENSION -- aborting" >&2
exit 1
fi
fi
cd "$SCRIPT_DIR"
./check_requirements
hash=`./get_repo_branch_hash $BRANCH`
./check_app_matches_commit $hash
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
VERSION="`cat \"$build_dir/version\"`"
./build_for_deploy -d "$build_dir" -p $BUILD_PLATFORMS -c $CHANNEL -i 1
ssh $DEPLOY_HOST "$DEPLOY_PATH/deploy" $CHANNEL "$VERSION" $BUILD_PLATFORMS

View file

@ -1,187 +0,0 @@
get_current_platform() {
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
echo l
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo m
elif [[ "$OSTYPE" == "cygwin" ]]; then
echo w
elif [[ "$OSTYPE" == "msys" ]]; then
echo w
# Unknown, so probably Unix-y
else
echo l
fi
}
get_canonical_arch() {
local _platform=$1
local _arch=$2
case $_platform in
w)
case $_arch in
x64) _arch="win-x64" ;;
arm64) _arch="win-arm64" ;;
win32|win-x64|win-arm64) ;;
*) echo "Invalid Windows archicture: $_arch" >&2;;
esac
echo $_arch
;;
l)
[[ $_arch == x64 ]] && _arch="x86_64"
echo $_arch
;;
*)
echo "Invalid platform '$platform'" 2>&1
exit 1
esac
}
# Print a sorted NUL-separated list of the files in the current directory to
# include in the build manifest. Follows symlinks, like the rsync calls in
# prepare_build and build.sh.
function build_manifest_file_list {
# Exclusions match the rsync call in prepare_build
find -L . -mindepth 1 \( -name '.*' -o -name '#*' \) -prune \
-o -type f ! -name 'package.json' ! -name 'package-lock.json' -print0 \
| LC_ALL=C sort -z
}
# Read a NUL-separated file list on stdin and print "size|mtime<TAB>path" for
# each file
function stat_file_list {
if [[ "$OSTYPE" == "darwin"* ]]; then
xargs -0 stat -L -f '%z|%Fm|%N'
else
xargs -0 stat -L -c '%s|%y|%n'
fi | sed -E 's,^([0-9]+[|][^|]+)[|](\./)?,\1'$'\t'','
}
# Read a NUL-separated file list on stdin and print "hash<TAB>path" for each
# file
function hash_file_list {
local md5_cmd
if command -v md5sum > /dev/null 2>&1; then
md5_cmd="md5sum"
else
md5_cmd="md5 -r"
fi
xargs -0 $md5_cmd | sed -E 's,^([0-9a-f]{32}) ?(\./)?,\1'$'\t'','
}
# Generate a manifest of a build directory for incremental updates, with one
# "hash<TAB>size|mtime<TAB>path" line per file, sorted by path
function generate_build_manifest {
local dir=$1
(
cd "$dir"
local file_list
file_list=$(mktemp)
build_manifest_file_list > "$file_list"
paste \
<(hash_file_list < "$file_list" | cut -f1) \
<(stat_file_list < "$file_list")
rm -f "$file_list"
)
}
# Generate a hash of the files in app/ that affect build output, for detecting
# when a staged build can't be updated incrementally. Since some of the files
# are large binaries, use file sizes and modification times rather than
# contents. The xulrunner runtimes are covered by the hash-* files written by
# fetch_xulrunner.
function generate_app_hash {
local app_dir=$1
local md5_cmd
if command -v md5sum > /dev/null 2>&1; then
md5_cmd="md5sum"
else
md5_cmd="md5 -r"
fi
(
cd "$app_dir"
local paths=(assets build.sh config.sh scripts mac win linux modules update-packaging)
if [ -f config-custom.sh ]; then
paths+=(config-custom.sh)
fi
{
if [[ "$OSTYPE" == "darwin"* ]]; then
find -L "${paths[@]}" -name '.*' -prune -o -type f -print0 | xargs -0 stat -L -f '%N|%z|%m'
else
find -L "${paths[@]}" -name '.*' -prune -o -type f -print0 | xargs -0 stat -L -c '%n|%s|%Y'
fi
cat xulrunner/hash-* 2> /dev/null || true
} | LC_ALL=C sort | $md5_cmd | awk '{print $1}'
)
}
function check_line {
pattern=$1
file=$2
if ! grep -E -q "$pattern" "$file"; then
echo "$pattern" not found in "$file" -- aborting 2>&1
exit 1
fi
}
function replace_line {
pattern=$1
replacement=$2
file=$3
if grep -E -q "$pattern" "$file"; then
perl -pi -e "s/$pattern/$replacement/" "$file"
else
echo "$pattern" not found in "$file" -- aborting 2>&1
exit 1
fi
}
function remove_line {
pattern=$1
file=$2
if grep -E -q "$pattern" "$file"; then
grep -E -v "$pattern" "$file" > "$file.tmp"
mv "$file.tmp" "$file"
else
echo "$pattern" not found in "$file" -- aborting 2>&1
exit 1
fi
}
function remove_between {
start_pattern=$1
end_pattern=$2
file=$3
if grep -E -q "$start_pattern" "$file" && grep -E -q "$end_pattern" "$file"; then
perl -ni -e '
if (/'"$start_pattern"'/) { $skip = 1; next; }
elsif ($skip && /'"$end_pattern"'/) { $skip = 0; next; }
print unless $skip;' "$file"
else
echo "$start_pattern" and "$end_pattern" not found in "$file" -- aborting 2>&1
exit 1
fi
}
# Set $notary_auth to notarytool authentication arguments: the keychain profile in
# NOTARIZATION_PROFILE if set, or else the Apple ID and app-specific password
function prepare_notary_auth {
if [[ -n "${NOTARIZATION_PROFILE:-}" ]]; then
notary_auth=(--keychain-profile "$NOTARIZATION_PROFILE")
if [[ -n "$KEYCHAIN" ]]; then
keychain_path="$HOME/Library/Keychains/$KEYCHAIN.keychain-db"
# The keychain may have auto-locked since signing
if [[ -n "$KEYCHAIN_PASSWORD" ]]; then
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path"
fi
notary_auth+=(--keychain "$keychain_path")
fi
else
notary_auth=(--apple-id "$NOTARIZATION_USER" --team-id "$NOTARIZATION_TEAM_ID" --password "$NOTARIZATION_PASSWORD")
fi
}

View file

@ -1,15 +1,4 @@
#!/bin/bash
# Calculates a hash for the processed Firefox package for a given platform and architecture
#
# Usage:
# xulrunner_hash.sh -p <m|w|l> [-a ARCH]
#
# * **Mac (m)** ‑a is disallowed (universal build).
# * **Windows (w)** ‑a is **required**. Accepted values:
# win32 | x64 | arm64
# * **Linux (l)** ‑a is **required**. Accepted values:
# x86_64 | arm64 (alias x64 → x86_64)
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
@ -17,79 +6,39 @@ APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
. "$APP_ROOT_DIR/config.sh"
cd "$APP_ROOT_DIR"
platform=""; arch=""
usage() {
cat >&2 <<'DONE'
Usage: xulrunner_hash.sh -p <m|w|l> [-a ARCH]
-p PLATFORM m (mac), w (windows), l (linux) [required]
-a ARCH *required* for Windows & Linux builds
Windows: x64 | arm64 | win32
Linux: x64 (or x86_64) | arm64
Mac: disallowed – always universal
DONE
exit 1
}
while getopts ':p:a:' opt; do
platform=""
while getopts ":p:" opt; do
case $opt in
p) platform="$OPTARG";;
a) arch="$OPTARG";;
:) echo "Option -$OPTARG requires an argument" >&2; usage;;
? ) echo "Invalid option: -$OPTARG" >&2; echo; usage;;
p)
platform="$OPTARG"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
shift $((OPTIND-1)); OPTIND=1
done
[[ -z $platform ]] && { echo "-p is required" >&2; echo; usage; }
case $platform in
m)
[[ -n $arch ]] && { echo "-a is not allowed for Mac (universal build)" >&2; echo; usage; }
arch="universal"
if [ $platform == "m" ]; then
GECKO_VERSION="$GECKO_VERSION_MAC"
;;
w)
[[ -z $arch ]] && { echo "-a is required for Windows builds" >&2; echo; usage; }
# Map CLI-friendly aliases → canonical form
case $arch in
x64) arch="win-x64" ;;
arm64) arch="win-arm64" ;;
win32|win-x64|win-arm64) ;; # already canonical
*) echo "Invalid Windows arch: $arch" >&2; echo; usage;;
esac
elif [ $platform == "w" ]; then
GECKO_VERSION="$GECKO_VERSION_WIN"
;;
l)
[[ -z $arch ]] && { echo "-a is required for Linux builds" >&2; echo; usage; }
[[ $arch == x64 ]] && arch="x86_64"
case $arch in
x86_64|arm64) ;;
*) echo "Invalid Linux arch: $arch" >&2; echo; usage;;
esac
elif [ $platform == "l" ]; then
GECKO_VERSION="$GECKO_VERSION_LINUX"
;;
*)
echo "Invalid platform: $platform" >&2; echo; usage;;
esac
# Concatenate scripts that affect the build
xulrunner_content=$(< "$SCRIPT_DIR/fetch_xulrunner")
xulrunner_content+=$(< "$SCRIPT_DIR/utils.sh")
xulrunner_content+=$(< "$APP_ROOT_DIR/assets/multilocale.txt")
# Get custom components hash
if [[ $platform = "m" ]]; then
components_hash="$custom_components_hash_mac"
else
hash_arch_name="${arch/-/_}"
arch_var_name="custom_components_hash_${hash_arch_name}"
components_hash=${!arch_var_name:-}
echo "Platform parameter incorrect. Usage: -p m(mac)/w(windows)/l(linux)"
exit 1
fi
input_string="${platform}-${arch}-${components_hash}|${GECKO_VERSION}|${xulrunner_content}"
xulrunner_content=$(< "$SCRIPT_DIR/fetch_xulrunner")
xulrunner_content+=$(< "$APP_ROOT_DIR/assets/multilocale.txt")
xulrunner_gecko_hash=$(echo -n "$GECKO_VERSION - '$xulrunner_content'" | openssl dgst -sha256)
echo "$xulrunner_gecko_hash"
printf "%s" "$input_string" | openssl dgst -sha256 | awk '{print $NF}'

View file

@ -97,8 +97,6 @@ if [[ $BUILD_MAC == 0 ]] && [[ $BUILD_WIN == 0 ]] && [[ $BUILD_LINUX == 0 ]]; th
usage
fi
TO_MAJOR_VERSION="${TO%%.*}"
rm -rf "$UPDATE_STAGE_DIR"
mkdir "$UPDATE_STAGE_DIR"
@ -138,43 +136,19 @@ for version in "$FROM" "$TO"; do
mkdir -p "$versiondir"
cd "$versiondir"
# Determine Linux archive extension by major Zotero version
MAJOR_VERSION="${version%%.*}"
if [ "$MAJOR_VERSION" -lt 8 ]; then
LINUX_EXT="tar.bz2"
else
LINUX_EXT="tar.xz"
fi
MAC_ARCHIVE="Zotero-${version}.dmg"
WIN32_ARCHIVE="Zotero-${version}_win32.zip"
WIN_X64_ARCHIVE="Zotero-${version}_win-x64.zip"
WIN_ARM64_ARCHIVE="Zotero-${version}_win-arm64.zip"
LINUX_X86_ARCHIVE="Zotero-${version}_linux-i686.${LINUX_EXT}"
LINUX_X86_64_ARCHIVE="Zotero-${version}_linux-x86_64.${LINUX_EXT}"
LINUX_ARM64_ARCHIVE="Zotero-${version}_linux-arm64.${LINUX_EXT}"
LINUX_X86_ARCHIVE="Zotero-${version}_linux-i686.tar.bz2"
LINUX_X86_64_ARCHIVE="Zotero-${version}_linux-x86_64.tar.bz2"
CACHE_DIR="$ROOT_DIR/cache"
if [ ! -e "$CACHE_DIR" ]; then
mkdir "$CACHE_DIR"
fi
# Build archive list (no Linux arm64 before Zotero 8, no Linux i686 from Zotero 11)
ARCHIVES=(
"$MAC_ARCHIVE"
"$WIN32_ARCHIVE"
"$WIN_X64_ARCHIVE"
"$WIN_ARM64_ARCHIVE"
"$LINUX_X86_64_ARCHIVE"
)
if [ "$TO_MAJOR_VERSION" -lt 11 ]; then
ARCHIVES+=("$LINUX_X86_ARCHIVE")
fi
if [ "$MAJOR_VERSION" -ge 8 ]; then
ARCHIVES+=("$LINUX_ARM64_ARCHIVE")
fi
for archive in "${ARCHIVES[@]}"; do
for archive in "$MAC_ARCHIVE" "$WIN32_ARCHIVE" "$WIN_X64_ARCHIVE" "$WIN_ARM64_ARCHIVE" "$LINUX_X86_ARCHIVE" "$LINUX_X86_64_ARCHIVE"; do
if [[ $archive = "$MAC_ARCHIVE" ]] && [[ $BUILD_MAC != 1 ]]; then
continue
fi
@ -193,9 +167,6 @@ for version in "$FROM" "$TO"; do
if [[ $archive = "$LINUX_X86_64_ARCHIVE" ]] && [[ $BUILD_LINUX != 1 ]]; then
continue
fi
if [[ $archive = "$LINUX_ARM64_ARCHIVE" ]] && [[ $BUILD_LINUX != 1 ]]; then
continue
fi
ETAG_FILE="$CACHE_DIR/$archive.etag"
@ -215,12 +186,10 @@ for version in "$FROM" "$TO"; do
set +e
# Cached version is available
if [ -n "$ETAG" ]; then
HTTP_CODE=$(curl -sS -L -D headers.txt -o $archive -w '%{http_code}' -H "If-None-Match: $ETAG" "$URL")
NEW_ETAG=$(awk 'tolower($1) == "etag:" {print $2}' headers.txt | tr -d '\r' | tail -n 1)
rm -f headers.txt
NEW_ETAG=$(wget -nv -S --header "If-None-Match: $ETAG" $URL 2>&1 | awk '/ *ETag: */ {print $2}')
# If ETag didn't match, cache newly downloaded version
if [ "$HTTP_CODE" = 200 ]; then
if [ -f $archive ]; then
echo "ETag for $archive didn't match! -- using new version"
rm -f "$CACHE_DIR/$archive.etag"
cp $archive "$CACHE_DIR/"
@ -231,9 +200,7 @@ for version in "$FROM" "$TO"; do
cp "$CACHE_DIR/$archive" .
fi
else
curl -sS -f -L -D headers.txt -o $archive "$URL"
NEW_ETAG=$(awk 'tolower($1) == "etag:" {print $2}' headers.txt | tr -d '\r' | tail -n 1)
rm -f headers.txt
NEW_ETAG=$(wget -nv -S $URL 2>&1 | awk '/ *ETag: */ {print $2}')
# Save archive to cache
rm -f "$CACHE_DIR/$archive.etag"
@ -243,13 +210,8 @@ for version in "$FROM" "$TO"; do
set -e
done
# Delete cached archives older than 14 days
find "$CACHE_DIR" -maxdepth 1 -type f -ctime +14 -delete
# Delete cached diffs and compressed files not created or used in 60 days
# (cache hits touch the entries)
if [ -d "$CACHE_DIR/update-packaging" ]; then
find "$CACHE_DIR/update-packaging" -type f -ctime +60 -delete
fi
# Delete cached files older than 14 days
find "$CACHE_DIR" -ctime +14 -delete
# Unpack Zotero.app
if [ $BUILD_MAC == 1 ]; then
@ -270,14 +232,9 @@ for version in "$FROM" "$TO"; do
# Unpack Windows zips
if [ $BUILD_WIN == 1 ]; then
if [[ -f "$WIN32_ARCHIVE" ]] && [[ -f "$WIN_X64_ARCHIVE" ]] && [[ -f "$WIN_ARM64_ARCHIVE" ]]; then
# Unpack architectures in parallel
UNPACK_PIDS=""
for build in "$WIN32_ARCHIVE" "$WIN_X64_ARCHIVE" "$WIN_ARM64_ARCHIVE"; do
(unzip -q "$build" && rm "$build") &
UNPACK_PIDS="$UNPACK_PIDS $!"
done
for pid in $UNPACK_PIDS; do
wait $pid
unzip -q "$build"
rm "$build"
done
INCREMENTALS_FOUND=1
else
@ -287,36 +244,14 @@ for version in "$FROM" "$TO"; do
# Unpack Linux tarballs
if [ $BUILD_LINUX == 1 ]; then
# Zotero 7 has no Linux arm64 build; Zotero 11 has no Linux i686 build
LINUX_BUILDS_TO_UNPACK=("$LINUX_X86_64_ARCHIVE")
if [ "$TO_MAJOR_VERSION" -lt 11 ]; then
LINUX_BUILDS_TO_UNPACK+=("$LINUX_X86_ARCHIVE")
fi
if [ "$MAJOR_VERSION" -ge 8 ]; then
LINUX_BUILDS_TO_UNPACK+=("$LINUX_ARM64_ARCHIVE")
fi
MISSING=0
for build in "${LINUX_BUILDS_TO_UNPACK[@]}"; do
if [ ! -f "$build" ]; then
MISSING=1
break
fi
done
if [ "$MISSING" -eq 0 ]; then
# Unpack architectures in parallel
UNPACK_PIDS=""
for build in "${LINUX_BUILDS_TO_UNPACK[@]}"; do
(tar -xf "$build" && rm "$build") &
UNPACK_PIDS="$UNPACK_PIDS $!"
done
for pid in $UNPACK_PIDS; do
wait $pid
if [[ -f "$LINUX_X86_ARCHIVE" ]] && [[ -f "$LINUX_X86_64_ARCHIVE" ]]; then
for build in "$LINUX_X86_ARCHIVE" "$LINUX_X86_64_ARCHIVE"; do
tar -xjf "$build"
rm "$build"
done
INCREMENTALS_FOUND=1
else
echo "Linux tarballs not found: ${LINUX_BUILDS_TO_UNPACK[*]}"
echo "$LINUX_X86_ARCHIVE/$LINUX_X86_64_ARCHIVE not found"
fi
fi
@ -327,21 +262,8 @@ done
export MOZ_PRODUCT_VERSION="$TO"
export MAR_CHANNEL_ID="$CHANNEL"
# Reuse diffs and compressed files across incremental builds, since FROM
# versions often share files with each other and with the TO version. Entries
# that haven't been used recently are cleaned up above.
export UPDATE_CACHE_DIR="$ROOT_DIR/cache/update-packaging"
CHANGES_MADE=0
for build in "mac" "win32" "win-x64" "win-arm64" "linux-i686" "linux-x86_64" "linux-arm64"; do
# Zotero 7 has no Linux arm64 builds
if [[ $build == "linux-arm64" ]] && [[ $FROM == 7.* ]]; then
continue
fi
# Zotero 11 has no Linux i686 builds, so there's nothing to update to
if [[ $build == "linux-i686" ]] && [[ $TO_MAJOR_VERSION -ge 11 ]]; then
continue
fi
for build in "mac" "win32" "win-x64" "win-arm64" "linux-i686" "linux-x86_64"; do
if [[ $build == "mac" ]]; then
if [[ $BUILD_MAC == 0 ]]; then
continue
@ -351,7 +273,7 @@ for build in "mac" "win32" "win-x64" "win-arm64" "linux-i686" "linux-x86_64" "li
if ([[ $build == "win32" ]] || [[ $build == "win-x64" ]] || [[ $build == "win-arm64" ]]) && [[ $BUILD_WIN == 0 ]]; then
continue
fi
if ([[ $build == "linux-i686" ]] || [[ $build == "linux-x86_64" ]] || [[ $build == "linux-arm64" ]]) && [[ $BUILD_LINUX == 0 ]]; then
if ([[ $build == "linux-i686" ]] || [[ $build == "linux-x86_64" ]]) && [[ $BUILD_LINUX == 0 ]]; then
continue
fi
dir="Zotero_$build"

View file

@ -9,9 +9,7 @@
#
# -----------------------------------------------------------------------------
# Preserve a value set by the parent so it survives in the child processes
# spawned by run_parallel_tasks()
QUIET=${QUIET:-0}
QUIET=0
# By default just assume that these tools exist on our path
MAR=${MAR:-mar}
@ -35,11 +33,7 @@ if [ $? -ne 0 ]; then
fi
fi
fi
# Ensure that we're always using the right compression settings.
# -T1 (single-threaded) is required: multi-threaded xz produces non-deterministic
# output, which would break both the reproducibility of MARs and the content-hash
# cache in process_update_task(). See get_cache_tag() for how changes here are
# accounted for in the cache.
# Ensure that we're always using the right compression settings
export XZ_OPT="-T1 -7e"
# -----------------------------------------------------------------------------
@ -99,14 +93,16 @@ make_add_instruction() {
check_for_add_if_not_update() {
add_if_not_file_chk="$1"
if [[ "$(basename "$add_if_not_file_chk")" = "channel-prefs.js" || \
"$add_if_not_file_chk" =~ (^|/)ChannelPrefs\.framework/ || \
"$(basename "$add_if_not_file_chk")" = "update-settings.ini" || \
"$add_if_not_file_chk" =~ (^|/)UpdateSettings\.framework/ ]]; then
## "true"
# XXX unconditional add-if for channel-prefs.js to fix mac signature on old installs (bug 1804303)
if [ "$add_if_not_file_chk" = "Contents/Resources/defaults/pref/channel-prefs.js" ]; then
return 1
fi
if [ `basename $add_if_not_file_chk` = "channel-prefs.js" -o \
`basename $add_if_not_file_chk` = "update-settings.ini" ]; then
## "true" *giggle*
return 0;
fi
## "false"
## 'false'... because this is bash. Oh yay!
return 1;
}
@ -162,10 +158,10 @@ append_remove_instructions() {
if [ -n "$f" ]; then
# Exclude comments
if [ ! $(echo "$f" | grep -c '^#') = 1 ]; then
if [ $(echo "$f" | grep -c '/$') = 1 ]; then
if [ $(echo "$f" | grep -c '\/$') = 1 ]; then
verbose_notice " rmdir \"$f\""
echo "rmdir \"$f\"" >> "$filev3"
elif [ $(echo "$f" | grep -c '/\*$') = 1 ]; then
elif [ $(echo "$f" | grep -c '\/\*$') = 1 ]; then
# Remove the *
f=$(echo "$f" | sed -e 's:\*$::')
verbose_notice " rmrfdir \"$f\""
@ -181,236 +177,6 @@ append_remove_instructions() {
fi
}
# Return the number of parallel jobs to use for diffing/compression, from
# UPDATE_PACKAGING_JOBS or the number of cores
get_parallel_jobs() {
if [ -n "${UPDATE_PACKAGING_JOBS:-}" ]; then
echo "$UPDATE_PACKAGING_JOBS"
return 0
fi
nproc 2>/dev/null && return 0
sysctl -n hw.ncpu 2>/dev/null && return 0
echo 1
}
# Run per-file tasks from a task file in parallel by re-invoking the calling
# script with --run-task for each line. Tasks are ordered largest-file-first so
# that long-running jobs (e.g., diffing xul) start as early as possible.
#
# $1 - task file, with one "<type><TAB><relative path>" task per line
# $2 - directory to measure file sizes against for job ordering
# $3 - script to re-invoke (callers pass "$0" and handle --run-task before
# option parsing by calling process_update_task)
#
# Callers must export the variables needed by process_update_task().
run_parallel_tasks() {
local taskfile="$1"
local basedir="$2"
local script="$3"
local jobs
if [ ! -s "$taskfile" ]; then
return 0
fi
# xargs requires a resolvable command path
script="$(cd "$(dirname "$script")" && pwd)/$(basename "$script")"
# Compute the cache tag once here and export it so the per-file child
# processes inherit it rather than recomputing it for every file
if [ -n "${UPDATE_CACHE_DIR:-}" ] && [ -z "${UPDATE_CACHE_TAG:-}" ]; then
UPDATE_CACHE_TAG="$(get_cache_tag)"
export UPDATE_CACHE_TAG
fi
jobs=$(get_parallel_jobs)
notice "Processing $(wc -l < "$taskfile" | tr -d ' ') files with $jobs parallel jobs"
(cd "$basedir" && du -ak .) > "$taskfile.sizes"
awk -F'\t' 'NR==FNR { sizes[substr($2, 3)] = $1; next } { print sizes[$2] "\t" $0 }' \
"$taskfile.sizes" "$taskfile" \
| sort -rn \
| cut -f 2- \
| tr '\n' '\000' \
| xargs -0 -n 1 -P "$jobs" "$script" --run-task
}
# -----------------------------------------------------------------------------
# Diff and compression cache
#
# Building updates repeats the same expensive work: the same file contents get
# xz-compressed once per architecture, per FROM version, and per release, and
# the same before/after file pair gets binary-diffed again and again. When
# UPDATE_CACHE_DIR is set, we cache the results by content hash and reuse them
# across builds. Two kinds of entry are stored:
#
# full-<tag>-<hash>.xz A single file compressed with xz, keyed
# on the file's contents. Reused whenever
# the same content shows up again, in any
# architecture, version, or release.
#
# patch-<tag>-<oldhash>-<newhash>.xz The compressed binary diff between a
# file's old and new contents, keyed on
# both. Reused whenever the same
# before/after pair recurs (e.g., a file
# unchanged across several FROM versions
# being patched to the same TO).
#
# Both entries are written even when only one is ultimately packaged, so a file
# that loses the patch-vs-full size comparison in one build still seeds the
# cache for the next. <tag> is get_cache_tag(), so a toolchain/settings change
# starts a fresh namespace rather than reusing stale output. Entries are touched
# on use and expire via the time-based cleanup in build_autoupdate.sh.
# -----------------------------------------------------------------------------
# Print a SHA-256 content hash to use as a cache key
hash_file() {
if command -v sha256sum > /dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}
# Print a SHA-256 hash of stdin
hash_stdin() {
if command -v sha256sum > /dev/null 2>&1; then
sha256sum | awk '{print $1}'
else
shasum -a 256 | awk '{print $1}'
fi
}
# Print a short tag identifying the toolchain and options that produce cached
# files, so that entries are invalidated when any of them change. The cache
# keys files by input content only, on the assumption that the same inputs
# always compress/diff to the same output; this tag makes that assumption hold
# across xz/mbsdiff upgrades and XZ_OPT/BCJ_OPTIONS changes. (A change to any
# input invalidates both the full and patch caches, which is more than strictly
# necessary but keeps this simple; a stale build is just a cold rebuild.)
# Stale-tag entries are removed by the time-based cleanup in build_autoupdate.sh.
get_cache_tag() {
local mbsdiff_bin
mbsdiff_bin="$(command -v "$MBSDIFF" 2>/dev/null)"
{
$XZ --version 2>/dev/null | head -1
echo "XZ_OPT=$XZ_OPT"
echo "BCJ_OPTIONS=${BCJ_OPTIONS:-}"
# mbsdiff has no --version, so identify it by its binary contents
if [ -n "$mbsdiff_bin" ]; then
hash_file "$mbsdiff_bin"
else
echo "MBSDIFF=$MBSDIFF"
fi
} | hash_stdin | cut -c 1-16
}
# Atomically store file $2 in the cache as $1
cache_store() {
mkdir -p "$(dirname "$1")"
cp "$2" "$1.tmp.$$" && mv -f "$1.tmp.$$" "$1"
}
# xz-compress $1 to $2, reusing a cached result when one exists (see the cache
# overview above)
compress_full_file() {
local src="$1"
local dest="$2"
local key=""
if [ -n "${UPDATE_CACHE_DIR:-}" ]; then
key="$UPDATE_CACHE_DIR/full-${UPDATE_CACHE_TAG:-}-$(hash_file "$src").xz"
if [ -f "$key" ]; then
# Keep entries that are still in use from expiring
touch -c "$key" 2>/dev/null || true
cp "$key" "$dest"
return 0
fi
fi
$XZ $XZ_OPT --compress $BCJ_OPTIONS --lzma2 --format=xz --check=crc64 --force --stdout "$src" > "$dest"
if [ -n "$key" ]; then
cache_store "$key" "$dest"
fi
}
# Process a single task line from run_parallel_tasks():
#
# full - compress the new file into the work directory
# diff - if the file changed, generate a binary diff and keep the smaller of
# the compressed patch ($f.patch) and the compressed file ($f)
#
# Expects newdir and workdir in the environment, plus olddir for diff tasks.
# The results are assembled into the manifest serially by the caller based on
# which files exist in the work directory. Diffs and compressed files are
# cached when enabled -- see the cache overview above.
process_update_task() {
local task="$1"
local tab=$(printf '\t')
local type="${task%%"$tab"*}"
local f="${task#*"$tab"}"
local oldfile_path newfile_path patch_path patchsize fullsize full_pid patchkey
if [ "$type" = "diff" ]; then
if diff "$olddir/$f" "$newdir/$f" > /dev/null; then
# Unchanged
return 0
fi
mkdir -p "$(dirname "$workdir/$f")"
verbose_notice "diffing \"$f\""
# Compress the full new file in the background while diffing, since both
# are needed to decide which to package
compress_full_file "$newdir/$f" "$workdir/$f" &
full_pid=$!
patchkey=""
if [ -n "${UPDATE_CACHE_DIR:-}" ]; then
patchkey="$UPDATE_CACHE_DIR/patch-${UPDATE_CACHE_TAG:-}-$(hash_file "$olddir/$f")-$(hash_file "$newdir/$f").xz"
fi
if [ -n "$patchkey" ] && [ -f "$patchkey" ]; then
# Keep entries that are still in use from expiring
touch -c "$patchkey" 2>/dev/null || true
cp "$patchkey" "$workdir/$f.patch.xz"
else
# mbsdiff doesn't like POSIX paths on Windows
if [ ${WIN_NATIVE:-0} -eq 1 ]; then
oldfile_path=$(cygpath -m "$olddir/$f")
newfile_path=$(cygpath -m "$newdir/$f")
patch_path=$(cygpath -m "$workdir/$f.patch")
else
oldfile_path="$olddir/$f"
newfile_path="$newdir/$f"
patch_path="$workdir/$f.patch"
fi
$MBSDIFF "$oldfile_path" "$newfile_path" "$patch_path"
$XZ $XZ_OPT --compress --lzma2 --format=xz --check=crc64 --force "$workdir/$f.patch"
if [ -n "$patchkey" ]; then
cache_store "$patchkey" "$workdir/$f.patch.xz"
fi
fi
wait $full_pid
copy_perm "$newdir/$f" "$workdir/$f"
patchsize=$(get_file_size "$workdir/$f.patch.xz")
fullsize=$(get_file_size "$workdir/$f")
if [ $patchsize -lt $fullsize ]; then
mv -f "$workdir/$f.patch.xz" "$workdir/$f.patch"
rm -f "$workdir/$f"
else
rm -f "$workdir/$f.patch.xz"
fi
else
mkdir -p "$(dirname "$workdir/$f")"
compress_full_file "$newdir/$f" "$workdir/$f"
copy_perm "$newdir/$f" "$workdir/$f"
fi
}
# List all files in the current directory, stripping leading "./"
# Pass a variable name and it will be filled as an array.
list_files() {

View file

@ -13,12 +13,6 @@ set -eo pipefail
. $(dirname "$0")/common.sh
# Process a single file task when invoked from run_parallel_tasks()
if [ "${1:-}" = "--run-task" ]; then
process_update_task "$2"
exit 0
fi
# -----------------------------------------------------------------------------
print_usage() {
@ -56,7 +50,7 @@ archive="$1"
targetdir="$2"
# Prevent the workdir from being inside the targetdir so it isn't included in
# the update mar.
if [ $(echo "$targetdir" | grep -c '/$') = 1 ]; then
if [ $(echo "$targetdir" | grep -c '\/$') = 1 ]; then
# Remove the /
targetdir=$(echo "$targetdir" | sed -e 's:\/$::')
fi
@ -94,23 +88,6 @@ notice ""
notice "Adding file add instructions to update manifests"
num_files=${#files[*]}
# Compress the files in parallel via run_parallel_tasks() in
# common.sh, and then assemble the manifest and archive list serially in the
# original order
tmpdir="$(mktemp -d)"
taskfile="$tmpdir/tasks"
TAB="$(printf '\t')"
> "$taskfile"
for ((i=0; $i<$num_files; i=$i+1)); do
echo "full$TAB${files[$i]}" >> "$taskfile"
done
newdir="$targetdir"
export newdir workdir BCJ_OPTIONS QUIET
run_parallel_tasks "$taskfile" "$targetdir" "$0"
rm -rf "$tmpdir"
for ((i=0; $i<$num_files; i=$i+1)); do
f="${files[$i]}"
@ -120,6 +97,11 @@ for ((i=0; $i<$num_files; i=$i+1)); do
make_add_instruction "$f" "$updatemanifestv3"
fi
dir=$(dirname "$f")
mkdir -p "$workdir/$dir"
$XZ $XZ_OPT --compress $BCJ_OPTIONS --lzma2 --format=xz --check=crc64 --force --stdout "$targetdir/$f" > "$workdir/$f"
copy_perm "$targetdir/$f" "$workdir/$f"
targetfiles="$targetfiles \"$f\""
done

View file

@ -13,12 +13,6 @@ set -eo pipefail
. $(dirname "$0")/common.sh
# Process a single file task when invoked from run_parallel_tasks()
if [ "${1:-}" = "--run-task" ]; then
process_update_task "$2"
exit 0
fi
# -----------------------------------------------------------------------------
print_usage() {
@ -87,6 +81,7 @@ if [ $# = 0 ]; then
exit 1
fi
# channel-prefs.js removed for Zotero
requested_forced_updates='Contents/MacOS/firefox'
while getopts "hqf:" flag
@ -118,7 +113,7 @@ olddir="$2"
newdir="$3"
# Prevent the workdir from being inside the targetdir so it isn't included in
# the update mar.
if [ $(echo "$newdir" | grep -c '/$') = 1 ]; then
if [ $(echo "$newdir" | grep -c '\/$') = 1 ]; then
# Remove the /
newdir=$(echo "$newdir" | sed -e 's:\/$::')
fi
@ -170,67 +165,93 @@ num_oldfiles=${#oldfiles[*]}
remove_array=
num_removes=0
# The main file loop is split into three passes so that the
# CPU-heavy work can run in parallel: classify each file into a task list, run
# the diffing/compression tasks in parallel via run_parallel_tasks() in
# common.sh, and then assemble the manifest and archive list serially in the
# original order based on the files the tasks left in the work directory.
#
# (This also drops the unused MBSDIFF_HOOK/funsize path from the upstream
# script -- the diff behavior itself is unchanged and now lives in
# process_update_task() in common.sh.)
tmpdir="$(mktemp -d)"
taskfile="$tmpdir/tasks"
TAB="$(printf '\t')"
> "$taskfile"
for ((i=0; $i<$num_oldfiles; i=$i+1)); do
f="${oldfiles[$i]}"
# If this file exists in the new directory as well, then check if it differs.
if [ -f "$newdir/$f" ]; then
if check_for_add_if_not_update "$f"; then
echo "full$TAB$f" >> "$taskfile"
elif check_for_forced_update "$requested_forced_updates" "$f"; then
echo "full$TAB$f" >> "$taskfile"
else
echo "diff$TAB$f" >> "$taskfile"
fi
fi
done
# Newly added files -- in the new directory but not the old one. (This replaces
# the O(n^2) nested skip loop from the upstream script.)
printf '%s\n' "${oldfiles[@]}" | sort > "$tmpdir/oldlist"
printf '%s\n' "${newfiles[@]}" | sort > "$tmpdir/newlist"
comm -13 "$tmpdir/oldlist" "$tmpdir/newlist" | sed '/^$/d' | sort -r > "$tmpdir/newonly"
while IFS= read -r f; do
echo "full$TAB$f" >> "$taskfile"
done < "$tmpdir/newonly"
export olddir newdir workdir MBSDIFF BCJ_OPTIONS QUIET
run_parallel_tasks "$taskfile" "$newdir" "$0"
# Assemble the manifest and archive list in the original order. A diffed file
# left $f.patch in the work directory if the patch was smaller, $f if the
# compressed file was smaller, and neither if the file was unchanged.
for ((i=0; $i<$num_oldfiles; i=$i+1)); do
f="${oldfiles[$i]}"
if [ -f "$newdir/$f" ]; then
if check_for_add_if_not_update "$f"; then
# The full workdir may not exist yet, so create it if necessary.
mkdir -p `dirname "$workdir/$f"`
$XZ $XZ_OPT --compress $BCJ_OPTIONS --lzma2 --format=xz --check=crc64 --force --stdout "$newdir/$f" > "$workdir/$f"
copy_perm "$newdir/$f" "$workdir/$f"
make_add_if_not_instruction "$f" "$updatemanifestv3"
archivefiles="$archivefiles \"$f\""
elif check_for_forced_update "$requested_forced_updates" "$f"; then
continue 1
fi
if check_for_forced_update "$requested_forced_updates" "$f"; then
# The full workdir may not exist yet, so create it if necessary.
mkdir -p `dirname "$workdir/$f"`
$XZ $XZ_OPT --compress $BCJ_OPTIONS --lzma2 --format=xz --check=crc64 --force --stdout "$newdir/$f" > "$workdir/$f"
copy_perm "$newdir/$f" "$workdir/$f"
make_add_instruction "$f" "$updatemanifestv3" 1
archivefiles="$archivefiles \"$f\""
elif [ -f "$workdir/$f.patch" ]; then
make_patch_instruction "$f" "$updatemanifestv3"
archivefiles="$archivefiles \"$f.patch\""
elif [ -f "$workdir/$f" ]; then
make_add_instruction "$f" "$updatemanifestv3"
archivefiles="$archivefiles \"$f\""
continue 1
fi
if ! diff "$olddir/$f" "$newdir/$f" > /dev/null; then
# Compute both the compressed binary diff and the compressed file, and
# compare the sizes. Then choose the smaller of the two to package.
dir=$(dirname "$workdir/$f")
mkdir -p "$dir"
verbose_notice "diffing \"$f\""
# MBSDIFF_HOOK represents the communication interface with funsize and,
# if enabled, caches the intermediate patches for future use and
# compute avoidance
#
# An example of MBSDIFF_HOOK env variable could look like this:
# export MBSDIFF_HOOK="myscript.sh -A https://funsize/api -c /home/user"
# where myscript.sh has the following usage:
# myscript.sh -A SERVER-URL [-c LOCAL-CACHE-DIR-PATH] [-g] [-u] \
# PATH-FROM-URL PATH-TO-URL PATH-PATCH SERVER-URL
#
# Note: patches are bzipped or xz stashed in funsize to gain more speed
# if service is not enabled then default to old behavior
# Disabled for Zotero
#if [ -z "$MBSDIFF_HOOK" ]; then
if true; then
# mbsdiff doesn't like POSIX paths on Windows
if [ $WIN_NATIVE -eq 1 ]; then
oldfile_path=$(cygpath -m "$olddir/$f")
newfile_path=$(cygpath -m "$newdir/$f")
patch_path=$(cygpath -m "$workdir/$f.patch")
else
oldfile_path="$olddir/$f"
newfile_path="$newdir/$f"
patch_path="$workdir/$f.patch"
fi
$MBSDIFF "$oldfile_path" "$newfile_path" "$patch_path"
$XZ $XZ_OPT --compress --lzma2 --format=xz --check=crc64 --force "$workdir/$f.patch"
else
# if service enabled then check patch existence for retrieval
if $MBSDIFF_HOOK -g "$olddir/$f" "$newdir/$f" "$workdir/$f.patch.xz"; then
verbose_notice "file \"$f\" found in funsize, diffing skipped"
else
# if not found already - compute it and cache it for future use
$MBSDIFF "$olddir/$f" "$newdir/$f" "$workdir/$f.patch"
$XZ $XZ_OPT --compress --lzma2 --format=xz --check=crc64 --force "$workdir/$f.patch"
$MBSDIFF_HOOK -u "$olddir/$f" "$newdir/$f" "$workdir/$f.patch.xz"
fi
fi
$XZ $XZ_OPT --compress $BCJ_OPTIONS --lzma2 --format=xz --check=crc64 --force --stdout "$newdir/$f" > "$workdir/$f"
copy_perm "$newdir/$f" "$workdir/$f"
patchfile="$workdir/$f.patch.xz"
patchsize=$(get_file_size "$patchfile")
fullsize=$(get_file_size "$workdir/$f")
if [ $patchsize -lt $fullsize ]; then
make_patch_instruction "$f" "$updatemanifestv3"
mv -f "$patchfile" "$workdir/$f.patch"
rm -f "$workdir/$f"
archivefiles="$archivefiles \"$f.patch\""
else
make_add_instruction "$f" "$updatemanifestv3"
rm -f "$patchfile"
archivefiles="$archivefiles \"$f\""
fi
fi
else
# remove instructions are added after add / patch instructions for
@ -245,18 +266,33 @@ done
# Newly added files
notice ""
notice "Adding file add instructions to update manifests"
num_newfiles=${#newfiles[*]}
for ((i=0; $i<$num_newfiles; i=$i+1)); do
f="${newfiles[$i]}"
# If we've already tested this file, then skip it
for ((j=0; $j<$num_oldfiles; j=$j+1)); do
if [ "$f" = "${oldfiles[j]}" ]; then
continue 2
fi
done
dir=$(dirname "$workdir/$f")
mkdir -p "$dir"
$XZ $XZ_OPT --compress $BCJ_OPTIONS --lzma2 --format=xz --check=crc64 --force --stdout "$newdir/$f" > "$workdir/$f"
copy_perm "$newdir/$f" "$workdir/$f"
while IFS= read -r f; do
if check_for_add_if_not_update "$f"; then
make_add_if_not_instruction "$f" "$updatemanifestv3"
else
make_add_instruction "$f" "$updatemanifestv3"
fi
archivefiles="$archivefiles \"$f\""
done < "$tmpdir/newonly"
rm -rf "$tmpdir"
archivefiles="$archivefiles \"$f\""
done
notice ""
notice "Adding file remove instructions to update manifests"

View file

@ -0,0 +1,24 @@
application.ini
chrome.manifest
chrome/*
components/*
defaults/*
deleted.txt
dictionaries/*
extensions/*
fonts/EmojiOneMozilla.ttf
gtk2/*
icudt58l.dat
install.rdf
libnssdbm3.chk
libnssdbm3.so
libsoftokn3.chk
pingsender
resource/*
run-zotero.sh
styles.zip
test/*
translators.index
translators.zip
xulrunner/*
zotero.jar

View file

@ -1,4 +1,4 @@
Contents/Frameworks/XUL.framework/
Contents/Frameworks/*
Contents/MacOS/Info.plist
Contents/MacOS/LICENSE
Contents/MacOS/README.xulrunner
@ -38,6 +38,12 @@ Contents/MacOS/updater.app/Contents/MacOS/updater-bin
Contents/MacOS/updates.xml
Contents/MacOS/updates/*
Contents/MacOS/zotero-bin
Contents/PlugIns/ZoteroSafariExtension.appex/Contents/Resources/safari/inject/translate_inject.js
Contents/PlugIns/ZoteroSafariExtension.appex/Contents/Resources/safari/node_modules.js
Contents/PlugIns/ZoteroSafariExtension.appex/Contents/Resources/safari/test/*
Contents/PlugIns/ZoteroSafariExtension.appex/Contents/Resources/safari/translate/cachedTypes.js
Contents/PlugIns/ZoteroSafariExtension.appex/Contents/Resources/safari/translate/resource/README.md
Contents/PlugIns/ZoteroSafariExtension.appex/Contents/Resources/safari/translate/resource/zoteroTypeSchemaData.js
Contents/Resources/CONTRIBUTING.md
Contents/Resources/COPYING
Contents/Resources/README.md

View file

@ -61,6 +61,6 @@ echo "MAR channel name: $channel"
echo "Product version: $version"
echo "Creating new MAR"
find . -type f -print0 | xargs -0 $mar -V $version -H $channel -c "$outfile"
$mar -V $version -H $channel -c "$outfile" `find . -type f | sed 's/^\.\///'`
echo
$mar -T "$outfile"

View file

@ -1,12 +1,12 @@
#!/bin/bash
set -euo pipefail
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
APP_ROOT_DIR="$(dirname "$SCRIPT_DIR")"
. "$APP_ROOT_DIR/config.sh"
if [ $# -lt 2 ]; then
echo "Usage: $0 /path/to/src/firefox x64|arm64|win32" >&2
echo "Usage: $0 /path/to/mozilla-unified x86_64|i686" >&2
exit 1
fi
@ -20,40 +20,20 @@ fi
# Install required Rust version
rustup toolchain install $RUST_VERSION
if [ $arch = "x64" ]; then
rust_target=x86_64
elif [ $arch = "arm64" ]; then
rust_target=aarch64
elif [ $arch = "win32" ]; then
rust_target=i686
if [[ $arch = "x86_64" ]] || [[ $arch == "i686" ]]; then
rustup target add $arch-pc-windows-msvc
else
echo "Unknown architecture $arch" >&2
exit 1
fi
rustup default $RUST_VERSION
rustup target add $rust_target-pc-windows-msvc
cp "$SCRIPT_DIR/mozconfig" "$gecko_path"
if [[ $arch == "arm64" || $arch == "win32" ]]; then
export Z_ARCH=$arch
export RUST_TARGET=$rust_target-pc-windows-msvc
if [ $arch = "i686" ]; then
export Z_ARCH=win32
fi
cd "$gecko_path"
./mach build
./mach package
# Copy the packaged ZIP back to app/win/ for launcher/xul.dll extraction
case $arch in
x64) zip_suffix=win64 ;;
arm64) zip_suffix=win64-aarch64 ;;
win32) zip_suffix=win32 ;;
esac
zip_file=$(ls "$gecko_path"/obj-*/dist/firefox-*.en-US.${zip_suffix}.zip 2>/dev/null | head -1) || true
if [ -z "$zip_file" ] || [ ! -f "$zip_file" ]; then
echo "Could not find packaged zip for $arch in $gecko_path/obj-*/dist/" >&2
exit 1
fi
cp "$zip_file" "$SCRIPT_DIR/"
echo "Copied $(basename "$zip_file") to $SCRIPT_DIR"

View file

@ -20,7 +20,7 @@ ROOT_DIR="$(dirname "$SCRIPT_DIR")"
#mkdir -p nsis
#cd nsis
#for plugin in $plugins; do
# curl -fsSLO $plugin
# wget $plugin
#done
#
#unzip_cmd="unzip -d Plugins -j"
@ -46,12 +46,6 @@ for plugin in $plugins; do
curl https://hg.mozilla.org/mozilla-central/raw-file/052d53200cf8/other-licenses/nsis/Plugins/$plugin.dll > $plugin.dll
done
# From https://nsis.sourceforge.io/NsProcess_plugin
curl https://download.zotero.org/dev/win/NsProcess-1.6.zip > NsProcess.zip
7z e NsProcess.zip "Plugin/nsProcessW.dll"
mv nsProcessW.dll nsProcess.dll
rm NsProcess.zip
echo
echo
echo "Files downloaded to ./Plugins -- move to ${NSIS_DIR}Plugins"

View file

@ -1,84 +0,0 @@
#!/bin/bash
#
# Extract xul.dll from custom Firefox build ZIPs in current folder and create a
# ZIP for each platform with its own SHA256 hash in the filename. The files
# should then be uploaded and their hashes updated in config.sh so that
# fetch_xulrunner will download and use the custom xul.dll.
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
if [ $# -ne 1 ]; then
echo "Usage: $0 <version>"
exit 1
fi
version="$1"
# Input ZIP filenames
files=(
"firefox-$version.en-US.win32.zip"
"firefox-$version.en-US.win64-aarch64.zip"
"firefox-$version.en-US.win64.zip"
)
# Corresponding architecture labels and output names
arches=(
"win32"
"arm64"
"x64"
)
# Platform-specific SHA command
if [[ "$(uname)" = "Darwin" ]]; then
shasum_cmd="shasum -a 256"
else
shasum_cmd="sha256sum"
fi
# Clean up any old artifacts
rm -f "$version"-*.zip
# Process whichever arch ZIPs are present
processed=0
for i in "${!files[@]}"; do
file="${files[$i]}"
arch="${arches[$i]}"
temp_zip="xul-temp-$arch.zip"
if [ ! -f "$file" ]; then
continue
fi
dlls=(xul.dll)
# TEMP: Fix startup error on win32 due to missing export numSamples@RLBoxSoundTouch
if [[ $arch == "win32" ]]; then
dlls+=(lgpllibs.dll)
fi
echo "Extracting ${dlls[*]} from $file"
for dll in "${dlls[@]}"; do
unzip -p "$file" "firefox/$dll" > "$dll"
done
zip -q "$temp_zip" "${dlls[@]}"
# Calculate hash
sum=$($shasum_cmd "$temp_zip" | awk '{print $1}')
# Rename to final output
final_zip="$version-$arch-$sum.zip"
mv "$temp_zip" "$final_zip"
rm -f xul.dll lgpllibs.dll
echo "Created $final_zip"
processed=$((processed + 1))
done
if [ $processed -eq 0 ]; then
echo "No firefox-$version.en-US.*.zip files found in $SCRIPT_DIR" >&2
exit 1
fi

Binary file not shown.

View file

@ -59,7 +59,6 @@ Var InstallType
!insertmacro WordFind
!insertmacro WordReplace
!include nsProcess.nsh
; The following includes are custom.
!include branding.nsi
!include defines.nsi
@ -174,12 +173,10 @@ Function UninstallOld
Push $R1
Push $R2
StrCpy $0 0
StrCpy $5 "HKLM"
SetShellVarContext all
enum_uninst_keys:
EnumRegKey $1 SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall" $0
StrCmp $1 "" switch_root_key_or_continue
EnumRegKey $1 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall" $0
StrCmp $1 "" continue_installation
; $R1 is "Zotero", registry key name contains version and locale e.g. "Zotero 6.0.0 (x86 en-US)" so:
StrLen $3 $R1 ; $3 = length of $R1, e.g. 6 for "Zotero"
StrCpy $2 $1 $3 ; $2 = first $3 characters of $1, i.e. name without version and locale, e.g. "Zotero"
@ -187,18 +184,9 @@ Function UninstallOld
IntOp $0 $0 + 1
Goto enum_uninst_keys ; loop through all keys
switch_root_key_or_continue:
# Previous installation was not found in HKLM, switch to HKCU (using SetShellVarContext) and search again
# $5 is used to ensure this only happens once
StrCmp $5 "HKLM" +1 continue_installation
StrCpy $5 "HKCU"
StrCpy $0 0
SetShellVarContext current
Goto enum_uninst_keys
get_uninst_exe:
ReadRegStr $2 SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString"
ReadRegStr $3 SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\$1" "InstallLocation"
ReadRegStr $2 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString"
ReadRegStr $3 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$1" "InstallLocation"
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \
$R2 \
@ -218,30 +206,28 @@ Function UninstallOld
${GetFileName} $3 $4
StrCpy $5 $4 6
StrCmp $5 "Zotero" +1 continue_installation
RMDir /r "$3\chrome"
RMDir /r "$3\components"
RMDir /r "$3\defaults"
RMDir /r "$3\dictionaries"
RMDir /r "$3\extensions"
RMDir /r "$3\fonts"
RMDir /r "$3\gmp-clearkey"
RMDir /r "$3\uninstall"
RMDir /r "$3\xulrunner"
Delete "$3\*.chk"
Delete "$3\*.dll"
Delete "$3\*.exe"
Delete "$3\Accessible.tlb"
Delete "$3\dependentlibs.list"
Delete "$3\firefox.VisualElementsManifest.xml"
Delete "$3\omni.ja"
Delete "$3\platform.ini"
Delete "$3\precomplete"
Delete "$3\voucher.bin"
RMDir $3
RMDir /r /REBOOTOK "$3\chrome"
RMDir /r /REBOOTOK "$3\components"
RMDir /r /REBOOTOK "$3\defaults"
RMDir /r /REBOOTOK "$3\dictionaries"
RMDir /r /REBOOTOK "$3\extensions"
RMDir /r /REBOOTOK "$3\fonts"
RMDir /r /REBOOTOK "$3\gmp-clearkey"
RMDir /r /REBOOTOK "$3\uninstall"
RMDir /r /REBOOTOK "$3\xulrunner"
Delete /REBOOTOK "$3\*.chk"
Delete /REBOOTOK "$3\*.dll"
Delete /REBOOTOK "$3\*.exe"
Delete /REBOOTOK "$3\Accessible.tlb"
Delete /REBOOTOK "$3\dependentlibs.list"
Delete /REBOOTOK "$3\firefox.VisualElementsManifest.xml"
Delete /REBOOTOK "$3\omni.ja"
Delete /REBOOTOK "$3\platform.ini"
Delete /REBOOTOK "$3\precomplete"
Delete /REBOOTOK "$3\voucher.bin"
RMDir /REBOOTOK $3
continue_installation:
; End uninstallation
SetShellVarContext current
Pop $R2
Pop $R1
FunctionEnd
@ -618,33 +604,29 @@ Function AddQuickLaunchShortcut
FunctionEnd
Function CheckExistingInstall
UserInfo::GetAccountType
pop $0 ; $0 = "Admin" or "User" or "Guest". This is the account type.
; If there is a pending file copy from a previous upgrade don't allow
; installing until after the system has rebooted.
${If} ${FileExists} "$INSTDIR\${FileMainEXE}.moz-upgrade"
StrCmp $0 "Admin" +1 +4
MessageBox MB_YESNO|MB_ICONEXCLAMATION "$(WARN_RESTART_REQUIRED_UPGRADE)" IDNO +2
Reboot
Quit
; Non-admins cannot depend on files being removed on restart.
; Our only option is to remove these files and continue with the installation.
Delete "$INSTDIR\${FileMainEXE}.moz-upgrade"
Delete "$INSTDIR\${FileMainEXE}"
${EndIf}
IfFileExists "$INSTDIR\${FileMainEXE}.moz-upgrade" +1 +5
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION "$(WARN_RESTART_REQUIRED_UPGRADE)" IDNO +3 IDCANCEL +2
Reboot
Quit
RMDir /r $INSTDIR
IfFileExists "$INSTDIR\${FileMainEXE}.moz-upgrade" +1 +3
MessageBox MB_OK|MB_ICONEXCLAMATION "Failed to remove the previous installation. Please delete $INSTDIR and try again."
Quit
; If there is a pending file deletion from a previous uninstall don't allow
; installing until after the system has rebooted.
${If} ${FileExists} "$INSTDIR\${FileMainEXE}.moz-delete"
StrCmp $0 "Admin" +1 +4
MessageBox MB_YESNO|MB_ICONEXCLAMATION "$(WARN_RESTART_REQUIRED_UNINSTALL)" IDNO +2
Reboot
Quit
; Non-admins cannot depend on files being removed on restart.
Delete "$INSTDIR\${FileMainEXE}.moz-delete"
Delete "$INSTDIR\${FileMainEXE}"
${EndIf}
IfFileExists "$INSTDIR\${FileMainEXE}.moz-delete" +1 +5
MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION "$(WARN_RESTART_REQUIRED_UNINSTALL)" IDNO +3 IDCANCEL +2
Reboot
Quit
RMDir /r $INSTDIR
IfFileExists "$INSTDIR\${FileMainEXE}.moz-delete" +1 +3
MessageBox MB_OK|MB_ICONEXCLAMATION "Failed to remove the previous installation. Please delete $INSTDIR and try again."
Quit
${If} ${FileExists} "$INSTDIR\${FileMainEXE}"
; Disable the next, cancel, and back buttons
@ -966,21 +948,6 @@ Function .onInit
${EndIf}
!endif
# Check if Zotero is running. If it is, prompt the user to close Zotero and exit.
# For silent installs, kill Zotero and if successful, continue with the install.
${nsProcess::FindProcess} ${FileMainEXE} $R0
${If} $R0 = 0
IfSilent +3
MessageBox MB_OK|MB_ICONSTOP "Zotero is already running. Please close Zotero before running the installer."
Quit
${nsProcess::KillProcess} ${FileMainEXE} $R0
Sleep 500
${nsProcess::FindProcess} ${FileMainEXE} $R0
${If} $R0 = 0
Quit
${EndIf}
${EndIf}
StrCpy $R1 "Zotero Standalone"
StrCpy $R2 "An older version of Zotero is installed. If you continue, the existing version will be removed.$\n$\nYour Zotero data will not be affected."
Call UninstallOld

View file

@ -1,28 +0,0 @@
!define nsProcess::FindProcess `!insertmacro nsProcess::FindProcess`
!macro nsProcess::FindProcess _FILE _ERR
nsProcess::_FindProcess /NOUNLOAD `${_FILE}`
Pop ${_ERR}
!macroend
!define nsProcess::KillProcess `!insertmacro nsProcess::KillProcess`
!macro nsProcess::KillProcess _FILE _ERR
nsProcess::_KillProcess /NOUNLOAD `${_FILE}`
Pop ${_ERR}
!macroend
!define nsProcess::CloseProcess `!insertmacro nsProcess::CloseProcess`
!macro nsProcess::CloseProcess _FILE _ERR
nsProcess::_CloseProcess /NOUNLOAD `${_FILE}`
Pop ${_ERR}
!macroend
!define nsProcess::Unload `!insertmacro nsProcess::Unload`
!macro nsProcess::Unload
nsProcess::_Unload
!macroend

View file

@ -564,7 +564,7 @@ Function .onInit
; Prevent launching the application when a reboot is required and this
; executable is the main application executable
IfFileExists "$EXEDIR\${FileMainEXE}.moz-upgrade" +1 +4
MessageBox MB_YESNO|MB_ICONEXCLAMATION "$(WARN_RESTART_REQUIRED_UPGRADE)" IDNO +2
MessageBox MB_YESNO|MB_ICONEXCLAMATION "$(WARN_RESTART_REQUIRED_UPGRADE_HELPER)" IDNO +2
Reboot
Quit ; Nothing initialized so no need to call OnEndCommon
@ -590,7 +590,7 @@ Function .onInit
; Prevent all operations (e.g. set as default, postupdate, etc.) when a
; reboot is required and the executable launched is helper.exe
IfFileExists "$INSTDIR\${FileMainEXE}.moz-upgrade" +1 +4
MessageBox MB_YESNO|MB_ICONEXCLAMATION "$(WARN_RESTART_REQUIRED_UPGRADE)" IDNO +2
MessageBox MB_YESNO|MB_ICONEXCLAMATION "$(WARN_RESTART_REQUIRED_UPGRADE_HELPER)" IDNO +2
Reboot
Quit ; Nothing initialized so no need to call OnEndCommon

View file

@ -1,9 +1,9 @@
# Set Z_ARCH=win32 or Z_ARCH=aarch64 on command line
# Set Z_ARCH=win32 on command line to build and package win32
if [ "$Z_ARCH" == "win32" ]; then
ac_add_options --target=i686-pc-windows-msvc
# https://bugzilla.mozilla.org/show_bug.cgi?id=1804548
ac_add_options --without-wasm-sandboxed-libraries
elif [ "$Z_ARCH" == "arm64" ]; then
elif [ "$Z_ARCH" == "aarch64" ]; then
ac_add_options --target=aarch64-pc-windows-msvc
fi
@ -23,17 +23,8 @@ ac_add_options --disable-webrtc
ac_add_options --disable-eme
ac_add_options --enable-official-branding
ac_add_options --disable-maintenance-service
ac_add_options --disable-notification-server
ac_add_options --disable-update-agent
export MOZILLA_OFFICIAL=1
export RELEASE_OR_BETA=1
# We don't need source-repo info embedded, and generating it would require a
# source-repo.h that Firefox only auto-populates for Mercurial, not our git
# checkout, which breaks packaging. Turn it off.
ac_add_options MOZ_INCLUDE_SOURCE_INFO=
MOZ_REQUIRE_SIGNING=
# Build updater without MAR signature verification

118
app/win/mozilla-115.patch Normal file
View file

@ -0,0 +1,118 @@
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,30 @@ 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
+ _putenv_s("MOZ_ALLOW_DOWNGRADE", "1");
+ // Don't create dedicated profile (default-esr)
+ _putenv_s("MOZ_LEGACY_PROFILES", "1");
+
// Allow firefox.exe to launch XULRunner apps via -app <application.ini>
// Note that -app must be the *first* argument.
- const char* appDataFile = getenv("XUL_APP_FILE");
+ UniqueFreePtr<char> 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/browser/app/winlauncher/LauncherProcessWin.cpp b/browser/app/winlauncher/LauncherProcessWin.cpp
--- a/browser/app/winlauncher/LauncherProcessWin.cpp
+++ b/browser/app/winlauncher/LauncherProcessWin.cpp
@@ -162,17 +162,31 @@ static mozilla::LauncherFlags ProcessCmd
mozilla::CheckArgFlag::None) == mozilla::ARG_FOUND ||
mozilla::CheckArg(aArgc, aArgv, "remote-debugging-port", nullptr,
mozilla::CheckArgFlag::None) == mozilla::ARG_FOUND ||
mozilla::EnvHasValue("MOZ_AUTOMATION") ||
mozilla::EnvHasValue("MOZ_HEADLESS")) {
result |= mozilla::LauncherFlags::eWaitForBrowser;
}
- if (mozilla::CheckArg(aArgc, aArgv, "no-deelevate") == mozilla::ARG_FOUND) {
+ // Disable deelevation for Zotero
+ //
+ // If people are running as Administrator, or in some cases running with
+ // UAC disabled, Word runs at integrity level High and deelevation drops
+ // Zotero down to Medium, which causes 'Could not find a running Word
+ // instance' errors.
+ //
+ // Even when not running as administrator, the process switch involved in
+ // deelevation also seems to be causing various security software, such as
+ // Cisco Secure Endpoint, to block Zotero from running:
+ // https://forums.zotero.org/discussion/116762
+ //
+ // Disabling deelevation returns us to the behavior of Zotero 6, Word, and
+ // most other programs
+ if (true || mozilla::CheckArg(aArgc, aArgv, "no-deelevate") == mozilla::ARG_FOUND) {
result |= mozilla::LauncherFlags::eNoDeelevate;
}
if (mozilla::CheckArg(aArgc, aArgv, ATTEMPTING_DEELEVATION_FLAG) ==
mozilla::ARG_FOUND) {
result |= mozilla::LauncherFlags::eDeelevating;
}
diff --git a/xpcom/build/BinaryPath.h b/xpcom/build/BinaryPath.h
--- a/xpcom/build/BinaryPath.h
+++ b/xpcom/build/BinaryPath.h
@@ -283,16 +283,43 @@ class BinaryPath {
if (NS_FAILED(Get(path))) {
return nullptr;
}
UniqueFreePtr<char> result;
result.reset(strdup(path));
return result;
}
+ static UniqueFreePtr<char> GetApplicationIni() {
+ char path[MAXPATHLEN];
+ if (NS_FAILED(Get(path))) {
+ return nullptr;
+ }
+
+ char *c = path + strlen(path);
+ while (c >= path && *c != '\\' && *c != '/') {
+ *c = NULL;
+ c--;
+ }
+
+ if (c < path) {
+ return nullptr;
+ }
+
+ char iniPath[MAXPATHLEN];
+ int n = snprintf(iniPath, MAXPATHLEN, "%s\\app\\application.ini", path);
+ if (n < 0 || n >= MAXPATHLEN) {
+ return nullptr;
+ }
+
+ UniqueFreePtr<char> result;
+ result.reset(strdup(iniPath));
+ return result;
+ }
+
#ifdef MOZILLA_INTERNAL_API
static nsresult GetFile(nsIFile** aResult) {
nsCOMPtr<nsIFile> lf;
# ifdef XP_WIN
wchar_t exePath[MAXPATHLEN];
nsresult rv = GetW(exePath);
# else
char exePath[MAXPATHLEN];

View file

@ -1,179 +0,0 @@
diff --git a/browser/app/nsBrowserApp.cpp b/browser/app/nsBrowserApp.cpp
index 4c5982c056fd..1ad4c9dfd597 100644
--- a/browser/app/nsBrowserApp.cpp
+++ b/browser/app/nsBrowserApp.cpp
@@ -198,9 +198,20 @@ static bool HasFlag(int argc, char* argv[], const char* s) {
constinit Bootstrap::UniquePtr gBootstrap;
static int do_main(int argc, char* argv[], char* envp[]) {
+ // Allow profile downgrade for Zotero
+ _putenv_s("MOZ_ALLOW_DOWNGRADE", "1");
+ // Don't create dedicated profile (default-esr)
+ _putenv_s("MOZ_LEGACY_PROFILES", "1");
+
// Allow firefox.exe to launch XULRunner apps via -app <application.ini>
// Note that -app must be the *first* argument.
- const char* appDataFile = getenv("XUL_APP_FILE");
+ UniqueFreePtr<char> iniPath = BinaryPath::GetApplicationIni();
+ if (!iniPath) {
+ Output("Couldn't find application.ini.\n");
+ return 255;
+
+ }
+ char *appDataFile = iniPath.get();
if ((!appDataFile || !*appDataFile) && (argc > 1 && IsFlag(argv[1], "app"))) {
if (argc == 2) {
Output("Incorrect number of arguments passed to -app");
diff --git a/browser/app/winlauncher/LauncherProcessWin.cpp b/browser/app/winlauncher/LauncherProcessWin.cpp
index 8337336dc894..c55677631c88 100644
--- a/browser/app/winlauncher/LauncherProcessWin.cpp
+++ b/browser/app/winlauncher/LauncherProcessWin.cpp
@@ -250,7 +250,21 @@ static mozilla::LauncherFlags ProcessCmdLine(int& aArgc, wchar_t* aArgv[]) {
result |= mozilla::LauncherFlags::eWaitForBrowser;
}
- if (mozilla::CheckArg(aArgc, aArgv, "no-deelevate") == mozilla::ARG_FOUND) {
+ // Disable deelevation for Zotero
+ //
+ // If people are running as Administrator, or in some cases running with
+ // UAC disabled, Word runs at integrity level High and deelevation drops
+ // Zotero down to Medium, which causes 'Could not find a running Word
+ // instance' errors.
+ //
+ // Even when not running as administrator, the process switch involved in
+ // deelevation also seems to be causing various security software, such as
+ // Cisco Secure Endpoint, to block Zotero from running:
+ // https://forums.zotero.org/discussion/116762
+ //
+ // Disabling deelevation returns us to the behavior of Zotero 6, Word, and
+ // most other programs
+ if (true || mozilla::CheckArg(aArgc, aArgv, "no-deelevate") == mozilla::ARG_FOUND) {
result |= mozilla::LauncherFlags::eNoDeelevate;
}
diff --git a/toolkit/components/remote/WinRemoteMessage.cpp b/toolkit/components/remote/WinRemoteMessage.cpp
index 16a59fa7beda..4aa85c4a7632 100644
--- a/toolkit/components/remote/WinRemoteMessage.cpp
+++ b/toolkit/components/remote/WinRemoteMessage.cpp
@@ -32,6 +32,31 @@ WinRemoteMessageSender::WinRemoteMessageSender(int32_t aArgc,
COPYDATASTRUCT* WinRemoteMessageSender::CopyData() { return &mData; }
+nsresult WinRemoteMessageReceiver::ParseV0(const nsACString& aBuffer) {
+ CommandLineParserWin<char> parser;
+ parser.HandleCommandLine(aBuffer);
+
+ mCommandLine = new nsCommandLine();
+ return mCommandLine->Init(parser.Argc(), parser.Argv(), nullptr,
+ nsICommandLine::STATE_REMOTE_AUTO);
+}
+
+nsresult WinRemoteMessageReceiver::ParseV1(const nsACString& aBuffer) {
+ CommandLineParserWin<char> parser;
+ size_t cch = parser.HandleCommandLine(aBuffer);
+ ++cch; // skip a null char
+
+ nsCOMPtr<nsIFile> workingDir;
+ if (cch < aBuffer.Length()) {
+ (void)NS_NewLocalFile(NS_ConvertUTF8toUTF16(Substring(aBuffer, cch)),
+ getter_AddRefs(workingDir));
+ }
+
+ mCommandLine = new nsCommandLine();
+ return mCommandLine->Init(parser.Argc(), parser.Argv(), workingDir,
+ nsICommandLine::STATE_REMOTE_AUTO);
+}
+
nsresult WinRemoteMessageReceiver::ParseV2(const nsAString& aBuffer) {
CommandLineParserWin<char16_t> parser;
size_t cch = parser.HandleCommandLine(aBuffer);
@@ -110,6 +135,12 @@ nsresult WinRemoteMessageReceiver::ParseV3(const nsACString& aBuffer) {
nsresult WinRemoteMessageReceiver::Parse(const COPYDATASTRUCT* aMessageData) {
switch (static_cast<WinRemoteMessageVersion>(aMessageData->dwData)) {
+ case WinRemoteMessageVersion::CommandLineOnly:
+ return ParseV0(nsDependentCSubstring(
+ reinterpret_cast<char*>(aMessageData->lpData), aMessageData->cbData));
+ case WinRemoteMessageVersion::CommandLineAndWorkingDir:
+ return ParseV1(nsDependentCSubstring(
+ reinterpret_cast<char*>(aMessageData->lpData), aMessageData->cbData));
case WinRemoteMessageVersion::CommandLineAndWorkingDirInUtf16:
return ParseV2(
nsDependentSubstring(reinterpret_cast<wchar_t*>(aMessageData->lpData),
diff --git a/toolkit/components/remote/WinRemoteMessage.h b/toolkit/components/remote/WinRemoteMessage.h
index b62fb73fa13d..ee6612f55f4b 100644
--- a/toolkit/components/remote/WinRemoteMessage.h
+++ b/toolkit/components/remote/WinRemoteMessage.h
@@ -28,10 +28,10 @@
// some sort, as v3 does, to reduce the chances of variants of bug 1847458.
enum class WinRemoteMessageVersion : uint32_t {
// "<CommandLine>\0" in utf8
- /* CommandLineOnly = 0, */
+ CommandLineOnly = 0,
// "<CommandLine>\0<WorkingDir>\0" in utf8
- /* CommandLineAndWorkingDir = 1, */
+ CommandLineAndWorkingDir = 1,
// L"<CommandLine>\0<WorkingDir>\0" in utf16, used by ESR 128
CommandLineAndWorkingDirInUtf16 = 2,
@@ -61,6 +61,8 @@ class WinRemoteMessageSender final {
class WinRemoteMessageReceiver final {
nsCOMPtr<nsICommandLineRunner> mCommandLine;
+ nsresult ParseV0(const nsACString& aBuffer);
+ nsresult ParseV1(const nsACString& aBuffer);
nsresult ParseV2(const nsAString& aBuffer);
nsresult ParseV3(const nsACString& aBuffer);
diff --git a/toolkit/mozapps/update/common/commonupdatedir.cpp b/toolkit/mozapps/update/common/commonupdatedir.cpp
index 0ba9fcef9417..ed286476fec3 100644
--- a/toolkit/mozapps/update/common/commonupdatedir.cpp
+++ b/toolkit/mozapps/update/common/commonupdatedir.cpp
@@ -43,7 +43,7 @@
// this problem in the future, we are including a UUID in the root update
// directory name to attempt to ensure that it will be created by this code and
// won't already exist with the wrong permissions.
-# define ROOT_UPDATE_DIR_NAME "Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38"
+# define ROOT_UPDATE_DIR_NAME "Zotero"
// This describes the directory between the "Mozilla" directory and the install
// path hash (i.e. C:\ProgramData\Mozilla\<UPDATE_PATH_MID_DIR_NAME>\<hash>)
# define UPDATE_PATH_MID_DIR_NAME "updates"
diff --git a/xpcom/build/BinaryPath.h b/xpcom/build/BinaryPath.h
index 2c8d78f02440..f55d3db32469 100644
--- a/xpcom/build/BinaryPath.h
+++ b/xpcom/build/BinaryPath.h
@@ -286,6 +286,33 @@ class BinaryPath {
return result;
}
+ static UniqueFreePtr<char> GetApplicationIni() {
+ char path[MAXPATHLEN];
+ if (NS_FAILED(Get(path))) {
+ return nullptr;
+ }
+
+ char *c = path + strlen(path);
+ while (c >= path && *c != '\\' && *c != '/') {
+ *c = NULL;
+ c--;
+ }
+
+ if (c < path) {
+ return nullptr;
+ }
+
+ char iniPath[MAXPATHLEN];
+ int n = snprintf(iniPath, MAXPATHLEN, "%s\\app\\application.ini", path);
+ if (n < 0 || n >= MAXPATHLEN) {
+ return nullptr;
+ }
+
+ UniqueFreePtr<char> result;
+ result.reset(strdup(iniPath));
+ return result;
+ }
+
#ifdef MOZILLA_INTERNAL_API
static nsresult GetFile(nsIFile** aResult) {
nsCOMPtr<nsIFile> lf;

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f1cb0ea50ccb7a609358131ae2a06e040761f012cc196ff0c436f4939895b156
size 876988
oid sha256:aa342dc2f3cb260505154fc823710955026f882ee8153bda8f4e592912c957de
size 363688

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94ac6f2b12672e5dfe166287fce4d7bf357a77fac231b6c2818ad39d6a9d3f15
size 614252
oid sha256:b676ce06f149e296b294cfea72003797eb53f874bcf3786e496191f38e5688e9
size 499708

View file

@ -7,6 +7,7 @@ content zotero-platform chrome/content/zotero-platform/unix/ os=FreeBSD
content zotero-platform chrome/content/zotero-platform/unix/ os=OpenBSD
content zotero-platform-version chrome/content/zotero-platform/default-version/
content zotero-platform-version chrome/content/zotero-platform/mac-big-sur/ os=Darwin osversion>=11
resource zotero resource/

View file

@ -23,6 +23,7 @@
***** END LICENSE BLOCK *****
*/
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
Services.scriptloader.loadSubScript("chrome://zotero/content/include.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/customElements.js", this);

View file

@ -13,17 +13,6 @@
padding: 0;
overflow: hidden;
}
.monaco-editor .empty-editor-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 0 64px;
color: #ffffff8c; /* dark fill-secondary */
font-size: 12px;
line-height: 1.5;
pointer-events: none;
}
</style>
</head>
<body>
@ -34,7 +23,6 @@
<script>
/**
* @param {Object} [params = {}]
* @param {string} [params.emptyOverlayMessage] Optional content for the empty overlay
* @return {Promise<void>}
*/
function loadMonaco(params = {}) {
@ -59,6 +47,7 @@
globalEditor = monaco;
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES6,
allowNonTsExtensions: true // needed for peeking definitions from in-memory models to work
});
@ -76,30 +65,6 @@
editor.getModel().setEOL(monaco.editor.EndOfLineSequence.LF);
// Display overlay message when editor is empty, if specified
if (params.emptyOverlayMessage) {
let overlayNode = document.createElement('div');
overlayNode.className = 'empty-editor-overlay';
overlayNode.innerText = params.emptyOverlayMessage;
let overlayWidget = {
getId: () => `empty-editor-overlay`,
getDomNode: () => overlayNode,
getPosition: () => null
};
editor.addOverlayWidget(overlayWidget);
function update() {
let overlayShown = editor.getModel().getValue() === "";
overlayNode.style.display = overlayShown ? '' : 'none';
}
overlayNode.addEventListener('click', () => editor.focus());
editor.onDidChangeModelContent(update)
update();
}
window.onresize = function () {
editor.layout();
};

File diff suppressed because it is too large Load diff

View file

@ -43,7 +43,6 @@
title="Scaffold"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
windowtype="zotero:scaffold"
style="display: flex;"
drawintitlebar-platforms="win, linux">
@ -52,15 +51,18 @@
<html:link rel="localization" href="browser/browserSets.ftl"/>
<html:link rel="localization" href="toolkit/global/textActions.ftl"/>
<html:link rel="localization" href="zotero.ftl" />
<html:link rel="localization" href="scaffold.ftl" />
</linkset>
<script>
var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
Services.scriptloader.loadSubScript("chrome://zotero/content/include.js", this);
Services.scriptloader.loadSubScript("resource://zotero/require.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/titlebar.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/platformKeys.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/editMenuOverlay.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/xpcom/translate/testTranslators/translatorTester.js", this);
Services.scriptloader.loadSubScript("chrome://scaffold/content/translators.js", this);
Services.scriptloader.loadSubScript("chrome://scaffold/content/scaffold.js", this);
@ -73,8 +75,6 @@
// Custom elements
Services.scriptloader.loadSubScript("chrome://zotero/content/customElements.js", this);
Services.scriptloader.loadSubScript("chrome://scaffold/content/scaffoldItemPreview.js", this);
Services.scriptloader.loadSubScript("chrome://scaffold/content/scaffoldItemPreviews.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/menuAccessKey.js", this);
</script>
@ -96,7 +96,7 @@
<command id="cmd_increase_font_size" oncommand="Scaffold.increaseFontSize()"/>
<command id="cmd_decrease_font_size" oncommand="Scaffold.decreaseFontSize()"/>
<command id="cmd_restore_font_size" oncommand="Scaffold.setFontSize(13)"/>
<command id="cmd_restore_font_size" oncommand="Scaffold.setFontSize(11)"/>
<commandset id="editMenuCommands"/>
</commandset>
@ -138,19 +138,15 @@
</keyset>
<popupset>
<menupopup id="testing-context-menu" onpopupshowing="Scaffold.handleTestingContextMenuShowing()">
<menupopup id="testing-context-menu">
<menuitem label="&scaffold.testing.copyToClipboard;" tooltiptext="Copy the URL or data for the current test to the clipboard" oncommand="Scaffold.copyToClipboard()"/>
<menuitem id="testing-editImport" label="&scaffold.testing.edit;" tooltiptext="Edit the input data for the current test" oncommand="Scaffold.editImportFromTest()"/>
<menu id="testing-openURL" label="&scaffold.testing.openUrl;">
<menuitem id="testing_editImport" label="&scaffold.testing.edit;" tooltiptext="Edit the input data for the current test" oncommand="Scaffold.editImportFromTest()"/>
<menu id="testing_openURL" label="&scaffold.testing.openUrl;">
<menupopup>
<menuitem label="&scaffold.testing.openUrl.internally;" tooltiptext="Open the URL for the current test in the Scaffold browser" oncommand="Scaffold.openURL(false)"/>
<menuitem label="&scaffold.testing.openUrl.externally;" tooltiptext="Open the URL for the current test in your default browser" oncommand="Scaffold.openURL(true)"/>
</menupopup>
</menu>
<menuseparator/>
<menuitem id="testing-delete" label="Delete" oncommand="Scaffold.deleteSelectedTests()"/>
<menuitem id="testing-run" label="Run" oncommand="Scaffold.runSelectedTests()"/>
<menuitem id="testing-applyUpdates" label="&scaffold.testing.applyUpdates;" oncommand="Scaffold.updateSelectedTests()"/>
</menupopup>
</popupset>
@ -293,16 +289,11 @@
</keyset>
<!-- nsMenuBarX hides these and uses them to build the Application menu. -->
<!-- macOS 13 changed "Preferences" to "Settings" so one of these gets picked -->
<menupopup id="menu_FilePopup">
<menuitem id="menu_preferences"
label="&preferencesCmdMac.label;"
key="key_preferencesCmdMac"
oncommand="Zotero.Utilities.Internal.openPreferences()"/>
<menuitem id="menu_settings"
data-l10n-id="menu-application-settings"
key="key_preferencesCmdMac"
oncommand="Zotero.Utilities.Internal.openPreferences()"/>
<menuitem id="menu_mac_services"
label="&servicesMenuMac.label;"/>
<menuitem id="menu_mac_hide_app"
@ -362,25 +353,21 @@
titlebar-btn="min"
oncommand="window.minimize();"
data-l10n-id="browser-window-minimize-button"
aria-hidden="true"
/>
<toolbarbutton class="titlebar-button titlebar-max"
titlebar-btn="max"
oncommand="window.maximize();"
data-l10n-id="browser-window-maximize-button"
aria-hidden="true"
/>
<toolbarbutton class="titlebar-button titlebar-restore"
titlebar-btn="max"
oncommand="window.fullScreen ? BrowserFullScreen() : window.restore();"
data-l10n-id="browser-window-restore-down-button"
aria-hidden="true"
/>
<toolbarbutton class="titlebar-button titlebar-close"
titlebar-btn="close"
oncommand="window.close();"
data-l10n-id="browser-window-close-button"
aria-hidden="true"
/>
</hbox>
</vbox>
@ -476,7 +463,7 @@
</tabpanel>
<tabpanel flex="1" id="tabpanel-code">
<vbox flex="1">
<iframe src="monaco/monaco.html" id="editor-code" focus-on-tab-select="true" flex="1" onmousedown="this.focus()"/>
<iframe src="monaco/monaco.html" id="editor-code" flex="1" onmousedown="this.focus()"/>
</vbox>
</tabpanel>
<tabpanel flex="1" id="tabpanel-tests">
@ -487,28 +474,32 @@
</keyset>
<listheader>
<treecol label="&scaffold.testing.input.label;" class="col-input"/>
<treecol label="&scaffold.testing.status.label;" class="col-status"/>
<treecol label="&scaffold.testing.defer.label;" class="col-defer"/>
<treecol label="&scaffold.testing.input.label;" flex="1"/>
<treecol label="&scaffold.testing.status.label;" width="150"/>
<treecol label="&scaffold.testing.defer.label;"/>
</listheader>
<richlistbox
id="testing-listbox"
observes="validate-tests"
flex="1"
seltype="multiple"
onselect="Scaffold.handleTestSelect(event)"
context="testing-context-menu"
focus-on-tab-select="true"
onselect="Scaffold.handleTestingListboxSelect()"
ondblclick="Scaffold.handleTestingListboxDblClick()"
/>
</vbox>
<hbox pack="end">
<checkbox
id="checkbox-remember-cookies"
label="&scaffold.testing.rememberCookies;"
native="true"
checked="true"
/>
<hbox id="testing-buttons">
<button observes="validate-tests" label="&scaffold.testing.delete;" tooltiptext="Delete the selected tests" oncommand="Scaffold.deleteSelectedTests()"/>
<button observes="validate-tests" label="&scaffold.testing.run;" tooltiptext="Run the selected tests" oncommand="Scaffold.runSelectedTests()"/>
<button observes="validate-tests" label="&scaffold.testing.update;" tooltiptext="Run the selected tests and update the test definitions with the latest data" oncommand="Scaffold.updateSelectedTests()"/>
<hbox flex="1" pack="end">
<checkbox
id="checkbox-remember-cookies"
label="&scaffold.testing.rememberCookies;"
native="true"
checked="true"
/>
</hbox>
</hbox>
<iframe src="monaco/monaco.html" id="editor-tests" onmousedown="this.focus()"/>
</vbox>
@ -518,9 +509,7 @@
<hbox id="browser-url-container" align="center">
<label control="browser-url" value="&scaffold.tabUrl.label;"/>
<html:input id="browser-url" style="flex: 1;"/>
<button id="load-url-button" label="→" tooltiptext="Load URL" oncommand="Scaffold.loadURLInBrowser()"/>
<image id="browser-loading" class="zotero-spinner-16" />
<button id="create-test-button" observes="validate-tests" label="&scaffold.testing.create.web;" tooltiptext="Create a new test from the current page" oncommand="Scaffold.saveTestFromCurrent('web')"/>
<button observes="validate-tests" label="&scaffold.testing.create.web;" tooltiptext="Create a new test from the current page" oncommand="Scaffold.saveTestFromCurrent('web')"/>
</hbox>
<browser
@ -539,15 +528,14 @@
<button observes="validate-tests" label="&scaffold.testing.create.import;" tooltiptext="Create a new test from the current import data" oncommand="Scaffold.saveTestFromCurrent('import')" />
<button observes="validate-tests" label="&scaffold.testing.create.search;" tooltiptext="Create a new test from the current search data" oncommand="Scaffold.saveTestFromCurrent('search')" />
</hbox>
<iframe src="monaco/monaco.html" id="editor-import" flex="1" focus-on-tab-select="true" onmousedown="this.focus()"/>
<iframe src="monaco/monaco.html" id="editor-import" flex="1" onmousedown="this.focus()"/>
</vbox>
</tabpanel>
</tabpanels>
</tabbox>
<splitter id="pane-splitter" resizeafter="farthest" />
<vbox id="right-pane">
<html:textarea id="output" readonly="true" data-l10n-id="scaffold-translator-output"/>
<scaffold-item-previews id="item-previews" hidden="true"/>
<html:textarea id="output" readonly="true"/>
</vbox>
</hbox>

View file

@ -1,219 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2025 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero 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.
Zotero 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 Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
{
let { diff } = ChromeUtils.importESModule('chrome://zotero/content/xpcom/translate/testTranslators/diff.mjs');
class ScaffoldItemPreview extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div class="diff" readonly="true"/>
<info-box/>
<abstract-box/>
<html:div class="attachments-preview"/>
<html:div class="notes-preview"/>
<tags-box/>
`);
_preItem = null;
_postItem = null;
_cachedDiff = null;
get itemPair() {
return [this._preItem, this._postItem];
}
set itemPair([preItem, postItem]) {
this._preItem = this._normalizeItem(preItem);
if (!postItem) {
// Default to cleaned version of preItem
let zoteroPreItem = new Zotero.Item();
zoteroPreItem.libraryID = Zotero.Libraries.userLibraryID;
zoteroPreItem.fromJSON(this._preItem);
postItem = Zotero.Utilities.Internal.itemToExportFormat(zoteroPreItem, {
legacy: true,
skipBaseFields: true
});
delete postItem.relations;
delete postItem.collections;
delete postItem.uniqueFields;
delete postItem.uri;
for (let [key, value] of Object.entries(postItem)) {
if (value === null) {
delete postItem[key];
}
}
// Because we're just diffing the result of cleaning, differences
// in these arrays are mostly meaningless
postItem.attachments = structuredClone(preItem.attachments);
postItem.notes = structuredClone(preItem.notes);
postItem.tags = structuredClone(preItem.tags);
}
this._postItem = this._normalizeItem(postItem);
this._cachedDiff = null;
this.render();
}
get diff() {
if (!this._cachedDiff) {
if (!this._preItem || !this._postItem) {
return null;
}
let diffString = diff(this._preItem, this._postItem)
.split('\n')
.filter(line => (
// Remove lines without differences
!/^\s*"\w+": (".*"|\d+|true|false)$/.test(line)
// And accessDate diff lines - they just show a meaningless format change
&& !/^\s*[-+]\s*"accessDate": "/.test(line)
))
.join('\n');
// Remove now-empty object/array literals
let emptyLiteralRe = /(\n|^)\s*("\w+": )?(\{\s*}|\[\s*])/g;
while (emptyLiteralRe.test(diffString)) {
diffString = diffString.replace(emptyLiteralRe, '');
}
this._cachedDiff = diffString;
}
return this._cachedDiff;
}
get diffStats() {
let added = 0;
let removed = 0;
for (let diffLine of this.diff.split('\n')) {
if (diffLine.startsWith('+')) {
added++;
}
else if (diffLine.startsWith('-')) {
removed++;
}
}
return { added, removed };
}
init() {
this.render();
}
render() {
if (!this.initialized) return;
let postItem = this._postItem;
let zoteroPostItem = new Zotero.Item();
zoteroPostItem.libraryID = Zotero.Libraries.userLibraryID;
if (Object.entries(postItem).length) {
zoteroPostItem.fromJSON(postItem);
}
let [diffBox, infoBox, abstractBox, attachmentsPreview, notesPreview, tagsBox] = this.children;
for (let box of [infoBox, abstractBox, tagsBox]) {
box.mode = 'view';
box.editable = false;
box.item = zoteroPostItem;
box._forceRenderAll();
}
attachmentsPreview.replaceChildren(
...(postItem.attachments ?? []).map((jsonAttachment) => {
let zoteroAttachment = new Zotero.Item('attachment');
let displayTitle = jsonAttachment.title;
let urlOrPath = jsonAttachment.url || jsonAttachment.path;
if (urlOrPath) {
displayTitle += ` (${urlOrPath})`;
}
zoteroAttachment.setField('title', displayTitle);
zoteroAttachment.attachmentContentType = jsonAttachment.mimeType;
if (jsonAttachment.snapshot === false && jsonAttachment.mimeType === 'text/html') {
zoteroAttachment.attachmentLinkMode = Zotero.Attachments.LINK_MODE_LINKED_URL;
}
else {
zoteroAttachment.attachmentLinkMode = Zotero.Attachments.LINK_MODE_IMPORTED_URL;
}
let row = document.createXULElement('attachment-row');
row.attachment = zoteroAttachment;
row._handleAttachmentClick = () => {};
return row;
})
);
notesPreview.replaceChildren(
...(postItem.notes ?? []).map((jsonNote) => {
let row = document.createXULElement('note-row');
row.note = {
title: '',
body: Zotero.Utilities.cleanTags(jsonNote.note),
};
return row;
})
);
for (let diffLine of this.diff.split('\n')) {
let lineContainer = document.createElement('div');
lineContainer.textContent = diffLine;
if (diffLine.startsWith('+')) {
lineContainer.classList.add('added');
}
else if (diffLine.startsWith('-')) {
lineContainer.classList.add('removed');
}
diffBox.append(lineContainer);
}
}
_normalizeItem(jsonItem) {
jsonItem = JSON.parse(JSON.stringify(jsonItem));
if (!Object.entries(jsonItem).length) {
return jsonItem;
}
// Delete ID (meaningless)
delete jsonItem.id;
// _itemDone() sets 'title' in addition to the item type's mapped
// title field for some reason. Delete it so it doesn't show up in the diff.
let titleField = Zotero.ItemFields.getName(
Zotero.ItemFields.getFieldIDFromTypeAndBase(jsonItem.itemType, 'title')
);
if (titleField !== 'title' && jsonItem[titleField]) {
delete jsonItem.title;
}
if (jsonItem.accessDate === 'CURRENT_TIMESTAMP') {
jsonItem.accessDate = Zotero.Date.dateToISO(new Date());
}
return jsonItem;
}
}
customElements.define('scaffold-item-preview', ScaffoldItemPreview);
}

View file

@ -1,146 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2025 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero 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.
Zotero 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 Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
{
class ScaffoldItemPreviews extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div class="switcher">
<button label="&lt;" class="previous"/>
<html:div><html:span class="current"/>/<html:span class="max"/></html:div>
<button label="&gt;" class="next"/>
</html:div>
<deck/>
`);
_previews = [];
_deck;
_switcher;
init() {
this._deck = this.querySelector('deck');
this._switcher = this.querySelector('.switcher');
this.querySelector('.previous').addEventListener('command', () => {
this._deck.selectedIndex--;
this._renderPreviews();
});
this.querySelector('.next').addEventListener('command', () => {
this._deck.selectedIndex++;
this._renderPreviews();
});
}
/**
* @param {any} preItem JSON item
* @param {any} [postItem] JSON item (defaults to cleaned version of preItem)
* @returns {ScaffoldItemPreview}
*/
createPreviewForItemPair(preItem, postItem) {
let preview = document.createXULElement('scaffold-item-preview');
preview.itemPair = [preItem, postItem];
return preview;
}
clearPreviews() {
this._previews = [];
this._deck.selectedIndex = 0;
this._deck.replaceChildren();
this._updateVisibility();
}
/**
* @param {ScaffoldItemPreview[]} previews
*/
setPreviews(previews) {
this._previews = previews;
this._deck.selectedIndex = 0;
this._renderPreviews();
this._updateVisibility();
}
/**
* @param {ScaffoldItemPreview} preview
*/
addPreview(preview) {
this._previews.push(preview);
this._renderPreviews();
this._updateVisibility();
}
/**
* @param {any} preItem JSON item
* @param {any} [postItem] JSON item (defaults to cleaned version of preItem)
* @returns {ScaffoldItemPreview}
*/
addItemPair(preItem, postItem) {
let preview = this.createPreviewForItemPair(preItem, postItem);
this.addPreview(preview);
return preview;
}
_updateVisibility() {
if (this._deck.childElementCount) {
if (this.hidden) {
let splitterPane = this.closest('splitter + *');
if (splitterPane) {
// If the pane hasn't been resized, it won't have a fixed width,
// so it'll grow when wrapping text is added. Fix its width now.
splitterPane.style.width = splitterPane.getBoundingClientRect().width + 'px';
}
this.hidden = false;
}
}
else {
this.hidden = true;
}
}
_renderPreviews() {
this._deck.replaceChildren(
...this._previews.map((preview, i) => {
if (i === this._deck.selectedIndex) {
return preview;
}
return document.createXULElement('hbox');
})
);
if (this._deck.children.length > 1) {
this._switcher.hidden = false;
this._switcher.querySelector('.current').textContent = this._deck.selectedIndex + 1;
this._switcher.querySelector('.max').textContent = this._deck.children.length;
}
else {
this._switcher.hidden = true;
}
}
}
customElements.define('scaffold-item-previews', ScaffoldItemPreviews);
}

View file

@ -1,77 +0,0 @@
import { AbstractWebTranslationEnvironment } from 'chrome://zotero/content/xpcom/translate/testTranslators/translatorTester.mjs';
export class ZoteroWebTranslationEnvironment extends AbstractWebTranslationEnvironment {
/**
* @param {string} url
* @param {TranslatorTester} tester
* @returns {Promise<HiddenBrowser>}
*/
async fetchPage(url, { tester }) {
const { HiddenBrowser } = ChromeUtils.importESModule('chrome://zotero/content/HiddenBrowser.mjs');
let browser = new HiddenBrowser({
docShell: { allowMetaRedirects: true },
userContextId: tester.cookieSandbox,
});
await browser.load(url, { requireSuccessfulStatus: true });
return browser;
}
/**
* @param {HiddenBrowser} browser
* @param {TranslatorTester} tester
* @returns {Promise<false>} False for now, because we aren't sure that
* detection would work without the defer delay.
*/
async waitForLoad(browser, { tester }) {
return false;
}
/**
* @param {HiddenBrowser} browser
* @param {TranslatorTester} tester
* @param {Record<string, Function>} handlers
* @param {AbortSignal} signal
* @returns {Promise<{
* detectedItemType?: string;
* items?: Zotero.Item[];
* reason?: string;
* }>}
*/
async runTranslation(browser, { tester, handlers, signal }) {
const { RemoteTranslate } = ChromeUtils.importESModule('chrome://zotero/content/RemoteTranslate.mjs');
let translate = new RemoteTranslate({ disableErrorReporting: true });
try {
await translate.setBrowser(browser);
await translate.setTranslatorProvider(tester.translatorProvider);
translate.setTranslator(tester.translator);
for (let [type, fn] of Object.entries(handlers)) {
translate.setHandler(type, fn);
}
let detectedTranslators = await translate.detect();
if (!detectedTranslators.length) {
return { items: null, reason: 'Detection failed' };
}
let detectedItemType = detectedTranslators[0].itemType;
let items = await translate.translate({ libraryID: false });
return { detectedItemType, items };
}
catch (e) {
return { items: null, reason: 'Translation failed: ' + e };
}
finally {
translate.dispose();
}
}
/**
* @param {HiddenBrowser} browser
*/
destroy(browser) {
browser.destroy();
}
}

View file

@ -0,0 +1,4 @@
/* Quick Format dialog, which is based on window corners, which are different on Big Sur */
#quick-format-iframe {
margin-top: 2px;
}

View file

@ -9,6 +9,11 @@ window.citation-dialog {
padding: 0;
}
.citation-dialog.progress-bar .citation-dialog.deck {
height: 37px;
}
.citation-dialog.entry {
background: -moz-linear-gradient(-90deg, rgb(243,123,119) 0, rgb(180,47,38) 50%, rgb(156,36,27) 50%);

View file

@ -0,0 +1,26 @@
/* 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/. */
window[windowtype="zotero:pref"] {
padding: 0;
font: -moz-dialog !important;
-moz-appearance: dialog;
color: -moz-DialogText;
}
groupbox > label > h2, groupbox > * > label > h2 {
border-bottom: #b5b5b5 1px solid;
}
groupbox > label > h2, groupbox > * > label > h2, caption {
padding-bottom: 0.2em;
font: caption;
}
.help-button {
appearance: auto;
-moz-default-appearance: -moz-mac-help-button;
min-width: 0;
}

View file

@ -10,11 +10,6 @@ tab {
border-right: 1px solid hsla(0, 0%, 0%, 0.2);
border-left: 1px solid hsla(0, 0%, 0%, 0.2);
}
tab:where([visuallyselected="true"]) {
background: Background;
}
/* Fixes misc Ubuntu 16.10 rendering issue fixes. */
#zotero-prefs menulist dropmarker{
width: 30px;
@ -22,3 +17,13 @@ tab:where([visuallyselected="true"]) {
#zotero-prefs .numberbox-input-box{
-moz-appearance: textfield;
}
#zotero-tags-splitter:not([state=collapsed]) {
min-height: 5px;
}
#zotero-context-splitter-stacked {
-moz-appearance: none;
background-color: #ececec;
border-top: 1px solid hsla(0, 0%, 0%, 0.2);
}

View file

@ -1,4 +1,5 @@
window.citation-dialog {
background: transparent;
padding: 0;
-moz-appearance: none;
}
@ -7,8 +8,16 @@ window.citation-dialog {
.citation-dialog.entry {
background: -moz-linear-gradient(-90deg, rgb(243,123,119) 0, rgb(180,47,38) 50%, rgb(156,36,27) 50%);
padding: 10px;
/* mouse events on win don't fire right at the edge of the window. The dialog has a gap
before the window edges to ensure the window can be dragged from any point of the dialog */
margin:10px;
}
.note-dialog .citation-dialog.entry {
background: -moz-linear-gradient(-90deg, rgb(249, 231, 179) 0, rgb(228, 193, 94) 50%, rgb(221, 184, 81) 50%);
}
.citation-dialog.entry:not([square="true"]) {
-moz-border-radius: 15px;
border-radius: 15px;
}

View file

@ -17,6 +17,14 @@ tree {
padding-left: 0.05em;
}
.zotero-editpane-item-box > scrollbox, .zotero-view-item > tabpanel > vbox,
#zotero-editpane-tags > scrollbox, .zotero-editpane-related {
padding-top: 5px;
}
#zotero-editpane-tags > scrollbox {
padding-left: 5px;
}
/*
Default scrollbar displays up/down arrows even

View file

@ -23,11 +23,15 @@
***** END LICENSE BLOCK *****
*/
ChromeUtils.defineESModuleGetters(globalThis, {
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.defineESModuleGetters(this, {
Zotero: "chrome://zotero/content/zotero.mjs",
});
export class BlockingObserver {
var EXPORTED_SYMBOLS = ["BlockingObserver"];
class BlockingObserver {
shouldBlock;
_observerAdded = false;

View file

@ -23,7 +23,9 @@
***** END LICENSE BLOCK *****
*/
ChromeUtils.defineESModuleGetters(globalThis, {
var EXPORTED_SYMBOLS = ["EPUB"];
ChromeUtils.defineESModuleGetters(this, {
Zotero: "chrome://zotero/content/zotero.mjs",
});
@ -36,7 +38,7 @@ const ZipReader = Components.Constructor(
const DC_NS = 'http://purl.org/dc/elements/1.1/';
const OPF_NS = 'http://www.idpf.org/2007/opf';
export class EPUB {
class EPUB {
_zipReader;
_contentOPF = null;

View file

@ -24,27 +24,30 @@
*/
import { BlockingObserver } from "chrome://zotero/content/BlockingObserver.mjs";
import "chrome://zotero/content/actors/ActorManager.mjs";
var EXPORTED_SYMBOLS = ["HiddenBrowser"];
/* global HiddenFrame, E10SUtils */
ChromeUtils.defineESModuleGetters(globalThis, {
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs",
Zotero: "chrome://zotero/content/zotero.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs"
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const { BlockingObserver } = ChromeUtils.import("chrome://zotero/content/BlockingObserver.jsm");
ChromeUtils.import("chrome://zotero/content/actors/ActorManager.jsm");
/* global HiddenFrame, E10SUtils, this */
XPCOMUtils.defineLazyModuleGetters(this, {
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
HiddenFrame: "resource://gre/modules/HiddenFrame.jsm",
Services: "resource://gre/modules/Services.jsm",
setTimeout: "resource://gre/modules/Timer.jsm",
});
ChromeUtils.defineESModuleGetters(this, {
Zotero: "chrome://zotero/content/zotero.mjs"
});
const progressListeners = new Set();
// From nsSandboxFlags.h
const SANDBOXED_ORIGIN = 0x10;
const SANDBOXED_SCRIPTS = 0x80;
/**
* Functions for creating and destroying hidden browser objects
**/
export class HiddenBrowser {
class HiddenBrowser {
/**
* @param {Object} options
* @param {Boolean} [options.allowJavaScript]
@ -52,13 +55,10 @@ export class HiddenBrowser {
* @param {Boolean} [options.blockRemoteResources] Block all remote (non-file:) resources
* @param {Boolean} [options.useHiddenFrame=true] Use a hidden frame to create the browser.
* Must be set to false if intending to call print().
* @param {Number} [options.userContextId] - From Zotero.HTTP.newCookieContext() for cookie isolation
* @param {String} [options.customUserAgent] - Override User-Agent for all requests
* from this browser's browsing context
* @param {Zotero.CookieSandbox} [options.cookieSandbox]
*/
constructor(options = {}) {
this._destroyed = false;
this._allowJavaScript = options.allowJavaScript !== false;
this._createdPromise = (async () => {
let doc;
if (options.useHiddenFrame !== false) {
@ -66,6 +66,7 @@ export class HiddenBrowser {
this._frame = frame;
var windowlessBrowser = await frame.get();
windowlessBrowser.browsingContext.allowJavascript = options.allowJavaScript !== false;
windowlessBrowser.docShell.allowImages = false;
if (options.docShell) {
Object.assign(windowlessBrowser.docShell, options.docShell);
@ -86,12 +87,17 @@ export class HiddenBrowser {
browser.setAttribute("remote", "true");
browser.setAttribute('maychangeremoteness', 'true');
browser.setAttribute("disableglobalhistory", "true");
if (options.userContextId) {
browser.setAttribute("usercontextid", String(options.userContextId));
}
browser.style.display = "none";
doc.documentElement.appendChild(browser);
if (options.cookieSandbox) {
options.cookieSandbox.attachToBrowser(browser);
}
if (Zotero.Debug.enabled) {
startLeakWarningTimer(browser);
}
if (options.blockRemoteResources) {
this._blockingObserver = new BlockingObserver({
shouldBlock(uri) {
@ -101,17 +107,6 @@ export class HiddenBrowser {
this._blockingObserver.register(browser);
}
if (options.customUserAgent) {
browser.browsingContext.customUserAgent = options.customUserAgent;
}
if (!this._allowJavaScript) {
// A system-principal document (e.g., a blob: URL created by chrome code) can run
// scripts even when scripting is otherwise disabled, so sandbox it with a null
// principal and no scripts.
browser.browsingContext.sandboxFlags |= SANDBOXED_ORIGIN | SANDBOXED_SCRIPTS;
}
this._browser = browser;
})();
@ -126,16 +121,15 @@ export class HiddenBrowser {
set(target, prop, val) {
if (prop in target) {
target[prop] = val;
return true;
}
else {
return Reflect.set(target._browser, prop, val);
}
Reflect.set(target._browser, prop, val)
return true;
}
});
}
/**
*
* @param {String} source - HTTP URL, file: URL, or file path
* @param {Object} [options]
* @param {Boolean} [options.requireSuccessfulStatus]
@ -155,12 +149,18 @@ export class HiddenBrowser {
Zotero.debug(`Loading ${uri} in hidden browser`);
// Next bit adapted from Mozilla's HeadlessShell.jsm
try {
// Figure out whether the browser should be remote
let remoteType = ChromeUtils.predictRemoteTypeForURI(uri, {
preferredRemoteType: E10SUtils.DEFAULT_REMOTE_TYPE,
useRemoteTabs: true,
useRemoteSubframes: false
});
// Figure out whether the browser should be remote. We actually
// perform the load in PageDataChild, but remoteness changes
// need to happen here.
let oa = E10SUtils.predictOriginAttributes({ browser: this });
let remoteType = E10SUtils.getRemoteTypeForURI(
uri,
true,
false,
E10SUtils.DEFAULT_REMOTE_TYPE,
null,
oa
);
if (this.remoteType !== remoteType) {
// The following functions need to be called on the <browser> directly,
// not through our proxy (aka 'this')
@ -215,18 +215,12 @@ export class HiddenBrowser {
);
});
let prepared = await this.browsingContext.currentWindowGlobal.getActor("PageData")
.sendQuery("prepareLoad");
if (!prepared) {
let loadURISuccess = await this.browsingContext.currentWindowGlobal.getActor("PageData")
.sendQuery("loadURI", { uri });
if (!loadURISuccess) {
Zotero.logError(new Error("Load failed"));
return false;
}
// Start the load from the parent process. A load started by the content process
// is restricted to URLs that process could load itself, which excludes blob: URLs
// created by chrome code.
this._browser.loadURI(Services.io.newURI(uri), {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal()
});
await loadCompletePromise;
}
catch (e) {
@ -240,7 +234,7 @@ export class HiddenBrowser {
let response = `${channelInfo.responseStatus} ${channelInfo.responseStatusText}`;
Zotero.debug(`HiddenBrowser.load: ${uri} failed with ${response}`, 2);
// HiddenBrowser will never get returned so we need to clean it up here
this.destroy();
this.destroy()
throw new Zotero.HTTP.UnexpectedStatusException(
{
status: channelInfo.responseStatus
@ -250,38 +244,16 @@ export class HiddenBrowser {
);
}
}
return true;
}
/**
* @param {number | false} [allowInteractiveAfter = false] Delay (in milliseconds) before resolving on 'interactive'.
* If false, documentIsReady() won't resolve until 'complete'.
* @returns {Promise<void>}
*/
waitForDocument({ allowInteractiveAfter = false } = {}) {
return this.browsingContext.currentWindowGlobal.getActor('DocumentIsReady')
.sendQuery('waitForDocument', { allowInteractiveAfter });
}
/**
* @param {String[]} props - 'characterSet', 'title', 'bodyText', 'documentHTML', 'cookie', 'channelInfo'
* @param {Object} [options]
* @param {Number} [options.timeout=30000] - Time to wait for each property in milliseconds.
* The queries wait for the document to be ready, so a page that never finishes loading
* would otherwise hang the query forever.
*/
async getPageData(props, { timeout = 30000 } = {}) {
async getPageData(props) {
var actor = this.browsingContext.currentWindowGlobal.getActor("PageData");
var data = {};
for (let prop of props) {
let timeoutPromise = new Promise((_, reject) => {
setTimeout(
() => reject(new Error(`Timed out getting '${prop}' from hidden browser`)),
timeout
);
});
data[prop] = await Promise.race([actor.sendQuery(prop), timeoutPromise]);
data[prop] = await actor.sendQuery(prop);
}
return data;
}
@ -341,3 +313,21 @@ export class HiddenBrowser {
}
}
}
function startLeakWarningTimer(browser) {
const CHECK_AFTER_SECONDS = 60;
// We need to use Cu.getWeakReference() to get an xpcIJSWeakReference here -
// DOM WeakRefs, paradoxically, keep the browser alive
let weakBrowser = Cu.getWeakReference(browser);
browser = null;
arguments.length = 0;
setTimeout(() => {
let browser = weakBrowser.get();
if (browser) {
Zotero.debug(`Browser object still alive after ${CHECK_AFTER_SECONDS} seconds - memory leak?`);
Zotero.debug('Viewing URI ' + browser.currentURI?.spec)
}
}, 1000 * CHECK_AFTER_SECONDS);
}

View file

@ -23,14 +23,20 @@
***** END LICENSE BLOCK *****
*/
ChromeUtils.defineESModuleGetters(globalThis, {
var EXPORTED_SYMBOLS = ["RemoteTranslate"];
ChromeUtils.import("chrome://zotero/content/actors/ActorManager.jsm");
ChromeUtils.defineESModuleGetters(this, {
Zotero: "chrome://zotero/content/zotero.mjs",
ZOTERO_CONFIG: "resource://zotero/config.mjs",
ActorManager: "chrome://zotero/content/actors/ActorManager.mjs",
TranslationManager: "chrome://zotero/content/actors/TranslationParent.mjs",
});
export class RemoteTranslate {
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
TranslationManager: "chrome://zotero/content/actors/TranslationParent.jsm",
ZOTERO_CONFIG: "resource://zotero/config.js",
});
class RemoteTranslate {
_browser = null;
_id = Zotero.Utilities.randomString();
@ -80,7 +86,8 @@ export class RemoteTranslate {
* Set a handler on the proxied Zotero.Translate instance.
* The handler function is passed this RemoteTranslate as its first argument.
*
* Supports all Zotero.Translate handlers.
* Supports all Zotero.Translate handlers in addition to newTestDetectionFailed, which can be called from
* {@link newTest} if detection fails to confirm the creation of an expected-fail test.
*
* @param {String} name
* @param {Function} handler
@ -241,13 +248,11 @@ export class RemoteTranslate {
/**
* Create a test on the browser's current page.
*
* @param {Object} [testInit]
* @param {boolean | number} [testInit.defer]
* @return {Promise<Object | null>} Resolves to the created test object (null on error)
*/
newTest(testInit) {
newTest() {
let actor = this._browser.browsingContext.currentWindowGlobal.getActor("Translation");
return actor.sendQuery("newTest", { translator: this._translator, testInit, id: this._id });
return actor.sendQuery("newTest", { translator: this._translator, id: this._id });
}
/**

View file

@ -27,12 +27,16 @@
***** END LICENSE BLOCK *****
*/
const EXPORTED_SYMBOLS = ['ZoteroProtocolHandler'];
const ZOTERO_SCHEME = "zotero";
const ZOTERO_PROTOCOL_CID = Components.ID("{9BC3D762-9038-486A-9D70-C997AF848A7C}");
const ZOTERO_PROTOCOL_CONTRACTID = "@mozilla.org/network/protocol;1?name=" + ZOTERO_SCHEME;
const ZOTERO_PROTOCOL_NAME = "Zotero Chrome Extension Protocol";
import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs";
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/ComponentUtils.jsm");
const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
const Cc = Components.classes;
const Ci = Components.interfaces;
@ -42,9 +46,9 @@ const ios = Services.io;
// Dummy chrome URL used to obtain a valid chrome channel
const DUMMY_CHROME_URL = "chrome://zotero/content/zoteroPane.xul";
import { Zotero } from "chrome://zotero/content/zotero.mjs";
var { Zotero } = ChromeUtils.importESModule("chrome://zotero/content/zotero.mjs");
export function ZoteroProtocolHandler() {
function ZoteroProtocolHandler() {
this.wrappedJSObject = this;
this._principal = null;
this._extensions = {};
@ -63,13 +67,13 @@ export function ZoteroProtocolHandler() {
loadAsChrome: false,
newChannel: function (uri, loadInfo) {
return new AsyncChannel(uri, loadInfo, async function () {
return new AsyncChannel(uri, loadInfo, function* () {
try {
var uriPath = uri.pathQueryRef;
if (!uriPath) {
return this._errorChannel('Invalid URL');
}
uriPath = uriPath.substring(1);
uriPath = uriPath.substr('//attachment/'.length);
var params = {};
var router = new Zotero.Router(params);
@ -85,7 +89,7 @@ export function ZoteroProtocolHandler() {
if (!params.itemKey) {
return this._errorChannel("Item key not provided");
}
var item = await Zotero.Items.getByLibraryAndKeyAsync(params.libraryID, params.itemKey);
var item = yield Zotero.Items.getByLibraryAndKeyAsync(params.libraryID, params.itemKey);
if (!item) {
return this._errorChannel(`No item found for ${uriPath}`);
@ -94,7 +98,7 @@ export function ZoteroProtocolHandler() {
return this._errorChannel(`Item for ${uriPath} is not a file attachment`);
}
var path = await item.getFilePathAsync();
var path = yield item.getFilePathAsync();
if (!path) {
return this._errorChannel(`${path} not found`);
}
@ -114,7 +118,7 @@ export function ZoteroProtocolHandler() {
Zotero.logError(e);
return this._errorChannel(`Resource ${resourcePathParts.join('/')} not found`);
}
if (!(await IOUtils.exists(path))) {
if (!(yield IOUtils.exists(path))) {
return this._errorChannel(`Resource ${resourcePathParts.join('/')} not found`);
}
}
@ -151,7 +155,7 @@ export function ZoteroProtocolHandler() {
loadAsChrome: false,
newChannel: function (uri, loadInfo) {
return new AsyncChannel(uri, loadInfo, async function () {
return new AsyncChannel(uri, loadInfo, function* () {
this.contentType = 'text/plain';
var path = uri.spec.match(/zotero:\/\/[^/]+(.*)/)[1];
@ -163,7 +167,7 @@ export function ZoteroProtocolHandler() {
}
catch (e) {
if (e instanceof Zotero.Router.InvalidPathException) {
return "URL could not be parsed";
return "URL could not be parsed";
}
}
});
@ -178,14 +182,14 @@ export function ZoteroProtocolHandler() {
loadAsChrome: true,
newChannel: function (uri, loadInfo) {
return new AsyncChannel(uri, loadInfo, async function () {
return new AsyncChannel(uri, loadInfo, function* () {
var userLibraryID = Zotero.Libraries.userLibraryID;
var path = uri.pathQueryRef;
if (!path) {
return 'Invalid URL';
}
path = path.substring(1);
path = path.substr('//report/'.length);
// Proxy CSS files
if (path.endsWith('.css')) {
@ -266,7 +270,7 @@ export function ZoteroProtocolHandler() {
try {
Zotero.API.parseParams(params);
var results = await Zotero.API.getResultsFromParams(params);
var results = yield Zotero.API.getResultsFromParams(params);
}
catch (e) {
Zotero.debug(e, 1);
@ -316,7 +320,7 @@ export function ZoteroProtocolHandler() {
for (let id of searchItemIDs) {
if (!searchChildIDs.has(id)) {
var children = [];
var item = await Zotero.Items.getAsync(id);
var item = yield Zotero.Items.getAsync(id);
if (!item.isRegularItem()) {
continue;
}
@ -347,7 +351,7 @@ export function ZoteroProtocolHandler() {
// Add parents of matches if parents aren't matches themselves
for (let id of searchParentIDs) {
if (!searchItemIDs.has(id) && !itemsHash[id]) {
var item = await Zotero.Items.getAsync(id);
var item = yield Zotero.Items.getAsync(id);
itemsHash[id] = items.length;
items.push(item.toJSON({ mode: 'full' }));
}
@ -355,7 +359,7 @@ export function ZoteroProtocolHandler() {
// Add children to reportChildren property of parents
for (let id of searchChildIDs) {
let item = await Zotero.Items.getAsync(id);
let item = yield Zotero.Items.getAsync(id);
var parentID = item.parentID;
if (!items[itemsHash[parentID]].reportChildren) {
items[itemsHash[parentID]].reportChildren = {
@ -375,7 +379,7 @@ export function ZoteroProtocolHandler() {
// for each matching child
else {
for (let id of searchChildIDs) {
var item = await Zotero.Items.getAsync(id);
var item = yield Zotero.Items.getAsync(id);
var parentID = item.parentID;
var parentItem = Zotero.Items.get(parentID);
@ -419,7 +423,7 @@ export function ZoteroProtocolHandler() {
var collation = Zotero.getLocaleCollation();
var compareFunction = function (a, b) {
var compareFunction = function(a, b) {
var index = 0;
// Multidimensional sort
@ -540,6 +544,267 @@ export function ZoteroProtocolHandler() {
}
};
/**
* Generate MIT SIMILE Timeline
*
* Query string key abbreviations: intervals = i
* dateType = t
* timelineDate = d
*
* interval abbreviations: day = d | month = m | year = y | decade = e | century = c | millennium = i
* dateType abbreviations: date = d | dateAdded = da | dateModified = dm
* timelineDate format: shortMonthName.day.year (year is positive for A.D. and negative for B.C.)
*
* Defaults: intervals = month, year, decade
* dateType = date
* timelineDate = today's date
*/
var TimelineExtension = {
loadAsChrome: true,
newChannel: function (uri, loadInfo) {
return new AsyncChannel(uri, loadInfo, function* () {
var userLibraryID = Zotero.Libraries.userLibraryID;
var path = uri.spec.match(/zotero:\/\/[^/]+(.*)/)[1];
if (!path) {
this.contentType = 'text/html';
return 'Invalid URL';
}
var params = {};
var router = new Zotero.Router(params);
// HTML
router.add('library/:scopeObject/:scopeObjectKey', function () {
params.libraryID = userLibraryID;
params.controller = 'html';
});
router.add('groups/:groupID/:scopeObject/:scopeObjectKey', function () {
params.controller = 'html';
});
router.add('library', function () {
params.libraryID = userLibraryID;
params.controller = 'html';
});
router.add('groups/:groupID', function () {
params.controller = 'html';
});
// Data
router.add('data/library/:scopeObject/:scopeObjectKey', function () {
params.libraryID = userLibraryID;
params.controller = 'data';
});
router.add('data/groups/:groupID/:scopeObject/:scopeObjectKey', function () {
params.controller = 'data';
});
router.add('data/library', function () {
params.libraryID = userLibraryID;
params.controller = 'data';
});
router.add('data/groups/:groupID', function () {
params.controller = 'data';
});
// Old-style HTML URLs
router.add('collection/:id', function () {
params.controller = 'html';
params.scopeObject = 'collections';
var lkh = Zotero.Collections.parseLibraryKeyHash(params.id);
if (lkh) {
params.libraryID = lkh.libraryID || userLibraryID;
params.scopeObjectKey = lkh.key;
}
else {
params.scopeObjectID = params.id;
}
delete params.id;
});
router.add('search/:id', function () {
params.controller = 'html';
params.scopeObject = 'searches';
var lkh = Zotero.Searches.parseLibraryKeyHash(params.id);
if (lkh) {
params.libraryID = lkh.libraryID || userLibraryID;
params.scopeObjectKey = lkh.key;
}
else {
params.scopeObjectID = params.id;
}
delete params.id;
});
router.add('/', function () {
params.controller = 'html';
params.libraryID = userLibraryID;
});
var parsed = router.run(path);
if (!parsed) {
this.contentType = 'text/html';
return "URL could not be parsed";
}
if (params.groupID) {
params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(params.groupID);
}
var intervals = params.i ? params.i : '';
var timelineDate = params.d ? params.d : '';
var dateType = params.t ? params.t : '';
// Get the collection or search object
var collection, search;
switch (params.scopeObject) {
case 'collections':
if (params.scopeObjectKey) {
collection = yield Zotero.Collections.getByLibraryAndKeyAsync(
params.libraryID, params.scopeObjectKey
);
}
else {
collection = yield Zotero.Collections.getAsync(params.scopeObjectID);
}
if (!collection) {
this.contentType = 'text/html';
return 'Invalid collection ID or key';
}
break;
case 'searches':
if (params.scopeObjectKey) {
var s = yield Zotero.Searches.getByLibraryAndKeyAsync(
params.libraryID, params.scopeObjectKey
);
}
else {
var s = yield Zotero.Searches.getAsync(params.scopeObjectID);
}
if (!s) {
return 'Invalid search ID or key';
}
// FIXME: Hack to exclude group libraries for now
var search = new Zotero.Search();
search.setScope(s);
var groups = Zotero.Groups.getAll();
for (let group of groups) {
search.addCondition('libraryID', 'isNot', group.libraryID);
}
break;
}
//
// Create XML file
//
if (params.controller == 'data') {
switch (params.scopeObject) {
case 'collections':
var results = collection.getChildItems();
break;
case 'searches':
var ids = yield search.search();
var results = yield Zotero.Items.getAsync(ids);
break;
default:
if (params.scopeObject) {
return "Invalid scope object '" + params.scopeObject + "'";
}
let s = new Zotero.Search();
s.addCondition('libraryID', 'is', params.libraryID);
s.addCondition('noChildren', 'true');
var ids = yield s.search();
var results = yield Zotero.Items.getAsync(ids);
}
var items = [];
// Only include parent items
for (let i=0; i<results.length; i++) {
if (!results[i].parentItemID) {
items.push(results[i]);
}
}
var dateTypes = {
d: 'date',
da: 'dateAdded',
dm: 'dateModified'
};
//default dateType = date
if (!dateType || !dateTypes[dateType]) {
dateType = 'd';
}
this.contentType = 'application/xml';
return Zotero.Utilities.Internal.getAsyncInputStream(
Zotero.Timeline.generateXMLDetails(items, dateTypes[dateType])
);
}
//
// Generate main HTML page
//
var content = Zotero.File.getContentsFromURL('chrome://zotero/skin/timeline/timeline.html');
this.contentType = 'text/html';
if(!timelineDate){
timelineDate=Date();
var dateParts=timelineDate.toString().split(' ');
timelineDate=dateParts[1]+'.'+dateParts[2]+'.'+dateParts[3];
}
if (!intervals || intervals.length < 3) {
intervals += "mye".substr(intervals.length);
}
var theIntervals = {
d: 'Timeline.DateTime.DAY',
m: 'Timeline.DateTime.MONTH',
y: 'Timeline.DateTime.YEAR',
e: 'Timeline.DateTime.DECADE',
c: 'Timeline.DateTime.CENTURY',
i: 'Timeline.DateTime.MILLENNIUM'
};
//sets the intervals of the timeline bands
var tempStr = '<body onload="onLoad(';
var a = (theIntervals[intervals[0]]) ? theIntervals[intervals[0]] : 'Timeline.DateTime.MONTH';
var b = (theIntervals[intervals[1]]) ? theIntervals[intervals[1]] : 'Timeline.DateTime.YEAR';
var c = (theIntervals[intervals[2]]) ? theIntervals[intervals[2]] : 'Timeline.DateTime.DECADE';
content = content.replace(tempStr, tempStr + a + ',' + b + ',' + c + ',\'' + timelineDate + '\'');
tempStr = 'document.write("<title>';
if (params.scopeObject == 'collections') {
content = content.replace(tempStr, tempStr + collection.name + ' - ');
}
else if (params.scopeObject == 'searches') {
content = content.replace(tempStr, tempStr + search.name + ' - ');
}
else {
content = content.replace(tempStr, tempStr + Zotero.getString('pane.collections.library') + ' - ');
}
tempStr = 'Timeline.loadXML("zotero://timeline/data/';
var d = '';
if (params.groupID) {
d += 'groups/' + params.groupID + '/';
}
else {
d += 'library/';
}
if (params.scopeObject) {
d += params.scopeObject + "/" + params.scopeObjectKey;
}
if (dateType) {
d += '?t=' + dateType;
}
return content.replace(tempStr, tempStr + d);
});
}
};
/**
* Select an item
@ -555,14 +820,15 @@ export function ZoteroProtocolHandler() {
var SelectExtension = {
noContent: true,
doAction: async function (uri) {
doAction: Zotero.Promise.coroutine(function* (uri) {
var userLibraryID = Zotero.Libraries.userLibraryID;
var path = uri.pathQueryRef;
if (!path) {
return 'Invalid URL';
}
path = path.substring(1);
path = path.substr('//select/'.length);
var mimeType, content = '';
var params = {
objectType: 'item'
@ -620,7 +886,7 @@ export function ZoteroProtocolHandler() {
return;
}
var results = await Zotero.API.getResultsFromParams(params);
var results = yield Zotero.API.getResultsFromParams(params);
if (!results.length) {
var msg = "Objects not found";
@ -655,7 +921,7 @@ export function ZoteroProtocolHandler() {
else {
col = Zotero.Collections.get(params.scopeObjectID);
}
await zp.collectionsView.selectCollection(col.id);
yield zp.collectionsView.selectCollection(col.id);
}
else if (params.scopeObject == 'searches') {
let s;
@ -667,15 +933,15 @@ export function ZoteroProtocolHandler() {
else {
s = Zotero.Searches.get(params.scopeObjectID);
}
await zp.collectionsView.selectSearch(s.id);
yield zp.collectionsView.selectSearch(s.id);
}
// If collection not specified, select library root
else {
await zp.collectionsView.selectLibrary(params.libraryID);
yield zp.collectionsView.selectLibrary(params.libraryID);
}
return zp.selectItems(results.map(x => x.id));
}
},
}),
newChannel: function (uri) {
this.doAction(uri);
@ -689,7 +955,7 @@ export function ZoteroProtocolHandler() {
loadAsChrome: false,
newChannel: function (uri, loadInfo) {
return new AsyncChannel(uri, loadInfo, async function () {
return new AsyncChannel(uri, loadInfo, function* () {
this.contentType = "text/plain";
try {
@ -703,6 +969,99 @@ export function ZoteroProtocolHandler() {
}
};
var ConnectorChannel = function(uri, data) {
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
.getService(Components.interfaces.nsIScriptSecurityManager);
this.name = uri;
this.URI = ios.newURI(uri, "UTF-8", null);
this.owner = (secMan.getCodebasePrincipal || secMan.getSimpleCodebasePrincipal)(this.URI);
this._isPending = true;
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
this._stream = converter.convertToInputStream(data);
this.contentLength = this._stream.available();
}
ConnectorChannel.prototype.contentCharset = "UTF-8";
ConnectorChannel.prototype.contentType = "text/html";
ConnectorChannel.prototype.notificationCallbacks = null;
ConnectorChannel.prototype.securityInfo = null;
ConnectorChannel.prototype.status = 0;
ConnectorChannel.prototype.loadGroup = null;
ConnectorChannel.prototype.loadFlags = 393216;
ConnectorChannel.prototype.__defineGetter__("originalURI", function() { return this.URI });
ConnectorChannel.prototype.__defineSetter__("originalURI", function() { });
ConnectorChannel.prototype.asyncOpen = function(streamListener) {
if(this.loadGroup) this.loadGroup.addRequest(this, null);
streamListener.onStartRequest(this);
streamListener.onDataAvailable(this, this._stream, 0, this.contentLength);
streamListener.onStopRequest(this, this.status);
this._isPending = false;
if(this.loadGroup) this.loadGroup.removeRequest(this, null, 0);
}
ConnectorChannel.prototype.isPending = function() {
return this._isPending;
}
ConnectorChannel.prototype.cancel = function(status) {
this.status = status;
this._isPending = false;
if(this._stream) this._stream.close();
}
ConnectorChannel.prototype.suspend = function() {}
ConnectorChannel.prototype.resume = function() {}
ConnectorChannel.prototype.open = function() {
return this._stream;
}
ConnectorChannel.prototype.QueryInterface = function(iid) {
if (!iid.equals(Components.interfaces.nsIChannel) && !iid.equals(Components.interfaces.nsIRequest) &&
!iid.equals(Components.interfaces.nsISupports)) {
throw Components.results.NS_ERROR_NO_INTERFACE;
}
return this;
}
/**
* zotero://connector/
*
* URI spoofing for transferring page data across boundaries
*/
var ConnectorExtension = new function() {
this.loadAsChrome = false;
this.newChannel = function(uri) {
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
.getService(Components.interfaces.nsIScriptSecurityManager);
var Zotero = Components.classes["@zotero.org/Zotero;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
try {
var originalURI = uri.pathQueryRef.substr('zotero://connector/'.length);
originalURI = decodeURIComponent(originalURI);
if(!Zotero.Server.Connector.Data[originalURI]) {
return null;
} else {
return new ConnectorChannel(originalURI, Zotero.Server.Connector.Data[originalURI]);
}
} catch(e) {
Zotero.debug(e);
throw e;
}
}
};
/*
zotero://pdf.js/viewer.html
zotero://pdf.js/pdf/1/ABCD5678
@ -711,7 +1070,7 @@ export function ZoteroProtocolHandler() {
loadAsChrome: true,
newChannel: function (uri) {
return new AsyncChannel(uri, async function () {
return new AsyncChannel(uri, function* () {
try {
uri = uri.spec;
// Proxy PDF.js files
@ -729,11 +1088,11 @@ export function ZoteroProtocolHandler() {
var [libraryID, key] = uri.substr(pdfPrefix.length).split('/');
libraryID = parseInt(libraryID);
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
var item = yield Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
if (!item) {
return this._errorChannel("Item not found");
}
var path = await item.getFilePathAsync();
var path = yield item.getFilePathAsync();
if (!path) {
return this._errorChannel("File not found");
}
@ -787,7 +1146,7 @@ export function ZoteroProtocolHandler() {
if (!uriPath) {
return 'Invalid URL';
}
uriPath = uriPath.substring(1);
uriPath = uriPath.replace(/^\/\/open(-pdf)?\//, '');
var params = {
objectType: 'item'
@ -838,7 +1197,7 @@ export function ZoteroProtocolHandler() {
var location = {};
if (page) {
location.pageIndex = parseInt(page) - 1;
location.pageIndex = parseInt(page);
}
if (annotation) {
location.annotationID = annotation;
@ -887,8 +1246,10 @@ export function ZoteroProtocolHandler() {
this._extensions[ZOTERO_SCHEME + "://attachment"] = AttachmentExtension;
this._extensions[ZOTERO_SCHEME + "://data"] = DataExtension;
this._extensions[ZOTERO_SCHEME + "://report"] = ReportExtension;
this._extensions[ZOTERO_SCHEME + "://timeline"] = TimelineExtension;
this._extensions[ZOTERO_SCHEME + "://select"] = SelectExtension;
this._extensions[ZOTERO_SCHEME + "://debug"] = DebugExtension;
this._extensions[ZOTERO_SCHEME + "://connector"] = ConnectorExtension;
this._extensions[ZOTERO_SCHEME + "://pdf.js"] = PDFJSExtension;
this._extensions[ZOTERO_SCHEME + "://open"] = OpenExtension;
this._extensions[ZOTERO_SCHEME + "://open-pdf"] = OpenExtension;
@ -1021,14 +1382,14 @@ ZoteroProtocolHandler.init = function () {
/**
* nsIChannel implementation that takes an async function that returns a
* nsIChannel implementation that takes a promise-yielding generator that returns a
* string, nsIAsyncInputStream, or file
*/
function AsyncChannel(uri, loadInfo, func) {
function AsyncChannel(uri, loadInfo, gen) {
this.URI = this.originalURI = uri;
this.loadInfo = loadInfo;
this._function = func;
this._generator = gen;
this._isPending = true;
// nsIRequest
@ -1050,7 +1411,7 @@ function AsyncChannel(uri, loadInfo, func) {
}
AsyncChannel.prototype = {
asyncOpen: async function (streamListener) {
asyncOpen: Zotero.Promise.coroutine(function* (streamListener) {
if (this.loadGroup) this.loadGroup.addRequest(this, null);
var channel = this;
@ -1095,17 +1456,17 @@ AsyncChannel.prototype = {
var data;
try {
if (!data) {
data = await channel._function();
data = yield Zotero.spawn(channel._generator, channel)
}
if (typeof data == 'string') {
//Zotero.debug("AsyncChannel: Got string from generator");
listenerWrapper.onStartRequest(this);
let inputStream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
inputStream.setUTF8Data(data);
let converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let inputStream = converter.convertToInputStream(data);
listenerWrapper.onDataAvailable(this, inputStream, 0, inputStream.available());
listenerWrapper.onStopRequest(this, this.status);
@ -1136,10 +1497,11 @@ AsyncChannel.prototype = {
uri.QueryInterface(Ci.nsIURL);
this.contentType = Zotero.MIME.getMIMETypeFromExtension(uri.fileExtension);
if (!this.contentType) {
let sample = await Zotero.File.getSample(uri.spec);
let sample = yield Zotero.File.getSample(uri.spec);
this.contentType = Zotero.MIME.getMIMETypeFromData(sample);
}
Components.utils.import("resource://gre/modules/NetUtil.jsm");
NetUtil.asyncFetch({ uri: data, loadUsingSystemPrincipal: true }, function (inputStream, status) {
if (!Components.isSuccessCode(status)) {
reject();
@ -1182,7 +1544,7 @@ AsyncChannel.prototype = {
}
catch (e) {}
}
},
}),
// nsIRequest
isPending: function () {

View file

@ -10,6 +10,7 @@
onload="moveToAlertPosition(); sizeToContent(); document.getElementById('zotero-about').getButton('accept').focus();"
inwindowmenu="false">
<script>
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
Services.scriptloader.loadSubScript("chrome://zotero/content/include.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/customElements.js", this);
</script>
@ -41,7 +42,7 @@
<p id="third-party-software"></p>
<p id="get-involved"></p>
<script><![CDATA[
const { ZOTERO_CONFIG } = ChromeUtils.importESModule('resource://zotero/config.mjs');
Components.utils.import("resource://zotero/config.js");
document.getElementById('changelog').href = ZOTERO_CONFIG.CHANGELOG_URL;

View file

@ -0,0 +1,65 @@
var EXPORTED_SYMBOLS = [];
// Register Mozilla actors
ChromeUtils.importESModule("resource://gre/modules/ActorManagerParent.sys.mjs");
ChromeUtils.registerWindowActor("PageData", {
child: {
moduleURI: "chrome://zotero/content/actors/PageDataChild.jsm"
}
});
ChromeUtils.registerWindowActor("SingleFile", {
child: {
moduleURI: "chrome://zotero/content/actors/SingleFileChild.jsm"
}
});
ChromeUtils.registerWindowActor("Translation", {
parent: {
moduleURI: "chrome://zotero/content/actors/TranslationParent.jsm"
},
child: {
moduleURI: "chrome://zotero/content/actors/TranslationChild.jsm"
}
});
ChromeUtils.registerWindowActor("FeedAbstract", {
parent: {
moduleURI: "chrome://zotero/content/actors/FeedAbstractParent.jsm",
},
child: {
moduleURI: "chrome://zotero/content/actors/FeedAbstractChild.jsm",
events: {
DOMDocElementInserted: {},
}
},
messageManagerGroups: ["feedAbstract"]
});
ChromeUtils.registerWindowActor("ZoteroPrint", {
parent: {
moduleURI: "chrome://zotero/content/actors/ZoteroPrintParent.jsm"
},
child: {
moduleURI: "chrome://zotero/content/actors/ZoteroPrintChild.jsm",
events: {
pageshow: {}
}
},
allFrames: true
});
ChromeUtils.registerWindowActor("ExternalLinkHandler", {
parent: {
moduleURI: "chrome://zotero/content/actors/ExternalLinkHandlerParent.jsm",
},
child: {
moduleURI: "chrome://zotero/content/actors/ExternalLinkHandlerChild.jsm",
events: {
click: {},
}
},
messageManagerGroups: ["feedAbstract", "basicViewer"]
});

View file

@ -1,92 +0,0 @@
// Register Mozilla actors
import "resource://gre/modules/ActorManagerParent.sys.mjs";
ChromeUtils.registerWindowActor("PageData", {
child: {
esModuleURI: "chrome://zotero/content/actors/PageDataChild.mjs"
}
});
ChromeUtils.registerWindowActor("SingleFile", {
child: {
esModuleURI: "chrome://zotero/content/actors/SingleFileChild.mjs"
}
});
ChromeUtils.registerWindowActor("Translation", {
parent: {
esModuleURI: "chrome://zotero/content/actors/TranslationParent.mjs"
},
child: {
esModuleURI: "chrome://zotero/content/actors/TranslationChild.mjs"
}
});
ChromeUtils.registerWindowActor("FeedAbstract", {
parent: {
esModuleURI: "chrome://zotero/content/actors/FeedAbstractParent.mjs",
},
child: {
esModuleURI: "chrome://zotero/content/actors/FeedAbstractChild.mjs",
events: {
DOMDocElementInserted: {},
}
},
messageManagerGroups: ["feedAbstract"]
});
ChromeUtils.registerWindowActor("ZoteroPrint", {
parent: {
esModuleURI: "chrome://zotero/content/actors/ZoteroPrintParent.mjs"
},
child: {
esModuleURI: "chrome://zotero/content/actors/ZoteroPrintChild.mjs",
events: {
pageshow: {}
}
},
allFrames: true
});
ChromeUtils.registerWindowActor("ExternalLinkHandler", {
parent: {
esModuleURI: "chrome://zotero/content/actors/ExternalLinkHandlerParent.mjs",
},
child: {
esModuleURI: "chrome://zotero/content/actors/ExternalLinkHandlerChild.mjs",
events: {
click: {},
}
},
messageManagerGroups: ["feedAbstract", "basicViewer"]
});
// On macOS only, register the Ctrl-Enter handler actor
// (No access to Zotero object here)
if (AppConstants.platform === "macosx") {
ChromeUtils.registerWindowActor("SequoiaContextMenu", {
parent: {
esModuleURI: "chrome://zotero/content/actors/SequoiaContextMenuParent.mjs",
},
child: {
esModuleURI: "chrome://zotero/content/actors/SequoiaContextMenuChild.mjs",
},
allFrames: true,
includeChrome: true
});
}
ChromeUtils.registerWindowActor("MendeleyAuth", {
parent: {
esModuleURI: "chrome://zotero/content/actors/MendeleyAuthParent.mjs"
},
child: {
esModuleURI: "chrome://zotero/content/actors/MendeleyAuthChild.mjs"
}
});
ChromeUtils.registerWindowActor("DocumentIsReady", {
child: {
esModuleURI: "chrome://zotero/content/actors/DocumentIsReadyChild.mjs"
}
});

Some files were not shown because too many files have changed in this diff Show more