Compare commits

...

109 commits
main ... 7.0.16

Author SHA1 Message Date
Abe Jellinek
6a611179d1 Undo Scholaread changes to user.js (#5313)
And any other fileHandler changes
2025-06-21 12:13:26 -04:00
Dan Stillman
7dc8d13543 Merge new English strings 2025-06-21 00:31:25 -04:00
Dan Stillman
244487b4de Update translators 2025-06-21 00:29:10 -04:00
Dan Stillman
0b97822e90 Update version 2025-06-21 00:28:26 -04:00
abaevbog
3aef6cddcb
qf: fix breakage with unloaded tab (#5347)
Properly load all item data for items opened in tabs
to account for the fact that they can belong to
libraries that are not yet loaded.

Similar fix as in 4421de63de
2025-06-20 00:19:17 -04:00
Tom Najdek
4cc52ec928 File renaming: Add pascal case. Fix unicode handling. (#4876) 2025-06-19 02:19:59 -04:00
Dan Stillman
0d5d43f930 Don't use Firefox user agent for files.zotero.net requests 2025-06-19 02:19:53 -04:00
Tom Najdek
7ffd1d4504 Unhide "title" column on "Restore Column Order" (#5299)
This actually checks for a "primary" column, so if an extension hides the "title" column but provides its own "primary" column, the "title" column remains hidden.
2025-06-19 02:18:24 -04:00
Adomas Venčkauskas
1c2ec3fd75 Fix Google Docs allowing to select note style (endnotes)
Report: https://forums.zotero.org/discussion/123928/zotero-citations-not-appearing-as-endnotes-despite-selected-setting
2025-06-19 02:16:53 -04:00
Abe Jellinek
7eeb666894 Skip invalid relative path when changing LABD (#5239) 2025-06-19 02:16:36 -04:00
Abe Jellinek
bd451196b0 Tags box: Disable Remove All Tags when not editable 2025-06-19 02:14:32 -04:00
abaevbog
427e8daaf9 add string for note-editor a11y link popup message (#5205)
String read out by screen readers when the link popup
appears instructing users to navigate it.

Addresses: #5199
2025-06-19 02:14:21 -04:00
abaevbog
a7e1035b77 a11y: clear aria-expanded on empty itemTree rows (#5201)
Per https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-expanded#treeitems
only treeitems that contain children should have
the aria-expanded attribute. This is instead of having
aria-expanded="false" on empty regular items or standalone
attachments, which erroneously indicates to screen readers
that the row has children but is collapsed.

Fixes: #5200
2025-06-19 02:13:58 -04:00
Dan Stillman
ec59f43a68 Mark locally missing files marked for upload for download instead
Z7 removed an `OS.File.open()` in `_checkForUpdatedFiles()` that would
throw on missing files and cause them to be marked for download in a
`catch`.

This likely caused the fix for #1753 not to work in Z7.
2025-06-19 02:13:02 -04:00
abaevbog
76b8556c16 runJS: better message if nothing is returned (#5181)
Display "===>undefined<=== (completed successfully)" instead
of current "===>undefined<=== (undefined)", which is a bit misleading.

Fixes: #5180
2025-06-19 02:05:23 -04:00
abaevbog
79feeaecb1 load all tabs items' data when restoring tab state (#5193)
This fixes the breakage in tabs menu on initial load
if one of the tab is for an item from not-loaded group.
Now, every component relying on tab items (e.g. tabs menu)
can just assume that items are all loaded.

Fixes: #5192
2025-06-19 02:05:05 -04:00
abaevbog
bfe28b5918 include mru types in main list of "New Item" menu (#5195)
Per https://forums.zotero.org/discussion/123269/bad-practice-in-new-item-drop-down-menu
2025-06-19 01:57:01 -04:00
Tom Najdek
730d3b1f34 File renaming: add support for counting creators (#5175)
Also extends the templating engine to support inequality comparisons.
2025-06-19 01:34:55 -04:00
abaevbog
9fb961fd63 transform menu: preserve cases in capitalize title (#5174)
Do not lowercase the entire title before capitalizing
the first letter of each word to preserve abbreviations
or spelling of unusual words (e.g. 3D or iPhone).

Fixes: #5165
2025-06-19 01:33:56 -04:00
Abe Jellinek
ea4f5fee60 FileHandlers: Fix unintended fallthrough in switch
May have been responsible for this:
https://forums.zotero.org/discussion/comment/483004#Comment_483004
2025-06-19 01:19:19 -04:00
Tom Najdek
e700f5ca98 Improve layout of the buttons in the locate manager (#5139) 2025-06-19 01:18:42 -04:00
Dan Stillman
ebecf0f4cc Indicate Troubleshooting Mode in error reports
And show plugins as disabled

Closes #4345
2025-06-19 01:06:47 -04:00
windingwind
9129625dd0 Fix plugin scope not loaded if plugin is disabled when starting Zotero (#5129) 2025-06-19 01:06:09 -04:00
Dan Stillman
a2b7d0674e Fix WebDAV progress updates for Zotero.HTTP.download() 2025-06-19 01:05:58 -04:00
Dan Stillman
e6f15d006f Remove hack for Issue search condition in French locale
https://forums.zotero.org/discussion/comment/473643/#Comment_473643
2025-06-19 01:02:19 -04:00
Dan Stillman
ce851ae42f "Select add-on to install" → "Select plugin to install"
https://forums.zotero.org/discussion/122235/title-in-the-plugin-selection-windows-dialog-add-on-plugin
2025-06-19 01:02:19 -04:00
abaevbog
bb79e10851 Fix unintentional title transfer between attachments on item switch (#5245)
Also added similar test for item details table

Fixes #5244
2025-06-19 01:02:19 -04:00
Dan Stillman
226ba8b489 Update builds to Firefox 115.24.0esr 2025-06-19 01:02:19 -04:00
Adomas Ven
cd856efef8
Connector attachment saving server changes for 7.0 (#5345) 2025-06-19 01:01:52 -04:00
Abe Jellinek
1dad1ae0f8 Accept length-7 sortIndexes for snapshots (#5209) 2025-06-11 09:58:55 -04:00
Abe Jellinek
3dfe2a5321 Update reader submodule 2025-06-10 14:56:02 -04:00
Abe Jellinek
c8c752663f Update reader submodule 2025-04-22 11:07:13 -04:00
Dan Stillman
bbf4692b99 Don't log entire byte array when downloading files 2025-03-15 23:07:17 -04:00
Dan Stillman
56f182f816 Fix file downloads timing out after 30 seconds
We switched file downloads to use XHR to fix downloads failing via
authenticated proxies in fx115+, but that made them subject to our
default 30-second timeout in `Zotero.HTTP.request()`. That timeout uses
the XHR `timeout` property, which applies to the whole request, even if
data is actively being downloaded. As a result, file downloads would
fail for people downloading large files or on very slow connections.

This implements manual connect and inactivity timeouts when using
`Zotero.HTTP.download()`. Currently, these both use `options.timeout`,
or the default 30 seconds, but we could probably take separate options
and have lower defaults now that they no longer apply to the whole
request.
2025-03-15 22:54:35 -04:00
Dan Stillman
6053321766 Update version 2025-03-15 22:54:27 -04:00
Dan Stillman
9828edc2e2 Update version 2025-03-12 23:25:33 -04:00
Dan Stillman
35064caddd Update translators and styles 2025-03-12 23:25:32 -04:00
Dan Stillman
601358f24b Fix very slow purging of itemDataValues at startup in large databases
When an item is erased (removed from the trash or cleaned up from a
feed), we set a flag to purge values in `itemDataValues` on the next
startup, with this query:

DELETE FROM itemDataValues WHERE valueID NOT IN (SELECT valueID FROM itemData);

For some people, that query was incredibly slow and would result in
Zotero intermittently hanging on "Loading items…" for a long time at
startup. It's possible this is mostly limited to people who subscribe to
high-volume feeds and have a lot of item churn. One affected person had
>900K values in `itemDataValues` despite having only 20K items.

It turns out the slow query is due to the foreign-key constraint on
`itemData(valueID)` that references`itemDataValues(valueID)`. SQLite is
checking every row being deleted from `itemDataValues` against
`itemData`, even though the query is specifically removing rows that
don't exist in `itemData`! For the 900K-value DB, disabling foreign-key
checks causes the `DELETE` query to take 25 seconds instead of...some
much longer time that I didn't wait for.

We already had an `executeTransaction()` flag, `disableForeignKeys`, to
temporarily disable foreign-key checks, but it didn't do so in a way
that was safe for post-initialization usage -- a write query outside of
a transaction could've run between the transaction commit and
foreign-key checks being re-enabled. This commit changes it to properly
block all other queries unless they include an `ignoreDBLock` option,
meaning that queries within the function passed to the transaction need
to include that option. (And since that's not realistic for the couple
other uses of `disableForeignKeys` -- one for a test and one in code
that almost certainly hasn't been run by anyone in 15 years -- those now
just run `PRAGMA foreign_keys=OFF|ON` explicitly, leaving this as the
only current use.)
2025-03-12 23:25:32 -04:00
PChemGuy
f4b531e657 Sort strings naturally or alphabetically (#5075)
Hidden setting. If true, use natural collection sorting matching the current behavior. If false, use conventional alphabetic sorting.
2025-03-12 23:25:32 -04:00
Abe Jellinek
c93a0e0e80 Add tests for renderItemTitle() 2025-03-12 23:25:32 -04:00
Abe Jellinek
096cc2e27b renderItemTitle(): Create <span> when inverting style
Creating an <i> with a style attribute that *removes* italics is just
too confusing.
2025-03-12 23:25:32 -04:00
Abe Jellinek
5ae1986f79 renderItemTitle(): Fix nested tag handling when rendering to string
Need to exit the loop regardless of whether we're rendering actual
nodes.

Fixes #5082
2025-03-12 23:25:32 -04:00
Dan Stillman
66d83ec4c8 Prevent DNS/TCP connections in snapshots 2025-03-12 23:25:32 -04:00
abaevbog
b250aed79d always trim editableText from itemBox on blur (#5081)
So that whitespaces are removed even if the itemBox
does not refresh.

Fixes: #5080
2025-03-12 23:25:32 -04:00
Dan Stillman
85b97f82e6 Fix /connector/saveSnapshot test for Zotero.HTTP.download() 2025-03-12 23:25:32 -04:00
Dan Stillman
64b444752a Update "Find Full Text" tests for Zotero.HTTP.download()
Downloads are now included in request() call counts
2025-03-12 23:25:32 -04:00
Dan Stillman
0542010659 Fix Zotero.HTTP.download() for fx115
Blob#bytes() was introduced in fx128.
2025-03-12 23:25:31 -04:00
Dan Stillman
4b78b2c130 Replace Zotero.Utilities.Internal.saveURI() with Zotero.HTTP.download()
Adds a new function, Zotero.HTTP.download(), that uses
Zotero.HTTP.request(). This fixes downloads via authenticated proxies in
Zotero 7 and gives us other request() functionality (e.g., 5xx retrying)
for free.

The downside is that this is probably less efficient, potentially
loading large downloads in memory. We should create a replacement for
request() based on fetch() that supports getting the body as a
ReadableStream.

Fixes #5062
2025-03-12 23:09:47 -04:00
Abe Jellinek
7f6a07b665 Advanced Search: Remove red background from condition tooltips
Fixes #5066
2025-03-12 22:59:36 -04:00
Martynas Bagdonas
fbd3fd5ceb Update reader submodule 2025-03-12 18:26:21 +02:00
Martynas Bagdonas
6ddc360145 Update reader submodule 2025-03-12 12:17:06 +02:00
Martynas Bagdonas
63b06f4346 Update reader submodule 2025-03-01 14:30:28 +02:00
Martynas Bagdonas
71375b15a5 Update reader submodule 2025-02-25 17:24:46 +02:00
Dan Stillman
c6b7829501 Update version 2025-02-21 01:37:48 -05:00
Dan Stillman
1bad050d7f Update build script for 7.0 branch 2025-02-21 01:37:48 -05:00
Dan Stillman
239530c515 CI: Build ZIP for 7.0 branch 2025-02-21 01:37:43 -05:00
Dan Stillman
828e0c942a Update Word for Mac submodule 2025-02-21 00:06:48 -05:00
Martynas Bagdonas
cf37dfb330 Update reader submodule 2025-02-20 20:13:17 +02:00
Dan Stillman
6af5a484fd Update build script for 7.0.12-hotfix branch 2025-02-19 04:08:36 -05:00
Dan Stillman
7de623accf CI: Install libdbus-glib
Needed for fx115, but apparently removed from the latest GitHub Actions
Ubuntu runner
2025-02-19 02:16:02 -05:00
Dan Stillman
10b3b5f915 Update translators 2025-02-19 02:03:27 -05:00
Adomas Venčkauskas
051280fc43 Update ScienceDirect cf captcha selector 2025-02-19 02:03:27 -05:00
Abe Jellinek
264f55545a Fix faulty siblings-of-type check in setAutoAttachmentTitle() (#5045) 2025-02-19 02:03:26 -05:00
Dan Stillman
184671f919 Fix sync not downloading queued objects on first sync of session
`firstInSession` wasn't being properly set, so previously failed data
objects weren't being retried until a manual sync. Local files also
weren't being scanned for changes for three hours, but that's probably
better in the age of DB-stored annotations, so leave that off for now
and address properly in #5025.
2025-02-19 02:03:26 -05:00
Dan Stillman
24c7f146b3 Don't open new copies of runJS/scaffold/csledit
Possible we'll get complaints about no longer being able to open
multiple copies of at least runJS and csledit, and we can reconsider if
so, but reopening the existing window is certainly the intended
behavior.
2025-02-19 02:03:26 -05:00
Abe Jellinek
707c71506f Check editability based on tab when adding attachments (#4936)
And make `get[Selected/Sorted]Items()` and `canEdit[Library/Files]()` tab-aware
2025-02-19 02:03:26 -05:00
Abe Jellinek
569bd1ffae Item box: Fix missing Copy/Paste menuitems on URL fields (#5001) 2025-02-19 02:03:26 -05:00
Dan Stillman
62d46d337a Fix selectItems() not selecting non-top-level item in trash
https://forums.zotero.org/discussion/comment/484380/#Comment_484380

Adds `Zotero.Item#isInTrash()` that returns true if the item or any of
its ancestors is in the trash (i.e., `.deleted == true`)
2025-02-19 02:03:26 -05:00
Dan Stillman
ce0f7c0378 Use commas in file size in over-quota error 2025-02-19 02:03:25 -05:00
Dan Stillman
d2c9a310a8 Tests for Item#fromJSON() changes (#3422) 2025-02-19 02:03:25 -05:00
Dan Stillman
b4bf86915b Item#fromJSON(): Don't process any Extra rows in strict mode
And enable migration of Extra rows to valid fields in non-strict mode,
but since that's only used by translators now, that probably won't do
anything

Fixes #3422
2025-02-19 02:03:25 -05:00
Dan Stillman
8804effba4 Don't auto-select item after sync on upload error
Bug from 2017
2025-02-19 02:03:25 -05:00
Dan Stillman
dfde08852a Update reader submodule 2025-02-19 02:03:25 -05:00
Abe Jellinek
006a787272 Add key to Advanced Search menuitem
Fixes #4978
2025-02-19 02:03:25 -05:00
Dan Stillman
0424faee42 Unset Mozilla environment variables when launching files and URLs (#4983)
Fixes #4981
2025-02-19 02:03:25 -05:00
windingwind
3c9dcc94bc Fix attachments box race condition (#4997)
fix: #4611
Since the attachment preview already has a task queue,
don't skip render calls when rendering
2025-02-19 02:03:24 -05:00
Adomas Venčkauskas
4bc61273a0 Fix integration session breaking when an error is thrown during getDocument() 2025-02-19 02:03:24 -05:00
windingwind
59b3268d01 Fix dense call to setItemDownloadPercentage freezes app (#4979) 2025-02-19 02:03:24 -05:00
Dan Stillman
ddb1daabbc Fix json[c.field] is undefined sync error
Fix error if local note or attachment is added to a parent item while
the remote version was added to a collection. Now, the new local parent
item will be added to any collections the remote child item was added
to.

Fixes #2934
2025-02-19 02:03:24 -05:00
Tom Najdek
d0debc0bed Fix Metadata Retrieval to correctly display the recognized item's title 2025-02-19 02:03:24 -05:00
Adomas Venčkauskas
b5b59062a7 Refresh document with delayed citation updates when changing style
Closes #4974
2025-02-19 02:03:24 -05:00
Dan Stillman
65461de4f5 Update note-editor submodule 2025-02-19 02:03:23 -05:00
Dan Stillman
b9e46ec660 CI: Update to dpl v2 2025-02-19 02:03:23 -05:00
Dan Stillman
22541ff8d8 Update note-editor and reader submodules 2025-02-19 02:03:23 -05:00
windingwind
f7b4cf907b Fix export error of item.tags being undefined (#4962)
Item generated from embedded image attachment may not have attribute `tags`.
2025-02-19 02:03:23 -05:00
abaevbog
36838e6a9a qf: default arrowUp handling on locator menulist (#4965)
Do not close details panel on arrowUp when locator
type menulist is focused to allow for default handling
(e.g. select the next/previous option on windows).

Fixes: #4953
2025-02-19 02:03:23 -05:00
abaevbog
5b427ac332 save tagsBox updates when another item is selected (#4945)
When tagsBox item is changed by clicking on an itemTree row,
the blur even never fires on the currently focused tag. So
whatever changes were made will be discarded. To avoid it,
blur any opened tag rows (which triggers a saveTx)
when an item is being set, same way it is done in itemBox.

Also, a small tweak to properly fetch the focused tag
via editable-text:focus-within selector, since editable-text:focus is
always empty because the focus is on the input inside of
editable-text.

Fixes: #4942
2025-02-19 02:03:23 -05:00
abaevbog
65c77ddaae hide switch creator type in non-editable itembox (#4940)
Fixes: #4937
2025-02-19 02:03:23 -05:00
Dan Stillman
489d747b44 Update reader submodule 2025-02-19 02:03:22 -05:00
Dan Stillman
09813be7e0 Fix handling of redirecting OpenURL base URLs with url query parameter
For a URL like
`https://ezproxy.school.edu/login?url=http://resolver.ebscohost.com/openurl`,
without a trailing `?`, we were just adding a `%` to the end, making the
URL invalid after the redirection (`/openurl&url_ver=Z39.88-2004`
instead of `/openurl?url_ver=Z39.88-2004`).

(And apparently no one who sent in these URLs actually tested them
in-app?)
2025-02-19 02:03:22 -05:00
Abe Jellinek
85b111de4a Serialize _ensureFeedAbstractBrowserExists()
Prevent a potential race condition.
2025-02-19 02:03:22 -05:00
Dan Stillman
788c48cc26 Update note-editor submodule 2025-02-19 02:03:22 -05:00
Martynas Bagdonas
e3ae991bb3 Render ink annotation images when previewing attachment annotations 2025-02-19 02:03:22 -05:00
Martynas Bagdonas
e4a21b174f Update pdf-worker submodule
Fixes https://forums.zotero.org/discussion/120554/consistent-failures-to-export-annotated-pdf
2025-02-19 02:03:22 -05:00
Abe Jellinek
06fe4ad346 CookieSandbox: Fix XHR interface check
Actually broken since fx115.
2025-02-19 02:03:22 -05:00
windingwind
2019ac39a1 Fix ZoteroPane#buildFieldTransformMenu document variable (#4927)
fix: https://groups.google.com/g/zotero-dev/c/FwfrxviKi8I
2025-02-19 02:03:21 -05:00
Dan Stillman
02b48318d7 Update locales from Transifex 2025-02-19 02:03:21 -05:00
Dan Stillman
f836e74996 Update reader submodule 2025-02-19 02:03:21 -05:00
abaevbog
1321447374 let Zotero_Tabs.undoClose reopen duplicate tabs (#4920)
Fixes: #4919
2025-02-19 02:03:21 -05:00
Abe Jellinek
16221ac348 Update reader submodule 2025-02-19 02:03:21 -05:00
Tom Najdek
c0c00752e7 Improve importing from Mendeley
- Make the annotations importer more resilient to missing data
- Fix notebooks not being imported in some cases
- Fix an instance of a hidden browser leaked by the Mendeley Importer
2025-02-19 02:03:21 -05:00
Abe Jellinek
9fc00782de Update reader submodule 2025-02-19 02:03:21 -05:00
Dan Stillman
b9d1f50559 Remove debug line 2025-02-19 02:03:20 -05:00
Dan Stillman
77bdb0b4cb Shower nicer error for malformed WebDAV URL
We fix `://` or `//` automatically after #3483, but a leading ':'
character would still show an internal `NS_ERROR_MALFORMED_URI` error.
Instead, just say "[url] is not a valid WebDAV URL".
2025-02-19 02:03:20 -05:00
Dan Stillman
320399dca9 Strip leading :// or // in WebDAV URL
Closes #3483
2025-02-19 02:03:20 -05:00
Dan Stillman
8c54b98483 Reset file sync history when changing WebDAV URL
Fixes #3535
2025-02-19 02:03:20 -05:00
Dan Stillman
a0009f0fa3 Clear library storage version when resetting file sync history
Fixes #1753
2025-02-19 02:03:20 -05:00
windingwind
cb0b2f9d58 Item pane scroll pos fix after switching from note (#4897)
Follow up to #4879
fix: #4276
2025-02-19 02:03:20 -05:00
Dan Stillman
a74117d824 Update version 2025-02-19 02:03:19 -05:00
178 changed files with 6171 additions and 11253 deletions

View file

@ -19,6 +19,9 @@ jobs:
node-version: 18
#cache: npm
- name: Install dbus-glib
run: sudo apt-get update && sudo apt-get install -y libdbus-glib-1-2
# On GitHub
- name: Install xvfb
if: env.ACT != 'true'
@ -57,18 +60,17 @@ jobs:
env:
NODE_OPTIONS: --openssl-legacy-provider
# Somehow forces dpl v1 to be installed, even though v2 is installed without
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3' # Not needed with a `.ruby-version` or `.tool-versions`
ruby-version: '3.3'
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- 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'))
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/7.0' || endsWith(github.ref, '-hotfix'))
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@ -77,11 +79,8 @@ jobs:
cd build
zip -r ../build-zip/$GITHUB_SHA.zip *
cd ..
# Once dpl 2 works:
#sudo gem install --no-document dpl
#dpl s3 --bucket zotero-download --local_dir build-zip --upload_dir ci/client --acl public_read
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
gem install --no-document dpl -v '>= 2.0'
dpl s3 --bucket zotero-download --local_dir build-zip --upload_dir ci/client --acl public_read
- name: Run tests
run: xvfb-run test/runtests.sh -f

View file

@ -6,6 +6,10 @@ 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);

View file

@ -1,9 +1,9 @@
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Version of Gecko to build with
GECKO_VERSION_MAC="115.9.1esr"
GECKO_VERSION_LINUX="115.14.0esr"
GECKO_VERSION_WIN="115.14.0esr"
GECKO_VERSION_MAC="115.24.0esr"
GECKO_VERSION_LINUX="115.24.0esr"
GECKO_VERSION_WIN="115.24.0esr"
RUST_VERSION=1.69.0
# URL prefix for custom builds of Firefox components

@ -1 +1 @@
Subproject commit 13c92c090c12f6555bf6bab98780025e93d75077
Subproject commit 01b4a2abfcdaa35f478168af805df627cfedcb6a

View file

@ -7,7 +7,7 @@ ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
. "$APP_ROOT_DIR/config.sh"
CHANNEL="release"
BRANCH="main"
BRANCH="7.0"
if [ "`uname`" = "Darwin" ]; then
export SAFARI_APPEX="$ROOT_DIR/../safari-app-extension-builds/release/ZoteroSafariExtension.appex"
fi

View file

@ -333,6 +333,7 @@ function modify_omni {
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 '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

View file

@ -43,6 +43,7 @@
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">

View file

@ -969,99 +969,6 @@ 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
@ -1249,7 +1156,6 @@ function ZoteroProtocolHandler() {
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;

View file

@ -89,7 +89,6 @@ var ZoteroAdvancedSearch = new function() {
isTrash: () => false
};
this.itemsView.changeCollectionTreeRow(collectionTreeRow);
// Focus the first field in the window
Services.focus.moveFocus(window, null, Services.focus.MOVEFOCUS_FORWARD, 0);
}

View file

@ -110,7 +110,7 @@ window.Zotero_File_Interface_Bibliography = new function () {
}
}
if (_io.supportedNotes?.length < 1) {
if (_io.supportedNotes?.length < 2) {
styleConfigurator.toggleAttribute("no-multi-notes", true);
}

View file

@ -736,9 +736,9 @@ var CollectionTree = class CollectionTree extends LibraryTree {
return numSelected;
}
// If there's a single item and it's in the trash, switch to that
if (items.length == 1 && items[0].deleted) {
Zotero.debug("Item is deleted; switching to trash");
// If there's a single item and it or an ancestor are in the trash, switch to that
if (items.length == 1 && items[0].isInTrash()) {
Zotero.debug("Item is in trash; switching to trash");
await this.selectTrash(libraryID);
}
else {
@ -1326,6 +1326,13 @@ var CollectionTree = class CollectionTree extends LibraryTree {
return this.getRow(this.selection.focused).editable;
}
/**
* Returns TRUE if the underlying view is editable
*/
get filesEditable() {
return this.getRow(this.selection.focused).filesEditable;
}
getRowString(index) {
// During filtering, context rows return an empty string to not be selectable
// with key-based navigation

View file

@ -1692,6 +1692,16 @@ var Columns = class {
}
}
this._columns.sort((a, b) => a.ordinal - b.ordinal);
// Recover from scenarios where a plugin disables the "title" column and
// does not provide its own primary column, or is later removed and the
// "title" column is never restored.
let hasPrimaryColumn = this._columns.some(c => c.primary && !c.hidden);
if (!hasPrimaryColumn) {
Zotero.debug(`VirtualizedTable: Missing primary column, re-enabling the "title" column.`);
this._columns.find(c => c.dataKey === 'title').hidden = false;
}
this._adjustColumnWidths();
this.onResize(Object.fromEntries(this._columns.map(c => [c.dataKey, c.width])));
this._storePrefs(prefs);

View file

@ -149,7 +149,7 @@
this._abstractField.setAttribute('aria-label', Zotero.ItemFields.getLocalizedString('abstractNote'));
}
async _ensureFeedAbstractBrowserExists() {
_ensureFeedAbstractBrowserExists = Zotero.Utilities.Internal.serial(async () => {
if (!this._feedAbstractBrowser) {
// dynamically create a browser element to avoid spawning a process for every tab (#4530)
this._feedAbstractBrowser = document.createXULElement("browser");
@ -181,7 +181,7 @@
});
}
return Promise.resolve();
}
});
_discardFeedAbstractBrowser() {
if (this._feedAbstractBrowser) {

View file

@ -106,7 +106,7 @@
let imageAnnotationRendered = false;
for (let annotation of this._annotationItems) {
if (!imageAnnotationRendered
&& annotation.annotationType === 'image'
&& ['image', 'ink'].includes(annotation.annotationType)
&& !await Zotero.Annotations.hasCacheImage(annotation)) {
try {
await Zotero.PDFWorker.renderAttachmentAnnotations(annotation.parentID);

View file

@ -221,6 +221,12 @@
if (!(val instanceof Zotero.Item)) {
throw new Error("'item' must be a Zotero.Item");
}
// Blur editable-text of old attachment. Otherwise, _handleTitleBlur() would fire after
// the new item is set but before updateInfo() sets the title field to have the new
// item's value, causing the old item's title to be set on the new item.
if (this._item && val.id != this._item.id && document.activeElement.closest("editable-text")) {
document.activeElement.blur();
}
if (val.isAttachment()) {
this._item = val;
this.hidden = false;
@ -322,7 +328,6 @@
async asyncRender() {
if (!this.item) return;
if (this._asyncRendering) return;
if (!this._section.open) return;
if (this._isAlreadyRendered("async")) {
if (this._previewDiscarded) {

View file

@ -63,6 +63,7 @@
this._selectFieldSelection = null;
this._addCreatorRow = false;
this._switchedModeOfCreator = null;
this._popupNode = null;
this._lastUpdateCustomRows = "";
// Keep in sync with itemPaneManager.js
@ -125,7 +126,7 @@
init() {
this.initCollapsibleSection();
this._creatorTypeMenu.addEventListener('command', async (event) => {
var typeBox = document.popupNode;
var typeBox = this._popupNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
var typeID = event.explicitOriginalTarget.getAttribute('typeid');
@ -144,7 +145,7 @@
});
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (_event) => {
var row = document.popupNode.closest('.meta-row');
var row = this._popupNode.closest('.meta-row');
var typeBox = row.querySelector('.creator-type-label').parentNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
var item = this.item;
@ -182,7 +183,7 @@
});
this._id('zotero-creator-transform-menu').addEventListener('command', async (event) => {
var row = document.popupNode.closest('.meta-row');
var row = this._popupNode.closest('.meta-row');
var typeBox = row.querySelector('.creator-type-label').parentNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
@ -666,12 +667,13 @@
}
if (addLinkContextMenu) {
rowData.oncontextmenu = (event) => {
this._linkMenu.dataset.link = link;
this._linkMenu.dataset.val = val;
document.popupNode = rowLabel.parentElement;
let menupopup = this._linkMenu;
menupopup.dataset.link = link;
menupopup.dataset.val = val;
this._popupNode = rowLabel.parentElement;
let menupopup = this._id('zotero-link-menu');
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, event.target.closest('input'));
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, event.target);
this.handlePopupOpening(event, menupopup);
};
}
@ -1189,7 +1191,7 @@
span.className = 'creator-type-dropmarker';
labelWrapper.appendChild(span);
labelWrapper.addEventListener('click', (e) => {
document.popupNode = rowLabel;
this._popupNode = rowLabel;
this._creatorTypeMenu.openPopup(rowLabel);
// If the creator menu is opened via mouse-click, add a special attribute to
// blur the focused field so that icons do not show up after the menu is closed.
@ -1296,7 +1298,7 @@
optionsButton.setAttribute('id', `creator-${rowIndex}-options`);
optionsButton.setAttribute('data-l10n-id', "itembox-button-options");
let triggerPopup = (e) => {
document.popupNode = firstlast;
this._popupNode = firstlast;
let menupopup = this._id('zotero-creator-transform-menu');
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, e.target.closest('input'));
@ -2196,6 +2198,8 @@
textbox.setAttribute("min-lines", 1);
}
var value = textbox.value.trim();
// Remove trailing whitespaces from editable-text in case itemBox is not refreshed
textbox.value = value;
var [field, creatorIndex, creatorField] = fieldName.split('-');
@ -2330,6 +2334,7 @@
row.querySelector(".zotero-clicky-plus").hidden = true;
row.querySelector(".zotero-clicky-minus").hidden = true;
row.querySelector(".zotero-clicky-options").hidden = true;
row.querySelector(".zotero-clicky-switch-type").hidden = true;
continue;
}
@ -2389,7 +2394,7 @@
* @return {Promise}
*/
async swapNames(_event) {
var row = document.popupNode.closest('.meta-row');
var row = this._popupNode.closest('.meta-row');
var typeBox = row.querySelector('[fieldname]');
var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
var fields = this.getCreatorFields(row);
@ -2414,7 +2419,7 @@
* @return {Promise}
*/
async capitalizeCreatorName(_event) {
var row = document.popupNode.closest('.meta-row');
var row = this._popupNode.closest('.meta-row');
let label = row.querySelector('.meta-label');
var creatorIndex = parseInt(label.getAttribute('fieldname').split('-')[1]);
let [lastName, firstName] = [...row.querySelectorAll("editable-text")];

View file

@ -36,6 +36,12 @@
}
};
const waitDOMUpdate = async (timeout = 50) => {
return new Promise((resolve) => {
requestIdleCallback(resolve, { timeout });
});
};
const waitNoLongerThan = async (promise, ms = 1000) => {
return Promise.race([
promise,
@ -269,6 +275,11 @@
this._restorePinnedPane();
let panes = this.getPanes();
// Unobserve intersection to prevent unwanted rendering.
// Checking flags in _handleIntersection is not reliable because it's async.
this._toggleIntersectionObserver(false);
for (let box of [this._header, ...panes]) {
box.editable = this.editable;
box.tabID = this.tabID;
@ -291,7 +302,7 @@
this._paneParent.style.paddingBottom = '';
if (pinnedPaneElem) {
let paneID = pinnedPaneElem.dataset.pane;
this.scrollToPane(paneID, 'instant');
await this.scrollToPane(paneID, 'instant');
this.pinnedPane = paneID;
}
else {
@ -310,10 +321,17 @@
continue;
}
await waitNoLongerThan(box.asyncRender(), 500);
// Make sure the layout is updated for next isPaneVisible check
await waitDOMUpdate();
}
if (this.item.id == item.id) {
this._isRendering = false;
}
// Re-enable intersection observer
this._toggleIntersectionObserver(true);
if (Zotero.test) {
resolve();
}
@ -439,7 +457,9 @@
}
async scrollToPane(paneID, behavior = 'smooth') {
let pane = this.getEnabledPane(paneID);
let panes = this.getEnabledPanes();
let paneIndex = panes.findIndex(elem => elem.dataset.pane == paneID);
let pane = panes[paneIndex];
if (!pane) return null;
let scrollPromise;
@ -450,6 +470,9 @@
return null;
}
// Temporarily disable intersection observer to prevent unwanted rendering
this._toggleIntersectionObserver(false);
// The pane should always be at the very top
// If there isn't enough stuff below it for it to be at the top, we add padding
// We use a ::before pseudo-element for this so that we don't need to add another level to the DOM
@ -459,9 +482,31 @@
scrollPromise = this._waitForScroll();
scrollPromise.then(() => this._disableScrollHandler = false);
}
else {
// Wait for the next DOM update to make sure the height is updated before rendering
scrollPromise = waitDOMUpdate();
}
pane.scrollIntoView({ block: 'start', behavior });
pane.focus();
return scrollPromise;
await scrollPromise;
// Check current and following panes for async render
for (let i = paneIndex; i < panes.length; i++) {
let nextPane = panes[i];
// Stop if the pane is not visible anymore
if (!this.isPaneVisible(nextPane.dataset.pane)) {
break;
}
if (nextPane.asyncRender) {
await nextPane.asyncRender();
// Wait for the next DOM update to make sure the layout is updated
await waitDOMUpdate();
}
}
// Re-enable intersection observer
this._toggleIntersectionObserver(true);
return true;
}
_makeSpaceForPane(pane) {
@ -528,7 +573,17 @@
this._intersectionOb.disconnect();
}
this._intersectionOb = new IntersectionObserver(this._handleIntersection);
this.getPanes().forEach(elem => this._intersectionOb.observe(elem));
this._toggleIntersectionObserver(true);
}
_toggleIntersectionObserver(enabled) {
let panes = this.getPanes();
if (enabled) {
panes.forEach(elem => this._intersectionOb.observe(elem));
}
else {
panes.forEach(elem => this._intersectionOb.unobserve(elem));
}
}
_handleContainerScroll = () => {

View file

@ -151,8 +151,17 @@
return true;
}
renderItemPane(item) {
async renderItemPane(item) {
let previousMode = this.mode;
this.mode = "item";
// Fix https://forums.zotero.org/discussion/115450/zotero-7-beta-wrong-vertical-position-in-the-item-pane-after-switching-from-a-note
if (previousMode === "note") {
// Wait for DOM to update and then trigger item-details render
await new Promise((resolve) => {
requestIdleCallback(resolve, { timeout: 50 });
});
}
this._itemDetails.editable = this.editable;
this._itemDetails.tabID = "zotero-pane";

View file

@ -129,8 +129,6 @@
_contextMenuTarget = null;
_disableScrollHandler = false;
get container() {
return this._container;
}

View file

@ -212,7 +212,6 @@ import ReactDOM from "react-dom";
choosePane(pane) {
Zotero.debug(new Error().stack);
let otherPane;
if (pane.id == 'left-pane') {
otherPane = this._rightPane;

View file

@ -60,7 +60,7 @@
let removeAllItemTags = this._id('remove-all-item-tags');
this._id('remove-all-item-tags').addEventListener('command', this.removeAll);
this.querySelector('.body').addEventListener('contextmenu', (event) => {
removeAllItemTags.disabled = !this.count;
removeAllItemTags.disabled = !this.editable || !this.count;
this._id('tags-context-menu').openPopupAtScreen(event.screenX, event.screenY, true);
});
// Register our observer with priority 101 (after Zotero.Tags) so we get updated tag colors
@ -82,6 +82,9 @@
if (this._item == val) {
return;
}
// Blur events do not fire when item is changed by clicking on an itemTree row.
// Make sure any changes to the tags get saved before item is updated.
this.blurOpenField();
this._item = val;
}
@ -132,7 +135,7 @@
this._tagColors = Zotero.Tags.getColors(this.item.libraryID);
let focusedTag = this._id('rows').querySelector('editable-text:focus')?.value;
let focusedTag = this._id('rows').querySelector('editable-text:focus-within')?.value;
let tagRows = this._id('rows');
tagRows.replaceChildren();

View file

@ -117,7 +117,7 @@ const apiFetchUrl = async (tokens, url, headers = {}, options = {}) => {
const apiFetch = async (tokens, endPoint, params = {}, headers = {}, options = {}) => {
const stringParams = Object.entries(params).map(p => p.join('=')).join('&');
const url = MENDELEY_API_URL + '/' + endPoint + '?' + stringParams;
const url = `${MENDELEY_API_URL}/${endPoint}${stringParams.length ? '?' + stringParams : ''}`;
return apiFetchUrl(tokens, url, headers, options);
};
@ -183,8 +183,8 @@ const obtainReferenceManagerToken = async (login, password) => {
hasEnteredLogin = await browser.browsingContext.currentWindowGlobal
.getActor("MendeleyAuth")
.sendQuery("login", { login });
Zotero.debug(`hasEnteredLogin: ${hasEnteredLogin}`);
if (!hasEnteredLogin) {
browser.destroy();
reject(new Error("Failed to enter login"));
}
}
@ -195,8 +195,8 @@ const obtainReferenceManagerToken = async (login, password) => {
hasEnteredPassword = await browser.browsingContext.currentWindowGlobal
.getActor("MendeleyAuth")
.sendQuery("password", { password });
Zotero.debug(`hasEnteredPassword: ${hasEnteredPassword}`);
if (!hasEnteredPassword) {
browser.destroy();
reject(new Error("Failed to enter password"));
}
}
@ -205,6 +205,7 @@ const obtainReferenceManagerToken = async (login, password) => {
const cookies = cookieSandbox.getCookiesForURI(
Services.io.newURI("https://www.mendeley.com/reference-manager/library")
);
browser.destroy();
if (!cookies.accessToken) {
reject(new Error("Failed to obtain Mendeley access token"));
}
@ -218,6 +219,7 @@ const obtainReferenceManagerToken = async (login, password) => {
browser.load("https://www.mendeley.com/sign-in?routeTo=https://www.mendeley.com/reference-manager/library/");
Zotero.Promise.delay(ACCESS_TOKEN_TIMEOUT).then(() => {
browser.destroy();
reject(new Error("Timed out while obtaining Mendeley access token"));
});
});

View file

@ -38,6 +38,7 @@ var Zotero_Import_Mendeley = function () {
this._tokens = null;
this._credentials = null;
this._refManagerToken = null;
this._db = null;
this._file = null;
this._saveOptions = null;
@ -327,15 +328,18 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
}
if (this._credentials && !this.skipNotebooks) {
const token = await obtainReferenceManagerTokenWithRetry(this._credentials.username, this._credentials.password);
this._progress += 1; // progress one arbitrary "tick" assigned to importing notebooks task, we have 4 more left
if (token) {
if (!this._refManagerToken) {
const token = await obtainReferenceManagerTokenWithRetry(this._credentials.username, this._credentials.password);
this._refManagerToken = {
kind: 'referenceManager',
accessToken: token,
username: this._credentials.username,
password: this._credentials.password
};
}
this._progress += 1; // progress one arbitrary "tick" assigned to importing notebooks task, we have 4 more left
if (this._refManagerToken) {
const notebooks = await this._getNotebooksAPI();
const notesContent = await Promise.all(notebooks.map(notebook => this._translateNotebookToNoteContent(libraryID, notebook)));
this._progress += 1; // progress one arbitrary "tick" assigned to importing notebooks task, we have 1 more left
@ -353,7 +357,7 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
const isMappedToExisting = !!existingItem;
Zotero.debug(isMappedToExisting ? `Updating existing notebook "${uuid}" -> "${existingItem.key}"` : `Importing new notebook "${uuid}"`, 5);
let item = existingItem ?? new Zotero.Item('note');
let item = isMappedToExisting ? existingItem : new Zotero.Item('note');
item.libraryID = libraryID;
item.setNote(noteContent);
item.addRelation(predicate, uuid);
@ -1085,14 +1089,15 @@ Zotero_Import_Mendeley.prototype._translateNotebookToNoteContent = async functio
'mendeleyDB:annotationUUID',
annotationUUID
);
let attachmentItem = Zotero.Items.get(annotation.parentID);
let jsonAnnotation = await Zotero.Annotations.toJSON(annotation);
jsonAnnotation.attachmentItemID = attachmentItem.id;
jsonAnnotation.id = annotation.key;
if (annotation) {
let attachmentItem = Zotero.Items.get(annotation.parentID);
let jsonAnnotation = await Zotero.Annotations.toJSON(annotation);
jsonAnnotation.attachmentItemID = attachmentItem.id;
jsonAnnotation.id = annotation.key;
const { html } = Zotero.EditorInstanceUtilities.serializeAnnotations([jsonAnnotation]);
return html;
const { html } = Zotero.EditorInstanceUtilities.serializeAnnotations([jsonAnnotation]);
return html;
}
}
}
}
@ -1744,6 +1749,11 @@ Zotero_Import_Mendeley.prototype._saveAnnotations = async function (annotations,
// do not attempt to import annotations for non-PDF files
return;
}
// Mendeley produces empty, type-less annotations that confuse the PDF worker
annotations = annotations.filter(
annotation => ['highlight', 'note', 'image', 'ink', 'underline', 'text'].includes(annotation.type)
);
let annotationMap = new Map();
for (let annotation of annotations) {
@ -1766,6 +1776,7 @@ Zotero_Import_Mendeley.prototype._saveAnnotations = async function (annotations,
for (let annotation of annotations) {
// Ignore empty highlights
if (annotation.type == 'highlight' && !annotation.text) {
Zotero.debug(`Skipping empty highlight with uuid ${annotation.uuid}`, 5);
continue;
}

View file

@ -656,13 +656,22 @@ var Zotero_QuickFormat = new function () {
}).map(t => t.data.itemID);
if (!itemIDs.length) return [];
let items = itemIDs.map((itemID) => {
let item = Zotero.Items.get(itemID);
// Fetch top-most items and load necessary data, in case tabs belong to an unloaded library
let items = [];
for (let itemID of itemIDs) {
let item = await Zotero.Items.getAsync(itemID);
if (item && item.parentItemID) {
itemID = item.parentItemID;
item = await Zotero.Items.getAsync(item.parentItemID);
}
return Zotero.Cite.getItem(itemID);
});
// Ignore tabs from libraries that are not specified
if (io.filterLibraryIDs) {
let itemInLibrary = io.filterLibraryIDs.some(id => id === item.libraryID);
if (!itemInLibrary) continue;
}
items.push(item);
}
await Zotero.Items.loadDataTypes(items);
let matchedItems = new Set(items);
if (options.searchString) {
Zotero.debug("QuickFormat: Searching open tabs");
@ -2287,7 +2296,8 @@ var Zotero_QuickFormat = new function () {
}
}
// Arrow up closes the popup
if (event.key === "ArrowUp") {
// (except on locator type dropdown to allow for default handling)
if (event.key === "ArrowUp" && event.target.tagName !== "menulist") {
skipInputRefocus = true;
itemPopover.hidePopup();
return stopEvent();

View file

@ -3097,7 +3097,10 @@ var ItemTree = class ItemTree extends LibraryTree {
// Accessibility
div.setAttribute('role', 'treeitem');
div.setAttribute('aria-level', this.getLevel(index) + 1);
if (!this.isContainerEmpty(index)) {
if (this.isContainerEmpty(index)) {
div.removeAttribute('aria-expanded');
}
else {
div.setAttribute('aria-expanded', this.isContainerOpen(index));
}
if (rowData.contextRow) {

View file

@ -59,13 +59,13 @@
<separator class="thin"/>
<hbox id="button-bar" align="center">
<hbox pack="start" flex="1">
<button label="Toggle" onclick="toggleLocateEngines()" flex="1"/>
<button id="locateManager-restoreDefaults" label="&zotero.preferences.locate.restoreDefaults;" oncommand="restoreDefaultLocateEngines()" flex="1"/>
<hbox id="button-bar">
<hbox pack="start">
<button label="Toggle" onclick="toggleLocateEngines()" />
<button id="locateManager-restoreDefaults" label="&zotero.preferences.locate.restoreDefaults;" oncommand="restoreDefaultLocateEngines()" />
</hbox>
<hbox pack="end" flex="2">
<button disabled="true" id="locateManager-delete" label="-" oncommand="deleteLocateEngine()" flex="0.5"/>
<hbox pack="end">
<button disabled="true" id="locateManager-delete" label="-" oncommand="deleteLocateEngine()" />
</hbox>
</hbox>

View file

@ -345,25 +345,13 @@ var Zotero_LocateMenu = new function() {
* Get the first 50 selected items
*/
function _getSelectedItems() {
if (Zotero_Tabs.selectedID == 'zotero-pane') {
var allSelectedItems = ZoteroPane_Local.getSelectedItems();
var selectedItems = [];
while (selectedItems.length < 50 && allSelectedItems.length) {
var item = allSelectedItems.shift();
if (!item.isNote()) selectedItems.push(item);
}
return selectedItems;
}
else {
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
let item = Zotero.Items.get(reader.itemID);
if (item.parentItem) {
return [item.parentItem];
}
}
return [];
var allSelectedItems = ZoteroPane.getSelectedItems();
var selectedItems = [];
while (selectedItems.length < 50 && allSelectedItems.length) {
var item = allSelectedItems.shift();
if (!item.isNote()) selectedItems.push(item);
}
return selectedItems;
}
var ViewOptions = {};

View file

@ -593,7 +593,7 @@ Zotero_Preferences.Attachment_Base_Directory = {
},
changePath: Zotero.Promise.coroutine(function* (basePath) {
changePath: async function (basePath) {
Zotero.debug(`New base directory is ${basePath}`);
if (Zotero.File.directoryContains(Zotero.DataDirectory.dir, basePath)) {
@ -605,38 +605,37 @@ Zotero_Preferences.Attachment_Base_Directory = {
);
}
// Find all current attachments with relative attachment paths
var sql = "SELECT itemID FROM itemAttachments WHERE linkMode=? AND path LIKE ?";
var params = [
Zotero.Attachments.LINK_MODE_LINKED_FILE,
Zotero.Attachments.BASE_PATH_PLACEHOLDER + "%"
];
var oldRelativeAttachmentIDs = yield Zotero.DB.columnQueryAsync(sql, params);
//Find all attachments on the new base path
// Find all attachments on the new base path
var sql = "SELECT itemID FROM itemAttachments WHERE linkMode=?";
var params = [Zotero.Attachments.LINK_MODE_LINKED_FILE];
var allAttachments = yield Zotero.DB.columnQueryAsync(sql, params);
var allAttachments = await Zotero.DB.columnQueryAsync(sql, params);
var newAttachmentPaths = {};
var numNewAttachments = 0;
var numOldAttachments = 0;
for (let i=0; i<allAttachments.length; i++) {
let attachmentID = allAttachments[i];
for (let attachmentID of allAttachments) {
let attachmentPath;
let relPath = false
let relPath;
try {
let attachment = yield Zotero.Items.getAsync(attachmentID);
let attachment = await Zotero.Items.getAsync(attachmentID);
// This will return FALSE for relative paths if base directory
// isn't currently set
attachmentPath = attachment.getFilePath();
// Get existing relative path
let storedPath = attachment.attachmentPath;
if (storedPath.startsWith(Zotero.Attachments.BASE_PATH_PLACEHOLDER)) {
relPath = storedPath.substr(Zotero.Attachments.BASE_PATH_PLACEHOLDER.length);
relPath = storedPath.substring(Zotero.Attachments.BASE_PATH_PLACEHOLDER.length);
// Use platform-specific slashes, which PathUtils.joinRelative() requires below
relPath = Zotero.Attachments.fixPathSlashes(relPath);
}
// If a file with the same relative path exists within the new base directory,
// don't touch the attachment, since it will continue to work
if (await IOUtils.exists(PathUtils.joinRelative(basePath, relPath))) {
Zotero.debug(`${relPath} found within new base path -- skipping`);
numNewAttachments++;
continue;
}
}
catch (e) {
// Don't deal with bad attachment paths. Just skip them.
@ -644,16 +643,6 @@ Zotero_Preferences.Attachment_Base_Directory = {
continue;
}
// If a file with the same relative path exists within the new base directory,
// don't touch the attachment, since it will continue to work
if (relPath) {
if (yield IOUtils.exists(PathUtils.joinRelative(basePath, relPath))) {
Zotero.debug(`${relPath} found within new base path -- skipping`);
numNewAttachments++;
continue;
}
}
// Files within the new base directory need to be updated to use
// relative paths (or, if the new base directory is an ancestor or
// descendant of the old one, new relative paths)
@ -674,7 +663,7 @@ Zotero_Preferences.Attachment_Base_Directory = {
}
}
//Confirm change of the base path
// Confirm change of the base path
var ps = Services.prompt;
var chooseStrPrefix = 'attachmentBasePath.chooseNewPath.';
@ -730,7 +719,7 @@ Zotero_Preferences.Attachment_Base_Directory = {
Zotero.Prefs.set('saveRelativeAttachmentPath', true);
// Resave all attachments on base path (so that their paths become relative)
// and all other relative attachments (so that their paths become absolute)
yield Zotero.Utilities.Internal.forEachChunkAsync(
await Zotero.Utilities.Internal.forEachChunkAsync(
Object.keys(newAttachmentPaths),
100,
function (chunk) {
@ -747,12 +736,12 @@ Zotero_Preferences.Attachment_Base_Directory = {
skipDateModifiedUpdate: true
});
}
})
});
}
);
return true;
}),
},
clearPath: Zotero.Promise.coroutine(function* () {

View file

@ -37,6 +37,7 @@ Zotero_Preferences.Sync = {
noChar: '\uD83D\uDEAB',
init: async function () {
this.storeLastStorageSettings();
this.updateStorageSettingsUI();
this.updateStorageSettingsGroupsUI();
@ -426,7 +427,17 @@ Zotero_Preferences.Sync = {
addRow(group.data.name, "G" + group.id, librariesToSkip.indexOf("G" + group.id) == -1);
}
},
_lastStorageProtocol: null,
_lastStorageURL: null,
storeLastStorageSettings: function () {
this._lastStorageProtocol = Zotero.Prefs.get('sync.storage.protocol');
this._lastStorageURL = Zotero.Prefs.get('sync.storage.url');
},
updateStorageSettingsUI: Zotero.Promise.coroutine(function* () {
this.unverifyStorageServer();
@ -480,17 +491,19 @@ Zotero_Preferences.Sync = {
onStorageSettingsChange: Zotero.Promise.coroutine(function* () {
// Clean URL
Zotero.Prefs.set('sync.storage.url', Zotero.Prefs.get('sync.storage.url').trim()
.replace(/(^https?:\/\/|\/zotero\/?$|\/$)/g, ''));
var oldProtocol = Zotero.Prefs.get('sync.storage.protocol');
var oldProtocol = this._lastStorageProtocol;
var oldURL = this._lastStorageURL;
// Necessary for pref to update
yield Zotero.Promise.delay(1);
var newProtocol = Zotero.Prefs.get('sync.storage.protocol');
if (oldProtocol != newProtocol) {
var newURL = Zotero.Prefs.get('sync.storage.url').trim()
// Strip scheme, leading '://' or '//' (#3483), and trailing '/zotero'
.replace(/(^https?:\/\/|^:?\/\/|\/zotero\/?$|\/$)/g, '')
Zotero.Prefs.set('sync.storage.url', newURL);
if (oldProtocol != newProtocol || oldURL != newURL) {
yield Zotero.Sync.Storage.Local.resetAllSyncStates(Zotero.Libraries.userLibraryID);
}
@ -547,6 +560,7 @@ Zotero_Preferences.Sync = {
}
this.updateStorageSettingsUI();
this.storeLastStorageSettings();
}),

View file

@ -1,4 +1,4 @@
<html id="run-js">
<html id="run-js" windowtype="zotero:run-js">
<head>
<title data-l10n-id="runJS-title"></title>
<link href="chrome://zotero-platform/content/zotero.css" rel="stylesheet"/>
@ -28,8 +28,9 @@
</div>
<div class="textbox-container">
<div class="textbox-header">
<label id="result-label" class="textbox-label" for="result" data-l10n-id="runJS-result"/>
<div class="textbox-header result-header">
<label id="result-label" class="textbox-label" for="result" data-l10n-id="runJS-result"></label>
<div id="loading-spinner" class="zotero-spinner-16"></div>
</div>
<textarea id="result" readonly aria-disabled="true"></textarea>
</div>

View file

@ -14,6 +14,8 @@ async function run() {
var isAsync = document.getElementById('run-as-async').checked;
var result;
var resultTextbox = document.getElementById('result');
var spinner = document.getElementById("loading-spinner");
spinner.setAttribute("status", "animate");
try {
if (isAsync) {
code = '(async function () {' + code + '})()';
@ -26,10 +28,27 @@ async function run() {
catch (e) {
resultTextbox.classList.add('error');
resultTextbox.textContent = e;
spinner.removeAttribute("status");
return;
}
// Hide the spinner after a small delay so it briefly appears even
// if the code runs fast to indicate that everything did run
setTimeout(() => {
spinner.removeAttribute("status");
}, 100);
resultTextbox.classList.remove('error');
resultTextbox.textContent = typeof result == 'string' ? result : Zotero.Utilities.varDump(result);
if (typeof result == 'string') {
resultTextbox.textContent = result;
}
else if (result !== undefined) {
resultTextbox.textContent = Zotero.Utilities.varDump(result);
}
else {
// when nothing is returned, log undefined as the return value but
// for clarity also add a note that the JS run was successful
resultTextbox.textContent = `===>undefined<=== (${Zotero.getString("runJS-completed")})`;
}
}
// eslint-disable-next-line no-unused-vars

View file

@ -309,23 +309,12 @@ const ZoteroStandalone = new function() {
let win = Zotero.getMainWindow();
if (win) {
if (win.Zotero_Tabs.selectedID == 'zotero-pane') {
try {
selected = win.ZoteroPane.getSelectedItems();
}
catch (e) {
}
win.ZoteroPane.updateQuickCopyCommands(selected);
try {
selected = win.ZoteroPane.getSelectedItems();
}
else {
let reader = Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID);
if (reader) {
let item = Zotero.Items.get(reader.itemID);
selected = item.parentItem && [item.parentItem] || [];
item = item.parentItem || item;
win.ZoteroPane.updateQuickCopyCommands([item]);
}
catch (e) {
}
win.ZoteroPane.updateQuickCopyCommands(selected);
}
var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);

View file

@ -228,7 +228,8 @@ var Zotero_Tabs = new function () {
});
};
this.restoreState = function (tabs) {
this.restoreState = async function (tabs) {
let itemIDs = [];
for (let i = 0; i < tabs.length; i++) {
let tab = tabs[i];
if (tab.type === 'library') {
@ -250,11 +251,17 @@ var Zotero_Tabs = new function () {
data: tab.data,
select: tab.selected
});
itemIDs.push(tab.data.itemID);
}
}
}
// Unset the previously selected tab id, because it was set when restoring tabs
this._prevSelectedID = null;
// Some items may belong to groups that are not yet loaded. Load them here so that
// every component related to tabs (e.g. tabs menu) does not have to handle
// potentially not-yet-loaded items.
let items = await Zotero.Items.getAsync(itemIDs);
await Zotero.Items.loadDataTypes(items);
};
/**
@ -399,7 +406,8 @@ var Zotero_Tabs = new function () {
null,
{
tabIndex: tab.index,
openInBackground: true
openInBackground: true,
allowDuplicate: true
}
));
if (tab.index > maxIndex) {

View file

@ -35,6 +35,7 @@
id="csl-edit"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
windowtype="zotero:style-editor"
onload="Zotero_CSL_Editor.init();"
title="&styles.editor;">

View file

@ -681,7 +681,7 @@ Zotero.Attachments = new function () {
*/
this.createURLAttachmentFromTemporaryStorageDirectory = async function (options) {
if (!options.directory) throw new Error("'directory' not provided");
if (!options.libraryID) throw new Error("'libraryID' not provided");
if (!options.libraryID && !options.parentItemID) throw new Error("'libraryID' or 'parentItemID' not provided");
if (!options.filename) throw new Error("'filename' not provided");
if (!options.url) throw new Error("'directory' not provided");
if (!options.contentType) throw new Error("'contentType' not provided");
@ -921,20 +921,7 @@ Zotero.Attachments = new function () {
}
}
else {
Zotero.debug("Saving file with saveURI()");
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(nsIWBP);
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_FROM_CACHE;
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var nsIURL = ioService.newURI(url, null, null);
var deferred = Zotero.Promise.defer();
wbp.progressListener = new Zotero.WebProgressFinishListener(function () {
deferred.resolve();
});
Zotero.Utilities.Internal.saveURI(wbp, nsIURL, tmpFile);
yield deferred.promise;
yield Zotero.HTTP.download(url, tmpFile);
}
var attachmentItem;
@ -990,6 +977,96 @@ Zotero.Attachments = new function () {
return attachmentItem;
});
/**
* Save an attachment from a nsIInputStream
*
* @param {Object} options
* @param {String} options.url
* @param {nsIStream} options.stream - Stream with data
* @param {Integer} options.byteCount - Number of bytes in the stream, usually from the
* 'Content-Length' HTTP header.
* @param {String} options.contentType - Expected content type
* @param {Integer} [options.libraryID] Parent item ID if child attachment
* @param {Integer} [options.parentItemID] Parent item ID if child attachment
* Either options.libraryID or options.parentItemID are mandatory
* @param {Array<String|Integer>} [options.collections] Collection ids or keys
* @param {String} [options.title]
* @param {Object} [options.saveOptions] - Options to pass to Zotero.Item::save()
* @return {Promise<Zotero.Item>} - A promise for the created attachment item
*/
this.importFromNetworkStream = async (options) => {
if (!options.url) throw new Error("'url' not provided");
if (!options.stream) throw new Error("'stream' not provided");
if (!options.byteCount) throw new Error("'byteCount' not provided");
if (!options.contentType) throw new Error("'contentType' not provided");
Zotero.debug("Importing attachment item from network stream");
let url = options.url;
let stream = options.stream;
let contentType = options.contentType;
let libraryID = options.libraryID;
let parentItemID = options.parentItemID;
let collections = options.collections;
let title = options.title;
let saveOptions = options.saveOptions;
if (parentItemID && collections) {
throw new Error("parentItemID and collections cannot both be provided");
}
// Create a temporary file
let filename;
if (parentItemID) {
let parentItem = Zotero.Items.get(parentItemID);
let fileBaseName = this.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
let ext = this._getExtensionFromURL(url, contentType);
filename = fileBaseName + (ext != '' ? '.' + ext : '');
}
else {
filename = Zotero.File.truncateFileName(this._getFileNameFromURL(url, contentType), 100);
}
let tmpDirectory = (await this.createTemporaryStorageDirectory()).path;
let destDirectory;
let attachmentItem;
try {
let tmpFile = OS.Path.join(tmpDirectory, filename);
await Zotero.File.putNetworkStream(tmpFile, stream, options.byteCount);
attachmentItem = await this.createURLAttachmentFromTemporaryStorageDirectory({
directory: tmpDirectory,
libraryID,
parentItemID,
title,
filename,
url,
contentType,
collections,
saveOptions
});
}
catch (e) {
Zotero.debug(e, 1);
// Clean up
try {
if (tmpDirectory) {
await OS.File.removeDir(tmpDirectory, { ignoreAbsent: true });
}
if (destDirectory) {
await OS.File.removeDir(destDirectory, { ignoreAbsent: true });
}
}
catch (e) {
Zotero.debug(e, 1);
}
throw e;
}
return attachmentItem;
};
/**
@ -1100,20 +1177,18 @@ Zotero.Attachments = new function () {
let enforcingFileType = false;
try {
await new Zotero.Promise(function (resolve) {
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(Components.interfaces.nsIWebBrowserPersist);
if (options.cookieSandbox) {
options.cookieSandbox.attachToInterfaceRequestor(wbp);
let headers = {};
if (options.referrer) {
headers.Referer = options.referrer;
}
await Zotero.HTTP.download(
url,
path,
{
headers,
cookieSandbox: options.cookieSandbox
}
wbp.progressListener = new Zotero.WebProgressFinishListener(() => resolve());
var headers = {};
if (options.referrer) {
headers.Referer = options.referrer;
}
Zotero.Utilities.Internal.saveURI(wbp, url, path, headers);
});
);
if (options.enforceFileType) {
enforcingFileType = true;
@ -2348,7 +2423,11 @@ Zotero.Attachments = new function () {
value = value.toLowerCase().replace(/\s+/g, '_');
break;
case 'camel':
value = value.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase());
value = value.toLowerCase().replace(/[^\p{L}\d]+(.)/gu, (m, chr) => chr.toUpperCase());
break;
case 'pascal':
value = value.toLowerCase().replace(/[^\p{L}\d]+(.)/gu, (m, chr) => chr.toUpperCase());
value = value.slice(0, 1).toUpperCase() + value.slice(1);
break;
}
return value;
@ -2413,6 +2492,9 @@ Zotero.Attachments = new function () {
obj[name] = (args) => {
return common(commonCreators(name, args), args);
};
obj[`${name}Count`] = (args) => {
return common(getSlicedCreatorsOfType(name, Infinity).length.toString(), args);
};
return obj;
}, {});

View file

@ -28,7 +28,7 @@ const { HiddenBrowser } = ChromeUtils.import("chrome://zotero/content/HiddenBrow
Zotero.BrowserDownload = {
HANDLED_URLS: {
'https://zotero-static.s3.amazonaws.com/test-pdf-redirect.html': "html",
'://www.sciencedirect.com': "#challenge-form"
'://www.sciencedirect.com': "#captcha-box"
},
/**

View file

@ -208,14 +208,14 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('filesEditable', function ()
}
var libraryID = this.ref.libraryID;
if (this.isGroup()) {
return this.ref.filesEditable;
return this.ref.editable && this.ref.filesEditable;
}
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled() || this.isRetracted()) {
var type = Zotero.Libraries.get(libraryID).libraryType;
if (type == 'group') {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
var group = Zotero.Groups.get(groupID);
return group.filesEditable;
return group.editable && group.filesEditable;
}
throw ("Unknown library type '" + type + "' in Zotero.CollectionTreeRow.filesEditable");
}

File diff suppressed because it is too large Load diff

View file

@ -343,11 +343,15 @@ Zotero.CookieSandbox.Observer = new function() {
// Zotero.debug(`CookieSandbox: Found the browser via doc of load group for ${channelURI}`, 5);
trackedBy = this.trackedBrowsers.get(browser);
}
else if (notificationCallbacks instanceof XMLHttpRequest) {
// Zotero.debug(`CookieSandbox: Found the browser via XHR for ${channelURI}`, 5);
tested = true;
}
else {
// try getting as an XHR or nsIWBP
// try getting as an nsIWBP
try {
notificationCallbacks.QueryInterface(Components.interfaces.nsIXMLHttpRequest);
// Zotero.debug(`CookieSandbox: Found the browser via XHR or nsIWBP for ${channelURI}`, 5);
notificationCallbacks.QueryInterface(Components.interfaces.nsIWebBrowserPersist);
// Zotero.debug(`CookieSandbox: Found the browser via nsIWBP for ${channelURI}`, 5);
tested = true;
}
catch (e) {}

View file

@ -2183,6 +2183,22 @@ Zotero.Item.prototype.setSourceKey = function(sourceItemKey) {
}
/**
* Determine whether the item or any of its ancestors is in the trash
*
* @return {Boolean}
*/
Zotero.Item.prototype.isInTrash = function () {
if (this.deleted) {
return true;
}
if (this.isTopLevelItem()) {
return false;
}
return this.parentItem.isInTrash();
};
////////////////////////////////////////////////////////
//
//
@ -2540,12 +2556,15 @@ Zotero.Item.prototype.numNonHTMLFileAttachments = function () {
};
Zotero.Item.prototype.numFileAttachmentsWithContentType = function (contentType) {
Zotero.Item.prototype.getFileAttachmentsWithContentType = function (contentType) {
this._requireData('childItems');
return this.getAttachments()
.map(itemID => Zotero.Items.get(itemID))
.filter(item => item.isFileAttachment() && item.attachmentContentType == contentType)
.length;
return Zotero.Items.get(this.getAttachments())
.filter(item => item.isFileAttachment() && item.attachmentContentType == contentType);
};
Zotero.Item.prototype.numFileAttachmentsWithContentType = function (contentType) {
return this.getFileAttachmentsWithContentType(contentType).length;
};
@ -3958,7 +3977,8 @@ Zotero.Item.prototype.setAutoAttachmentTitle = function ({ ignoreAutoRenamePrefs
// If this is the only attachment of its type on the parent item and the
// file is being renamed, give it a default title ("PDF", "Webpage", etc.)
let isFirstOfType = this.parentItemID
&& this.parentItem.numFileAttachmentsWithContentType(this.attachmentContentType) <= 1;
&& this.parentItem.getFileAttachmentsWithContentType(this.attachmentContentType)
.every(item => item === this);
let isBeingRenamed = ignoreAutoRenamePrefs || Zotero.Attachments.shouldAutoRenameAttachment(this);
if (isFirstOfType && isBeingRenamed) {
let defaultTitle = this._getDefaultTitleForAttachmentContentType();
@ -4046,7 +4066,7 @@ for (let name of ['type', 'authorName', 'text', 'comment', 'color', 'pageLabel',
break;
case 'sortIndex':
if (!/^(\d{5}\|\d{6}\|\d{5}|\d{5}\|\d{8}|\d{8})$/.test(value)) {
if (!/^(\d{5}\|\d{6}\|\d{5}|\d{5}\|\d{8}|\d{7,8})$/.test(value)) {
throw new Error(`Invalid sortIndex '${value}'`);
}
break;
@ -5124,6 +5144,7 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
*/
Zotero.Item.prototype.fromJSON = function (json, options = {}) {
var strict = !!options.strict;
var migrateExtra = !strict;
if (!json.itemType && !this._itemTypeID) {
throw new Error("itemType property not provided");
@ -5139,29 +5160,34 @@ Zotero.Item.prototype.fromJSON = function (json, options = {}) {
var isValidForType = {};
var setFields = new Set();
var { itemType, fields: extraFields, creators: extraCreators, extra } =
Zotero.Utilities.Internal.extractExtraFields(
json.extra || '',
this,
Object.keys(json)
// TEMP until we move creator lines to real creators
.concat('creators')
);
// If a different item type was parsed out of Extra, use that instead
if (itemType && json.itemType != itemType) {
itemTypeID = Zotero.ItemTypes.getID(itemType);
this.setType(itemTypeID);
var extraFields = new Map();
var extraCreators = [];
var extra = json.extra || '';
// Transfer valid fields from Extra to regular fields
if (migrateExtra) {
let itemType;
({ itemType, fields: extraFields, creators: extraCreators, extra } =
Zotero.Utilities.Internal.extractExtraFields(
extra,
this,
Object.keys(json)
// TEMP until we move creator lines to real creators
.concat('creators')
));
// If a different item type was parsed out of Extra, use that instead
if (itemType && json.itemType != itemType) {
itemTypeID = Zotero.ItemTypes.getID(itemType);
this.setType(itemTypeID);
}
for (let [field, value] of extraFields) {
this.setField(field, value);
setFields.add(field);
extraFields.delete(field);
}
}
var invalidFieldLogLines = new Map();
// Transfer valid fields from Extra to regular fields
// Currently disabled
/*for (let [field, value] of extraFields) {
this.setField(field, value);
setFields.add(field);
extraFields.delete(field);
}*/
for (let field in json) {
let val = json[field];

View file

@ -1653,19 +1653,19 @@ Zotero.Items = function() {
/**
* Purge unused data values
*/
this.purge = Zotero.Promise.coroutine(function* () {
Zotero.DB.requireTransaction();
this.purge = async function () {
if (!Zotero.Prefs.get('purge.items')) {
return;
}
var sql = "DELETE FROM itemDataValues WHERE valueID NOT IN "
+ "(SELECT valueID FROM itemData)";
yield Zotero.DB.queryAsync(sql);
await Zotero.DB.executeTransaction(async function () {
let sql = "DELETE FROM itemDataValues WHERE valueID NOT IN "
+ "(SELECT valueID FROM itemData)";
await Zotero.DB.queryAsync(sql, [], { ignoreDBLock: true });
}, { disableForeignKeys: true });
Zotero.Prefs.set('purge.items', false)
});
};

View file

@ -291,10 +291,12 @@ Zotero.Library.prototype._set = function(prop, val) {
if (newVal != val) {
throw new Error(`${prop} must be an integer (${typeof val} '${val}' given)`);
}
// Ensure that it is never decreasing, unless it is being set to -1
// by Reset File Sync History
if (val != -1 && val < this._libraryStorageVersion) {
throw new Error(prop + ' cannot decrease');
}
val = newVal;
// Ensure that it is never decreasing
if (val < this._libraryStorageVersion) throw new Error(prop + ' cannot decrease');
break;
case '_libraryLastSync':

View file

@ -662,17 +662,9 @@ Zotero.SearchConditions = new function(){
continue;
}
let localized = self.getLocalizedName(i);
// Hack to use a different name for "issue" in French locale,
// where 'number' and 'issue' are translated the same
// https://forums.zotero.org/discussion/14942/
if (Zotero.ItemFields.getName(fieldID) == 'issue' && locale.substr(0, 2) == 'fr') {
localized = "Num\u00E9ro (p\u00E9riodique)";
}
_standardConditions.push({
name: i,
localized: localized,
localized: self.getLocalizedName(i),
operators: _conditions[i]['operators'],
flags: _conditions[i]['flags']
});

View file

@ -418,12 +418,14 @@ Zotero.DBConnection.prototype.getNextName = async function (libraryID, table, fi
/**
* @param {Function} func - Async function containing `await Zotero.DB.queryAsync()` and similar
* @param {Object} [options]
* @param {Boolean} [options.disableForeignKeys] - Disable foreign key constraints before
* transaction and re-enable after. (`PRAGMA foreign_keys=0|1` is a no-op during a transaction.)
* @param {Boolean} [options.disableForeignKeys] - Disable foreign key checks before the
* transaction and re-enable after, while preventing any other queries from running.
* `queryAsync()` and similar within `func` must pass `ignoreDBLock: true` or they'll hang.
* (`PRAGMA foreign_keys=OFF|ON` is a no-op during a transaction, so it can't just be set within
* the function.)
* @return {Promise} - Promise for result of generator function
*/
Zotero.DBConnection.prototype.executeTransaction = async function (func, options) {
options = options || {};
Zotero.DBConnection.prototype.executeTransaction = async function (func, options = {}) {
var resolve;
// Set temporary options for this transaction that will be reset at the end
@ -461,34 +463,49 @@ Zotero.DBConnection.prototype.executeTransaction = async function (func, options
}
}
if (options.disableForeignKeys) {
await this.queryAsync("PRAGMA foreign_keys = 0");
let result;
let resolveDBLockPromise;
try {
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
if (func.constructor.name == 'GeneratorFunction') {
throw new Error("Zotero.DB.executeTransaction() no longer takes a generator function "
+ "-- pass an async function instead");
}
if (options.disableForeignKeys) {
this._dbLockPromise = new Promise(function () {
resolveDBLockPromise = arguments[0];
});
await this.queryAsync("PRAGMA foreign_keys=OFF", [], { ignoreDBLock: true });
}
result = await conn.executeTransaction(func);
Zotero.debug(`Committed DB transaction ${id}`, 4);
}
var conn = this._getConnection(options) || (await this._getConnectionAsync(options));
if (func.constructor.name == 'GeneratorFunction') {
throw new Error("Zotero.DB.executeTransaction() no longer takes a generator function "
+ "-- pass an async function instead");
finally {
if (options.disableForeignKeys) {
await this.queryAsync("PRAGMA foreign_keys=ON", [], { ignoreDBLock: true });
if (resolveDBLockPromise) {
resolveDBLockPromise();
this._dbLockPromise = undefined;
}
}
}
var result = await conn.executeTransaction(func);
Zotero.debug(`Committed DB transaction ${id}`, 4);
// Clear transaction time
if (this._transactionDate) {
this._transactionDate = null;
}
this._transactionID = null;
if (options.vacuumOnCommit) {
Zotero.debug('Vacuuming database');
await this.queryAsync('VACUUM');
Zotero.debug('Done vacuuming');
}
this._transactionID = null;
// Function to run once transaction has been committed but before any
// permanent callbacks
if (options.onCommit) {
@ -545,10 +562,6 @@ Zotero.DBConnection.prototype.executeTransaction = async function (func, options
throw e;
}
finally {
if (options.disableForeignKeys) {
await this.queryAsync("PRAGMA foreign_keys = 1");
}
// Reset options back to their previous values
if (options) {
for (let option in options) {
@ -596,13 +609,19 @@ Zotero.DBConnection.prototype.requireTransaction = function () {
* rows are Proxy objects that return values from the
* underlying mozIStorageRows based on column names.
*/
Zotero.DBConnection.prototype.queryAsync = async function (sql, params, options) {
Zotero.DBConnection.prototype.queryAsync = async function (sql, params, options = {}) {
try {
let onRow = null;
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
if (!options || !options.noParseParams) {
[sql, params] = this.parseQueryAndParams(sql, params);
}
if (this._dbLockPromise && !options.ignoreDBLock) {
Zotero.debug(`Waiting for DB lock to be released: ${sql}`, 2);
await this._dbLockPromise;
}
if (Zotero.Debug.enabled) {
this.logQuery(sql, params, options);
}
@ -718,6 +737,12 @@ Zotero.DBConnection.prototype.valueQueryAsync = async function (sql, params, opt
try {
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
[sql, params] = this.parseQueryAndParams(sql, params);
if (this._dbLockPromise && !options.ignoreDBLock) {
Zotero.debug(`Waiting for DB lock to be released: ${sql}`, 2);
await this._dbLockPromise;
}
if (Zotero.Debug.enabled) {
this.logQuery(sql, params, options);
}
@ -765,6 +790,12 @@ Zotero.DBConnection.prototype.columnQueryAsync = async function (sql, params, op
try {
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
[sql, params] = this.parseQueryAndParams(sql, params);
if (this._dbLockPromise && !options.ignoreDBLock) {
Zotero.debug(`Waiting for DB lock to be released: ${sql}`, 2);
await this._dbLockPromise;
}
if (Zotero.Debug.enabled) {
this.logQuery(sql, params, options);
}

View file

@ -446,18 +446,79 @@ Zotero.File = new function(){
});
};
/**
* Asynchronously writes data from an nsIAsyncInputStream to a file.
*
* Designed to handle input streams where data may not be
* immediately or fully available, such as network streams.
*
* @param {nsIInputStream} inputStream - The input stream to read from. This
* stream should implement nsIAsyncInputStream.
* @param {string} path - The file path where the data will be written.
* @param {number} byteCount - The expected number of bytes to write.
*
* @returns {Promise<number>} A promise that resolves with the number of bytes
* written when the operation is complete, or rejects with an error
* if any issues occur during reading or writing.
*/
this.putNetworkStream = async function (path, stream, byteCount) {
return new Promise((resolve, reject) => {
let bytesRead = 0;
var os = FileUtils.openSafeFileOutputStream(new FileUtils.File(path));
let binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
binaryInputStream.setInputStream(stream);
let readNextChunk = () => {
stream.asyncWait({
onInputStreamReady: (input) => {
try {
// Check available data in the stream
let available = input.available();
if (available > 0) {
os.write(binaryInputStream.readBytes(available), available);
bytesRead += available;
if (bytesRead < byteCount) {
// Continue reading
readNextChunk();
}
else {
// Finished writing all expected bytes
FileUtils.closeSafeFileOutputStream(os);
resolve(bytesRead);
}
}
else {
// No more data, finish the stream
FileUtils.closeSafeFileOutputStream(os);
resolve(bytesRead);
}
}
catch (e) {
os.close();
reject(new Components.Exception("File write operation failed", e));
}
}
}, 0, 0, null);
};
// Start reading the first chunk of data
readNextChunk();
});
};
this.download = async function (uri, path) {
var uriStr = uri.spec || uri;
const isHTTP = uriStr.startsWith('http');
if (uriStr.startsWith('http')) {
Zotero.warn("Zotero.File.download() is deprecated for HTTP(S) URLs -- use Zotero.HTTP.download()");
return Zotero.HTTP.download(uri, path);
}
Zotero.debug(`Saving ${uriStr} to ${path.pathQueryRef || path}`);
if (isHTTP && Zotero.HTTP.browserIsOffline()) {
let msg = `Download failed: ${Zotero.appName} is currently offline`;
Zotero.debug(msg, 2);
throw new Error(msg);
}
var deferred = Zotero.Promise.defer();
const inputChannel = NetUtil.newChannel({
uri,

View file

@ -149,6 +149,7 @@ Zotero.FileHandlers = {
handler = handler || systemHandler;
if (handler) {
if (Zotero.isMac) {
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
try {
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', handler, path]);
return true;
@ -156,6 +157,9 @@ Zotero.FileHandlers = {
catch (e) {
Zotero.logError(e);
}
finally {
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
}
}
try {
@ -309,11 +313,13 @@ Zotero.FileHandlers = {
switch (appPath.toLowerCase()) {
case 'okular':
appPath = '/usr/bin/okular';
break;
// It's "Document Viewer" on stock Ubuntu
case 'document viewer':
case 'evince':
appPath = '/usr/bin/evince';
break;
}
}
else if (await IOUtils.exists('/usr/bin/okular')) {
@ -498,8 +504,13 @@ Zotero.FileHandlers = {
if (!Zotero.File.pathToFile(command).isExecutable()) {
throw new Error(`${command} is not an executable`);
}
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
// Do not await
Zotero.Utilities.Internal.exec(command, args);
var promise = Zotero.Utilities.Internal.exec(command, args);
promise.finally(() => Zotero.Utilities.Internal.Environment.restoreMozillaVariables());
},
};

View file

@ -3,6 +3,7 @@
* @namespace
*/
Zotero.HTTP = new function() {
this.disableErrorRetry = false;
var _errorDelayIntervals = [2500, 5000, 10000, 20000, 40000, 60000, 120000, 240000, 300000];
var _errorDelayMax = 60 * 60 * 1000; // 1 hour
@ -169,7 +170,7 @@ Zotero.HTTP = new function() {
continue;
}
// Don't retry if errorDelayMax is 0
if (options.errorDelayMax === 0) {
if (options.errorDelayMax === 0 || Zotero.HTTP.disableErrorRetry) {
throw e;
}
// Automatically retry other 5xx errors by default
@ -269,7 +270,10 @@ Zotero.HTTP = new function() {
var deferred = Zotero.Promise.defer();
if (!this.mock || url.startsWith('resource://') || url.startsWith('chrome://')) {
if (!this.mock
|| options.noMock
|| url.startsWith('resource://')
|| url.startsWith('chrome://')) {
var xmlhttp = new XMLHttpRequest();
}
else {
@ -324,11 +328,13 @@ Zotero.HTTP = new function() {
channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
}
// Don't follow redirects
let notificationCallbacks = options.notificationCallbacks || {};
if (options.followRedirects === false) {
channel.notificationCallbacks = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIInterfaceRequestor, Ci.nsIChannelEventSync]),
getInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]),
if (notificationCallbacks.asyncOnChannelRedirect) {
throw new Error("Can't set asyncOnChannelRedirect and followRedirects = false");
}
notificationCallbacks = {
...notificationCallbacks,
asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
redirectStatus = (flags & Ci.nsIChannelEventSink.REDIRECT_PERMANENT) ? 301 : 302;
redirectLocation = newChannel.URI.spec;
@ -337,6 +343,9 @@ Zotero.HTTP = new function() {
}
};
}
if (notificationCallbacks) {
channel.notificationCallbacks = wrapNotificationCallbacks(notificationCallbacks);
}
}
// Set responseType
@ -380,15 +389,54 @@ Zotero.HTTP = new function() {
// Convert numbers to string to make Sinon happy
xmlhttp.setRequestHeader(header, value);
}
// Set timeout
const defaultTimeout = 30000;
let requestTimeout;
let connectTimeout;
let inactivityTimeout;
if (options.timeout !== 0) {
xmlhttp.timeout = options.timeout || 30000;
// For downloads, manually implement connect and inactivity timeouts, since the XHR
// `timeout` property applies to the whole request, even if data is being downloaded
if (options.isDownload) {
// TODO: Try a lower default connect timeout and take a separate option?
connectTimeout = options.timeout || defaultTimeout;
inactivityTimeout = options.timeout || defaultTimeout;
}
else {
requestTimeout = options.timeout || defaultTimeout;
}
}
let connectTimerID = null;
let inactivityTimerID = null;
let timedOutAfter;
function clearConnectTimer() {
if (connectTimerID) {
clearTimeout(connectTimerID);
connectTimerID = null;
}
}
function resetInactivityTimer() {
clearTimeout(inactivityTimerID);
inactivityTimerID = setTimeout(() => {
Zotero.warn(`Inactivity timeout for ${method} ${dispURL} -- aborting request`);
timedOutAfter = inactivityTimeout;
xmlhttp.abort();
}, inactivityTimeout);
}
if (requestTimeout) {
xmlhttp.timeout = requestTimeout;
xmlhttp.ontimeout = function() {
deferred.reject(new Zotero.HTTP.TimeoutException(requestTimeout));
};
}
else if (inactivityTimeout) {
xmlhttp.onprogress = function () {
resetInactivityTimer();
};
}
xmlhttp.ontimeout = function() {
deferred.reject(new Zotero.HTTP.TimeoutException(options.timeout));
};
// Provide caller with a callback to cancel a request in progress
if (options.cancellerReceiver) {
@ -402,7 +450,17 @@ Zotero.HTTP = new function() {
});
}
if (connectTimeout || inactivityTimeout) {
xmlhttp.onloadstart = () => {
clearConnectTimer();
resetInactivityTimer();
};
}
xmlhttp.onloadend = async function() {
clearConnectTimer();
clearTimeout(inactivityTimerID);
var status = redirectStatus || xmlhttp.status;
try {
@ -504,6 +562,11 @@ Zotero.HTTP = new function() {
}
Zotero.debug(msg, 1);
if (timedOutAfter) {
deferred.reject(new Zotero.HTTP.TimeoutException(timedOutAfter));
return;
}
if (xmlhttp.status == 0) {
try {
this.checkSecurity(channel, { isProxyAuthRequest: options.isProxyAuthRequest });
@ -529,22 +592,92 @@ Zotero.HTTP = new function() {
}
// Send binary data
let body;
if (compressedBody) {
let numBytes = compressedBody.length;
let ui8Data = new Uint8Array(numBytes);
for (let i = 0; i < numBytes; i++) {
ui8Data[i] = compressedBody.charCodeAt(i) & 0xff;
}
xmlhttp.send(ui8Data);
body = ui8Data;
}
// Send regular request
else {
xmlhttp.send(options.body || null);
body = options.body || null;
}
if (connectTimeout) {
connectTimerID = setTimeout(() => {
Zotero.warn(`Connect timeout for ${method} ${dispURL} -- aborting request`);
timedOutAfter = connectTimeout;
xmlhttp.abort();
}, connectTimeout);
}
xmlhttp.send(body);
return deferred.promise;
};
/**
* Create an nsIInterfaceRequestor object based on the callbacks provided
*/
function wrapNotificationCallbacks(callbacks) {
return {
QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
getInterface(aIID) {
// Handle nsIProgressEventSink (for onProgress, onStatus)
if (aIID.equals(Ci.nsIProgressEventSink) && (callbacks.onProgress || callbacks.onStatus)) {
return {
onProgress: callbacks.onProgress || function () {},
onStatus: callbacks.onStatus || function () {},
};
}
// Handle nsIChannelEventSink (for asyncOnChannelRedirect)
if (aIID.equals(Ci.nsIChannelEventSink) && callbacks.asyncOnChannelRedirect) {
return {
asyncOnChannelRedirect: callbacks.asyncOnChannelRedirect,
};
}
throw Components.Exception("No interface available", Cr.NS_ERROR_NO_INTERFACE);
}
};
}
/**
* Download a file
*
* @param {nsIURI|String} url - URL to request
* @param {String} path - Path to save file to
* @param {Object} [options] - See `Zotero.HTTP.request()`
*/
this.download = async function(uri, path, options = {}) {
// TODO: Convert request() to fetch() and use ReadableStream
var req = await this.request(
'GET',
uri,
{
...options,
isDownload: true,
responseType: 'blob',
// Downloads can have channel notification callbacks, etc., so always do them for real
noMock: true
}
);
var bytes = new Uint8Array(await req.response.arrayBuffer());
await IOUtils.write(path, bytes);
var byteLength = bytes.byteLength;
Zotero.debug(`Saved file to ${path} (${byteLength} byte${byteLength != 1 ? 's' : ''})`);
return req;
};
/**
* Send an HTTP GET request via XMLHTTPRequest
*
@ -1086,9 +1219,19 @@ Zotero.HTTP = new function() {
};
this.getDisplayURI = function (uri) {
this.getDisplayURI = function (uri, noCredentials) {
if (typeof uri == 'string') {
uri = NetUtil.newURI(uri);
}
if (!uri.password) return uri;
return uri.mutate().setPassword('********').finalize();
uri = uri.mutate();
if (noCredentials) {
uri.setUserPass('');
}
else {
uri.setPassword('********');
}
return uri.finalize();
}

View file

@ -329,7 +329,7 @@ Zotero.Integration = new function() {
if (document) {
try {
await document.cleanup();
if (!Zotero.Integration.currentSession._dontActivateDocument) {
if (!Zotero.Integration.currentSession?._dontActivateDocument) {
await document.activate();
}
@ -898,7 +898,8 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu
// Perform noteType or fieldType conversion
let fields = yield this._session.getFields();
const styleHasChanged = oldData.style.styleID != this._session.data.style.styleID;
var convertBibliographies = oldData.prefs.fieldType != this._session.data.prefs.fieldType;
var convertItems = convertBibliographies
|| oldData.prefs.noteType != this._session.data.prefs.noteType;
@ -931,7 +932,7 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu
this._session._fields = null;
this._session.ignoreEmptyBibliography = true;
if (this._session.data.prefs.delayCitationUpdates && !fieldsToConvert.length) return;
if (this._session.data.prefs.delayCitationUpdates && !fieldsToConvert.length && !styleHasChanged) return;
yield this._session.updateFromDocument(FORCE_CITATIONS_RESET_TEXT);
return this._session.updateDocument(FORCE_CITATIONS_RESET_TEXT, true, true);

View file

@ -244,12 +244,14 @@ Zotero.Intl = new function () {
}
function getLocaleCollation() {
var naturalSorting = Zotero.Prefs.get('naturalSorting');
try {
// DEBUG: Is this necessary, or will Intl.Collator just default to the same locales we're
// passing manually?
let locales = Services.locale.appLocalesAsBCP47;
var collator = new Intl.Collator(locales, {
numeric: true,
numeric: naturalSorting,
sensitivity: 'base'
});
}
@ -260,7 +262,7 @@ Zotero.Intl = new function () {
try {
Zotero.logError("Falling back to en-US sorting");
collator = new Intl.Collator(['en-US'], {
numeric: true,
numeric: naturalSorting,
sensitivity: 'base'
});
}

View file

@ -122,6 +122,9 @@ Zotero.Plugins = new function () {
* https://searchfox.org/mozilla-esr60/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm#4233
*/
function _loadScope(addon) {
if (scopes.has(addon.id)) {
return;
}
var scope = new Cu.Sandbox(
Services.scriptSecurityManager.getSystemPrincipal(),
{
@ -662,6 +665,7 @@ Zotero.Plugins = new function () {
return;
}
Zotero.debug("Enabling plugin " + addon.id);
_loadScope(addon);
setDefaultPrefs(addon);
await registerLocales(addon);
await _callMethod(addon, 'startup', REASONS.ADDON_ENABLE);

View file

@ -41,6 +41,8 @@ Zotero.Prefs = new function() {
if (Zotero.addShutdownListener) {
Zotero.addShutdownListener(this.unregister.bind(this));
}
this._checkUserJS();
// Process pref version updates
var fromVersion = this.get('prefVersion');
@ -510,4 +512,53 @@ Zotero.Prefs = new function() {
return `{{ ${[field, truncate, prefix, suffix].filter(f => f !== null).join(' ')} }}`;
});
};
/**
* Preferences set in <profile>/user.js override prefs.js, so they can't
* be changed via the UI and may be hard to locate and remove. Warn if
* user.js contains user_pref() directives.
*/
this._checkUserJS = async function () {
let userJSPath = PathUtils.join(Zotero.Profile.dir, 'user.js');
let userJS;
try {
userJS = await Zotero.File.getContentsAsync(userJSPath);
}
catch {
return;
}
const DISALLOWED_PREF_RE = /^\s*user_pref\s*\(\s*['"]extensions\.zotero\.fileHandler\..+$/g;
let updatedUserJS = userJS
.split('\n')
.filter((prefLine) => {
if (DISALLOWED_PREF_RE.test(prefLine)) {
Zotero.debug('user.js contains disallowed pref: ' + prefLine);
return false;
}
return true;
})
.join('\n');
if (updatedUserJS === userJS) {
return;
}
try {
await Zotero.File.putContentsAsync(userJSPath, updatedUserJS);
Zotero.alert(null,
Zotero.getString('general-error'),
Zotero.getString('userjs-pref-warning')
);
Services.startup.quit(
Components.interfaces.nsIAppStartup.eAttemptQuit
| Components.interfaces.nsIAppStartup.eRestart
);
}
catch (e) {
Zotero.logError(e);
}
};
};

View file

@ -73,7 +73,7 @@ Zotero.Prompt = {
Zotero.warn("Zotero.Prompt.confirm() option 'delayButtons' is deprecated -- use 'buttonDelay'");
buttonDelay = true;
}
let flags = (buttonDelay && !Zotero.automatedTest) ? Services.prompt.BUTTON_DELAY_ENABLE : 0;
let flags = (buttonDelay && !Zotero.test) ? Services.prompt.BUTTON_DELAY_ENABLE : 0;
if (typeof button0 == 'number') flags += Services.prompt.BUTTON_POS_0 * button0;
else if (typeof button0 == 'string') flags += Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING;
if (typeof button1 == 'number') flags += Services.prompt.BUTTON_POS_1 * button1;

View file

@ -58,8 +58,8 @@ Zotero.RecognizeDocument = new function () {
async function _processQueue() {
await Zotero.Schema.schemaUpdatePromise;
if (_queueProcessing) return;
_queueProcessing = true;
if (_queueProcessing) return _queueProcessing.promise;
_queueProcessing = Zotero.Promise.defer();
while (1) {
// While all current progress queue usages are related with
@ -83,8 +83,8 @@ Zotero.RecognizeDocument = new function () {
throw new Error();
}
let res = await _processItem(item);
_progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, item.getField('title'));
let parentItem = await _processItem(item);
_progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, parentItem.getField('title'));
}
catch (e) {
Zotero.logError(e);
@ -99,6 +99,7 @@ Zotero.RecognizeDocument = new function () {
}
}
_queueProcessing.resolve();
_queueProcessing = false;
_processingItemID = null;
}
@ -250,9 +251,10 @@ Zotero.RecognizeDocument = new function () {
/**
* Processes the item and places it as a children of the new item
* @param itemID
* @return {Promise}
* @return {Promise} A promise that resolves to a newly created, recognized parent item
*/
async function _processItem(attachment) {
Zotero.debug(`RecognizeDocument: Recognizing attachment ${attachment.getDisplayTitle()}`);
// Make sure the attachment still doesn't have a parent
if (attachment.parentItemID) {
throw new Error('Already has parent');
@ -268,10 +270,12 @@ Zotero.RecognizeDocument = new function () {
}
}
let parentItem = await _recognize(attachment);
let parentItem = await Zotero.RecognizeDocument._recognize(attachment);
if (!parentItem) {
Zotero.debug(`RecognizeDocument: No matches for attachment ${attachment.getDisplayTitle()}`);
throw new Zotero.Exception.Alert("recognizePDF.noMatches");
}
Zotero.debug(`RecognizeDocument: Recognized attachment ${attachment.getDisplayTitle()}`);
// Put new item in same collections as the old one
let collections = attachment.getCollections();
@ -388,11 +392,7 @@ Zotero.RecognizeDocument = new function () {
* @param {Zotero.Item} item
* @return {Promise<Zotero.Item>} - New item
*/
async function _recognize(item) {
if (Zotero.RecognizeDocument.recognizeStub) {
return Zotero.RecognizeDocument.recognizeStub(item);
}
this._recognize = async function (item) {
let filePath = await item.getFilePath();
if (!filePath || !await OS.File.exists(filePath)) throw new Zotero.Exception.Alert('recognizePDF.fileNotFound');

View file

@ -600,9 +600,15 @@ Zotero.Schema = new function(){
this._updateGlobalSchemaForTest = async function (schema) {
await Zotero.DB.executeTransaction(async function () {
await _updateGlobalSchema(schema);
}.bind(this), { disableForeignKeys: true });
await Zotero.DB.queryAsync("PRAGMA foreign_keys=OFF");
try {
await Zotero.DB.executeTransaction(async function () {
await _updateGlobalSchema(schema);
});
}
finally {
await Zotero.DB.queryAsync("PRAGMA foreign_keys=ON");
}
};
@ -733,58 +739,64 @@ Zotero.Schema = new function(){
var itemTypeID = Zotero.ID.get('customItemTypes');
yield Zotero.DB.executeTransaction(async function () {
await Zotero.DB.queryAsync("INSERT INTO customItemTypes VALUES (?, 'nsfReviewer', 'NSF Reviewer', 1, 'chrome://zotero/skin/report_user.png')", itemTypeID);
var fields = [
['name', 'Name'],
['institution', 'Institution'],
['address', 'Address'],
['telephone', 'Telephone'],
['email', 'Email'],
['homepage', 'Webpage'],
['discipline', 'Discipline'],
['nsfID', 'NSF ID'],
['dateSent', 'Date Sent'],
['dateDue', 'Date Due'],
['accepted', 'Accepted'],
['programDirector', 'Program Director']
];
for (var i=0; i<fields.length; i++) {
var fieldID = Zotero.ItemFields.getID(fields[i][0]);
if (!fieldID) {
var fieldID = Zotero.ID.get('customFields');
await Zotero.DB.queryAsync("INSERT INTO customFields VALUES (?, ?, ?)", [fieldID, fields[i][0], fields[i][1]]);
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, NULL, ?, 1, ?)", [itemTypeID, fieldID, i+1]);
}
else {
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, ?, NULL, 1, ?)", [itemTypeID, fieldID, i+1]);
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=OFF");
try {
yield Zotero.DB.executeTransaction(async function () {
await Zotero.DB.queryAsync("INSERT INTO customItemTypes VALUES (?, 'nsfReviewer', 'NSF Reviewer', 1, 'chrome://zotero/skin/report_user.png')", itemTypeID);
var fields = [
['name', 'Name'],
['institution', 'Institution'],
['address', 'Address'],
['telephone', 'Telephone'],
['email', 'Email'],
['homepage', 'Webpage'],
['discipline', 'Discipline'],
['nsfID', 'NSF ID'],
['dateSent', 'Date Sent'],
['dateDue', 'Date Due'],
['accepted', 'Accepted'],
['programDirector', 'Program Director']
];
for (var i=0; i<fields.length; i++) {
var fieldID = Zotero.ItemFields.getID(fields[i][0]);
if (!fieldID) {
var fieldID = Zotero.ID.get('customFields');
await Zotero.DB.queryAsync("INSERT INTO customFields VALUES (?, ?, ?)", [fieldID, fields[i][0], fields[i][1]]);
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, NULL, ?, 1, ?)", [itemTypeID, fieldID, i+1]);
}
else {
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, ?, NULL, 1, ?)", [itemTypeID, fieldID, i+1]);
}
switch (fields[i][0]) {
case 'name':
var baseFieldID = Zotero.ItemFields.getID('title');
break;
case 'dateSent':
var baseFieldID = Zotero.ItemFields.getID('date');
break;
case 'homepage':
var baseFieldID = Zotero.ItemFields.getID('url');
break;
default:
var baseFieldID = null;
}
if (baseFieldID) {
await Zotero.DB.queryAsync("INSERT INTO customBaseFieldMappings VALUES (?, ?, ?)", [itemTypeID, baseFieldID, fieldID]);
}
}
switch (fields[i][0]) {
case 'name':
var baseFieldID = Zotero.ItemFields.getID('title');
break;
case 'dateSent':
var baseFieldID = Zotero.ItemFields.getID('date');
break;
case 'homepage':
var baseFieldID = Zotero.ItemFields.getID('url');
break;
default:
var baseFieldID = null;
}
if (baseFieldID) {
await Zotero.DB.queryAsync("INSERT INTO customBaseFieldMappings VALUES (?, ?, ?)", [itemTypeID, baseFieldID, fieldID]);
}
}
await _reloadSchema();
}, { disableForeignKeys: true });
await _reloadSchema();
});
}
finally {
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=ON");
}
var s = new Zotero.Search;
s.name = "Overdue NSF Reviewers";
@ -814,26 +826,32 @@ Zotero.Schema = new function(){
}
Zotero.debug("Uninstalling nsfReviewer item type");
yield Zotero.DB.executeTransaction(async function () {
await Zotero.DB.queryAsync("DELETE FROM customItemTypeFields WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
await Zotero.DB.queryAsync("DELETE FROM customBaseFieldMappings WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
for (let fieldID of fields) {
if (Zotero.ItemFields.isCustom(fieldID)) {
await Zotero.DB.queryAsync("DELETE FROM customFields WHERE customFieldID=?", fieldID - Zotero.ItemTypes.customIDOffset);
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=OFF");
try {
yield Zotero.DB.executeTransaction(async function () {
await Zotero.DB.queryAsync("DELETE FROM customItemTypeFields WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
await Zotero.DB.queryAsync("DELETE FROM customBaseFieldMappings WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
for (let fieldID of fields) {
if (Zotero.ItemFields.isCustom(fieldID)) {
await Zotero.DB.queryAsync("DELETE FROM customFields WHERE customFieldID=?", fieldID - Zotero.ItemTypes.customIDOffset);
}
}
}
await Zotero.DB.queryAsync("DELETE FROM customItemTypes WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
var searches = Zotero.Searches.getByLibrary(Zotero.Libraries.userLibraryID);
for (let search of searches) {
if (search.name == 'Overdue NSF Reviewers') {
await search.erase();
await Zotero.DB.queryAsync("DELETE FROM customItemTypes WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
var searches = Zotero.Searches.getByLibrary(Zotero.Libraries.userLibraryID);
for (let search of searches) {
if (search.name == 'Overdue NSF Reviewers') {
await search.erase();
}
}
}
await _reloadSchema();
}.bind(this), { disableForeignKeys: true });
await _reloadSchema();
});
}
finally {
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=ON");
}
ps.alert(null, "Zotero Item Type Removed", "The 'NSF Reviewer' item type has been uninstalled.");
}

View file

@ -0,0 +1,283 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2024 Corporation for Digital Scholarship
Vienna, Virginia, USA
http://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 *****
*/
Zotero.Server.Connector.SessionManager = {
_sessions: new Map(),
get: function (id) {
return this._sessions.get(id);
},
create: function (id, action, requestData) {
if (typeof id === 'undefined') {
id = Zotero.Utilities.randomString();
}
if (this._sessions.has(id)) {
throw new Error(`Session ID ${id} exists`);
}
Zotero.debug(`Creating connector save session ${id}`);
var session = new Zotero.Server.Connector.SaveSession(id, action, requestData);
this._sessions.set(id, session);
this.gc();
return session;
},
gc: function () {
// Delete sessions older than 10 minutes, or older than 1 minute if more than 10 sessions
var ttl = this._sessions.size >= 10 ? 60 : 600;
var deleteBefore = new Date() - ttl * 1000;
for (let session of this._sessions) {
if (session.created < deleteBefore) {
this._session.delete(session.id);
}
}
}
};
Zotero.Server.Connector.SaveSession = class {
constructor(id, action, requestData) {
this.id = id;
this.created = new Date();
this._action = action;
this._requestData = requestData;
this._items = {};
this._progressItems = {};
this._orderedProgressItems = [];
}
async saveItems(target) {
var { library, collection } = Zotero.Server.Connector.resolveTarget(target);
var data = this._requestData.data;
var headers = this._requestData.headers;
var cookieSandbox = data.uri
? new Zotero.CookieSandbox(
null,
data.uri,
data.detailedCookies ? "" : data.cookie || "",
headers["User-Agent"]
)
: null;
if (cookieSandbox && data.detailedCookies) {
cookieSandbox.addCookiesFromHeader(data.detailedCookies);
}
var proxy = data.proxy && new Zotero.Proxy(data.proxy);
this.itemSaver = new Zotero.Translate.ItemSaver({
libraryID: library.libraryID,
collections: collection ? [collection.id] : undefined,
// All attachments come from the Connector
attachmentMode: Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE,
forceTagType: 1,
referrer: data.uri,
cookieSandbox,
proxy
});
let items = await this.itemSaver.saveItems(data.items, () => 0, () => 0);
// If more itemSaver calls are made, it means we are saving attachments explicitly (like
// a snapshot) and we don't want to ignore those.
this.itemSaver.attachmentMode = Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD;
items.forEach((item, index) => {
this.addItem(data.items[index].id, item);
});
return items;
}
async saveSnapshot(target) {
var { library, collection } = Zotero.Server.Connector.resolveTarget(target);
var libraryID = library.libraryID;
var data = this._requestData.data;
let title = data.title || data.url;
// Create new webpage item
let item = new Zotero.Item("webpage");
item.libraryID = libraryID;
item.setField("title", title);
item.setField("url", data.url);
item.setField("accessDate", "CURRENT_TIMESTAMP");
if (collection) {
item.setCollections([collection.id]);
}
await item.saveTx();
// SingleFile snapshot may be coming later
this.addItem(data.url, item);
return item;
}
async addItem(key, item) {
return this.addItems({ [key]: item });
}
async addItems(items) {
this._items = Object.assign(this._items, items);
// Update the items with the current target data, in case it changed since the save began
await this._updateItems(items);
}
getItemByConnectorKey(key) {
return this._items[key];
}
// documentRecognizer doesn't return recognized items and it's complicated to make it
// do it, so we just retrieve the parent item which is a little hacky but does the job
getRecognizedItem() {
try {
return Object.values(this._items)[0].parentItem;
}
catch (_) {}
}
remove() {
delete Zotero.Server.Connector.SessionManager._sessions[this.id];
}
/**
* Change the target data for this session and update any items that have already been saved
*/
async update(targetID, tags) {
var previousTargetID = this._currentTargetID;
this._currentTargetID = targetID;
this._currentTags = tags || "";
// Select new destination in collections pane
var zp = Zotero.getActiveZoteroPane();
if (zp && zp.collectionsView) {
await zp.collectionsView.selectByID(targetID);
}
// If window is closed, select target collection re-open
else {
Zotero.Prefs.set('lastViewedFolder', targetID);
}
await this._updateItems(this._items);
// If a single item was saved, select it (or its parent, if it now has one)
if (zp && zp.collectionsView && Object.values(this._items).length == 1) {
let item = Object.values(this._items)[0];
item = item.isTopLevelItem() ? item : item.parentItem;
// Don't select if in trash
if (!item.deleted) {
await zp.selectItem(item.id);
}
}
}
/**
* Update the passed items with the current target and tags
*/
_updateItems = Zotero.serial(async function (items) {
if (Object.values(items).length == 0) {
return;
}
var { library, collection } = Zotero.Server.Connector.resolveTarget(this._currentTargetID);
var libraryID = library.libraryID;
var tags = this._currentTags.trim();
tags = tags ? tags.split(/\s*,\s*/).filter(x => x) : [];
Zotero.debug("Updating items for connector save session " + this.id);
for (let key in items) {
let item = items[key];
// If the item is now a child item (e.g., from Retrieve Metadata), update the
// parent item instead
if (!item.isTopLevelItem()) {
item = item.parentItem;
}
// Skip deleted items
if (!Zotero.Items.exists(item.id)) {
Zotero.debug(`Item ${item.id} in save session no longer exists`);
continue;
}
if (item.libraryID != libraryID) {
let newItem = await item.moveToLibrary(libraryID);
this._items[key] = newItem;
}
// Keep automatic tags
let originalTags = item.getTags().filter(tag => tag.type == 1);
item.setTags(originalTags.concat(tags));
item.setCollections(collection ? [collection.id] : []);
await item.saveTx();
}
this._updateRecents();
});
_updateRecents() {
var targetID = this._currentTargetID;
try {
let numRecents = 7;
let recents = Zotero.Prefs.get('recentSaveTargets') || '[]';
recents = JSON.parse(recents);
// If there's already a target from this session in the list, update it
for (let recent of recents) {
if (recent.sessionID == this.id) {
recent.id = targetID;
break;
}
}
// If a session is found with the same target, move it to the end without changing
// the sessionID. This could be the current session that we updated above or a different
// one. (We need to leave the old sessionID for the same target or we'll end up removing
// the previous target from the history if it's changed in the current one.)
let pos = recents.findIndex(r => r.id == targetID);
if (pos != -1) {
recents = [
...recents.slice(0, pos),
...recents.slice(pos + 1),
recents[pos]
];
}
// Otherwise just add this one to the end
else {
recents = recents.concat([{
id: targetID,
sessionID: this.id
}]);
}
recents = recents.slice(-1 * numRecents);
Zotero.Prefs.set('recentSaveTargets', JSON.stringify(recents));
}
catch (e) {
Zotero.logError(e);
Zotero.Prefs.clear('recentSaveTargets');
}
}
};

View file

@ -23,6 +23,9 @@
***** END LICENSE BLOCK *****
*/
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");
Components.utils.import("resource://gre/modules/NetUtil.jsm");
Zotero.Server = new function() {
var _onlineObserverRegistered, serv;
this.responseCodes = {
@ -42,50 +45,55 @@ Zotero.Server = new function() {
504:"Gateway Timeout"
};
Object.defineProperty(this, 'port', {
get() {
if (!serv) {
throw new Error('Server not initialized');
}
return serv.identity.primaryPort;
}
});
/**
* initializes a very rudimentary web server
*/
this.init = function(port, bindAllAddr, maxConcurrentConnections) {
if (Zotero.HTTP.browserIsOffline()) {
Zotero.debug('Browser is offline -- not initializing HTTP server');
_registerOnlineObserver();
return;
}
this.init = function (port) {
if(serv) {
Zotero.debug("Already listening on port " + serv.port);
return;
}
// start listening on socket
serv = Components.classes["@mozilla.org/network/server-socket;1"]
.createInstance(Components.interfaces.nsIServerSocket);
port = port || Zotero.Prefs.get('httpServer.port');
try {
// bind to a random port on loopback only
serv.init(port ? port : Zotero.Prefs.get('httpServer.port'), !bindAllAddr, -1);
serv.asyncListen(Zotero.Server.SocketListener);
Zotero.debug("HTTP server listening on "+(bindAllAddr ? "*": " 127.0.0.1")+":"+serv.port);
serv = new HttpServer();
serv.registerPrefixHandler('/', this.handleRequest)
serv.start(port);
Zotero.debug(`HTTP server listening on 127.0.0.1:${serv.identity.primaryPort}`);
// Close port on Zotero shutdown (doesn't apply to translation-server)
if (Zotero.addShutdownListener) {
Zotero.addShutdownListener(this.close.bind(this));
}
} catch(e) {
}
catch (e) {
Zotero.logError(e);
Zotero.debug("Not initializing HTTP server");
serv = undefined;
}
_registerOnlineObserver()
};
this.handleRequest = function (request, response) {
let requestHandler = new Zotero.Server.RequestHandler(request, response);
return requestHandler.handleRequest();
}
/**
* releases bound port
*/
this.close = function() {
if(!serv) return;
serv.close();
this.close = function () {
if (!serv) return;
serv.stop();
serv = undefined;
};
@ -102,294 +110,100 @@ Zotero.Server = new function() {
}
return decodedData;
}
function _registerOnlineObserver() {
if (_onlineObserverRegistered) {
return;
}
// Observer to enable the integration when we go online
var observer = {
observe: function(subject, topic, data) {
if (data == 'online') {
Zotero.Server.init();
}
// A proxy headers class to make header retrieval case-insensitive
Zotero.Server.Headers = class {
constructor() {
return new Proxy(this, {
get(target, name, receiver) {
if (typeof name !== 'string') {
return Reflect.get(target, name, receiver);
}
return Reflect.get(target, name.toLowerCase(), receiver);
},
has(target, name, receiver) {
if (typeof name !== 'string') {
return Reflect.has(target, name, receiver);
}
return Reflect.has(target, name.toLowerCase(), receiver);
},
set(target, name, value, receiver) {
return Reflect.set(target, name.toLowerCase(), value, receiver);
}
};
var observerService =
Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
observerService.addObserver(observer, "network:offline-status-changed", false);
_onlineObserverRegistered = true;
});
}
}
};
Zotero.Server.SocketListener = new function() {
this.onSocketAccepted = onSocketAccepted;
this.onStopListening = onStopListening;
/*
* called when a socket is opened
*/
function onSocketAccepted(socket, transport) {
// get an input stream
var iStream = transport.openInputStream(0, 0, 0);
var oStream = transport.openOutputStream(Components.interfaces.nsITransport.OPEN_BLOCKING, 0, 0);
var dataListener = new Zotero.Server.DataListener(iStream, oStream);
var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"]
.createInstance(Components.interfaces.nsIInputStreamPump);
try {
pump.init(iStream, 0, 0, false);
}
catch (e) {
pump.init(iStream, -1, -1, 0, 0, false);
}
pump.asyncRead(dataListener, null);
}
function onStopListening(serverSocket, status) {
Zotero.debug("HTTP server going offline");
}
}
/*
* handles the actual acquisition of data
*/
Zotero.Server.DataListener = function(iStream, oStream) {
Components.utils.import("resource://gre/modules/NetUtil.jsm");
this.header = "";
this.headerFinished = false;
Zotero.Server.networkStreamToString = function (stream, length) {
let data = NetUtil.readInputStreamToString(stream, length);
return Zotero.Utilities.Internal.decodeUTF8(data);
};
Zotero.Server.RequestHandler = function (request, response) {
this.body = "";
this.bodyLength = 0;
this.iStream = iStream;
this.oStream = oStream;
this.foundReturn = false;
}
/*
* called when a request begins (although the request should have begun before
* the DataListener was generated)
*/
Zotero.Server.DataListener.prototype.onStartRequest = function(request) {}
/*
* called when a request stops
*/
Zotero.Server.DataListener.prototype.onStopRequest = function(request, status) {
this.iStream.close();
this.oStream.close();
}
/*
* called when new data is available
*/
Zotero.Server.DataListener.prototype.onDataAvailable = function (request, inputStream, offset, count) {
var readData = NetUtil.readInputStreamToString(inputStream, count);
if(this.headerFinished) { // reading body
this.body += readData;
// check to see if data is done
this._bodyData();
} else { // reading header
// see if there's a magic double return
var lineBreakIndex = readData.indexOf("\r\n\r\n");
if(lineBreakIndex != -1) {
if(lineBreakIndex != 0) {
this.header += readData.substr(0, lineBreakIndex+4);
this.body = readData.substr(lineBreakIndex+4);
}
this._headerFinished();
return;
}
var lineBreakIndex = readData.indexOf("\n\n");
if(lineBreakIndex != -1) {
if(lineBreakIndex != 0) {
this.header += readData.substr(0, lineBreakIndex+2);
this.body = readData.substr(lineBreakIndex+2);
}
this._headerFinished();
return;
}
if(this.header && this.header[this.header.length-1] == "\n" &&
(readData[0] == "\n" || readData[0] == "\r")) {
if(readData.length > 1 && readData[1] == "\n") {
this.header += readData.substr(0, 2);
this.body = readData.substr(2);
} else {
this.header += readData[0];
this.body = readData.substr(1);
}
this._headerFinished();
return;
}
this.header += readData;
}
}
/*
* processes an HTTP header and decides what to do
*/
Zotero.Server.DataListener.prototype._headerFinished = function() {
this.headerFinished = true;
Zotero.debug(this.header, 5);
// Parse headers into this.headers with lowercase names
this.headers = {};
var headerLines = this.header.trim().split(/\r\n/);
for (let line of headerLines) {
line = line.trim();
let pos = line.indexOf(':');
if (pos == -1) {
continue;
}
let k = line.substr(0, pos).toLowerCase();
let v = line.substr(pos + 1).trim();
this.headers[k] = v;
}
if (this.headers.origin) {
this.origin = this.headers.origin;
}
else if (this.headers['zotero-bookmarklet']) {
this.origin = "https://www.zotero.org";
}
if (!Zotero.isServer) {
// Make sure the Host header is set to localhost/127.0.0.1 to prevent DNS rebinding attacks
const hostRe = /^(localhost|127\.0\.0\.1)(:[0-9]+)?$/i;
if (!hostRe.test(this.headers.host)) {
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid Host header\n"));
return;
}
}
// get first line of request
const methodRe = /^([A-Z]+) ([^ \r\n?]+)(\?[^ \r\n]+)?/;
var method = methodRe.exec(this.header);
// get content-type
var contentType = this.headers['content-type'];
if (contentType) {
let splitContentType = contentType.split(/\s*;/);
this.contentType = splitContentType[0];
}
if(!method) {
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid method specified\n"));
return;
}
this.pathParams = {};
if (Zotero.Server.Endpoints[method[2]]) {
this.endpoint = Zotero.Server.Endpoints[method[2]];
}
else {
let router = new Zotero.Router(this.pathParams);
for (let [potentialTemplate, endpoint] of Object.entries(Zotero.Server.Endpoints)) {
if (!potentialTemplate.includes(':')) continue;
router.add(potentialTemplate, () => {
this.pathParams._endpoint = endpoint;
}, true, /* Do not allow missing params */ false);
}
if (router.run(method[2].split('?')[0])) { // Don't let parser handle query params - we do that already
this.endpoint = this.pathParams._endpoint;
delete this.pathParams._endpoint;
delete this.pathParams.url;
}
else {
this._requestFinished(this._generateResponse(404, "text/plain", "No endpoint found\n"));
return;
}
}
this.pathname = method[2];
this.query = method[3];
if(method[1] == "HEAD" || method[1] == "OPTIONS") {
this._requestFinished(this._generateResponse(200));
} else if(method[1] == "GET") {
this._processEndpoint("GET", null); // async
} else if(method[1] == "POST") {
const contentLengthRe = /^([0-9]+)$/;
// parse content length
var m = contentLengthRe.exec(this.headers['content-length']);
if(!m) {
this._requestFinished(this._generateResponse(400, "text/plain", "Content-length not provided\n"));
return;
}
this.bodyLength = parseInt(m[1]);
this._bodyData();
} else {
this._requestFinished(this._generateResponse(501, "text/plain", "Method not implemented\n"));
return;
}
this.request = request;
this.response = response;
}
/*
* checks to see if Content-Length bytes of body have been read and, if so, processes the body
*/
Zotero.Server.DataListener.prototype._bodyData = function() {
if(this.body.length >= this.bodyLength) {
let logContentTypes = [
'text/plain',
'application/json'
];
Zotero.Server.RequestHandler.prototype._bodyData = function () {
const PLAIN_TEXT_CONTENT_TYPES = new Set([
'text/plain',
'application/json',
'application/x-www-form-urlencoded'
]);
let data = null;
if (this.bodyLength > 0) {
if (PLAIN_TEXT_CONTENT_TYPES.has(this.contentType)) {
this.body = data = Zotero.Server.networkStreamToString(this.request.bodyInputStream, this.bodyLength);
}
else if (this.contentType === 'multipart/form-data') {
data = NetUtil.readInputStreamToString(this.request.bodyInputStream, this.bodyLength);
try {
data = this._decodeMultipartData(data);
}
catch (e) {
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
}
}
}
if (this.body.length >= this.bodyLength) {
let noLogEndpoints = [
'/connector/saveSingleFile'
];
if (this.body != '{}'
&& logContentTypes.includes(this.contentType)
&& PLAIN_TEXT_CONTENT_TYPES.has(this.contentType)
&& !noLogEndpoints.includes(this.pathname)) {
Zotero.debug(Zotero.Utilities.ellipsize(this.body, 1000, false, true), 5);
}
// handle envelope
this._processEndpoint("POST", this.body); // async
}
// handle envelope
this._processEndpoint("POST", data); // async
}
/**
* Generates the response to an HTTP request
*/
Zotero.Server.DataListener.prototype._generateResponse = function (status, contentTypeOrHeaders, body) {
Zotero.Server.RequestHandler.prototype._generateResponse = function (status, contentTypeOrHeaders, body) {
var response = "HTTP/1.0 "+status+" "+Zotero.Server.responseCodes[status]+"\r\n";
// Translation server
if (Zotero.isServer) {
// Add CORS headers if Origin header matches the allowed origins
if (this.origin) {
let allowedOrigins = Zotero.Prefs.get('httpServer.allowedOrigins')
.split(/, */).filter(x => x);
let allAllowed = allowedOrigins.includes('*');
if (allAllowed || allowedOrigins.includes(this.origin)) {
response += "Access-Control-Allow-Origin: " + (allAllowed ? '*' : this.origin) + "\r\n";
response += "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n";
response += "Access-Control-Allow-Headers: Content-Type\r\n";
response += "Access-Control-Expose-Headers: Link\r\n";
}
}
}
// Client
else {
response += "X-Zotero-Version: "+Zotero.version+"\r\n";
response += "X-Zotero-Connector-API-Version: "+CONNECTOR_API_VERSION+"\r\n";
response += "X-Zotero-Version: "+Zotero.version+"\r\n";
response += "X-Zotero-Connector-API-Version: "+CONNECTOR_API_VERSION+"\r\n";
if (this.origin === ZOTERO_CONFIG.BOOKMARKLET_ORIGIN) {
response += "Access-Control-Allow-Origin: " + this.origin + "\r\n";
response += "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n";
response += "Access-Control-Allow-Headers: Content-Type,X-Zotero-Connector-API-Version,X-Zotero-Version\r\n";
}
if (this.origin === ZOTERO_CONFIG.BOOKMARKLET_ORIGIN) {
response += "Access-Control-Allow-Origin: " + this.origin + "\r\n";
response += "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n";
response += "Access-Control-Allow-Headers: Content-Type,X-Zotero-Connector-API-Version,X-Zotero-Version\r\n";
}
if (contentTypeOrHeaders) {
@ -403,7 +217,7 @@ Zotero.Server.DataListener.prototype._generateResponse = function (status, conte
}
}
if(body) {
if (body) {
response += "\r\n"+body;
} else {
response += "Content-Length: 0\r\n\r\n";
@ -412,34 +226,98 @@ Zotero.Server.DataListener.prototype._generateResponse = function (status, conte
return response;
}
Zotero.Server.RequestHandler.prototype.handleRequest = async function () {
const request = this.request;
const response = this.response;
// Tell httpd that we will be constructing our own response
// without its custom methods, asynchronously
response.seizePower();
let requestDebug = `${request.method} ${request.path} HTTP/${request.httpVersion}\n`
// Parse headers into this.headers with lowercase names
this.headers = new Zotero.Server.Headers();
for (let { data: name } of request.headers) {
requestDebug += `${name}: ${request.getHeader(name)}\n`;
this.headers[name.toLowerCase()] = request.getHeader(name);
}
Zotero.debug(requestDebug, 5);
if (this.headers.origin) {
this.origin = this.headers.origin;
}
this.pathname = request.path;
this.query = request.queryString;
// get content-type
var contentType = this.headers['content-type'];
if (contentType) {
let splitContentType = contentType.split(/\s*;/);
this.contentType = splitContentType[0];
}
this.pathParams = {};
if (Zotero.Server.Endpoints[this.pathname]) {
this.endpoint = Zotero.Server.Endpoints[this.pathname];
}
else {
let router = new Zotero.Router(this.pathParams);
for (let [potentialTemplate, endpoint] of Object.entries(Zotero.Server.Endpoints)) {
if (!potentialTemplate.includes(':')) continue;
router.add(potentialTemplate, () => {
this.pathParams._endpoint = endpoint;
}, true, /* Do not allow missing params */ false);
}
if (router.run(this.pathname)) {
this.endpoint = this.pathParams._endpoint;
delete this.pathParams._endpoint;
delete this.pathParams.url;
}
else {
this._requestFinished(this._generateResponse(404, "text/plain", "No endpoint found\n"));
return;
}
}
if (request.method == "HEAD" || request.method == "OPTIONS") {
this._requestFinished(this._generateResponse(200));
}
else if (request.method == "GET") {
this._processEndpoint("GET", null); // async
}
else if (request.method == "POST") {
const contentLengthRe = /^([0-9]+)$/;
// parse content length
var m = contentLengthRe.exec(this.headers['content-length']);
if(!m) {
this._requestFinished(this._generateResponse(400, "text/plain", "Content-length not provided\n"));
return;
}
this.bodyLength = parseInt(m[1]);
this._bodyData();
} else {
this._requestFinished(this._generateResponse(501, "text/plain", "Method not implemented\n"));
}
}
/**
* Generates a response based on calling the function associated with the endpoint
*
* Note: postData contains raw bytes and should be decoded before use
*/
Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine(function* (method, postData) {
Zotero.Server.RequestHandler.prototype._processEndpoint = async function (method, postData) {
try {
var endpoint = new this.endpoint;
// Check that endpoint supports method
if(endpoint.supportedMethods && endpoint.supportedMethods.indexOf(method) === -1) {
if (endpoint.supportedMethods && endpoint.supportedMethods.indexOf(method) === -1) {
this._requestFinished(this._generateResponse(400, "text/plain", "Endpoint does not support method\n"));
return;
}
// Check that endpoint supports bookmarklet
if(this.origin) {
var isBookmarklet = this.origin === "https://www.zotero.org" || this.origin === "http://www.zotero.org";
// Disallow bookmarklet origins to access endpoints without permitBookmarklet
// set. We allow other origins to access these endpoints because they have to
// be privileged to avoid being blocked by our headers.
if(isBookmarklet && !endpoint.permitBookmarklet) {
this._requestFinished(this._generateResponse(403, "text/plain", "Access forbidden to bookmarklet\n"));
return;
}
}
// Reject browser-based requests that don't require a CORS preflight request [1] if they
// don't come from the connector or include Zotero-Allowed-Request
//
@ -471,53 +349,44 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
return;
}
var decodedData = null;
if(postData && this.contentType) {
var data = null;
if (method === 'POST' && this.contentType) {
// check that endpoint supports contentType
var supportedDataTypes = endpoint.supportedDataTypes;
if(supportedDataTypes && supportedDataTypes != '*'
if (supportedDataTypes && supportedDataTypes != '*'
&& supportedDataTypes.indexOf(this.contentType) === -1) {
this._requestFinished(this._generateResponse(400, "text/plain", "Endpoint does not support content-type\n"));
return;
}
// decode content-type post data
if(this.contentType === "application/json") {
if (this.contentType === "application/json") {
try {
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
decodedData = JSON.parse(postData);
} catch(e) {
data = JSON.parse(postData);
}
catch(e) {
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid JSON provided\n"));
return;
}
} else if(this.contentType === "application/x-www-form-urlencoded") {
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
decodedData = Zotero.Server.decodeQueryString(postData);
} else if(this.contentType === "multipart/form-data") {
let boundary = /boundary=([^\s]*)/i.exec(this.header);
if (!boundary) {
Zotero.debug('Invalid boundary: ' + this.header, 1);
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
}
boundary = '--' + boundary[1];
try {
decodedData = this._decodeMultipartData(postData, boundary);
} catch(e) {
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
}
} else {
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
decodedData = postData;
}
else if (this.contentType === "application/x-www-form-urlencoded") {
data = Zotero.Server.decodeQueryString(postData);
}
else if (postData) {
data = postData;
}
else {
data = this.request.bodyInputStream;
}
}
// set up response callback
var sendResponseCallback = function (code, contentTypeOrHeaders, arg, options) {
var sendResponseCallback = (code, contentTypeOrHeaders, arg, options) => {
this._requestFinished(
this._generateResponse(code, contentTypeOrHeaders, arg),
options
);
}.bind(this);
};
// Pass to endpoint
//
@ -528,30 +397,18 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
if (endpoint.init.length === 1
// Return value from Zotero.Promise.coroutine()
|| endpoint.init.length === 0) {
let headers = {};
let headerLines = this.header.trim().split(/\r\n/);
for (let line of headerLines) {
line = line.trim();
let pos = line.indexOf(':');
if (pos == -1) {
continue;
}
let k = line.substr(0, pos);
let v = line.substr(pos + 1).trim();
headers[k] = v;
}
let maybePromise = endpoint.init({
method,
pathname: this.pathname,
pathParams: this.pathParams,
searchParams: new URLSearchParams(this.query ? this.query.substring(1) : ''),
headers,
data: decodedData
searchParams: new URLSearchParams(this.query || ''),
headers: this.headers,
data
});
let result;
if (maybePromise.then) {
result = yield maybePromise;
result = await maybePromise;
}
else {
result = maybePromise;
@ -565,71 +422,75 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
}
// Two-parameter endpoint takes data and a callback
else if (endpoint.init.length === 2) {
endpoint.init(decodedData, sendResponseCallback);
endpoint.init(data, sendResponseCallback);
}
// Three-parameter endpoint takes a URL, data, and a callback
else {
const uaRe = /[\r\n]User-Agent: +([^\r\n]+)/i;
var m = uaRe.exec(this.header);
var url = {
pathname: this.pathname,
searchParams: new URLSearchParams(this.query ? this.query.substring(1) : ''),
userAgent: m && m[1]
searchParams: new URLSearchParams(this.query || ''),
userAgent: this.headers['user-agent']
};
endpoint.init(url, decodedData, sendResponseCallback);
endpoint.init(url, data, sendResponseCallback);
}
} catch(e) {
Zotero.debug(e);
this._requestFinished(this._generateResponse(500), "text/plain", "An error occurred\n");
throw e;
}
});
};
/*
* returns HTTP data from a request
*/
Zotero.Server.DataListener.prototype._requestFinished = function (response, options) {
if(this._responseSent) {
Zotero.Server.RequestHandler.prototype._requestFinished = function (responseBody, options) {
if (this._responseSent) {
Zotero.debug("Request already finished; not sending another response");
return;
}
this._responseSent = true;
// close input stream
this.iStream.close();
// open UTF-8 converter for output stream
var intlStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
.createInstance(Components.interfaces.nsIConverterOutputStream);
// write
try {
intlStream.init(this.oStream, "UTF-8", 1024, "?".charCodeAt(0));
intlStream.init(this.response.bodyOutputStream, "UTF-8", 1024, "?".charCodeAt(0));
// Filter logged response
if (Zotero.Debug.enabled) {
let maxLogLength = 2000;
let str = response;
let str = responseBody;
if (options && options.logFilter) {
str = options.logFilter(str);
}
if (str.length > maxLogLength) {
str = str.substr(0, maxLogLength) + `\u2026 (${response.length} chars)`;
str = str.substr(0, maxLogLength) + `\u2026 (${responseBody.length} chars)`;
}
Zotero.debug(str, 5);
}
intlStream.writeString(response);
} finally {
intlStream.close();
intlStream.writeString(responseBody);
}
finally {
this.response.finish();
}
}
Zotero.Server.DataListener.prototype._decodeMultipartData = function(data, boundary) {
var contentDispositionRe = /^Content-Disposition:\s*(.*)$/i;
let contentTypeRe = /^Content-Type:\s*(.*)$/i
Zotero.Server.RequestHandler.prototype._decodeMultipartData = function(data) {
const contentDispositionRe = /^Content-Disposition:\s*(.*)$/i;
const contentTypeRe = /^Content-Type:\s*(.*)$/i
var results = [];
let results = [];
let boundary = /boundary=([^\s]*)/i.exec(this.headers['content-type']);
if (!boundary) {
Zotero.debug('Invalid boundary: ' + this.headers['content-type'], 1);
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
}
boundary = '--' + boundary[1];
data = data.split(boundary);
// Ignore pre first boundary and post last boundary
data = data.slice(1, data.length-1);

File diff suppressed because it is too large Load diff

View file

@ -64,7 +64,7 @@ Zotero.Sync.Storage = new function () {
* @param {String} libraryKey
* @param {Number|NULL}
*/
this.setItemDownloadPercentage = function (libraryKey, percentage) {
this.setItemDownloadPercentage = Zotero.Utilities.throttle(function (libraryKey, percentage) {
Zotero.debug("Setting image download percentage to " + percentage
+ " for item " + libraryKey);
@ -93,7 +93,7 @@ Zotero.Sync.Storage = new function () {
}
Zotero.Notifier.trigger('redraw', 'item', parentItem.id, { column: "hasAttachment" });
}
}
}, 100);
function error(e) {

View file

@ -149,7 +149,9 @@ Zotero.Sync.Storage.Engine.prototype.start = Zotero.Promise.coroutine(function*
// full check of this library, check only files that were previously modified or opened
// recently
else if (this.background
&& !this.firstInSession
// TEMP: Don't check all files at startup
// https://github.com/zotero/zotero/issues/5025
//&& !this.firstInSession
&& this.local.lastFullFileCheck[libraryID]
&& (this.local.lastFullFileCheck[libraryID]
+ (this.maxCheckAge * 1000)) > new Date().getTime()) {

View file

@ -330,21 +330,21 @@ Zotero.Sync.Storage.Local = {
var fileName = PathUtils.filename(path);
try {
// If file is already marked for upload, skip check. Even if the file was changed
// both locally and remotely, conflicts are checked at upload time, so we don't need
// to worry about it here.
//
// This is after open() so that a missing file is properly marked for download.
if (item.attachmentSyncState == this.SYNC_STATE_TO_UPLOAD) {
Zotero.debug("File is already marked for upload");
return false;
}
let { lastModified: fmtime } = yield IOUtils.stat(path);
//Zotero.debug("Memory usage: " + memmgr.resident);
//Zotero.debug("File modification time for item " + lk + " is " + fmtime);
// If file is already marked for upload, skip check. Even if the file was changed
// both locally and remotely, conflicts are checked at upload time, so we don't need
// to worry about it here.
//
// This is after stat() so that a missing file is properly marked for download.
if (item.attachmentSyncState == this.SYNC_STATE_TO_UPLOAD) {
Zotero.debug("File is already marked for upload");
return false;
}
if (fmtime < 0) {
Zotero.debug("File mod time " + fmtime + " is less than 0 -- interpreting as 0", 2);
fmtime = 0;
@ -550,6 +550,10 @@ Zotero.Sync.Storage.Local = {
}
sql = "UPDATE itemAttachments SET syncState=? WHERE itemID IN (" + sql + ")";
await Zotero.DB.queryAsync(sql, [this.SYNC_STATE_TO_UPLOAD].concat(params));
var library = Zotero.Libraries.get(libraryID);
library.storageVersion = -1;
await library.save();
}.bind(this));
},

View file

@ -1,276 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://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 *****
*/
/**
* Stream listener that can handle both download and upload requests
*
* Possible properties of data object:
* - onStart: f(request)
* - onProgress: f(request, progress, progressMax)
* - onStop: f(request, status, response)
* - onCancel: f(request, status)
* - streams: array of streams to close on completion
*/
Zotero.Sync.Storage.StreamListener = function (data) {
this._data = data;
}
Zotero.Sync.Storage.StreamListener.prototype = {
_channel: null,
// nsIProgressEventSink
onProgress: function (request, progress, progressMax) {
Zotero.debug("onProgress with " + progress + "/" + progressMax);
this._onProgress(request, progress, progressMax);
},
onStatus: function (request, status, statusArg) {
Zotero.debug('onStatus with ' + status);
},
// nsIRequestObserver
// Note: For uploads, this isn't called until data is done uploading
onStartRequest: function (request) {
Zotero.debug('onStartRequest');
this._response = "";
this._onStart(request);
},
onStopRequest: function (request, status) {
Zotero.debug('onStopRequest with ' + status);
// Some errors from https://developer.mozilla.org/en-US/docs/Table_Of_Errors
var msg = "";
switch (status) {
// Normal
case 0:
break;
// NS_BINDING_ABORTED
case 0x804b0002:
msg = "Request cancelled";
break;
// NS_ERROR_NET_INTERRUPT
case 0x804B0047:
msg = "Request interrupted";
break;
// NS_ERROR_NET_TIMEOUT
case 0x804B000E:
msg = "Request timed out";
break;
default:
msg = "Request failed";
break;
}
if (msg) {
msg += " in Zotero.Sync.Storage.StreamListener.onStopRequest() (" + status + ")";
Components.utils.reportError(msg);
Zotero.debug(msg, 1);
}
this._onStop(request, status);
},
// nsIWebProgressListener
onProgressChange: function (wp, request, curSelfProgress,
maxSelfProgress, curTotalProgress, maxTotalProgress) {
//Zotero.debug("onProgressChange with " + curTotalProgress + "/" + maxTotalProgress);
// onProgress gets called too, so this isn't necessary
//this._onProgress(request, curTotalProgress, maxTotalProgress);
},
onStateChange: function (wp, request, stateFlags, status) {
Zotero.debug("onStateChange with " + stateFlags);
if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_REQUEST) {
if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_START) {
this._onStart(request);
}
else if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP) {
this._onStop(request, status);
}
}
},
onStatusChange: function (progress, request, status, message) {
Zotero.debug("onStatusChange with '" + message + "'");
},
onLocationChange: function () {
Zotero.debug('onLocationChange');
},
onSecurityChange: function () {
Zotero.debug('onSecurityChange');
},
// nsIStreamListener
onDataAvailable: function (request, stream, sourceOffset, length) {
Zotero.debug('onDataAvailable');
var scriptableInputStream =
Components.classes["@mozilla.org/scriptableinputstream;1"]
.createInstance(Components.interfaces.nsIScriptableInputStream);
scriptableInputStream.init(stream);
var data = scriptableInputStream.read(length);
Zotero.debug(data);
this._response += data;
},
// nsIChannelEventSink
//
// If this._data.onChannelRedirect exists, it should return a promise resolving to true to
// follow the redirect or false to cancel it
onChannelRedirect: Zotero.Promise.coroutine(function* (oldChannel, newChannel, flags) {
Zotero.debug('onChannelRedirect');
if (this._data && this._data.onChannelRedirect) {
let result = yield this._data.onChannelRedirect(oldChannel, newChannel, flags);
if (!result) {
oldChannel.cancel(Components.results.NS_BINDING_ABORTED);
newChannel.cancel(Components.results.NS_BINDING_ABORTED);
Zotero.debug("Cancelling redirect");
// TODO: Prevent onStateChange error
return false;
}
}
// if redirecting, store the new channel
this._channel = newChannel;
}),
asyncOnChannelRedirect: function (oldChan, newChan, flags, redirectCallback) {
Zotero.debug('asyncOnRedirect');
this.onChannelRedirect(oldChan, newChan, flags)
.then(function (result) {
redirectCallback.onRedirectVerifyCallback(
result ? Components.results.NS_SUCCEEDED : Components.results.NS_FAILED
);
})
.catch(function (e) {
Zotero.logError(e);
redirectCallback.onRedirectVerifyCallback(Components.results.NS_FAILED);
});
},
// nsIHttpEventSink
onRedirect: function (oldChannel, newChannel) {
Zotero.debug('onRedirect');
var newURL = Zotero.HTTP.getDisplayURI(newChannel.URI).spec;
Zotero.debug("Redirecting to " + newURL);
},
//
// Private methods
//
_onStart: function (request) {
Zotero.debug('Starting request');
if (this._data && this._data.onStart) {
this._data.onStart(request);
}
},
_onProgress: function (request, progress, progressMax) {
if (this._data && this._data.onProgress) {
this._data.onProgress(request, progress, progressMax);
}
},
_onStop: function (request, status) {
var cancelled = status == 0x804b0002; // NS_BINDING_ABORTED
if (!cancelled && status == 0 && request instanceof Components.interfaces.nsIHttpChannel) {
request.QueryInterface(Components.interfaces.nsIHttpChannel);
try {
status = request.responseStatus;
}
catch (e) {
Zotero.debug("Request responseStatus not available", 1);
status = 0;
}
Zotero.debug('Request ended with status code ' + status);
request.QueryInterface(Components.interfaces.nsIRequest);
}
else {
Zotero.debug('Request ended with status ' + status);
status = 0;
}
if (this._data.streams) {
for (let stream of this._data.streams) {
stream.close();
}
}
if (cancelled) {
if (this._data.onCancel) {
this._data.onCancel(request, status);
}
}
else {
if (this._data.onStop) {
this._data.onStop(request, status, this._response);
}
}
this._channel = null;
},
// nsIInterfaceRequestor
getInterface: function (iid) {
try {
return this.QueryInterface(iid);
}
catch (e) {
throw Components.results.NS_NOINTERFACE;
}
},
QueryInterface: function(iid) {
if (iid.equals(Components.interfaces.nsISupports) ||
iid.equals(Components.interfaces.nsIInterfaceRequestor) ||
iid.equals(Components.interfaces.nsIChannelEventSink) ||
iid.equals(Components.interfaces.nsIProgressEventSink) ||
iid.equals(Components.interfaces.nsIHttpEventSink) ||
iid.equals(Components.interfaces.nsIStreamListener) ||
iid.equals(Components.interfaces.nsIWebProgressListener)) {
return this;
}
throw Components.results.NS_NOINTERFACE;
},
_safeSpec: function (uri) {
return uri.scheme + '://' + uri.username + ':********@'
+ uri.hostPort + uri.pathQueryRef
},
};

View file

@ -31,10 +31,10 @@ if (!Zotero.Sync.Storage.Mode) {
Zotero.Sync.Storage.Mode.WebDAV = function (options) {
this.options = options;
this.VerificationError = function (error, uri) {
this.VerificationError = function (error, url) {
this.message = `WebDAV verification error (${error})`;
this.error = error;
this.uri = uri;
this.url = url;
}
this.VerificationError.prototype = Object.create(Error.prototype);
}
@ -196,14 +196,23 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
throw new this.VerificationError("NO_PASSWORD");
}
url = scheme + '://'
var spec = scheme + '://'
+ encodeURIComponent(username) + ':' + encodeURIComponent(password) + '@'
+ url
+ (url.endsWith('/') ? '' : '/');
var io = Services.io;
this._parentURI = io.newURI(url, null, null);
this._rootURI = io.newURI(url + "zotero/", null, null);
try {
this._parentURI = io.newURI(spec, null, null);
}
catch (e) {
if (e.message.includes('NS_ERROR_MALFORMED_URI')) {
let displayURL = scheme + '://' + url + (url.endsWith('/') ? '' : '/');
throw new this.VerificationError("INVALID_URL", displayURL);
}
throw e;
}
this._rootURI = io.newURI(spec + "zotero/", null, null);
Zotero.HTTP.CookieBlocker.addURL(this._rootURI.spec);
},
@ -258,7 +267,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
* @param {Zotero.Sync.Storage.Request} request
* @return {Promise<Zotero.Sync.Storage.Result>}
*/
downloadFile: Zotero.Promise.coroutine(function* (request) {
downloadFile: async function (request) {
var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request);
if (!item) {
throw new Error("Item '" + request.name + "' not found");
@ -272,7 +281,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
}
// Retrieve modification time from server
var metadata = yield this._getStorageFileMetadata(item, request);
var metadata = await this._getStorageFileMetadata(item, request);
if (!request.isRunning()) {
Zotero.debug("Download request '" + request.name
@ -285,7 +294,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
return new Zotero.Sync.Storage.Result;
}
var fileModTime = yield item.attachmentModificationTime;
var fileModTime = await item.attachmentModificationTime;
if (metadata.mtime == fileModTime) {
Zotero.debug("File mod time matches remote file -- skipping download of "
+ item.libraryKey);
@ -293,10 +302,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
var updateItem = item.attachmentSyncState != 1
item.attachmentSyncedModificationTime = metadata.mtime;
item.attachmentSyncState = "in_sync";
yield item.saveTx({ skipAll: true });
await item.saveTx({ skipAll: true });
// DEBUG: Necessary?
if (updateItem) {
yield item.updateSynced(false);
await item.updateSynced(false);
}
return new Zotero.Sync.Storage.Result({
@ -307,9 +316,8 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
var uri = this._getItemURI(item);
var destPath = OS.Path.join(Zotero.getTempDirectory().path, item.key + '.tmp');
yield Zotero.File.removeIfExists(destPath);
await Zotero.File.removeIfExists(destPath);
var deferred = Zotero.Promise.defer();
var requestData = {
item,
mtime: metadata.mtime,
@ -317,88 +325,71 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
compressed: true
};
var listener = new Zotero.Sync.Storage.StreamListener(
{
onStart: function (req) {
if (request.isFinished()) {
Zotero.debug("Download request " + request.name
+ " stopped before download started -- closing channel");
req.cancel(0x804b0002); // NS_BINDING_ABORTED
deferred.resolve(new Zotero.Sync.Storage.Result);
return new Promise(async (resolve, reject) => {
try {
let req = await Zotero.HTTP.download(
uri,
destPath,
{
successCodes: [200, 404],
noCache: true,
notificationCallbacks: {
onProgress: function (req, progress, progressMax) {
request.onProgress(progress, progressMax)
},
},
errorDelayIntervals: this.ERROR_DELAY_INTERVALS,
errorDelayMax: this.ERROR_DELAY_MAX,
}
},
onProgress: function (a, b, c) {
request.onProgress(a, b, c)
},
onStop: Zotero.Promise.coroutine(function* (req, status, res) {
request.setChannel(false);
if (status == 404) {
let msg = "Remote ZIP file not found for item " + item.libraryKey;
Zotero.debug(msg, 2);
Components.utils.reportError(msg);
// Delete the orphaned prop file
try {
yield this._deleteStorageFiles([item.key + ".prop"]);
}
catch (e) {
Zotero.logError(e);
}
deferred.resolve(new Zotero.Sync.Storage.Result);
return;
}
else if (status != 200) {
try {
this._throwFriendlyError("GET", dispURL, status);
}
catch (e) {
deferred.reject(e);
}
return;
}
// Don't try to process if the request has been cancelled
if (request.isFinished()) {
Zotero.debug("Download request " + request.name
+ " is no longer running after file download");
deferred.resolve(new Zotero.Sync.Storage.Result);
return;
}
Zotero.debug("Finished download of " + destPath);
);
if (req.status == 404) {
let msg = "Remote ZIP file not found for item " + item.libraryKey;
Zotero.debug(msg, 2);
Cu.reportError(msg);
// Delete the orphaned prop file
try {
deferred.resolve(
Zotero.Sync.Storage.Local.processDownload(requestData)
);
await this._deleteStorageFiles([item.key + ".prop"]);
}
catch (e) {
deferred.reject(e);
}
}.bind(this)),
onCancel: function (req, status) {
Zotero.debug("Request cancelled");
if (deferred.promise.isPending()) {
deferred.resolve(new Zotero.Sync.Storage.Result);
Zotero.logError(e);
}
resolve(new Zotero.Sync.Storage.Result);
return;
}
// Don't try to process if the request has been cancelled
if (request.isFinished()) {
Zotero.debug("Download request " + request.name
+ " is no longer running after file download");
resolve(new Zotero.Sync.Storage.Result);
return;
}
Zotero.debug("Finished download of " + destPath);
resolve(
Zotero.Sync.Storage.Local.processDownload(requestData)
);
}
);
// Don't display password in console
var dispURL = Zotero.HTTP.getDisplayURI(uri).spec;
Zotero.debug('Saving ' + dispURL);
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(nsIWBP);
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
wbp.progressListener = listener;
Zotero.Utilities.Internal.saveURI(wbp, uri, destPath);
return deferred.promise;
}),
catch (e) {
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
try {
let dispURL = Zotero.HTTP.getDisplayURI(uri).spec;
this._throwFriendlyError("GET", dispURL, e.xmlhttp.status);
}
catch (e) {
reject(e);
}
return;
}
Zotero.logError(e);
reject(new Error(Zotero.Sync.Storage.defaultError));
}
});
},
uploadFile: Zotero.Promise.coroutine(function* (request) {
@ -641,7 +632,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
var dav = req.getResponseHeader("DAV");
if (dav == null) {
throw new this.VerificationError("NOT_DAV", uri);
throw new this.VerificationError("NOT_DAV", Zotero.HTTP.getDisplayURI(uri, true).spec);
}
var headers = { Depth: 0 };
@ -683,7 +674,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
catch (e) {
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
if (e.status >= 200 && e.status < 300) {
throw new this.VerificationError("NONEXISTENT_FILE_NOT_MISSING", uri);
throw new this.VerificationError(
"NONEXISTENT_FILE_NOT_MISSING",
Zotero.HTTP.getDisplayURI(uri, true).spec
);
}
}
throw e;
@ -730,7 +724,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
// This can also be from IIS 6+, which is configured not to serve .prop files.
// http://support.microsoft.com/kb/326965
else if (req.status == 404) {
throw new this.VerificationError("FILE_MISSING_AFTER_UPLOAD", uri);
throw new this.VerificationError(
"FILE_MISSING_AFTER_UPLOAD",
Zotero.HTTP.getDisplayURI(uri, true).spec
);
}
}
else if (req.status == 404) {
@ -751,10 +748,16 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
});
if (req.status == 207) {
throw new this.VerificationError("ZOTERO_DIR_NOT_FOUND", uri);
throw new this.VerificationError(
"ZOTERO_DIR_NOT_FOUND",
Zotero.HTTP.getDisplayURI(uri, true).spec
);
}
else if (req.status == 404) {
throw new this.VerificationError("PARENT_DIR_NOT_FOUND", uri);
throw new this.VerificationError(
"PARENT_DIR_NOT_FOUND",
Zotero.HTTP.getDisplayURI(uri, true).spec
);
}
}
@ -805,10 +808,6 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
}
}
else if (err instanceof this.VerificationError) {
let spec;
if (err.uri) {
spec = err.uri.scheme + '://' + err.uri.hostPort + err.uri.pathQueryRef;
}
switch (err.error) {
case "NO_URL":
errorMsg = Zotero.getString('sync.storage.error.webdav.enterURL');
@ -822,21 +821,22 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
errorMsg = Zotero.getString('sync.error.enterPassword');
break;
case "INVALID_URL":
case "NOT_DAV":
errorMsg = Zotero.getString('sync.storage.error.webdav.invalidURL', spec);
errorMsg = Zotero.getString('sync.storage.error.webdav.invalidURL', err.url);
break;
case "PARENT_DIR_NOT_FOUND":
errorTitle = Zotero.getString('sync.storage.error.directoryNotFound');
var parentSpec = spec.replace(/zotero\/$/, "");
errorMsg = Zotero.getString('sync.storage.error.doesNotExist', parentSpec);
var parentURL = err.url.replace(/zotero\/$/, "");
errorMsg = Zotero.getString('sync.storage.error.doesNotExist', parentURL);
break;
case "ZOTERO_DIR_NOT_FOUND":
var create = promptService.confirmEx(
window,
Zotero.getString('sync.storage.error.directoryNotFound'),
Zotero.getString('sync.storage.error.doesNotExist', spec) + "\n\n"
Zotero.getString('sync.storage.error.doesNotExist', err.url) + "\n\n"
+ Zotero.getString('sync.storage.error.createNow'),
promptService.BUTTON_POS_0
* promptService.BUTTON_TITLE_IS_STRING

View file

@ -49,7 +49,7 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
* @param {Zotero.Sync.Storage.Request} request
* @return {Promise<Zotero.Sync.Storage.Result>}
*/
downloadFile: Zotero.Promise.coroutine(function* (request) {
downloadFile: async function (request) {
var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request);
if (!item) {
throw new Error("Item '" + request.name + "' not found");
@ -63,195 +63,147 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
var destPath = OS.Path.join(Zotero.getTempDirectory().path, item.key + '.tmp');
// saveURI() below appears not to create empty files for Content-Length: 0,
// so we create one here just in case, which also lets us check file access
// Create an empty file to check file access
try {
yield IOUtils.write(destPath, new Uint8Array());
await IOUtils.write(destPath, new Uint8Array());
}
catch (e) {
Zotero.File.checkFileAccessError(e, destPath, 'create');
}
var deferred = Zotero.Promise.defer();
var requestData = {item};
var listener = new Zotero.Sync.Storage.StreamListener(
{
onStart: function (req) {
if (request.isFinished()) {
Zotero.debug("Download request " + request.name
+ " stopped before download started -- closing channel");
req.cancel(Components.results.NS_BINDING_ABORTED);
deferred.resolve(new Zotero.Sync.Storage.Result);
}
},
onChannelRedirect: Zotero.Promise.coroutine(function* (oldChannel, newChannel, flags) {
// These will be used in processDownload() if the download succeeds
oldChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
Zotero.debug("CHANNEL HERE FOR " + item.libraryKey + " WITH " + oldChannel.status);
Zotero.debug(oldChannel.URI.spec);
Zotero.debug(newChannel.URI.spec);
var header;
try {
header = "Zotero-File-Modification-Time";
requestData.mtime = parseInt(oldChannel.getResponseHeader(header));
header = "Zotero-File-MD5";
requestData.md5 = oldChannel.getResponseHeader(header);
header = "Zotero-File-Compressed";
requestData.compressed = oldChannel.getResponseHeader(header) == 'Yes';
}
catch (e) {
deferred.reject(new Error(`${header} header not set in file request for ${item.libraryKey}`));
return false;
}
if (!(yield OS.File.exists(path))) {
return true;
}
var updateHash = false;
var fileModTime = yield item.attachmentModificationTime;
if (requestData.mtime == fileModTime) {
Zotero.debug("File mod time matches remote file -- skipping download of "
+ item.libraryKey);
}
// If not compressed, check hash, in case only timestamp changed
else if (!requestData.compressed && (yield item.attachmentHash) == requestData.md5) {
Zotero.debug("File hash matches remote file -- skipping download of "
+ item.libraryKey);
updateHash = true;
}
else {
return true;
}
// Update local metadata and stop request, skipping file download
yield OS.File.setDates(path, null, new Date(requestData.mtime));
item.attachmentSyncedModificationTime = requestData.mtime;
if (updateHash) {
item.attachmentSyncedHash = requestData.md5;
}
item.attachmentSyncState = "in_sync";
yield item.saveTx({ skipAll: true });
deferred.resolve(new Zotero.Sync.Storage.Result({
localChanges: true
}));
return false;
}),
onProgress: function (req, progress, progressMax) {
request.onProgress(progress, progressMax);
},
onStop: function (req, status, res) {
request.setChannel(false);
if (status != 200) {
if (status == 404) {
Zotero.debug("Remote file not found for item " + item.libraryKey);
// Don't refresh item pane rows when nothing happened
request.skipProgressBarUpdate = true;
deferred.resolve(new Zotero.Sync.Storage.Result);
return;
}
// Check for SSL certificate error
if (status == 0) {
try {
Zotero.HTTP.checkSecurity(req);
}
catch (e) {
deferred.reject(e);
return;
}
}
// If S3 connection is interrupted, delay and retry, or bail if too many
// consecutive failures
if (status == 0 || status == 500 || status == 503) {
if (++this._s3ConsecutiveFailures < this._maxS3ConsecutiveFailures) {
let libraryKey = item.libraryKey;
let msg = "S3 returned 0 for " + libraryKey + " -- retrying download"
Components.utils.reportError(msg);
Zotero.debug(msg, 1);
if (this._s3Backoff < this._maxS3Backoff) {
this._s3Backoff *= 2;
}
Zotero.debug("Delaying " + libraryKey + " download for "
+ this._s3Backoff + " seconds", 2);
Zotero.Promise.delay(this._s3Backoff * 1000)
.then(function () {
deferred.resolve(this.downloadFile(request));
}.bind(this));
return;
}
Zotero.debug(this._s3ConsecutiveFailures
+ " consecutive S3 failures -- aborting", 1);
this._s3ConsecutiveFailures = 0;
}
var msg = "Unexpected status code " + status + " for GET " + uri;
Zotero.debug(msg, 1);
Components.utils.reportError(msg);
// Output saved content, in case an error was captured
try {
let sample = Zotero.File.getContents(destPath, null, 4096);
if (sample) {
Zotero.debug(sample, 1);
}
}
catch (e) {
Zotero.debug(e, 1);
}
deferred.reject(new Error(Zotero.Sync.Storage.defaultError));
return;
}
// Don't try to process if the request has been cancelled
if (request.isFinished()) {
Zotero.debug("Download request " + request.name
+ " is no longer running after file download", 2);
deferred.resolve(new Zotero.Sync.Storage.Result);
return;
}
Zotero.debug("Finished download of " + destPath);
try {
deferred.resolve(
Zotero.Sync.Storage.Local.processDownload(requestData)
);
}
catch (e) {
deferred.reject(e);
}
}.bind(this),
onCancel: function (req, status) {
Zotero.debug("Request cancelled");
if (deferred.promise.isPending()) {
deferred.resolve(new Zotero.Sync.Storage.Result);
}
}
}
);
var params = this._getRequestParams(item.libraryID, `items/${item.key}/file`);
var uri = this.apiClient.buildRequestURI(params);
var headers = this.apiClient.getHeaders();
Zotero.debug('Saving ' + uri);
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(nsIWBP);
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
wbp.progressListener = listener;
Zotero.Utilities.Internal.saveURI(wbp, uri, destPath, headers);
return deferred.promise;
}),
return new Promise(async (resolve, reject) => {
try {
let req = await Zotero.HTTP.download(
uri,
destPath,
{
successCodes: [200, 404],
headers: this.apiClient.getHeaders(),
noCache: true,
notificationCallbacks: {
asyncOnChannelRedirect: async function (oldChannel, newChannel, flags, callback) {
// These will be used in processDownload() if the download succeeds
oldChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
Zotero.debug(`Handling ${oldChannel.responseStatus} redirect for ${item.libraryKey}`);
Zotero.debug(oldChannel.URI.spec);
Zotero.debug(newChannel.URI.spec);
var header;
try {
header = "Zotero-File-Modification-Time";
requestData.mtime = parseInt(oldChannel.getResponseHeader(header));
header = "Zotero-File-MD5";
requestData.md5 = oldChannel.getResponseHeader(header);
header = "Zotero-File-Compressed";
requestData.compressed = oldChannel.getResponseHeader(header) == 'Yes';
}
catch (_e) {
reject(new Error(`${header} header not set in file request for ${item.libraryKey}`));
callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
return;
}
if (!(await IOUtils.exists(path))) {
callback.onRedirectVerifyCallback(Cr.NS_OK);
return;
}
var updateHash = false;
var fileModTime = await item.attachmentModificationTime;
if (requestData.mtime == fileModTime) {
Zotero.debug("File mod time matches remote file -- skipping download of "
+ item.libraryKey);
}
// If not compressed, check hash, in case only timestamp changed
else if (!requestData.compressed && (await item.attachmentHash) == requestData.md5) {
Zotero.debug("File hash matches remote file -- skipping download of "
+ item.libraryKey);
updateHash = true;
}
else {
callback.onRedirectVerifyCallback(Cr.NS_OK);
return;
}
// Update local metadata and stop request, skipping file download
await OS.File.setDates(path, null, new Date(requestData.mtime));
item.attachmentSyncedModificationTime = requestData.mtime;
if (updateHash) {
item.attachmentSyncedHash = requestData.md5;
}
item.attachmentSyncState = "in_sync";
await item.saveTx({ skipAll: true });
resolve(new Zotero.Sync.Storage.Result({
localChanges: true
}));
callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
},
onProgress: function (req, progress, progressMax) {
request.onProgress(progress, progressMax);
},
},
}
);
if (req.status == 404) {
Zotero.debug("Remote file not found for item " + item.libraryKey);
// Don't refresh item pane rows when nothing happened
request.skipProgressBarUpdate = true;
resolve(new Zotero.Sync.Storage.Result);
return;
}
// Don't try to process if the request has been cancelled
if (request.isFinished()) {
Zotero.debug(`Download request ${request.name} is no longer running after file download`, 2);
resolve(new Zotero.Sync.Storage.Result);
return;
}
Zotero.debug("Finished download of " + destPath);
resolve(await Zotero.Sync.Storage.Local.processDownload(requestData));
}
catch (e) {
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
// If S3 connection is interrupted, delay and retry, or bail if too many
// consecutive failures
if (e.xmlhttp.status == 0) {
if (++this._s3ConsecutiveFailures < this._maxS3ConsecutiveFailures) {
let libraryKey = item.libraryKey;
let msg = "S3 returned 0 for " + libraryKey + " -- retrying download";
Zotero.logError(msg);
if (this._s3Backoff < this._maxS3Backoff) {
this._s3Backoff *= 2;
}
Zotero.debug("Delaying " + libraryKey + " download for "
+ this._s3Backoff + " seconds", 2);
Zotero.Promise.delay(this._s3Backoff * 1000)
.then(function () {
resolve(this.downloadFile(request));
}.bind(this));
return;
}
Zotero.debug(this._s3ConsecutiveFailures
+ " consecutive S3 failures -- aborting", 1);
this._s3ConsecutiveFailures = 0;
}
}
Zotero.logError(e);
reject(new Error(Zotero.Sync.Storage.defaultError));
}
});
},
uploadFile: Zotero.Promise.coroutine(function* (request) {
@ -1076,7 +1028,9 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
var filename = item.attachmentFilename;
var fileSize = (await OS.File.stat(item.getFilePath())).size;
text += "\n\n" + filename + " (" + Math.round(fileSize / 1024) + " KB)";
text += "\n\n" + filename
// TODO: Format more intelligently (e.g., use MB or GB for large files)
+ " (" + Zotero.Utilities.numberFormat(Math.round(fileSize / 1024), 0) + " KB)";
var e = new Zotero.Error(
text,

View file

@ -933,6 +933,22 @@ Zotero.Sync.Data.Local = {
['mtime', 'md5', 'dateAdded', 'dateModified']
);
// If local object became a child item and remote was added to any
// collections, we need to remove the 'collections' changes and add
// the parent item to those collections instead
if (objectType == 'item'
&& !obj.isTopLevelItem()
&& (obj.isNote() || obj.isAttachment())) {
let collections = result.changes
.filter(x => x.field == 'collections' && x.op == 'member-add')
.map(x => x.value);
if (collections.length) {
result.changes = result.changes
.filter(x => !(x.field == 'collections' && x.op == 'member-add'));
saveOptions.newParentItemCollections = collections;
}
}
// If no changes, just update local version number and mark as synced
if (!result.changes.length && !result.conflicts.length) {
Zotero.debug("No remote changes to apply to local "
@ -1542,6 +1558,24 @@ Zotero.Sync.Data.Local = {
Zotero.Libraries.get(obj.libraryID).storageDownloadNeeded = true;
}
}
// See explanation in processObjectsFromJSON()
if (options.newParentItemCollections) {
let parentItem = obj.parentItem;
for (let c of options.newParentItemCollections) {
parentItem.addToCollection(c);
}
yield parentItem.save({
skipEditCheck: true,
skipDateModifiedUpdate: true,
skipSelect: true,
notifierQueue: options.notifierQueue,
// Errors are logged elsewhere, so skip in DataObject.save()
errorHandler: function (e) {
return;
}
});
}
}
catch (e) {
// For now, allow sync to proceed after all errors

View file

@ -230,7 +230,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
}
}.bind(this),
background: !!options.background,
firstInSession: _firstInSession,
firstInSession: options.firstInSession,
resetMode: options.resetMode
};
@ -913,14 +913,25 @@ Zotero.Sync.Runner_Module = function (options = {}) {
}
this.end = Zotero.Promise.coroutine(function* (options) {
this.end = async function (options) {
_syncInProgress = false;
yield this.checkErrors(_errors, options);
await this.checkErrors(_errors, options);
if (!options.restartSync) {
this.updateIcons(_errors);
// If foreground sync, trigger dialog button immediately for some errors
// (e.g., long tag fixer)
if (!options.background && _errors.length) {
if (_errors[0].dialogButtonImmediate) {
let maybePromise = _errors[0].dialogButtonCallback();
if (maybePromise && maybePromise.then) {
await maybePromise;
}
}
}
}
_errors = [];
});
};
/**
@ -1253,6 +1264,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
options.restartSync = true;
});
e.dialogButtonImmediate = true;
}
else {
// Note too long
@ -1296,14 +1308,6 @@ Zotero.Sync.Runner_Module = function (options = {}) {
};
}
}
// If not a background sync, show dialog immediately
if (!options.background && e.dialogButtonCallback) {
let maybePromise = e.dialogButtonCallback();
if (maybePromise && maybePromise.then) {
yield maybePromise;
}
}
}
}
// Show warning for unknown data that couldn't be saved

View file

@ -59,6 +59,7 @@ Zotero.Translate.ItemSaver = function(options) {
this._referrer = options.referrer;
this._cookieSandbox = options.cookieSandbox;
this._proxy = options.proxy;
this._itemToJSONItem = new Map();
// the URI to which other URIs are assumed to be relative
if(typeof options.baseURI === "object" && options.baseURI instanceof Components.interfaces.nsIURI) {
@ -82,6 +83,7 @@ Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES = new Set([
]);
Zotero.Translate.ItemSaver.prototype = {
/**
* Saves items to Standalone or the server
* @param {Object[]} jsonItems - Items in Zotero.Item.toArray() format
@ -90,21 +92,17 @@ Zotero.Translate.ItemSaver.prototype = {
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
* @param {Function} [itemsDoneCallback] A callback that is called once all top-level items are
* done saving with a list of items. Will include saved notes, but exclude attachments.
* @param {Function} [pendingAttachmentsCallback] A callback that is called for every
* pending attachment to an item. pendingAttachmentsCallback(parentItemID, jsonAttachment)
*/
saveItems: async function (jsonItems, attachmentCallback, itemsDoneCallback, pendingAttachmentsCallback) {
saveItems: async function (jsonItems, attachmentCallback, itemsDoneCallback) {
var items = [];
var standaloneAttachments = [];
var childAttachments = [];
var jsonByItem = new Map();
await Zotero.DB.executeTransaction(async function () {
for (let jsonItem of jsonItems) {
jsonItem = Object.assign({}, jsonItem);
let item;
let itemID;
// Type defaults to "webpage"
let type = jsonItem.itemType || "webpage";
@ -121,84 +119,25 @@ Zotero.Translate.ItemSaver.prototype = {
continue;
}
else {
item = new Zotero.Item(type);
item.libraryID = this._libraryID;
if (jsonItem.creators) this._cleanCreators(jsonItem.creators);
if (jsonItem.tags) jsonItem.tags = this._cleanTags(jsonItem.tags);
if (jsonItem.accessDate == 'CURRENT_TIMESTAMP') {
jsonItem.accessDate = Zotero.Date.dateToISO(new Date());
}
item.fromJSON(this._copyJSONItemForImport(jsonItem));
// deproxify url
if (this._proxy && jsonItem.url) {
let url = this._proxy.toProper(jsonItem.url);
Zotero.debug(`Deproxifying item url ${jsonItem.url} with scheme ${this._proxy.scheme} to ${url}`, 5);
item.setField('url', url);
}
if (this._collections) {
item.setCollections(this._collections);
}
// save item
itemID = await item.save(this._saveOptions);
// handle notes
if (jsonItem.notes) {
for (let note of jsonItem.notes) {
await this._saveNote(note, itemID);
}
}
item = await this._saveItem(jsonItem, type);
// handle attachments
if (jsonItem.attachments) {
let attachmentsToSave = [];
let foundPrimary = false;
for (let jsonAttachment of jsonItem.attachments) {
if (!this._canSaveAttachment(jsonAttachment)) {
continue;
}
// The first PDF/EPUB is the primary one. If that one fails to download,
// we might check for an open-access PDF below.
if (Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(jsonAttachment.mimeType)
&& !foundPrimary) {
jsonAttachment.isPrimary = true;
foundPrimary = true;
}
attachmentsToSave.push(jsonAttachment);
attachmentCallback(jsonAttachment, 0);
if (jsonAttachment.singleFile) {
// SingleFile attachments are saved in 'saveSingleFile'
// connector endpoint
if (pendingAttachmentsCallback) {
pendingAttachmentsCallback(itemID, jsonAttachment);
}
continue;
}
childAttachments.push([jsonAttachment, itemID]);
}
jsonItem.attachments = attachmentsToSave;
}
// handle see also
this._handleRelated(jsonItem, item);
// process attachments
let attachments = this._processChildAttachments(jsonItem, attachmentCallback);
attachments.forEach(attachment => childAttachments.push([attachment, item.id]));
}
// Add to new item list
items.push(item);
jsonByItem.set(item, jsonItem);
this._itemToJSONItem.set(item, jsonItem);
}
}.bind(this));
// Done saving top-level items. Call the callback so that UI code can update
if (itemsDoneCallback) {
itemsDoneCallback(items.map(item => jsonByItem.get(item)), items);
itemsDoneCallback(items.map(item => this._itemToJSONItem.get(item)), items);
}
// Save standalone attachments
// Download standalone attachments
for (let jsonItem of standaloneAttachments) {
let item = await this._saveAttachment(jsonItem, null, attachmentCallback);
if (item) {
@ -210,45 +149,21 @@ Zotero.Translate.ItemSaver.prototype = {
// open-access PDFs. There's no guarantee that either translated PDFs or OA PDFs will
// successfully download, but this lets us update the progress window sooner with
// possible downloads.
//
this._openAccessPDFURLs = new Map();
// TODO: Separate pref?
var shouldDownloadOAPDF = this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD
&& Zotero.Prefs.get('downloadAssociatedFiles');
var openAccessPDFURLs = new Map();
&& Zotero.Prefs.get('downloadAssociatedFiles');
if (shouldDownloadOAPDF) {
for (let item of items) {
let jsonItem = jsonByItem.get(item);
// Skip items with translated PDF attachments
if (jsonItem.attachments
&& jsonItem.attachments.some(x => Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(x.mimeType))) {
continue;
}
try {
let resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
if (!resolvers.length) {
openAccessPDFURLs.set(item, []);
continue;
}
let urlObjects = await resolvers[0]();
openAccessPDFURLs.set(item, urlObjects);
// If there are possible URLs, create a status line for the PDF
if (urlObjects.length) {
let title = Zotero.getString('findPDF.openAccessPDF');
let jsonAttachment = this._makeJSONAttachment(jsonItem.id, title);
if (!jsonItem.attachments) jsonItem.attachments = [];
jsonItem.attachments.push(jsonAttachment);
attachmentCallback(jsonAttachment, 0);
}
}
catch (e) {
Zotero.logError(e);
let urlObjects = await this._getOpenAccessPDFURLs(item, attachmentCallback);
if (urlObjects) {
this._openAccessPDFURLs.set(item, urlObjects);
}
}
}
// Save translated child attachments, and keep track of whether the save was successful
// Save translated child attachments
var itemIDsWithPrimaryAttachments = new Set();
for (let [jsonAttachment, parentItemID] of childAttachments) {
let attachment = await this._saveAttachment(
@ -267,93 +182,15 @@ Zotero.Translate.ItemSaver.prototype = {
}
}
// If a translated PDF attachment wasn't saved successfully, either because there wasn't
// one or there was but it failed, look for another PDF (if enabled)
if (shouldDownloadOAPDF) {
// If a translated PDF attachment wasn't saved successfully, either because there wasn't
// one or there was but it failed, look for another PDF (if enabled)
for (let item of items) {
// Already have a primary attachment from translation
if (itemIDsWithPrimaryAttachments.has(item.id)) {
continue;
}
let jsonItem = jsonByItem.get(item);
// Reuse the existing status line if there is one. This could be a failed
// translator attachment or a possible OA PDF found above.
// Explicitly check that the attachment is a PDF, not just any primary type,
// since we're reusing it for a PDF attachment.
let jsonAttachment = jsonItem.attachments && jsonItem.attachments.find(
x => x.mimeType == 'application/pdf' && x.isPrimary
);
// If no translated, no OA, and no custom, don't show a line
// If no translated and potential OA, show "Open-Access PDF"
// If no translated, no OA, but custom, show custom when it starts
// If translated fails and potential OA, show "Open-Access PDF"
// If translated fails, no OA, no custom, fail original
// If translated fails, no OA, but custom, change to custom when it starts
let resolvers = openAccessPDFURLs.get(item);
// No translated PDF, so we checked for OA PDFs above
if (resolvers) {
// Add custom resolvers
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
// No translated, no OA, no custom, no status line
if (!resolvers.length) {
continue;
}
// No translated, no OA, just potential custom, so create a status line
if (!jsonAttachment) {
jsonAttachment = this._makeJSONAttachment(
jsonItem.id, Zotero.getString('findPDF.searchingForAvailableFiles')
);
}
}
// There was a translated PDF, so we didn't check for OA PDFs yet and didn't
// update the status line
else {
// Look for OA PDFs now
resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
if (resolvers.length) {
resolvers = await resolvers[0]();
}
// Add custom resolvers
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
// Failed translated, no OA, no custom, so fail the existing translator line
if (!resolvers.length) {
attachmentCallback(jsonAttachment, false);
continue;
}
}
let attachment;
try {
attachment = await Zotero.Attachments.addFileFromURLs(
item,
resolvers,
{
// When a new access method starts, update the status line
onAccessMethodStart: (method) => {
jsonAttachment.title = this._getPDFTitleForAccessMethod(method);
attachmentCallback(jsonAttachment, 0);
}
}
);
}
catch (e) {
Zotero.logError(e);
attachmentCallback(jsonAttachment, false, e);
continue;
}
if (attachment) {
attachmentCallback(jsonAttachment, 100);
}
else {
attachmentCallback(jsonAttachment, false, "PDF not found");
}
await this.saveOpenAccessAttachment(item, attachmentCallback);
}
}
@ -364,23 +201,233 @@ Zotero.Translate.ItemSaver.prototype = {
/**
* Save pending snapshot attachments to disk and library
*
* @param {Array} pendingAttachments - A list of snapshot attachments
* @param {Object} content - Snapshot content from SingleFile
* @param {Function} attachmentCallback - Callback with progress of attachments
* @param {Object} options - A list of snapshot attachments
* - title {String}
* - url {String}
* - parentItemID {Number}
* - snapshotContent {String}
*/
saveSnapshotAttachments: Zotero.Promise.coroutine(function* (pendingAttachments, snapshotContent, attachmentCallback) {
for (let [parentItemID, attachment] of pendingAttachments) {
Zotero.debug('Saving pending attachment: ' + JSON.stringify(attachment));
if (snapshotContent) {
attachment.snapshotContent = snapshotContent;
}
yield this._saveAttachment(
saveSnapshotAttachments: async function (options) {
let { title, url, parentItemID, snapshotContent } = options;
let attachment = { title, url };
Zotero.debug('Saving pending attachment: ' + JSON.stringify(attachment));
if (snapshotContent) {
attachment.snapshotContent = snapshotContent;
}
await new Promise(async (resolve, reject) => {
await this._saveAttachment(
attachment,
parentItemID,
attachmentCallback
(attachment, progress, e) => {
if (e) reject(e);
if (progress === 100) {
resolve(progress);
}
}
);
});
},
async _saveItem(jsonItem, type) {
let itemID;
let item = new Zotero.Item(type);
item.libraryID = this._libraryID;
if (jsonItem.creators) this._cleanCreators(jsonItem.creators);
if (jsonItem.tags) jsonItem.tags = this._cleanTags(jsonItem.tags);
if (jsonItem.accessDate == 'CURRENT_TIMESTAMP') {
jsonItem.accessDate = Zotero.Date.dateToISO(new Date());
}
item.fromJSON(this._copyJSONItemForImport(jsonItem));
// deproxify url
if (this._proxy && jsonItem.url) {
let url = this._proxy.toProper(jsonItem.url);
Zotero.debug(`Deproxifying item url ${jsonItem.url} with scheme ${this._proxy.scheme} to ${url}`, 5);
item.setField('url', url);
}
// save item
if (this._collections) {
item.setCollections(this._collections);
}
itemID = await item.save(this._saveOptions);
// handle notes
if (jsonItem.notes) {
for (let note of jsonItem.notes) {
await this._saveNote(note, itemID);
}
}
// handle see also
this._handleRelated(jsonItem, item);
return item;
},
/**
* Processes attachments to be saved either via Zotero or externally (Connector)
*
* Calls attachment callbacks for initial attachment progress (0)
*/
_processChildAttachments(jsonItem, attachmentCallback) {
let childAttachments = [];
let foundPrimary = false;
// Attachments to be saved within Zotero
if (jsonItem.attachments) {
let attachmentsToSave = [];
for (let jsonAttachment of jsonItem.attachments) {
if (!this._canSaveAttachment(jsonAttachment)) {
continue;
}
// The first PDF/EPUB is the primary one. If that one fails to download,
// we might check for an open-access PDF below.
if (Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(jsonAttachment.mimeType)
&& !foundPrimary) {
jsonAttachment.isPrimary = true;
foundPrimary = true;
}
attachmentsToSave.push(jsonAttachment);
attachmentCallback(jsonAttachment, 0);
childAttachments.push(jsonAttachment);
}
jsonItem.attachments = attachmentsToSave;
}
return childAttachments;
},
/**
* Gets a list of OA PDF URLs for items that did not receive a PDF attachment
* from the translator
*
* Calls attachmentCallback to update UI
* @param items
* @param attachmentCallback
* @returns {Promise<Map<any, any>>}
* @private
*/
async _getOpenAccessPDFURLs(item, attachmentCallback) {
let jsonItem = this._itemToJSONItem.get(item);
let urlObjects = [];
// Has a primary attachment or a pending (from Connector) primary attachment
if (jsonItem.attachments?.some(x => Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(x.mimeType))
|| jsonItem.pendingPrimaryAttachment) {
return null;
}
// If no primary attachments available look for an OA one and call attachmentCallback to update UI
try {
let resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
if (!resolvers.length) {
return urlObjects;
}
urlObjects = await resolvers[0]();
// If there are possible URLs, create a status line for the PDF
if (urlObjects.length) {
let title = Zotero.getString('findPDF.openAccessPDF');
let jsonAttachment = this._makeJSONAttachment(jsonItem.id, title);
if (!jsonItem.attachments) jsonItem.attachments = [];
jsonItem.attachments.push(jsonAttachment);
attachmentCallback(jsonAttachment, 0);
}
}
catch (e) {
Zotero.logError(e);
}
return urlObjects;
},
async saveOpenAccessAttachment(item, attachmentCallback) {
let jsonItem = this._itemToJSONItem.get(item);
// Reuse the existing status line if there is one. This could be a failed
// translator attachment or a possible OA PDF found above.
// Explicitly check that the attachment is a PDF, not just any primary type,
// since we're reusing it for a PDF attachment.
let jsonAttachment = jsonItem.attachments && jsonItem.attachments.find(
x => x.mimeType == 'application/pdf' && x.isPrimary
);
// If no translated, no OA, no custom, don't show a line
// If translated fails, no OA, no custom, fail original
// If no translated, potential OA, show "Open-Access PDF" (set in _getOpenAccessPDFURLs())
// If translated fails, potential OA, show "Open-Access PDF" (set here)
// If no translated
// or translated fails, no OA, but custom, show custom when it starts
let resolvers = this._openAccessPDFURLs.get(item);
// We checked for OA PDFs in _getOpenAccessPDFURLs() so there was no translated pdf
if (resolvers) {
// Add custom resolvers
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
// No translated, no OA, no custom, no status line
if (!resolvers.length) {
return;
}
// No translated, no OA, just potential custom, so create a status line
if (!jsonAttachment) {
jsonAttachment = this._makeJSONAttachment(
jsonItem.id, Zotero.getString('findPDF.searchingForAvailableFiles')
);
}
}
else {
// Translated attachment failed, so we didn't check for OA PDFs yet and didn't
// update the status line
// Look for OA PDFs now
resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
if (resolvers.length) {
resolvers = await resolvers[0]();
}
// Add custom resolvers
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
// Failed translated, no OA, no custom, so fail the existing translator line
if (!resolvers.length) {
attachmentCallback(jsonAttachment, false);
return
}
}
let attachment;
try {
attachment = await Zotero.Attachments.addFileFromURLs(
item,
resolvers,
{
// When a new access method starts, update the status line
onAccessMethodStart: (method) => {
jsonAttachment.title = this._getPDFTitleForAccessMethod(method);
attachmentCallback(jsonAttachment, 0);
}
}
);
}
}),
catch (e) {
Zotero.logError(e);
attachmentCallback(jsonAttachment, false, e);
return;
}
if (attachment) {
attachmentCallback(jsonAttachment, 100);
}
else {
attachmentCallback(jsonAttachment, false, "PDF not found");
}
},
_makeJSONAttachment: function (parentID, title) {
@ -534,16 +581,23 @@ Zotero.Translate.ItemSaver.prototype = {
try {
let newAttachment;
const isSinglefileSnapshot = !!attachment.snapshotContent;
// determine whether to save files and attachments
var isLink = Zotero.MIME.isWebPageType(attachment.mimeType)
// .snapshot coming from most translators, .linkMode coming from RDF
&& (attachment.snapshot === false || attachment.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL);
if (isLink || this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) {
// .snapshot coming from most translators, .linkMode coming from RDF
var isLink = attachment.snapshot === false
|| attachment.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL;
if (isLink || this.attachmentMode === Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE) {
newAttachment = yield this._saveAttachmentLink.apply(this, arguments);
}
else if (isSinglefileSnapshot || this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) {
newAttachment = yield this._saveAttachmentDownload.apply(this, arguments);
} else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) {
}
else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) {
newAttachment = yield this._saveAttachmentFile.apply(this, arguments);
} else {
Zotero.debug('Translate: Ignoring attachment due to ATTACHMENT_MODE_IGNORE');
}
else {
Zotero.debug(`Translate: Ignoring attachment ${attachment.title} due to ATTACHMENT_MODE_IGNORE`);
}
if (!newAttachment) return false; // attachmentCallback should not have been called in this case
@ -821,6 +875,50 @@ Zotero.Translate.ItemSaver.prototype = {
return false;
},
_saveAttachmentLink: async function(attachment, parentItemID, attachmentCallback) {
attachment.linkMode = "linked_url";
let url, mimeType;
if(attachment.document) {
url = attachment.document.location.href;
mimeType = attachment.mimeType || attachment.document.contentType;
} else {
url = attachment.url
mimeType = attachment.mimeType || undefined;
}
// If no title provided, use "Attachment" as title for progress UI (but not for item)
let title = attachment.title || null;
if(!attachment.title) {
attachment.title = Zotero.getString("itemTypes.attachment");
}
if(!mimeType || !title) {
Zotero.debug("Translate: mimeType or title is missing; attaching link to URL will be slower");
}
let cleanURI = Zotero.Attachments.cleanAttachmentURI(url);
if (!cleanURI) {
throw new Error("Translate: Invalid attachment URL specified <" + url + ">");
}
url = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService)
.newURI(cleanURI, null, null); // This cannot fail, since we check above
// Only HTTP/HTTPS links are allowed
if(url.scheme != "http" && url.scheme != "https") {
throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators.");
}
return Zotero.Attachments.linkFromURL({
url: cleanURI,
parentItemID,
contentType: mimeType,
title,
collections: !parentItemID ? this._collections : undefined,
saveOptions: this._saveOptions,
});
},
_saveAttachmentDownload: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) {
Zotero.debug("Translate: Adding attachment", 4);
@ -839,51 +937,11 @@ Zotero.Translate.ItemSaver.prototype = {
// Commit to saving
attachmentCallback(attachment, 0);
var isLink = attachment.snapshot === false
|| attachment.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL;
if (isLink || this.attachmentMode === Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE) {
// if snapshot is explicitly set to false, attach as link
attachment.linkMode = "linked_url";
let url, mimeType;
if(attachment.document) {
url = attachment.document.location.href;
mimeType = attachment.mimeType || attachment.document.contentType;
} else {
url = attachment.url
mimeType = attachment.mimeType || undefined;
}
if(!mimeType || !title) {
Zotero.debug("Translate: mimeType or title is missing; attaching link to URL will be slower");
}
let cleanURI = Zotero.Attachments.cleanAttachmentURI(url);
if (!cleanURI) {
throw new Error("Translate: Invalid attachment URL specified <" + url + ">");
}
url = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService)
.newURI(cleanURI, null, null); // This cannot fail, since we check above
// Only HTTP/HTTPS links are allowed
if(url.scheme != "http" && url.scheme != "https") {
throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators.");
}
return Zotero.Attachments.linkFromURL({
url: cleanURI,
parentItemID,
contentType: mimeType,
title,
collections: !parentItemID ? this._collections : undefined,
saveOptions: this._saveOptions,
});
}
// Snapshot is not explicitly set to false, import as file attachment
attachment.linkMode = "imported_url";
// Import from document
if(attachment.document) {
if (attachment.document) {
Zotero.debug('Importing attachment from document');
attachment.linkMode = "imported_url";
@ -897,17 +955,6 @@ Zotero.Translate.ItemSaver.prototype = {
});
}
let mimeType = attachment.mimeType ? attachment.mimeType : null;
let fileBaseName;
if (parentItemID) {
let parentItem = yield Zotero.Items.getAsync(parentItemID);
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
}
attachment.linkMode = "imported_url";
attachmentCallback(attachment, 0);
// Import from SingleFile content
if (attachment.snapshotContent) {
Zotero.debug('Importing attachment from SingleFile');
@ -924,6 +971,13 @@ Zotero.Translate.ItemSaver.prototype = {
}
// Import from URL
let mimeType = attachment.mimeType ? attachment.mimeType : null;
let fileBaseName;
if (parentItemID) {
let parentItem = yield Zotero.Items.getAsync(parentItemID);
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
}
Zotero.debug('Importing attachment from URL');
return Zotero.Attachments.importFromURL({
libraryID: this._libraryID,

View file

@ -468,6 +468,8 @@ Zotero.Utilities.Internal = {
* @param {Zotero.CookieSandbox} [cookieSandbox]
*/
saveURI: function (wbp, uri, target, headers, cookieSandbox) {
Zotero.warn("Zotero.Utilities.Internal.saveURI() is deprecated -- use Zotero.HTTP.download()");
// Handle gzip encoding
wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
// If not explicitly using cache, skip it
@ -938,11 +940,13 @@ Zotero.Utilities.Internal = {
}
// Tags
for (let i = 0; i < item.tags.length; i++) {
if (!item.tags[i].type) {
item.tags[i].type = 0;
if (item.tags) {
for (let i = 0; i < item.tags.length; i++) {
if (!item.tags[i].type) {
item.tags[i].type = 0;
}
// No translator ever used "primary", "fields", or "linkedItems" objects
}
// No translator ever used "primary", "fields", or "linkedItems" objects
}
// "related" was never used (array of itemIDs)
@ -2178,14 +2182,32 @@ Zotero.Utilities.Internal = {
return [operator, args];
};
// We allow unquoted numbers in conditions, e.g. `a == 1` or `a == 1.0` but not `a == 1.0.0` or `a == 1st edition`
const asNumber = (string) => {
if (typeof string === 'number') {
return string;
}
const number = parseFloat(string);
if (!Number.isNaN(number) && string?.trim().match(/^[+-]?\d+(\.\d+)?$/)) {
return number;
}
return null;
};
// evaluates a condition (e.g. `a == "b"`) into a boolean value
const evaluateCondition = (condition) => {
const comparators = ['==', '!='];
const comparators = ['==', '!=', "<=", ">=", '<', '>'];
condition = condition.trim();
// match[1] if left is statement, match[3] if left is literal, match[4] if left is identifier
// match[6] if right is statement, match[8] if right is literal, match[9] if right is identifier
// match[2] and match[7] are used to match the quotes around the literal (and then check that the other quote is the same)
// Regular expression breakdown for condition matching:
// - `match[1]`: Left operand if it's a statement enclosed in `{{...}}`.
// - `match[3]`: Left operand if it's a string literal, extracted without quotes.
// - `match[4]`: Left operand if it's a standalone identifier (e.g., a variable) or a number
// - `match[6]`: Right operand if it's a statement enclosed in `{{...}}`.
// - `match[8]`: Right operand if it's a string literal, extracted without quotes.
// - `match[9]`: Right operand if it's a standalone identifier or a number.
// - `match[2]` and `match[7]`: Captured quotes around string literals, used to ensure matching pairs.
// - `match[5]`: The comparator (e.g., `==`, `!=`, `<`, `>`, etc.), extracted from `comparators.join('|')`.
const match = condition.match(new RegExp(String.raw`(?:{{(.*?)}}|(?:(['"])(.*?)\2)|([^ ]+)) *(${comparators.join('|')}) *(?:{{(.*?)}}|(?:(['"])(.*?)\7)|([^ ]+))`));
if (!match) {
@ -2197,16 +2219,24 @@ Zotero.Utilities.Internal = {
return !!evaluateIdentifier(condition);
}
const left = match[1] ? evaluateStatement(match[1]) : match[3] ?? evaluateIdentifier(match[4]) ?? '';
const left = match[1] ? evaluateStatement(match[1]) : match[3] ?? asNumber(match[4]) ?? evaluateIdentifier(match[4]) ?? '';
const comparator = match[5];
const right = match[6] ? evaluateStatement(match[6]) : match[8] ?? evaluateIdentifier(match[9]) ?? '';
const right = match[6] ? evaluateStatement(match[6]) : match[8] ?? asNumber(match[9]) ?? evaluateIdentifier(match[9]) ?? '';
switch (comparator) {
default:
case '==':
return left.toLowerCase() == right.toLowerCase();
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() == right.toLowerCase() : asNumber(left) == asNumber(right);
case '!=':
return left.toLowerCase() != right.toLowerCase();
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() != right.toLowerCase() : asNumber(left) != asNumber(right);
case ">=":
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() >= right.toLowerCase() : asNumber(left) >= asNumber(right);
case "<=":
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() <= right.toLowerCase() : asNumber(left) <= asNumber(right);
case '>':
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() > right.toLowerCase() : asNumber(left) > asNumber(right);
case '<':
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() < right.toLowerCase() : asNumber(left) < asNumber(right);
}
};
@ -2442,13 +2472,17 @@ Zotero.Utilities.Internal = {
let markup = this._titleMarkup[token];
if (markup.beginsTag) {
if (targetNode) {
let node = targetNode.ownerDocument.createElement(markup.beginsTag);
if (markup.style) {
Object.assign(node.style, markup.style);
}
let node;
if (markup.inverseStyle && markupStack.some(otherMarkup => otherMarkup.beginsTag === markup.beginsTag)) {
node = targetNode.ownerDocument.createElement('span');
Object.assign(node.style, markup.inverseStyle);
}
else {
node = targetNode.ownerDocument.createElement(markup.beginsTag);
if (markup.style) {
Object.assign(node.style, markup.style);
}
}
nodeStack.push(node);
}
markupStack.push({ ...markup, token });
@ -2461,12 +2495,14 @@ Zotero.Utilities.Internal = {
let discardedNode = nodeStack.pop();
if (discardedMarkup.beginsTag === markup.endsTag) {
nodeStack[nodeStack.length - 1].append(discardedNode);
break;
}
else {
nodeStack[nodeStack.length - 1].append(discardedMarkup.token, ...discardedNode.childNodes);
}
}
if (discardedMarkup.beginsTag === markup.endsTag) {
break;
}
}
continue;
@ -2906,6 +2942,85 @@ Zotero.Utilities.Internal.getProcessID = function () {
};
Zotero.Utilities.Internal.Environment = {
/**
* Unset an environment variable
*
* Since nsIEnvironment doesn't have an unset method
*/
unset: function (varName) {
Components.utils.import("resource://gre/modules/ctypes.jsm");
let lib;
let result;
let success;
// Windows
// https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-setenvironmentvariablew
if (Zotero.isWin) {
lib = ctypes.open("kernel32.dll");
let SetEnvironmentVariable = lib.declare(
"SetEnvironmentVariableW",
ctypes.winapi_abi,
ctypes.int, // Return type
ctypes.char16_t.ptr, // Argument type: LPCTSTR (variable name)
ctypes.char16_t.ptr // Argument type: LPCTSTR (value, NULL to unset)
);
result = SetEnvironmentVariable(varName, null);
success = result != 0; // "If the function succeeds, the return value is nonzero."
}
// macOS or Linux
else {
lib = ctypes.open(Zotero.isMac ? "/usr/lib/libSystem.dylib" : "libc.so.6");
let unsetenv = lib.declare(
"unsetenv",
ctypes.default_abi,
ctypes.int, // Return type
ctypes.char.ptr // Argument type: const char *
);
result = unsetenv(varName);
success = result == 0;
}
// Check the result
if (success) {
//Zotero.debug(`Unset environment variable ${varName}`);
}
else {
Zotero.logError(`Failed to unset environment variable ${varName} (${result})`);
}
lib.close();
},
/**
* Clear the Mozilla environment variables that we changed in the launcher
*
* Call this before launching another process that might be Firefox so that we don't end up
* using the wrong Firefox profile when launching URLs or PDFs. On Windows, it's not necessary
* to call this when launching URLs, only processes.
*
* https://github.com/zotero/zotero/issues/4981
*/
clearMozillaVariables: function () {
this.unset("MOZ_ALLOW_DOWNGRADE");
this.unset("MOZ_LEGACY_PROFILES");
},
/**
* Re-set the Mozilla environment variables that we changed in the launcher
*
* Call this in a finally() after using unsetMozillaVariables(). This mostly isn't necessary,
* since most new launches of Zotero would use the modified launcher, but a restart on Linux
* skips our shell script where we set these variables.
*/
restoreMozillaVariables: function () {
var env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
env.set("MOZ_ALLOW_DOWNGRADE", "1");
env.set("MOZ_LEGACY_PROFILES", "1");
},
};
/**
* Base64 encode / decode
* From http://www.webtoolkit.info/
@ -3048,9 +3163,18 @@ Zotero.Utilities.Internal.OpenURL = {
);
if (co) {
let base = Zotero.Prefs.get("openURL.resolver");
// Add & if there's already a ?
let splice = base.indexOf("?") == -1 ? "?" : "&";
return base + splice + co;
if (!base.endsWith('?')) {
if (base.includes('login?url=')) {
base += '?';
}
else if (base.includes('?')) {
base += '&';
}
else {
base += '?';
}
}
return base + co;
}
return false;
},

View file

@ -996,6 +996,9 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
*/
this.launchFile = function (file) {
file = Zotero.File.pathToFile(file);
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
try {
Zotero.debug("Launching " + file.path);
file.launch();
@ -1038,6 +1041,9 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
);
}
}
finally {
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
}
};
@ -1062,8 +1068,12 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
args = [filePath];
}
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
// Async, but we don't want to block
Zotero.Utilities.Internal.exec(applicationPath, args);
var promise = Zotero.Utilities.Internal.exec(applicationPath, args);
promise.finally(() => Zotero.Utilities.Internal.Environment.restoreMozillaVariables());
};
@ -1093,12 +1103,29 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
if (!found.value) {
throw new Error(`Handler not found for '${scheme}' URLs`);
}
svc.loadURI(Services.io.newURI(url, null, null));
try {
if (!Zotero.isWin) {
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
}
svc.loadURI(Services.io.newURI(url, null, null));
}
finally {
if (!Zotero.isWin) {
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
}
}
return;
}
}
var mozCleared = false;
try {
if (!Zotero.isWin) {
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
mozCleared = true;
}
var uri = Services.io.newURI(url, null, null);
var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1']
.getService(Components.interfaces.nsIExternalProtocolService)
@ -1123,6 +1150,11 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
+ "check extensions.zotero." + pref + " in about:config");
}
if (!mozCleared) {
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
mozCleared = true;
}
var proc = Components.classes["@mozilla.org/process/util;1"]
.createInstance(Components.interfaces.nsIProcess);
proc.init(exec);
@ -1130,6 +1162,11 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
var args = [url];
proc.runw(false, args, args.length);
}
finally {
if (mozCleared) {
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
}
}
}
@ -1425,6 +1462,10 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
locale: Zotero.locale,
};
if (Services.appinfo.inSafeMode) {
info.safeMode = true;
}
var extensions = await Zotero.getInstalledExtensions();
info.extensions = extensions.join(', ');
@ -1484,6 +1525,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
((b.appDisabled || b.userDisabled) ? 1 : 0);
});
var addons = [];
var isSafeMode = Services.appinfo.inSafeMode;
for (let addon of installed) {
if (addon.type == "theme") {
continue;
@ -1491,7 +1533,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
addons.push(addon.name + " (" + addon.version
+ (addon.type != 2 ? ", " + addon.type : "")
+ ((addon.appDisabled || addon.userDisabled) ? ", disabled" : "")
+ ((addon.appDisabled || addon.userDisabled || isSafeMode) ? ", disabled" : "")
+ ")");
}
@ -1721,9 +1763,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
return Zotero.Tags.purge();
});
yield Zotero.Fulltext.purgeUnusedWords();
yield Zotero.DB.executeTransaction(async function () {
return Zotero.Items.purge();
});
yield Zotero.Items.purge();
// DEBUG: this might not need to be permanent
//yield Zotero.DB.executeTransaction(async function () {
// return Zotero.Relations.purge();
@ -1979,7 +2019,7 @@ Zotero.VersionHeader = {
}
else {
// Use "Firefox/[version]" in user agent if not a proxy check or file sync request
let s3RE = /(zoteroproxycheck|zoterofilestorage(test)?)\.s3\.(us-east-1\.)?amazonaws\.com/;
let s3RE = /(zoteroproxycheck|zoterofilestorage(test)?)\.s3\.(us-east-1\.)?amazonaws\.com|files\.zotero\.net/;
let isAppNameDomain = s3RE.test(domain);
if (!isAppNameDomain) {
let ua = channel.getRequestHeader('User-Agent');

View file

@ -105,6 +105,7 @@ const xpcomFilesLocal = [
'feedReader',
'fileDragDataProvider',
'fulltext',
'httpIntegrationClient',
'id',
'integration',
'locale',
@ -124,8 +125,12 @@ const xpcomFilesLocal = [
'retractions',
'router',
'schema',
'server',
'server_integration',
'server/server',
'server/server_integration',
'server/server_connector',
'server/server_connectorIntegration',
'server/server_localAPI',
'server/saveSession',
'session',
'streamer',
'style',
@ -144,7 +149,6 @@ const xpcomFilesLocal = [
'storage/storageRequest',
'storage/storageResult',
'storage/storageUtilities',
'storage/streamListener',
'storage/zfs',
'storage/webdav',
'syncedSettings',
@ -153,10 +157,6 @@ const xpcomFilesLocal = [
'users',
'translation/translate_item',
'translation/translators',
'connector/httpIntegrationClient',
'connector/server_connector',
'connector/server_connectorIntegration',
'localAPI/server_localAPI',
];
Components.utils.import("resource://gre/modules/ComponentUtils.jsm");

View file

@ -50,8 +50,6 @@ var ZoteroPane = new function()
this.handleKeyPress = handleKeyPress;
this.getSelectedCollection = getSelectedCollection;
this.getSelectedSavedSearch = getSelectedSavedSearch;
this.getSelectedItems = getSelectedItems;
this.getSortedItems = getSortedItems;
this.getSortField = getSortField;
this.getSortDirection = getSortDirection;
this.setItemsPaneMessage = setItemsPaneMessage;
@ -701,15 +699,18 @@ var ZoteroPane = new function()
localized: Zotero.ItemTypes.getLocalizedString(type.id)
};
});
let allItemTypes = [...primaryItemTypes, ...secondaryItemTypes];
var collation = Zotero.getLocaleCollation();
primaryItemTypes.sort(function (a, b) {
return collation.compareString(1, a.localized, b.localized);
});
secondaryItemTypes.sort(function (a, b) {
allItemTypes.sort(function (a, b) {
return collation.compareString(1, a.localized, b.localized);
});
let lastPrimaryType = primaryItemTypes[primaryItemTypes.length - 1];
let itemTypes = primaryItemTypes.concat(secondaryItemTypes);
// The array of all item types with MRU prepended to the top
let itemTypes = primaryItemTypes.concat(allItemTypes);
for (let itemType of itemTypes) {
let menuitem = document.createXULElement("menuitem");
menuitem.setAttribute("label", itemType.localized);
@ -720,7 +721,7 @@ var ZoteroPane = new function()
});
addMenu.appendChild(menuitem);
// Add a separator between primary and secondary types
if (lastPrimaryType.id == type) {
if (addMenu.childElementCount == primaryItemTypes.length) {
let separator = document.createXULElement("menuseparator");
addMenu.appendChild(separator);
}
@ -1936,7 +1937,7 @@ var ZoteroPane = new function()
// Tab selection observer in standalone.js makes sure that
// updateQuickCopyCommands is called
if (Zotero_Tabs.selectedID == 'zotero-pane') {
if (Zotero_Tabs.selectedType == 'library') {
this.updateQuickCopyCommands(selectedItems);
}
@ -2654,21 +2655,10 @@ var ZoteroPane = new function()
this.copySelectedItemsToClipboard = function (asCitations) {
var items = [];
if (Zotero_Tabs.selectedID == 'zotero-pane') {
let itemIDs = this.getSelectedItems(true);
// Get selected item IDs in the item tree order
itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id));
items = Zotero.Items.get(itemIDs);
}
else {
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
let item = Zotero.Items.get(reader.itemID);
if (item.parentItem) {
items = [item.parentItem];
}
}
}
let itemIDs = this.getSelectedItems(true);
// Get selected item IDs in the item tree order
itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id));
items = Zotero.Items.get(itemIDs);
if (!items.length) {
return;
@ -3070,14 +3060,28 @@ var ZoteroPane = new function()
*
* If asIDs is true, return an array of itemIDs instead
*/
function getSelectedItems(asIDs)
{
if (!this.itemsView) {
return [];
this.getSelectedItems = function (asIDs) {
switch (Zotero_Tabs.selectedType) {
case 'library':
if (!this.itemsView) {
return [];
}
return this.itemsView.getSelectedItems(asIDs);
case 'reader': {
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
let item = Zotero.Items.get(reader.itemID);
if (item.parentItem) {
item = item.parentItem;
}
return asIDs ? [item.id] : [item];
}
return [];
}
default:
return [];
}
return this.itemsView.getSelectedItems(asIDs);
}
};
/*
@ -3085,13 +3089,18 @@ var ZoteroPane = new function()
*
* If asIDs is true, return an array of itemIDs instead
*/
function getSortedItems(asIDs) {
if (!this.itemsView) {
return [];
this.getSortedItems = function (asIDs) {
switch (Zotero_Tabs.selectedType) {
case 'library':
if (!this.itemsView) {
return [];
}
return this.itemsView.getSortedItems(asIDs);
default:
// ALl non-library tabs: Visible items == "selected" items
return this.getSelectedItems(asIDs);
}
return this.itemsView.getSortedItems(asIDs);
}
};
function getSortField() {
@ -4510,22 +4519,14 @@ var ZoteroPane = new function()
/**
* @param {Boolean} [link]
* @param {Number} [parentItemID]
* @param {nsIFile[]} [files] Used instead of showing a file picker - for tests
* @param {String[]} [files] Used instead of showing a file picker - for tests
* @returns {Promise<Zotero.Item[] | null>}
*/
this.addAttachmentFromDialog = async function (link, parentItemID, files = null) {
if (!this.canEdit()) {
this.displayCannotEditLibraryMessage();
return null;
}
var collectionTreeRow = this.getCollectionTreeRow();
if (link) {
if (collectionTreeRow.isWithinGroup()) {
Zotero.alert(null, "", "Linked files cannot be added to group libraries.");
return null;
}
else if (collectionTreeRow.isPublications()) {
var libraryID;
if (Zotero_Tabs.selectedType === 'library') {
let collectionTreeRow = this.getCollectionTreeRow();
if (link && collectionTreeRow.isPublications()) {
Zotero.alert(
null,
Zotero.getString('general.error'),
@ -4533,15 +4534,25 @@ var ZoteroPane = new function()
);
return null;
}
libraryID = collectionTreeRow.ref.libraryID;
}
else {
libraryID = Zotero.Items.get(parentItemID).libraryID;
}
if (!this.canEdit()) {
this.displayCannotEditLibraryMessage();
return null;
}
// TODO: disable in menu
if (!this.canEditFiles()) {
this.displayCannotEditLibraryFilesMessage();
return;
return null;
}
if (link && Zotero.Libraries.get(libraryID).isGroup) {
Zotero.alert(null, "", "Linked files cannot be added to group libraries.");
return null;
}
var libraryID = collectionTreeRow.ref.libraryID;
if (!files) {
var fp = new FilePicker();
@ -5369,36 +5380,48 @@ var ZoteroPane = new function()
/**
* Test if the user can edit the currently selected view
*
* @param {Integer} [row]
*
* @return {Boolean} TRUE if user can edit, FALSE if not
* @param {Integer} [row] Row index - ignored if not in library tab
* @return {Boolean} TRUE if user can edit, FALSE if not
*/
this.canEdit = function (row) {
// Currently selected row
if (row === undefined) {
row = this.collectionsView.selection.focused;
switch (Zotero_Tabs.selectedType) {
case 'library':
// Currently selected row
if (row === undefined) {
row = this.collectionsView.selection.focused;
}
return this.collectionsView.getRow(row).editable;
case 'reader': {
let itemID = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)?.itemID;
if (!itemID) {
throw new Error('Reader tab has no itemID');
}
return Zotero.Items.get(itemID).library.editable;
}
default:
return false;
}
var collectionTreeRow = this.collectionsView.getRow(row);
return collectionTreeRow.editable;
}
};
/**
* Test if the user can edit the parent library of the selected view
*
* @param {Integer} [row]
* @return {Boolean} TRUE if user can edit, FALSE if not
* @param {Integer} [row] Row index - ignored if not in library tab
* @return {Boolean} TRUE if user can edit, FALSE if not
*/
this.canEditLibrary = function (row) {
// Currently selected row
if (row === undefined) {
row = this.collectionsView.selection.focused;
switch (Zotero_Tabs.selectedType) {
case 'library': // Currently selected row
if (row === undefined) {
row = this.collectionsView.selection.focused;
}
return Zotero.Libraries.get(this.collectionsView.getRow(row).ref.libraryID).editable;
default:
// All non-library tabs: canEditLibrary() == canEdit()
return this.canEdit(row);
}
var collectionTreeRow = this.collectionsView.getRow(row);
return Zotero.Libraries.get(collectionTreeRow.ref.libraryID).editable;
}
};
/**
@ -5409,14 +5432,24 @@ var ZoteroPane = new function()
* @return {Boolean} TRUE if user can edit, FALSE if not
*/
this.canEditFiles = function (row) {
// Currently selected row
if (row === undefined) {
row = this.collectionsView.selection.focused;
switch (Zotero_Tabs.selectedType) {
case 'library':
// Currently selected row
if (row === undefined) {
row = this.collectionsView.selection.focused;
}
return this.collectionsView.getRow(row).filesEditable;
case 'reader': {
let itemID = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)?.itemID;
if (!itemID) {
throw new Error('Reader tab has no itemID');
}
return Zotero.Items.get(itemID).library.filesEditable;
}
default:
return false;
}
var collectionTreeRow = this.collectionsView.getRow(row);
return collectionTreeRow.filesEditable;
}
};
this.displayCannotEditLibraryMessage = function () {
@ -6814,13 +6847,14 @@ var ZoteroPane = new function()
};
this.buildFieldTransformMenu = function ({ target, onTransform }) {
let doc = target.ownerDocument;
let value = target.value;
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value, true);
let valueSentenceCased = Zotero.Utilities.sentenceCase(value);
let menupopup = document.createXULElement('menupopup');
let menupopup = doc.createXULElement('menupopup');
let titleCase = document.createXULElement('menuitem');
let titleCase = doc.createXULElement('menuitem');
titleCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
titleCase.addEventListener('command', () => {
onTransform(valueTitleCased);
@ -6828,7 +6862,7 @@ var ZoteroPane = new function()
titleCase.disabled = valueTitleCased == value;
menupopup.append(titleCase);
let sentenceCase = document.createXULElement('menuitem');
let sentenceCase = doc.createXULElement('menuitem');
sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
sentenceCase.addEventListener('command', () => {
onTransform(valueSentenceCased);

View file

@ -418,6 +418,7 @@
command="cmd_find"/>
<menuitem id="menu_advancedSearch"
label="&zotero.toolbar.advancedSearch;"
key="key_advancedSearch"
command="cmd_zotero_advancedSearch"/>
<menuseparator hidden="true" id="textfieldDirection-separator"/>
<menuitem id="textfieldDirection-swap"

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
noteEditor.deleteRow=Delete Row
noteEditor.deleteColumn=Delete Column
noteEditor.deleteTable=Delete Table
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Annotations
pdfReader.showAnnotations=Show Annotations

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
noteEditor.deleteRow=Delete Row
noteEditor.deleteColumn=Delete Column
noteEditor.deleteTable=Delete Table
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Annotations
pdfReader.showAnnotations=عرض التعليقات

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
noteEditor.deleteRow=Delete Row
noteEditor.deleteColumn=Delete Column
noteEditor.deleteTable=Delete Table
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Annotations
pdfReader.showAnnotations=Show Annotations

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Enlakaat ur golonenn a zehoù
noteEditor.deleteRow=Delete Row
noteEditor.deleteColumn=Delete Column
noteEditor.deleteTable=Delete Table
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Ennotadurioù
pdfReader.showAnnotations=Diskouez an ennotadurioù

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insereix una columna a la dreta
noteEditor.deleteRow=Suprimeix la fila
noteEditor.deleteColumn=Suprimeix la columna
noteEditor.deleteTable=Suprimeix la taula
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Anotacions
pdfReader.showAnnotations=Mostra les anotacions

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Vložit sloupec napravo
noteEditor.deleteRow=Smazat řádek
noteEditor.deleteColumn=Smazat sloupec
noteEditor.deleteTable=Smazat tabulku
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Anotace
pdfReader.showAnnotations=Zobrazit anotace

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
noteEditor.deleteRow=Delete Row
noteEditor.deleteColumn=Delete Column
noteEditor.deleteTable=Delete Table
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Annotations
pdfReader.showAnnotations=Show Annotations

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Spalte rechts einfügen
noteEditor.deleteRow=Zeile löschen
noteEditor.deleteColumn=Spalte löschen
noteEditor.deleteTable=Tabelle löschen
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Anmerkungen
pdfReader.showAnnotations=Anmerkungen anzeigen

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
noteEditor.deleteRow=Delete Row
noteEditor.deleteColumn=Delete Column
noteEditor.deleteTable=Delete Table
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Annotations
pdfReader.showAnnotations=Show Annotations

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
noteEditor.deleteRow=Delete Row
noteEditor.deleteColumn=Delete Column
noteEditor.deleteTable=Delete Table
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Annotations
pdfReader.showAnnotations=Show Annotations

View file

@ -0,0 +1,582 @@
/*
Localizable.strings
Zotero
Created by Michal Rentka on 21/04/2020.
Copyright © 2020 Corporation for Digital Scholarship. All rights reserved.
*/
"close" = "Close";
"delete" = "Delete";
"edit" = "Edit";
"last_updated" = "Last Updated";
"name" = "Name";
"save" = "Save";
"creator" = "Creator";
"date" = "Date";
"date_added" = "Date Added";
"date_modified" = "Date Modified";
"item_type" = "Item Type";
"publication_title" = "Publication Title";
"publisher" = "Publisher";
"title" = "Title";
"year" = "Year";
"abstract" = "Abstract";
"error" = "Error";
"warning" = "Warning";
"yes" = "Yes";
"no" = "No";
"cancel" = "Cancel";
"done" = "Done";
"ok" = "OK";
"add" = "Add";
"unknown" = "Unknown";
"retry" = "Retry";
"page" = "Page";
"report" = "Report";
"total" = "Total";
"restore" = "Restore";
"copy" = "Copy";
"privacy_policy" = "Privacy Policy";
"support_feedback" = "Support and Feedback";
"about_zotero" = "About Zotero";
"recent" = "Recents";
"look_up" = "Look Up";
"not_found" = "Not Found";
"share" = "Share";
"select" = "Select";
"create" = "Create";
"more_information" = "More Information";
"clear" = "Clear";
"scan_text" = "Scan Text";
"move_to_trash" = "Move to Trash";
"size" = "Size";
"remove" = "Remove";
"keep" = "Keep";
"cancel_all" = "Cancel All";
"back" = "Back";
"forward" = "Forward";
"go_to_settings" = "Go to Settings";
"close_without_saving" = "Close without saving";
"stay" = "Stay";
"beta_wipe_title" = "Resync Required";
"beta_wipe_message" = "Due to a beta update, your data must be redownloaded from zotero.org.";
"migration_debug" = "App failed to initialize and can’t function properly. Please report Debug ID %@ in the Zotero Forums.";
"login_debug" = "App failed to log in. Please log in again and report Debug ID %@ in the Zotero Forums.";
"full_sync_debug" = "Full sync has finished. Please report Debug ID %@ in the Zotero Forums.";
"day_suffixes" = "st,nd,rd,th";
"onboarding.access" = "<b>Zotero organizes research</b> however you want. Sort your items into collections and tag them with keywords.";
"onboarding.sync" = "<b>Synchronize and collaborate</b> across devices, keeping your reading and notes seamlessly up to date.";
"onboarding.annotate" = "<b>Highlight and take notes</b> directly in your PDFs as you read them.";
"onboarding.share" = "<b>Tap to collect</b> articles and books directly from the web, including their PDFs and full metadata.";
"onboarding.sign_in" = "Sign In";
"onboarding.create_account" = "Sign Up";
"login.username" = "Username";
"login.password" = "Password";
"login.email" = "Email";
"login.repeat_password" = "Repeat password";
"login.forgot_password" = "Forgot Password?";
"libraries.my_library" = "My Library";
"libraries.group_libraries" = "Group Libraries";
"libraries.delete_question" = "Do you really want to delete “%@”?";
"collections.search_title" = "Find Collection";
"collections.all_items" = "All Items";
"collections.my_publications" = "My Publications";
"collections.trash" = "Trash";
"collections.unfiled" = "Unfiled Items";
"collections.new_subcollection" = "New Subcollection";
"collections.edit_title" = "Edit Collection";
"collections.delete" = "Delete Collection";
"collections.delete_with_items" = "Delete Collection and Items";
"collections.create_title" = "Create Collection";
"collections.picker_title" = "Choose Parent";
"collections.empty_trash" = "Empty Trash";
"collections.collapse_all" = "Collapse All";
"collections.expand_all" = "Expand All";
"collections.create_bibliography" = "Create Bibliography from Collection";
"collections.download_attachments" = "Download Attachments";
"collections.delete_attachment_files" = "Remove Downloads";
"sync_toolbar.starting" = "Sync starting";
"sync_toolbar.groups" = "Syncing groups";
"sync_toolbar.groups_with_data" = "Syncing groups (%d / %d)";
"sync_toolbar.library" = "Syncing %@";
"sync_toolbar.object" = "Syncing %@ in %@";
"sync_toolbar.object_with_data" = "Syncing %@ (%d / %d) in %@";
"sync_toolbar.finished" = "Finished sync";
"sync_toolbar.deletion" = "Applying remote deletions in %@";
"sync_toolbar.aborted" = "Sync failed (%@)";
"sync_toolbar.writes" = "Uploading changes (%d / %d)";
"sync_toolbar.uploads" = "Uploading attachment (%d / %d)";
"sync_toolbar.object.collections" = "collections";
"sync_toolbar.object.groups" = "groups";
"sync_toolbar.object.items" = "items";
"sync_toolbar.object.searches" = "searches";
"sync.conflict_resolution.changed_item_deleted" = "The item “%@” has been removed. Do you want to keep your changes?";
"items.search_title" = "Search Items";
"items.select_all" = "Select All";
"items.deselect_all" = "Deselect All";
"items.sort_by" = "Sort By";
"items.sort_order" = "Sort Order";
"items.ascending" = "Ascending";
"items.descending" = "Descending";
"items.lookup" = "Add by Identifier";
"items.barcode" = "Scan Barcode";
"items.new" = "Add Manually";
"items.new_note" = "New Standalone Note";
"items.new_file" = "Add File";
"items.remove_from_collection_title" = "Remove from Collection";
"items.action.add_to_collection" = "Add to Collection";
"items.action.remove_from_collection" = "Remove from Collection";
"items.action.duplicate" = "Duplicate";
"items.action.create_parent" = "Create Parent Item";
"items.action.download" = "Download";
"items.action.remove_download" = "Remove Download";
"items.filters.title" = "Filters";
"items.filters.downloads" = "Downloaded Files";
"items.filters.tags" = "Tags";
"items.toolbar_saved" = "Saved %d / %d";
"items.toolbar_downloaded" = "Downloaded %d / %d";
"items.generating_bib" = "Generating Bibliography";
"items.creator_summary.and" = "%@ and %@";
"items.creator_summary.etal" = "%@ et al.";
"lookup.title" = "Enter ISBNs, DOls, PMIDs, arXiv IDs, or ADS Bibcodes to add to your library:";
"citation.title" = "Citation Preview";
"citation.copy_citation" = "Copy Citation";
"citation.copy_bibliography" = "Copy Bibliography";
"citation.preview" = "Preview";
"citation.omit_author" = "Omit Author";
"citation.locator_placeholder" = "Number";
"citation.locator.page" = "Page";
"citation.locator.book" = "Book";
"citation.locator.chapter" = "Chapter";
"citation.locator.column" = "Column";
"citation.locator.figure" = "Figure";
"citation.locator.folio" = "Folio";
"citation.locator.issue" = "Issue";
"citation.locator.line" = "Line";
"citation.locator.note" = "Note";
"citation.locator.opus" = "Opus";
"citation.locator.paragraph" = "Paragraph";
"citation.locator.part" = "Part";
"citation.locator.section" = "Section";
"citation.locator.sub verbo" = "Sub verbo";
"citation.locator.volume" = "Volume";
"citation.locator.verse" = "Verse";
"citation.style" = "Style";
"citation.language" = "Language";
"citation.output_mode" = "Output Mode";
"citation.citations" = "Citations";
"citation.bibliography" = "Bibliography";
"citation.notes" = "Notes";
"citation.output_method" = "Output Method";
"citation.save_html" = "Save as HTML";
"citation.copy" = "Copy to Clipboard";
"item_detail.add_creator" = "Add Creator";
"item_detail.notes" = "Notes";
"item_detail.add_note" = "Add note";
"item_detail.tags" = "Tags";
"item_detail.add_tag" = "Add tag";
"item_detail.attachments" = "Attachments";
"item_detail.add_attachment" = "Add attachment";
"item_detail.split_name" = "Split name";
"item_detail.merge_name" = "Merge name";
"item_detail.untitled" = "Untitled";
"item_detail.show_more" = "Show more";
"item_detail.show_less" = "Show less";
"item_detail.view_pdf" = "View PDF";
"item_detail.data_reloaded" = "This item has been changed remotely. It will now reload.";
"item_detail.delete_attachment_file" = "Remove Download";
"item_detail.deleted_title" = "Deleted";
"item_detail.deleted_message" = "This item has been deleted. Do you want to restore it?";
"item_detail.move_to_standalone_attachment" = "Convert to Standalone Attachment";
"tag_picker.title" = "%d selected";
"tag_picker.placeholder" = "Tag name";
"tag_picker.create_tag" = "Create Tag \“%@\”";
"tag_picker.confirm_deletion_question" = "Delete Automatic Tags";
"tag_picker.search_placeholder" = "Search Tags";
"tag_picker.deselect_all" = "Deselect All";
"tag_picker.show_auto" = "Show Automatic Tags";
"tag_picker.show_all" = "Display All Tags in This Library";
"tag_picker.delete_automatic" = "Delete Automatic Tags in This Library";
"creator_editor.creator" = "Creator Type";
"creator_editor.first_name" = "First Name";
"creator_editor.last_name" = "Last Name";
"creator_editor.switch_to_single" = "Switch to single field";
"creator_editor.switch_to_dual" = "Switch to two fields";
"creator_editor.delete_confirmation" = "Do you really want to delete this creator?";
"pdf.annotations_sidebar.search_title" = "Search";
"pdf.annotations_sidebar.add_comment" = "Add comment";
"pdf.annotations_sidebar.add_tags" = "Add tags";
"pdf.annotations_sidebar.merge" = "Merge";
"pdf.annotations_sidebar.filter.title" = "Filter Annotations";
"pdf.annotations_sidebar.filter.tags_placeholder" = "Select Tags…";
"pdf.search.title" = "Search in Document";
"pdf.search.failed" = "Search failed";
"pdf.search.dismiss" = "Dismiss Search";
"pdf.annotation_popover.title" = "Edit Annotation";
"pdf.annotation_popover.delete" = "Delete Annotation";
"pdf.annotation_popover.update_subsequent_pages" = "Update subsequent pages";
"pdf.annotation_popover.page_label_title" = "Edit Page Number";
"pdf.annotation_popover.no_comment" = "No comment";
"pdf.annotation_popover.line_width" = "Width";
"pdf.annotation_popover.size" = "Size";
"pdf.deleted_title" = "Deleted";
"pdf.deleted_message" = "This document has been deleted. Do you want to restore it?";
"pdf.line_width_point" = "%0.1f pt";
"pdf.highlight" = "Highlight";
"pdf.underline" = "Underline";
"pdf.annotation_share.image.share" = "Share image";
"pdf.annotation_share.image.medium" = "Medium";
"pdf.annotation_share.image.large" = "Large";
"pdf.annotation_toolbar.eraser" = "Eraser";
"pdf.annotation_toolbar.highlight" = "Highlight";
"pdf.annotation_toolbar.note" = "Note";
"pdf.annotation_toolbar.image" = "Image";
"pdf.annotation_toolbar.ink" = "Ink";
"pdf.annotation_toolbar.underline" = "Underline";
"pdf.annotation_toolbar.text" = "Text";
"pdf.locked.locked" = "Locked";
"pdf.locked.enter_password" = "Please enter the password to open this PDF.";
"pdf.locked.failed" = "Incorrect password. Please try again.";
"pdf.settings.scroll_direction.title" = "Scroll Direction";
"pdf.settings.scroll_direction.horizontal" = "Horizontal";
"pdf.settings.scroll_direction.vertical" = "Vertical";
"pdf.settings.page_transition.title" = "Page Transition";
"pdf.settings.page_transition.jump" = "Jump";
"pdf.settings.page_transition.continuous" = "Continuous";
"pdf.settings.appearance.title" = "Appearance";
"pdf.settings.appearance.auto" = "Automatic";
"pdf.settings.appearance.light_mode" = "Light";
"pdf.settings.appearance.dark_mode" = "Dark";
"pdf.settings.appearance.sepia_mode" = "Sepia";
"pdf.settings.page_mode.title" = "Page Mode";
"pdf.settings.page_mode.single" = "Single";
"pdf.settings.page_mode.double" = "Double";
"pdf.settings.page_mode.automatic" = "Automatic";
"pdf.settings.page_fitting.title" = "Page Fitting";
"pdf.settings.page_fitting.fit" = "Fit";
"pdf.settings.page_fitting.fill" = "Fill";
"pdf.settings.page_spreads.title" = "Page Spreads";
"pdf.settings.page_spreads.odd" = "Odd";
"pdf.settings.page_spreads.even" = "Even";
"pdf.settings.page_fitting.automatic" = "Automatic";
"pdf.export.export" = "Export PDF";
"pdf.export.export_annotated" = "Export Annotated PDF";
"pdf.sidebar.no_annotations" = "No Annotations";
"pdf.sidebar.no_outline" = "No Outline";
"pdf.delete_annotation" = "Do you really want to delete annotation?";
"pdf.previous_page" = "Previous Page";
"pdf.next_page" = "Next Page";
"pdf.previous_viewport" = "Previous Viewport";
"pdf.next_viewport" = "Next Viewport";
"settings.title" = "Settings";
"settings.logout" = "Sign Out";
"settings.logout_warning" = "Any local data that was not synced will be deleted. Do you really want to sign out?";
"settings.general.title" = "General";
"settings.general.show_subcollections_title" = "Show Items from Subcollections";
"settings.general.show_collection_item_counts" = "Show collection sizes";
"settings.general.open_links_in_external_browser" = "Open links in external browser";
"settings.general.autoempty_title" = "Delete Items in Trash";
"settings.general.never" = "Never";
"settings.item_count" = "Item count";
"settings.item_count_subtitle" = "Show item count for all collections.";
"settings.sync.title" = "Account";
"settings.sync.data_syncing" = "Data Syncing";
"settings.sync.file_syncing" = "File Syncing";
"settings.sync.file_syncing_type_message" = "Sync attachment files in My Library using";
"settings.sync.account" = "Account";
"settings.sync.manage_account" = "Manage Account";
"settings.sync.delete_account" = "Delete Account";
"settings.sync.username" = "Username";
"settings.sync.password" = "Password";
"settings.sync.verify" = "Verify Server";
"settings.sync.directory_not_found.title" = "Directory not found";
"settings.sync.directory_not_found.message" = "%@ does not exist.\n\nDo you want to create it now?";
"settings.sync.verified" = "Verified";
"settings.permission" = "User Permission";
"settings.permission_subtitle" = "Ask for user permission for each write action";
"settings.translators" = "Translators";
"settings.translators_update" = "Update translators";
"settings.translators_updating" = "Updating…";
"settings.reset_to_bundled" = "Reset to bundled";
"settings.debug" = "Debug Output Logging";
"settings.logging_title" = "Logging";
"settings.start_logging" = "Start Logging";
"settings.start_logging_on_launch" = "Start Logging on Next App Launch";
"settings.cancel_logging" = "Cancel Logging";
"settings.stop_logging" = "Stop Logging";
"settings.view_output" = "View Output";
"settings.clear_output" = "Clear Output";
"settings.logging_desc1" = "To debug a startup issue, force-quit the app and start it again.";
"settings.logging_desc2" = "To debug a share extension issue, open the share extension.";
"settings.send_manually" = "Send Manually";
"settings.log_alert.progress_title" = "Sending Logs";
"settings.log_alert.title" = "Logs Sent";
"settings.log_alert.message" = "Your Debug ID is %@";
"settings.crash_alert.title" = "Crash Log Sent";
"settings.crash_alert.message" = "Your Report ID is %@.\n\nPlease post a message to the Zotero Forums (forums.zotero.org) with this Report ID and any steps necessary to reproduce the crash.";
"settings.crash_alert.message_with_db" = "Your Report ID is %@.\n\nPlease post a message to the Zotero Forums (forums.zotero.org) with this Report ID and any steps necessary to reproduce the crash.\n\nIf Zotero crashes repeatedly, please tap “Export Database” and send exported files to support@zotero.org.";
"settings.crash_alert.submit_forum" = "Submit to Zotero Forums";
"settings.crash_alert.export_db" = "Copy and Export Database";
"settings.crash_alert.copy" = "Copy to Clipboard";
"settings.websocket_title" = "Server Connection";
"settings.websocket_connected" = "Connected";
"settings.websocket_connecting" = "Connecting";
"settings.websocket_disconnected" = "Disconnected";
"settings.websocket_disconnect" = "Disconnect from Server";
"settings.websocket_connect" = "Connect to Server";
"settings.storage" = "Local Storage";
"settings.storage.delete_all" = "Delete All Local Attachment Files";
"settings.storage.delete_all_question" = "Are you sure you want to delete all attachment files from this device?\n\nOther synced devices will not be affected.";
"settings.storage.delete_library_question" = "Are you sure you want to delete all attachment files in %@ from this device?\n\nOther synced devices will not be affected.";
"settings.saving.title" = "Saving";
"settings.saving.files_title" = "Save Files";
"settings.saving.files_message" = "Automatically attach associated PDFs and other files when saving items";
"settings.saving.tags_title" = "Save Automatic Tags";
"settings.saving.tags_message" = "Automatically tag items with keywords and subject headings";
"settings.cite.title" = "Cite";
"settings.cite.styles_title" = "Citation Styles";
"settings.cite.get_more_styles" = "Get additional styles";
"settings.cite_search.search_title" = "Search styles";
"settings.export.title" = "Quick Copy";
"settings.export.default_format" = "Default Format";
"settings.export.language" = "Language";
"settings.export.copy_as_html" = "Copy as HTML";
"settings.versionAndBuild" = "Version %@ Build %@";
"settings.export_db" = "Export Database File";
"settings.full_sync_debug" = "Full Sync Debugging";
"settings.full_sync.start" = "Start Full Sync Debugging";
"settings.full_sync.other_in_progress" = "Sync is already in progress. Wait for it to finish.";
"settings.full_sync.in_progress" = "A full sync is in progress. Wait for it to finish and post the Debug ID to the Zotero Forums.";
"shareext.save" = "Save to Zotero";
"shareext.decoding_attachment" = "Searching for items";
"shareext.translation.item_selection" = "Choose an item";
"shareext.translation.translating_with" = "Saving with %@";
"shareext.loading_collections" = "Loading Collections";
"shareext.sync_error" = "Can't sync collections";
"shareext.collection_other" = "More";
"shareext.item_title" = "Item";
"shareext.collection_title" = "Collection";
"shareext.tags_title" = "Tags";
"searchbar.placeholder" = "Search";
"searchbar.accessibility_cancel" = "Cancel Search";
"searchbar.accessibility_clear" = "Clear Search";
"errors.schema" = "Some data in My Library could not be downloaded. It may have been saved with a newer version of Zotero.\n\nOther data will continue to sync.";
"errors.parsing" = "Could not parse some data. Other data will continue to sync.";
"errors.api" = "Failed API response: %@";
"errors.db" = "Could not connect to database. The device storage might be full.";
"errors.versionMismatch" = "A remote change was made during the sync";
"errors.unknown" = "Unknown error";
"errors.attachments.cant_unzip_snapshot" = "Unable to unzip snapshot";
"errors.attachments.incompatible_attachment" = "Linked files are not supported on iOS. You can open them using the Zotero desktop app.";
"errors.attachments.cant_open_attachment" = "The attached file could not be found.";
"errors.attachments.missing_zotero" = "The attached file is not available in the online library.";
"errors.attachments.missing_webdav" = "The attached file is not available on the WebDAV server.";
"errors.attachments.missing_additional" = "Please check that the file has synced on the device where it was added.";
"errors.attachments.unauthorized_webdav" = "The WebDAV server did not accept the username and password you have saved in settings.";
"errors.attachments.forbidden_webdav" = "You don’t have permission to access %@ on the WebDAV server.";
"errors.attachments.generic_filename" = "the file";
"errors.login.invalid_password" = "Invalid password";
"errors.login.invalid_username" = "Invalid username";
"errors.login.invalid_credentials" = "Invalid username or password";
"errors.libraries.cantLoad" = "Unable to load libraries";
"errors.collections.empty_name" = "Please enter a collection name";
"errors.collections.save_failed" = "Unable to save collection";
"errors.collections.bibliography_failed" = "Could not load items for bibliography.";
"errors.sync.group_removed" = "Group “%@” is no longer accessible. What would you like to do?";
"errors.sync.metadata_write_denied" = "You can no longer write to the group “%@”. What would you like to do?";
"errors.sync.revert_to_original" = "Revert to original";
"errors.sync.file_write_denied" = "You no longer have file-editing access for the group “%@”, and files you’ve changed locally cannot be uploaded. If you continue, all group files will be reset to their state on %@.\n\nIf you would like a chance to copy modified files elsewhere or to request file-editing access from a group administrator, you can skip syncing of the group now.";
"errors.sync.reset_group_files" = "Reset Group Files and Sync";
"errors.sync.skip_group" = "Skip Group";
"errors.sync_toolbar.finished_with_errors" = "Finished sync (%@)";
"errors.sync_toolbar.attachment_missing" = "Unable to upload attachment: %@. Please try removing and re-adding the attachment.";
"errors.sync_toolbar.libraries_missing" = "No libraries found. Please sign out and back in again.";
"errors.sync_toolbar.groups_failed" = "Could not sync groups. Please try again.";
"errors.sync_toolbar.conflict_retry_limit" = "Remote sync in progress. Please try again in a few minutes.";
"errors.sync_toolbar.group_permissions" = "You don’t have permission to edit groups.";
"errors.sync_toolbar.internet_connection" = "Unable to connect to the network. Please try again.";
"errors.sync_toolbar.personal_quota_reached" = "You have reached your Zotero Storage quota. Some files were not uploaded. Other Zotero data will continue to sync.\n\nSee your zotero.org account settings for additional storage options.";
"errors.sync_toolbar.group_quota_reached" = "The group “%@” has reached its Zotero Storage quota. Some files were not uploaded. Other Zotero data will continue to sync.\n\nThe group owner can increase the group’s storage capacity from their storage settings on zotero.org.";
"errors.sync_toolbar.insufficient_space" = "You have insufficient space on your WebDAV server. Some files were not uploaded. Other Zotero data will continue to sync.";
"errors.sync_toolbar.quota_reached_short" = "Quota Reached.";
"errors.sync_toolbar.unavailable" = "Zotero services are temporarily unavailable. Please try again in a few minutes.";
"errors.sync_toolbar.webdav_error" = "Could not delete files from your WebDAV server: \"%@\".";
"errors.sync_toolbar.webdav_item_prop" = "Invalid prop file: %@";
"errors.sync_toolbar.show_item" = "Show Item";
"errors.sync_toolbar.show_items" = "Show Items";
"errors.sync_toolbar.forbidden" = "Invalid username or password";
"errors.sync_toolbar.forbidden_message" = "The Zotero sync server did not accept your username and password.\n\nPlease log out and log in with correct login information.";
"errors.sync_toolbar.webdav_request_failed" = "Your WebDAV server returned an HTTP %d error for a %@ request.";
"errors.items.loading" = "Could not load items.";
"errors.items.deletion" = "Could not remove item.";
"errors.items.deletion_from_collection" = "Could not remove item from collection.";
"errors.items.add_to_collection" = "Could not add item to collection.";
"errors.items.move_item" = "Could not move item.";
"errors.items.save_note" = "Could not save note.";
"errors.items.add_attachment" = "Could not add attachment.";
"errors.items.add_some_attachments" = "Some attachments were not added: %@.";
"errors.items.load_duplication" = "Could not load item to duplicate.";
"errors.items.generating_bib" = "Could not generate bibliography";
"errors.citation_preview" = "Could not generate citation preview";
"errors.item_detail.dropped_fields_title" = "Change Item Type";
"errors.item_detail.dropped_fields_message" = "Are you sure you want to change the item type?\n\nThe following fields will be lost:\n\n%@";
"errors.item_detail.cant_load_data" = "Could not load data. Please try again.";
"errors.item_detail.cant_create_attachments_with_names" = "Could not create attachments: %@";
"errors.item_detail.cant_create_attachments" = "Could not create attachments.";
"errors.item_detail.cant_save_note" = "Could not save note.";
"errors.item_detail.cant_save_changes" = "Could not save changes.";
"errors.item_detail.cant_trash_item" = "Could not move item to trash.";
"errors.item_detail.cant_save_tags" = "Could not save tags.";
"errors.item_detail.unsupported_type" = "Type \"%@\" not supported.";
"errors.settings.storage" = "Could not collect storage data";
"errors.settings.webdav.file_missing_after_upload" = "A potential problem was found with your WebDAV server.\n\nAn uploaded file was not immediately available for download. There may be a short delay between when you upload files and when they become available, particularly if you are using a cloud storage service.\n\nIf Zotero file syncing appears to work normally, you can ignore this message. If you have trouble, please post to the Zotero Forums.";
"errors.settings.webdav.invalid_url" = "WebDAV verification error";
"errors.settings.webdav.no_password" = "WebDAV verification error";
"errors.settings.webdav.no_url" = "WebDAV verification error";
"errors.settings.webdav.no_username" = "WebDAV verification error";
"errors.settings.webdav.non_existent_file_not_missing" = "WebDAV verification error";
"errors.settings.webdav.not_dav" = "Not a valid WebDAV URL";
"errors.settings.webdav.parent_dir_not_found" = "WebDAV verification error";
"errors.settings.webdav.zotero_dir_not_found" = "WebDAV verification error";
"errors.settings.webdav.unauthorized" = "The WebDAV server did not accept the username and password you entered.";
"errors.settings.webdav.forbidden" = "You don’t have permission to access the specified folder on the WebDAV server.";
"errors.settings.webdav.internet_connection" = "Unable to connect to the network. Please try again.";
"errors.settings.webdav.host_not_found" = "Could not connect to WebDAV server";
"errors.settings.webdav.ats" = "Due to iOS restrictions, you must use HTTPS to connect to a non-local WebDAV server.";
"errors.shareext.logged_out" = "Please log into the app before using this extension.";
"errors.shareext.cant_load_data" = "Failed to load data. Please try again.";
"errors.shareext.unknown" = "An unknown error occurred.";
"errors.shareext.cant_load_schema" = "An error occurred. Please open the Zotero app, sync, and try again.";
"errors.shareext.download_failed" = "Could not download file";
"errors.shareext.items_not_found" = "No items found on page";
"errors.shareext.parsing_error" = "Error parsing translator response";
"errors.shareext.schema_error" = "Some data could not be downloaded. It may have been saved with a newer version of Zotero.";
"errors.shareext.incompatible_item" = "No data returned";
"errors.shareext.javascript_failed" = "JS call failed";
"errors.shareext.translation_failed" = "Saving failed";
"errors.shareext.missing_base_files" = "Translator missing";
"errors.shareext.response_missing_data" = "An error occurred. Please try again.";
"errors.shareext.missing_file" = "Could not find file to upload";
"errors.shareext.background_uploader_failure" = "Background uploader not initialized";
"errors.shareext.failed_additional" = "You can still save this page as a webpage item.";
"errors.shareext.file_not_pdf" = "Unable to save PDF";
"errors.shareext.api_error" = "Error uploading item. The item was saved to your local library.";
"errors.shareext.webdav_error" = "Error uploading attachment to WebDAV server";
"errors.shareext.personal_quota_reached" = "You have reached your Zotero Storage quota, and the file could not be uploaded. See your account settings for additional storage options.\n\nThe file was saved to your local library.";
"errors.shareext.group_quota_reached" = "The group “%@” has reached its Zotero Storage quota, and the file could not be uploaded. The group owner can view their account settings for additional storage options.\n\nThe file was saved to the local library.";
"errors.shareext.forbidden" = "You don’t have necessary rights to submit this item to %@.";
"errors.shareext.webdav_not_verified" = "WebDAV verification error";
"errors.db_failure" = "Error creating database. Please try logging in again.";
"errors.logging.title" = "Debugging Error";
"errors.logging.start" = "Unable to start debug logging";
"errors.logging.content_reading" = "Log files could not be found";
"errors.logging.no_logs_recorded" = "No debug output occurred during logging";
"errors.logging.upload" = "Could not upload logs. Please try again.";
"errors.logging.response_parsing" = "Unexpected response from server";
"errors.translators.bundle_loading" = "Could not update translators from bundle. Would you like to try again?";
"errors.translators.bundle_reset" = "Could not load bundled translators.";
"errors.styles.addition" = "Could not add style \“%@\”.";
"errors.styles.deletion" = "Could not delete style \“%@\”.";
"errors.styles.loading" = "Could not load styles.";
"errors.styles_search.loading" = "Could not load styles. Do you want to try again?";
"errors.citation.generate_citation" = "Could not generate citation.";
"errors.citation.generate_bibliography" = "Could not generate bibliography.";
"errors.citation.invalid_types" = "Invalid item types selected.";
"errors.citation.missing_style" = "No citation style selected. Go to Settings → Quick Copy and select a new style.";
"errors.citation.open_settings" = "Open Settings";
"errors.lookup.no_identifiers_and_no_lookup_data" = "Zotero could not find any identifiers in your input. Please verify your input and try again.";
"errors.lookup.no_identifiers_with_lookup_data" = "Zotero could not find any new identifiers in your input, or they are already being added. Please verify your input and try again.";
"errors.pdf.merge_too_big_title" = "Unable to merge annotations";
"errors.pdf.merge_too_big" = "The combined annotation would be too large.";
"errors.pdf.cant_update_annotation" = "Can’t update annotation.";
"errors.pdf.cant_add_annotations" = "Can’t add annotations.";
"errors.pdf.cant_delete_annotations" = "Can’t delete annotations.";
"errors.pdf.incompatible_document" = "This document is not supported.";
"errors.pdf.empty_document" = "This document is empty.";
"errors.pdf.page_index_not_int" = "Incorrect format of page stored for this document.";
"errors.pdf.document_changed" = "This document has been changed on another device. Please reopen it to continue editing.";
"accessibility.untitled" = "Untitled";
"accessibility.locked" = "Locked";
"accessibility.archived" = "Archived";
"accessibility.collections.items" = "items";
"accessibility.collections.expand" = "Expand";
"accessibility.collections.collapse" = "Collapse";
"accessibility.collections.create_collection" = "Create collection";
"accessibility.collections.search_collections" = "Search collections";
"accessibility.collections.expand_all_collections" = "Expand all collections";
"accessibility.items.open_item" = "Open item info";
"accessibility.items.select_items" = "Select items";
"accessibility.items.select_all_items" = "Select All Items";
"accessibility.items.deselect_all_items" = "Deselect All Items";
"accessibility.items.filter_items" = "Filter items";
"accessibility.items.sort_items" = "Sort items";
"accessibility.items.add_to_collection" = "Add selected items to collection";
"accessibility.items.remove_from_collection" = "Remove selected items from collection";
"accessibility.items.trash" = "Move selected items to trash";
"accessibility.items.delete" = "Delete selected items";
"accessibility.items.restore" = "Restore selected items";
"accessibility.items.duplicate" = "Duplicate selected item";
"accessibility.items.share" = "Share selected items";
"accessibility.items.download_attachments" = "Download attachments for selected items";
"accessibility.items.remove_downloads" = "Remove downloads for selected items";
"accessibility.items.collection" = "Collection";
"accessibility.item_detail.download_and_open" = "Double-tap to download and open";
"accessibility.item_detail.open" = "Double-tap to open";
"accessibility.pdf.sidebar_open" = "Open sidebar";
"accessibility.pdf.sidebar_close" = "Close sidebar";
"accessibility.pdf.share" = "Share PDF";
"accessibility.pdf.export" = "Export PDF";
"accessibility.pdf.export_annotated" = "Export annotated PDF";
"accessibility.pdf.settings" = "Settings";
"accessibility.pdf.color_picker" = "Color picker";
"accessibility.pdf.highlight_annotation_tool" = "Create highlight annotation";
"accessibility.pdf.note_annotation_tool" = "Create note annotation";
"accessibility.pdf.image_annotation_tool" = "Create image annotation";
"accessibility.pdf.ink_annotation_tool" = "Create ink annotation";
"accessibility.pdf.underline_annotation_tool" = "Create underline annotation";
"accessibility.pdf.text_annotation_tool" = "Create text annotation";
"accessibility.pdf.eraser_annotation_tool" = "Eraser";
"accessibility.pdf.highlight_annotation" = "Highlight annotation";
"accessibility.pdf.note_annotation" = "Note annotation";
"accessibility.pdf.image_annotation" = "Image annotation";
"accessibility.pdf.ink_annotation" = "Ink annotation";
"accessibility.pdf.underline_annotation" = "Underline annotation";
"accessibility.pdf.text_annotation" = "Text annotation";
"accessibility.pdf.edit_annotation" = "Edit annotation";
"accessibility.pdf.share_annotation" = "Share annotation";
"accessibility.pdf.share_annotation_image" = "Share annotation image";
"accessibility.pdf.eraser_annotation" = "Eraser";
"accessibility.pdf.highlighted_text" = "Highlighted text";
"accessibility.pdf.underlined_text" = "Underlined text";
"accessibility.pdf.author" = "Author";
"accessibility.pdf.comment" = "Comment";
"accessibility.pdf.tags" = "Tags";
"accessibility.pdf.annotation_hint" = "Double-tap to select and edit";
"accessibility.pdf.tags_hint" = "Double-tap to edit tags";
"accessibility.pdf.selected" = "Selected";
"accessibility.pdf.open_reader" = "Open reading mode";
"accessibility.pdf.search_pdf" = "Search PDF";
"accessibility.pdf.redo" = "Redo";
"accessibility.pdf.undo" = "Undo";
"accessibility.pdf.toggle_annotation_toolbar" = "Toggle annotation toolbar";
"accessibility.pdf.show_more_tools" = "Show more";

View file

@ -0,0 +1,215 @@
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>items.collections_selected</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@collections_selected@</string>
<key>collections_selected</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>Select a Collection</string>
<key>one</key>
<string>1 Collection Selected</string>
<key>other</key>
<string>%d Collections Selected</string>
</dict>
</dict>
<key>items.delete_question</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@delete_question@</string>
<key>delete_question</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Are you sure you want to delete the selected item?</string>
<key>other</key>
<string>Are you sure you want to delete the selected items?</string>
</dict>
</dict>
<key>items.remove_from_collection_question</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@remove_from_collection_question@</string>
<key>remove_from_collection_question</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Are you sure you want to remove the selected item from this collection?</string>
<key>other</key>
<string>Are you sure you want to remove the selected items from this collection?</string>
</dict>
</dict>
<key>items.toolbar_filter</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@toolbar_filter@</string>
<key>toolbar_filter</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Filter: 1 item</string>
<key>other</key>
<string>Filter: %d items</string>
</dict>
</dict>
<key>pdf.search.matches</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@matches@</string>
<key>matches</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Found 1 match</string>
<key>other</key>
<string>Found %d matches</string>
</dict>
</dict>
<key>settings.storage.files_size_and_unit</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@files@ (%.2f %@)</string>
<key>files</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>1 file</string>
<key>other</key>
<string>%d files</string>
</dict>
</dict>
<key>errors.sync_toolbar.errors</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@errors@</string>
<key>errors</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>1 issue</string>
<key>other</key>
<string>%d issues</string>
</dict>
</dict>
<key>tag_picker.tags_selected</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@tags_selected@</string>
<key>tags_selected</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>1 tag selected</string>
<key>other</key>
<string>%d tags selected</string>
</dict>
</dict>
<key>tag_picker.confirm_deletion</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@confirm_deletion@</string>
<key>confirm_deletion</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Are you sure you want to delete 1 automatic tag in this library?\n\nThis action cannot be undone.</string>
<key>other</key>
<string>Are you sure you want to delete %d automatic tags in this library?\n\nThis action cannot be undone.</string>
</dict>
</dict>
<key>settings.lines_logged</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@lines_logged@</string>
<key>lines_logged</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>1 line logged</string>
<key>other</key>
<string>%d lines logged</string>
</dict>
</dict>
<key>settings.lines</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@lines@</string>
<key>lines</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>1 line</string>
<key>other</key>
<string>%d lines</string>
</dict>
</dict>
<key>errors.sync_toolbar.webdav_error2</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@webdav_error2@</string>
<key>webdav_error2</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Could not delete 1 file from your WebDAV server</string>
<key>other</key>
<string>Could not delete %d files from your WebDAV server</string>
</dict>
</dict>
<key>settings.general.after_x_days</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@after_x_days@</string>
<key>after_x_days</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>After 1 Day</string>
<key>other</key>
<string>After %d Days</string>
</dict>
</dict>
</dict>
</plist>

View file

@ -294,6 +294,7 @@ runJS-title = Run JavaScript
runJS-editor-label = Code:
runJS-run = Run
runJS-help = { general-help }
runJS-completed = completed successfully
runJS-result = {
$type ->
[async] Return value:
@ -766,3 +767,5 @@ mac-word-plugin-install-remind-later-button =
.label = { general-remind-me-later }
mac-word-plugin-install-dont-ask-again-button =
.label = { general-dont-ask-again }
userjs-pref-warning = Some { -app-name } settings have been overridden using an unsupported method. { -app-name } will revert them and restart.

View file

@ -1355,6 +1355,7 @@ noteEditor.insertColumnAfter = Insert Column Right
noteEditor.deleteRow = Delete Row
noteEditor.deleteColumn = Delete Column
noteEditor.deleteTable = Delete Table
noteEditor.a11yLinkPopupAppearedAlert = Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations = Annotations
pdfReader.showAnnotations = Show Annotations

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insertar columna a la derecha
noteEditor.deleteRow=Borrar fila
noteEditor.deleteColumn=Borrar columna
noteEditor.deleteTable=Borrar tabla
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Anotaciones
pdfReader.showAnnotations=Mostrar anotaciones

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
noteEditor.deleteRow=Delete Row
noteEditor.deleteColumn=Delete Column
noteEditor.deleteTable=Delete Table
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Annotations
pdfReader.showAnnotations=Show Annotations

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Txertatu zutabea eskubian
noteEditor.deleteRow=Ezabatu errenkada
noteEditor.deleteColumn=Ezabatu zutabea
noteEditor.deleteTable=Ezabatu taula
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Oharpenak
pdfReader.showAnnotations=Erakutsi oharpenak

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
noteEditor.deleteRow=Delete Row
noteEditor.deleteColumn=Delete Column
noteEditor.deleteTable=Delete Table
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Annotations
pdfReader.showAnnotations=Show Annotations

View file

@ -74,9 +74,9 @@ preferences-advanced-language-and-region-title = Kieli ja alue
preferences-advanced-enable-bidi-ui =
.label = Käytä kaksisuuntaisen tekstin editointiohjelmia
preferences-advanced-reset-data-dir =
.label = Revert to Default Location…
.label = Palauta vakiosijaintiin…
preferences-advanced-custom-data-dir =
.label = Use Custom Location…
.label = Käytä omaa sijaintia…
preferences-advanced-default-data-dir =
.value = (Default: { $directory })
.value = (Oletus: { $directory })
.aria-label = Oletussijainti

View file

@ -48,18 +48,18 @@ pdfReader-import-from-epub =
.label = Tuo e-kirjan huomautukset...
pdfReader-import-from-epub-prompt-title = Tuo e-kirjan huomautukset
pdfReader-import-from-epub-prompt-text =
{ -app-name } found { $count ->
[1] { $count } { $tool } annotation
*[other] { $count } { $tool } annotations
}, last edited { $lastModifiedRelative }.
{ -app-name } löysi { $count ->
[1] { $count } { $tool } huomautuksen
*[other] { $count } { $tool } huomautusta
}, muokattu viimeksi { $lastModifiedRelative }.
Any { -app-name } annotations that were previously imported from this ebook will be updated.
Kaikki { -app-name }-huomautukset jotka on tuotu aiemmin tästä e-kirjasta päivitetään.
pdfReader-import-from-epub-no-annotations-current-file =
This ebook does not appear to contain any importable annotations.
Tämä e-kirja ei vaikuta sisältävän tuotavia huomautuksia.
{ -app-name } can import ebook annotations created in Calibre and KOReader.
{ -app-name } voi tuoda e-kirjan huomautukset jotka on luotu Calibrella tai KOReaderilla.
pdfReader-import-from-epub-no-annotations-other-file =
“{ $filename }” does not appear to contain any Calibre or KOReader annotations.
“{ $filename }” ei vaikuta sisältävän Calibrella tai KOReaderilla luotuja huomautuksia.
If this ebook has been annotated with KOReader, try selecting a “metadata.epub.lua” file directly.
pdfReader-import-from-epub-select-other = Select Other File…
Jos tämän e-kirjan huomautukset on tehty KOReaderilla, koita valita “metadata.epub.lua”-tiedosto suoraan.
pdfReader-import-from-epub-select-other = Valitse toinen tiedosto…

View file

@ -285,11 +285,11 @@ integration-editBibliography-remove-button =
.aria-label = { general-remove }
integration-editBibliography-editor =
.aria-label = Vuokkaa viitettä
-integration-editBibliography-include-uncited = To include an uncited item in your bibliography, select it from the items list and press { general-add }.
-integration-editBibliography-exclude-cited = You can also exclude a cited item by selecting it from the list of references and pressing { general-remove }.
-integration-editBibliography-edit-reference = To change how a reference is formatted, use the text editor.
-integration-editBibliography-include-uncited = Sisällyttääksesi viittaamattoman nimikkeen lähdeluetteloosi, valitse se nimikelistalta ja paina { general-add }.
-integration-editBibliography-exclude-cited = Voit myös jättää pois viitatun nimikkeen lähdeluettelosta valitsemalla sen ja painamalla { general-remove }.
-integration-editBibliography-edit-reference = Muuttaaksesi viitteen muotoilua, käytä tekstieditoria.
integration-editBibliography-wrapper =
.aria-label = Edit Bibliography dialog
.aria-label = Muokkaa lähdeluetteloa -valintaikkuna
.aria-description =
{ -integration-editBibliography-include-uncited }
{ -integration-editBibliography-exclude-cited }
@ -391,7 +391,7 @@ menu-ui-density-comfortable =
.label = Väljempi
menu-ui-density-compact =
.label = Tiivis
pane-item-details = Item Details
pane-item-details = Nimikkeen tiedot
pane-info = Tiedot
pane-abstract = Tiivistelmä
pane-attachments = Liitteet
@ -641,7 +641,7 @@ find-pdf-files-added =
select-items-dialog =
.buttonlabelaccept = Valitse
select-items-convertToStandalone =
.label = Convert to Standalone
.label = Muuta erilliseksi
select-items-convertToStandaloneAttachment =
.label =
{ $count ->
@ -651,8 +651,8 @@ select-items-convertToStandaloneAttachment =
select-items-convertToStandaloneNote =
.label =
{ $count ->
[one] Convert to Standalone Note
*[other] Convert to Standalone Notes
[one] Muuta erilliseksi muistiinpanoksi
*[other] Muuta erillisiksi muistiinpanoiksi
}
file-type-webpage = Verkkosivu
file-type-image = Kuva

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Lisää sarake oikealle
noteEditor.deleteRow=Poista rivi
noteEditor.deleteColumn=Poista sarake
noteEditor.deleteTable=Poista taulukko
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Huomautukset
pdfReader.showAnnotations=Näytä huomautukset

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insérer une colonne à droite
noteEditor.deleteRow=Supprimer la ligne
noteEditor.deleteColumn=Supprimer la colonne
noteEditor.deleteTable=Supprimer le tableau
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Annotations
pdfReader.showAnnotations=Afficher les annotations

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Inserir columna á dereita
noteEditor.deleteRow=Eliminar fila
noteEditor.deleteColumn=Eliminar columna
noteEditor.deleteTable=Eliminar táboa
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=Anotacións
pdfReader.showAnnotations=Amosar anotacións

View file

@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=הוספת עמודה מימין
noteEditor.deleteRow=מחיקת שורה
noteEditor.deleteColumn=מחיקת עמודה
noteEditor.deleteTable=מחיקת טבלה
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
pdfReader.annotations=הסברים
pdfReader.showAnnotations=הצגת הסברים

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