Compare commits

...

77 commits
main ... 7.0.15

Author SHA1 Message Date
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
94 changed files with 1876 additions and 1150 deletions

View file

@ -19,6 +19,9 @@ jobs:
node-version: 18 node-version: 18
#cache: npm #cache: npm
- name: Install dbus-glib
run: sudo apt-get update && sudo apt-get install -y libdbus-glib-1-2
# On GitHub # On GitHub
- name: Install xvfb - name: Install xvfb
if: env.ACT != 'true' if: env.ACT != 'true'
@ -57,18 +60,17 @@ jobs:
env: env:
NODE_OPTIONS: --openssl-legacy-provider NODE_OPTIONS: --openssl-legacy-provider
# Somehow forces dpl v1 to be installed, even though v2 is installed without
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: 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 bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Upload deployment ZIP - name: Upload deployment ZIP
if: | if: |
env.ACT != 'true' env.ACT != 'true'
&& github.repository == 'zotero/zotero' && github.repository == 'zotero/zotero'
&& github.event_name == 'push' && 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: env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@ -77,11 +79,8 @@ jobs:
cd build cd build
zip -r ../build-zip/$GITHUB_SHA.zip * zip -r ../build-zip/$GITHUB_SHA.zip *
cd .. cd ..
# Once dpl 2 works: gem install --no-document dpl -v '>= 2.0'
#sudo gem install --no-document dpl dpl s3 --bucket zotero-download --local_dir build-zip --upload_dir ci/client --acl public_read
#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
- name: Run tests - name: Run tests
run: xvfb-run test/runtests.sh -f 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 // Don't retrieve unrequested links when performing standalone translation
pref("network.prefetch-next", false); 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 // Let operations run as long as necessary
pref("dom.max_chrome_script_run_time", 0); pref("dom.max_chrome_script_run_time", 0);

@ -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" . "$APP_ROOT_DIR/config.sh"
CHANNEL="release" CHANNEL="release"
BRANCH="main" BRANCH="7.0"
if [ "`uname`" = "Darwin" ]; then if [ "`uname`" = "Darwin" ]; then
export SAFARI_APPEX="$ROOT_DIR/../safari-app-extension-builds/release/ZoteroSafariExtension.appex" export SAFARI_APPEX="$ROOT_DIR/../safari-app-extension-builds/release/ZoteroSafariExtension.appex"
fi fi

View file

@ -43,6 +43,7 @@
title="Scaffold" title="Scaffold"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml" xmlns:html="http://www.w3.org/1999/xhtml"
windowtype="zotero:scaffold"
style="display: flex;" style="display: flex;"
drawintitlebar-platforms="win, linux"> drawintitlebar-platforms="win, linux">

View file

@ -736,9 +736,9 @@ var CollectionTree = class CollectionTree extends LibraryTree {
return numSelected; return numSelected;
} }
// If there's a single item and it's in the trash, switch to that // If there's a single item and it or an ancestor are in the trash, switch to that
if (items.length == 1 && items[0].deleted) { if (items.length == 1 && items[0].isInTrash()) {
Zotero.debug("Item is deleted; switching to trash"); Zotero.debug("Item is in trash; switching to trash");
await this.selectTrash(libraryID); await this.selectTrash(libraryID);
} }
else { else {

View file

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

View file

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

View file

@ -322,7 +322,6 @@
async asyncRender() { async asyncRender() {
if (!this.item) return; if (!this.item) return;
if (this._asyncRendering) return;
if (!this._section.open) return; if (!this._section.open) return;
if (this._isAlreadyRendered("async")) { if (this._isAlreadyRendered("async")) {
if (this._previewDiscarded) { if (this._previewDiscarded) {

View file

@ -63,6 +63,7 @@
this._selectFieldSelection = null; this._selectFieldSelection = null;
this._addCreatorRow = false; this._addCreatorRow = false;
this._switchedModeOfCreator = null; this._switchedModeOfCreator = null;
this._popupNode = null;
this._lastUpdateCustomRows = ""; this._lastUpdateCustomRows = "";
// Keep in sync with itemPaneManager.js // Keep in sync with itemPaneManager.js
@ -125,7 +126,7 @@
init() { init() {
this.initCollapsibleSection(); this.initCollapsibleSection();
this._creatorTypeMenu.addEventListener('command', async (event) => { this._creatorTypeMenu.addEventListener('command', async (event) => {
var typeBox = document.popupNode; var typeBox = this._popupNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]); var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
var typeID = event.explicitOriginalTarget.getAttribute('typeid'); var typeID = event.explicitOriginalTarget.getAttribute('typeid');
@ -144,7 +145,7 @@
}); });
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (_event) => { 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 typeBox = row.querySelector('.creator-type-label').parentNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]); var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
var item = this.item; var item = this.item;
@ -182,7 +183,7 @@
}); });
this._id('zotero-creator-transform-menu').addEventListener('command', async (event) => { 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 typeBox = row.querySelector('.creator-type-label').parentNode;
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]); var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
@ -666,12 +667,13 @@
} }
if (addLinkContextMenu) { if (addLinkContextMenu) {
rowData.oncontextmenu = (event) => { rowData.oncontextmenu = (event) => {
this._linkMenu.dataset.link = link; let menupopup = this._linkMenu;
this._linkMenu.dataset.val = val;
document.popupNode = rowLabel.parentElement; 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);
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, event.target.closest('input'));
this.handlePopupOpening(event, menupopup); this.handlePopupOpening(event, menupopup);
}; };
} }
@ -1189,7 +1191,7 @@
span.className = 'creator-type-dropmarker'; span.className = 'creator-type-dropmarker';
labelWrapper.appendChild(span); labelWrapper.appendChild(span);
labelWrapper.addEventListener('click', (e) => { labelWrapper.addEventListener('click', (e) => {
document.popupNode = rowLabel; this._popupNode = rowLabel;
this._creatorTypeMenu.openPopup(rowLabel); this._creatorTypeMenu.openPopup(rowLabel);
// If the creator menu is opened via mouse-click, add a special attribute to // 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. // 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('id', `creator-${rowIndex}-options`);
optionsButton.setAttribute('data-l10n-id', "itembox-button-options"); optionsButton.setAttribute('data-l10n-id', "itembox-button-options");
let triggerPopup = (e) => { let triggerPopup = (e) => {
document.popupNode = firstlast; this._popupNode = firstlast;
let menupopup = this._id('zotero-creator-transform-menu'); let menupopup = this._id('zotero-creator-transform-menu');
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, e.target.closest('input')); Zotero.Utilities.Internal.updateEditContextMenu(menupopup, e.target.closest('input'));
@ -2196,6 +2198,8 @@
textbox.setAttribute("min-lines", 1); textbox.setAttribute("min-lines", 1);
} }
var value = textbox.value.trim(); 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('-'); var [field, creatorIndex, creatorField] = fieldName.split('-');
@ -2330,6 +2334,7 @@
row.querySelector(".zotero-clicky-plus").hidden = true; row.querySelector(".zotero-clicky-plus").hidden = true;
row.querySelector(".zotero-clicky-minus").hidden = true; row.querySelector(".zotero-clicky-minus").hidden = true;
row.querySelector(".zotero-clicky-options").hidden = true; row.querySelector(".zotero-clicky-options").hidden = true;
row.querySelector(".zotero-clicky-switch-type").hidden = true;
continue; continue;
} }
@ -2389,7 +2394,7 @@
* @return {Promise} * @return {Promise}
*/ */
async swapNames(_event) { async swapNames(_event) {
var row = document.popupNode.closest('.meta-row'); var row = this._popupNode.closest('.meta-row');
var typeBox = row.querySelector('[fieldname]'); var typeBox = row.querySelector('[fieldname]');
var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]); var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
var fields = this.getCreatorFields(row); var fields = this.getCreatorFields(row);
@ -2414,7 +2419,7 @@
* @return {Promise} * @return {Promise}
*/ */
async capitalizeCreatorName(_event) { async capitalizeCreatorName(_event) {
var row = document.popupNode.closest('.meta-row'); var row = this._popupNode.closest('.meta-row');
let label = row.querySelector('.meta-label'); let label = row.querySelector('.meta-label');
var creatorIndex = parseInt(label.getAttribute('fieldname').split('-')[1]); var creatorIndex = parseInt(label.getAttribute('fieldname').split('-')[1]);
let [lastName, firstName] = [...row.querySelectorAll("editable-text")]; 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) => { const waitNoLongerThan = async (promise, ms = 1000) => {
return Promise.race([ return Promise.race([
promise, promise,
@ -269,6 +275,11 @@
this._restorePinnedPane(); this._restorePinnedPane();
let panes = this.getPanes(); 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]) { for (let box of [this._header, ...panes]) {
box.editable = this.editable; box.editable = this.editable;
box.tabID = this.tabID; box.tabID = this.tabID;
@ -291,7 +302,7 @@
this._paneParent.style.paddingBottom = ''; this._paneParent.style.paddingBottom = '';
if (pinnedPaneElem) { if (pinnedPaneElem) {
let paneID = pinnedPaneElem.dataset.pane; let paneID = pinnedPaneElem.dataset.pane;
this.scrollToPane(paneID, 'instant'); await this.scrollToPane(paneID, 'instant');
this.pinnedPane = paneID; this.pinnedPane = paneID;
} }
else { else {
@ -310,10 +321,17 @@
continue; continue;
} }
await waitNoLongerThan(box.asyncRender(), 500); await waitNoLongerThan(box.asyncRender(), 500);
// Make sure the layout is updated for next isPaneVisible check
await waitDOMUpdate();
} }
if (this.item.id == item.id) { if (this.item.id == item.id) {
this._isRendering = false; this._isRendering = false;
} }
// Re-enable intersection observer
this._toggleIntersectionObserver(true);
if (Zotero.test) { if (Zotero.test) {
resolve(); resolve();
} }
@ -439,7 +457,9 @@
} }
async scrollToPane(paneID, behavior = 'smooth') { 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; if (!pane) return null;
let scrollPromise; let scrollPromise;
@ -450,6 +470,9 @@
return null; return null;
} }
// Temporarily disable intersection observer to prevent unwanted rendering
this._toggleIntersectionObserver(false);
// The pane should always be at the very top // 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 // 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 // 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 = this._waitForScroll();
scrollPromise.then(() => this._disableScrollHandler = false); 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.scrollIntoView({ block: 'start', behavior });
pane.focus(); 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) { _makeSpaceForPane(pane) {
@ -528,7 +573,17 @@
this._intersectionOb.disconnect(); this._intersectionOb.disconnect();
} }
this._intersectionOb = new IntersectionObserver(this._handleIntersection); 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 = () => { _handleContainerScroll = () => {

View file

@ -151,8 +151,17 @@
return true; return true;
} }
renderItemPane(item) { async renderItemPane(item) {
let previousMode = this.mode;
this.mode = "item"; 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.editable = this.editable;
this._itemDetails.tabID = "zotero-pane"; this._itemDetails.tabID = "zotero-pane";

View file

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

View file

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

View file

@ -82,6 +82,9 @@
if (this._item == val) { if (this._item == val) {
return; 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; this._item = val;
} }
@ -132,7 +135,7 @@
this._tagColors = Zotero.Tags.getColors(this.item.libraryID); 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'); let tagRows = this._id('rows');
tagRows.replaceChildren(); tagRows.replaceChildren();

View file

@ -117,7 +117,7 @@ const apiFetchUrl = async (tokens, url, headers = {}, options = {}) => {
const apiFetch = async (tokens, endPoint, params = {}, headers = {}, options = {}) => { const apiFetch = async (tokens, endPoint, params = {}, headers = {}, options = {}) => {
const stringParams = Object.entries(params).map(p => p.join('=')).join('&'); 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); return apiFetchUrl(tokens, url, headers, options);
}; };
@ -183,8 +183,8 @@ const obtainReferenceManagerToken = async (login, password) => {
hasEnteredLogin = await browser.browsingContext.currentWindowGlobal hasEnteredLogin = await browser.browsingContext.currentWindowGlobal
.getActor("MendeleyAuth") .getActor("MendeleyAuth")
.sendQuery("login", { login }); .sendQuery("login", { login });
Zotero.debug(`hasEnteredLogin: ${hasEnteredLogin}`);
if (!hasEnteredLogin) { if (!hasEnteredLogin) {
browser.destroy();
reject(new Error("Failed to enter login")); reject(new Error("Failed to enter login"));
} }
} }
@ -195,8 +195,8 @@ const obtainReferenceManagerToken = async (login, password) => {
hasEnteredPassword = await browser.browsingContext.currentWindowGlobal hasEnteredPassword = await browser.browsingContext.currentWindowGlobal
.getActor("MendeleyAuth") .getActor("MendeleyAuth")
.sendQuery("password", { password }); .sendQuery("password", { password });
Zotero.debug(`hasEnteredPassword: ${hasEnteredPassword}`);
if (!hasEnteredPassword) { if (!hasEnteredPassword) {
browser.destroy();
reject(new Error("Failed to enter password")); reject(new Error("Failed to enter password"));
} }
} }
@ -205,6 +205,7 @@ const obtainReferenceManagerToken = async (login, password) => {
const cookies = cookieSandbox.getCookiesForURI( const cookies = cookieSandbox.getCookiesForURI(
Services.io.newURI("https://www.mendeley.com/reference-manager/library") Services.io.newURI("https://www.mendeley.com/reference-manager/library")
); );
browser.destroy();
if (!cookies.accessToken) { if (!cookies.accessToken) {
reject(new Error("Failed to obtain Mendeley access token")); 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/"); browser.load("https://www.mendeley.com/sign-in?routeTo=https://www.mendeley.com/reference-manager/library/");
Zotero.Promise.delay(ACCESS_TOKEN_TIMEOUT).then(() => { Zotero.Promise.delay(ACCESS_TOKEN_TIMEOUT).then(() => {
browser.destroy();
reject(new Error("Timed out while obtaining Mendeley access token")); 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._tokens = null;
this._credentials = null; this._credentials = null;
this._refManagerToken = null;
this._db = null; this._db = null;
this._file = null; this._file = null;
this._saveOptions = null; this._saveOptions = null;
@ -327,15 +328,18 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
} }
if (this._credentials && !this.skipNotebooks) { if (this._credentials && !this.skipNotebooks) {
const token = await obtainReferenceManagerTokenWithRetry(this._credentials.username, this._credentials.password); if (!this._refManagerToken) {
this._progress += 1; // progress one arbitrary "tick" assigned to importing notebooks task, we have 4 more left const token = await obtainReferenceManagerTokenWithRetry(this._credentials.username, this._credentials.password);
if (token) {
this._refManagerToken = { this._refManagerToken = {
kind: 'referenceManager', kind: 'referenceManager',
accessToken: token, accessToken: token,
username: this._credentials.username, username: this._credentials.username,
password: this._credentials.password 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 notebooks = await this._getNotebooksAPI();
const notesContent = await Promise.all(notebooks.map(notebook => this._translateNotebookToNoteContent(libraryID, notebook))); 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 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; const isMappedToExisting = !!existingItem;
Zotero.debug(isMappedToExisting ? `Updating existing notebook "${uuid}" -> "${existingItem.key}"` : `Importing new notebook "${uuid}"`, 5); 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.libraryID = libraryID;
item.setNote(noteContent); item.setNote(noteContent);
item.addRelation(predicate, uuid); item.addRelation(predicate, uuid);
@ -1085,14 +1089,15 @@ Zotero_Import_Mendeley.prototype._translateNotebookToNoteContent = async functio
'mendeleyDB:annotationUUID', 'mendeleyDB:annotationUUID',
annotationUUID annotationUUID
); );
if (annotation) {
let attachmentItem = Zotero.Items.get(annotation.parentID); let attachmentItem = Zotero.Items.get(annotation.parentID);
let jsonAnnotation = await Zotero.Annotations.toJSON(annotation); let jsonAnnotation = await Zotero.Annotations.toJSON(annotation);
jsonAnnotation.attachmentItemID = attachmentItem.id; jsonAnnotation.attachmentItemID = attachmentItem.id;
jsonAnnotation.id = annotation.key; jsonAnnotation.id = annotation.key;
const { html } = Zotero.EditorInstanceUtilities.serializeAnnotations([jsonAnnotation]); const { html } = Zotero.EditorInstanceUtilities.serializeAnnotations([jsonAnnotation]);
return html; return html;
}
} }
} }
} }
@ -1744,6 +1749,11 @@ Zotero_Import_Mendeley.prototype._saveAnnotations = async function (annotations,
// do not attempt to import annotations for non-PDF files // do not attempt to import annotations for non-PDF files
return; 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(); let annotationMap = new Map();
for (let annotation of annotations) { for (let annotation of annotations) {
@ -1766,6 +1776,7 @@ Zotero_Import_Mendeley.prototype._saveAnnotations = async function (annotations,
for (let annotation of annotations) { for (let annotation of annotations) {
// Ignore empty highlights // Ignore empty highlights
if (annotation.type == 'highlight' && !annotation.text) { if (annotation.type == 'highlight' && !annotation.text) {
Zotero.debug(`Skipping empty highlight with uuid ${annotation.uuid}`, 5);
continue; continue;
} }

View file

@ -2287,7 +2287,8 @@ var Zotero_QuickFormat = new function () {
} }
} }
// Arrow up closes the popup // 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; skipInputRefocus = true;
itemPopover.hidePopup(); itemPopover.hidePopup();
return stopEvent(); return stopEvent();

View file

@ -345,25 +345,13 @@ var Zotero_LocateMenu = new function() {
* Get the first 50 selected items * Get the first 50 selected items
*/ */
function _getSelectedItems() { function _getSelectedItems() {
if (Zotero_Tabs.selectedID == 'zotero-pane') { var allSelectedItems = ZoteroPane.getSelectedItems();
var allSelectedItems = ZoteroPane_Local.getSelectedItems(); var selectedItems = [];
var selectedItems = []; while (selectedItems.length < 50 && allSelectedItems.length) {
while (selectedItems.length < 50 && allSelectedItems.length) { var item = allSelectedItems.shift();
var item = allSelectedItems.shift(); if (!item.isNote()) selectedItems.push(item);
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 [];
} }
return selectedItems;
} }
var ViewOptions = {}; var ViewOptions = {};

View file

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

View file

@ -1,4 +1,4 @@
<html id="run-js"> <html id="run-js" windowtype="zotero:run-js">
<head> <head>
<title data-l10n-id="runJS-title"></title> <title data-l10n-id="runJS-title"></title>
<link href="chrome://zotero-platform/content/zotero.css" rel="stylesheet"/> <link href="chrome://zotero-platform/content/zotero.css" rel="stylesheet"/>

View file

@ -309,23 +309,12 @@ const ZoteroStandalone = new function() {
let win = Zotero.getMainWindow(); let win = Zotero.getMainWindow();
if (win) { if (win) {
if (win.Zotero_Tabs.selectedID == 'zotero-pane') { try {
try { selected = win.ZoteroPane.getSelectedItems();
selected = win.ZoteroPane.getSelectedItems();
}
catch (e) {
}
win.ZoteroPane.updateQuickCopyCommands(selected);
} }
else { catch (e) {
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]);
}
} }
win.ZoteroPane.updateQuickCopyCommands(selected);
} }
var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL); var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);

View file

@ -399,7 +399,8 @@ var Zotero_Tabs = new function () {
null, null,
{ {
tabIndex: tab.index, tabIndex: tab.index,
openInBackground: true openInBackground: true,
allowDuplicate: true
} }
)); ));
if (tab.index > maxIndex) { if (tab.index > maxIndex) {

View file

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

View file

@ -921,20 +921,7 @@ Zotero.Attachments = new function () {
} }
} }
else { else {
Zotero.debug("Saving file with saveURI()"); yield Zotero.HTTP.download(url, tmpFile);
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;
} }
var attachmentItem; var attachmentItem;
@ -1100,20 +1087,18 @@ Zotero.Attachments = new function () {
let enforcingFileType = false; let enforcingFileType = false;
try { try {
await new Zotero.Promise(function (resolve) { let headers = {};
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] if (options.referrer) {
.createInstance(Components.interfaces.nsIWebBrowserPersist); headers.Referer = options.referrer;
if (options.cookieSandbox) { }
options.cookieSandbox.attachToInterfaceRequestor(wbp); 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) { if (options.enforceFileType) {
enforcingFileType = true; enforcingFileType = true;

View file

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

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); // Zotero.debug(`CookieSandbox: Found the browser via doc of load group for ${channelURI}`, 5);
trackedBy = this.trackedBrowsers.get(browser); trackedBy = this.trackedBrowsers.get(browser);
} }
else if (notificationCallbacks instanceof XMLHttpRequest) {
// Zotero.debug(`CookieSandbox: Found the browser via XHR for ${channelURI}`, 5);
tested = true;
}
else { else {
// try getting as an XHR or nsIWBP // try getting as an nsIWBP
try { try {
notificationCallbacks.QueryInterface(Components.interfaces.nsIXMLHttpRequest); notificationCallbacks.QueryInterface(Components.interfaces.nsIWebBrowserPersist);
// Zotero.debug(`CookieSandbox: Found the browser via XHR or nsIWBP for ${channelURI}`, 5); // Zotero.debug(`CookieSandbox: Found the browser via nsIWBP for ${channelURI}`, 5);
tested = true; tested = true;
} }
catch (e) {} 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'); this._requireData('childItems');
return this.getAttachments() return Zotero.Items.get(this.getAttachments())
.map(itemID => Zotero.Items.get(itemID)) .filter(item => item.isFileAttachment() && item.attachmentContentType == contentType);
.filter(item => item.isFileAttachment() && item.attachmentContentType == contentType) };
.length;
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 // 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.) // file is being renamed, give it a default title ("PDF", "Webpage", etc.)
let isFirstOfType = this.parentItemID 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); let isBeingRenamed = ignoreAutoRenamePrefs || Zotero.Attachments.shouldAutoRenameAttachment(this);
if (isFirstOfType && isBeingRenamed) { if (isFirstOfType && isBeingRenamed) {
let defaultTitle = this._getDefaultTitleForAttachmentContentType(); let defaultTitle = this._getDefaultTitleForAttachmentContentType();
@ -5124,6 +5144,7 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
*/ */
Zotero.Item.prototype.fromJSON = function (json, options = {}) { Zotero.Item.prototype.fromJSON = function (json, options = {}) {
var strict = !!options.strict; var strict = !!options.strict;
var migrateExtra = !strict;
if (!json.itemType && !this._itemTypeID) { if (!json.itemType && !this._itemTypeID) {
throw new Error("itemType property not provided"); throw new Error("itemType property not provided");
@ -5139,29 +5160,34 @@ Zotero.Item.prototype.fromJSON = function (json, options = {}) {
var isValidForType = {}; var isValidForType = {};
var setFields = new Set(); var setFields = new Set();
var { itemType, fields: extraFields, creators: extraCreators, extra } =
Zotero.Utilities.Internal.extractExtraFields( var extraFields = new Map();
json.extra || '', var extraCreators = [];
this, var extra = json.extra || '';
Object.keys(json) // Transfer valid fields from Extra to regular fields
// TEMP until we move creator lines to real creators if (migrateExtra) {
.concat('creators') let itemType;
); ({ itemType, fields: extraFields, creators: extraCreators, extra } =
// If a different item type was parsed out of Extra, use that instead Zotero.Utilities.Internal.extractExtraFields(
if (itemType && json.itemType != itemType) { extra,
itemTypeID = Zotero.ItemTypes.getID(itemType); this,
this.setType(itemTypeID); 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(); 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) { for (let field in json) {
let val = json[field]; let val = json[field];

View file

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

View file

@ -291,10 +291,12 @@ Zotero.Library.prototype._set = function(prop, val) {
if (newVal != val) { if (newVal != val) {
throw new Error(`${prop} must be an integer (${typeof val} '${val}' given)`); 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; val = newVal;
// Ensure that it is never decreasing
if (val < this._libraryStorageVersion) throw new Error(prop + ' cannot decrease');
break; break;
case '_libraryLastSync': case '_libraryLastSync':

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

View file

@ -448,16 +448,15 @@ Zotero.File = new function(){
this.download = async function (uri, path) { this.download = async function (uri, path) {
var uriStr = uri.spec || uri; var uriStr = uri.spec || uri;
const isHTTP = uriStr.startsWith('http'); 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}`); 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(); var deferred = Zotero.Promise.defer();
const inputChannel = NetUtil.newChannel({ const inputChannel = NetUtil.newChannel({
uri, uri,

View file

@ -149,6 +149,7 @@ Zotero.FileHandlers = {
handler = handler || systemHandler; handler = handler || systemHandler;
if (handler) { if (handler) {
if (Zotero.isMac) { if (Zotero.isMac) {
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
try { try {
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', handler, path]); await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', handler, path]);
return true; return true;
@ -156,6 +157,9 @@ Zotero.FileHandlers = {
catch (e) { catch (e) {
Zotero.logError(e); Zotero.logError(e);
} }
finally {
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
}
} }
try { try {
@ -498,8 +502,13 @@ Zotero.FileHandlers = {
if (!Zotero.File.pathToFile(command).isExecutable()) { if (!Zotero.File.pathToFile(command).isExecutable()) {
throw new Error(`${command} is not an executable`); throw new Error(`${command} is not an executable`);
} }
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
// Do not await // 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 * @namespace
*/ */
Zotero.HTTP = new function() { Zotero.HTTP = new function() {
this.disableErrorRetry = false;
var _errorDelayIntervals = [2500, 5000, 10000, 20000, 40000, 60000, 120000, 240000, 300000]; var _errorDelayIntervals = [2500, 5000, 10000, 20000, 40000, 60000, 120000, 240000, 300000];
var _errorDelayMax = 60 * 60 * 1000; // 1 hour var _errorDelayMax = 60 * 60 * 1000; // 1 hour
@ -169,7 +170,7 @@ Zotero.HTTP = new function() {
continue; continue;
} }
// Don't retry if errorDelayMax is 0 // Don't retry if errorDelayMax is 0
if (options.errorDelayMax === 0) { if (options.errorDelayMax === 0 || Zotero.HTTP.disableErrorRetry) {
throw e; throw e;
} }
// Automatically retry other 5xx errors by default // Automatically retry other 5xx errors by default
@ -269,7 +270,10 @@ Zotero.HTTP = new function() {
var deferred = Zotero.Promise.defer(); 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(); var xmlhttp = new XMLHttpRequest();
} }
else { else {
@ -324,11 +328,13 @@ Zotero.HTTP = new function() {
channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
} }
// Don't follow redirects let notificationCallbacks = options.notificationCallbacks || {};
if (options.followRedirects === false) { if (options.followRedirects === false) {
channel.notificationCallbacks = { if (notificationCallbacks.asyncOnChannelRedirect) {
QueryInterface: ChromeUtils.generateQI([Ci.nsIInterfaceRequestor, Ci.nsIChannelEventSync]), throw new Error("Can't set asyncOnChannelRedirect and followRedirects = false");
getInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]), }
notificationCallbacks = {
...notificationCallbacks,
asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) { asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
redirectStatus = (flags & Ci.nsIChannelEventSink.REDIRECT_PERMANENT) ? 301 : 302; redirectStatus = (flags & Ci.nsIChannelEventSink.REDIRECT_PERMANENT) ? 301 : 302;
redirectLocation = newChannel.URI.spec; redirectLocation = newChannel.URI.spec;
@ -337,6 +343,9 @@ Zotero.HTTP = new function() {
} }
}; };
} }
if (notificationCallbacks) {
channel.notificationCallbacks = wrapNotificationCallbacks(notificationCallbacks);
}
} }
// Set responseType // Set responseType
@ -380,15 +389,54 @@ Zotero.HTTP = new function() {
// Convert numbers to string to make Sinon happy // Convert numbers to string to make Sinon happy
xmlhttp.setRequestHeader(header, value); xmlhttp.setRequestHeader(header, value);
} }
// Set timeout const defaultTimeout = 30000;
let requestTimeout;
let connectTimeout;
let inactivityTimeout;
if (options.timeout !== 0) { 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 // Provide caller with a callback to cancel a request in progress
if (options.cancellerReceiver) { if (options.cancellerReceiver) {
@ -402,7 +450,17 @@ Zotero.HTTP = new function() {
}); });
} }
if (connectTimeout || inactivityTimeout) {
xmlhttp.onloadstart = () => {
clearConnectTimer();
resetInactivityTimer();
};
}
xmlhttp.onloadend = async function() { xmlhttp.onloadend = async function() {
clearConnectTimer();
clearTimeout(inactivityTimerID);
var status = redirectStatus || xmlhttp.status; var status = redirectStatus || xmlhttp.status;
try { try {
@ -504,6 +562,11 @@ Zotero.HTTP = new function() {
} }
Zotero.debug(msg, 1); Zotero.debug(msg, 1);
if (timedOutAfter) {
deferred.reject(new Zotero.HTTP.TimeoutException(timedOutAfter));
return;
}
if (xmlhttp.status == 0) { if (xmlhttp.status == 0) {
try { try {
this.checkSecurity(channel, { isProxyAuthRequest: options.isProxyAuthRequest }); this.checkSecurity(channel, { isProxyAuthRequest: options.isProxyAuthRequest });
@ -529,22 +592,92 @@ Zotero.HTTP = new function() {
} }
// Send binary data // Send binary data
let body;
if (compressedBody) { if (compressedBody) {
let numBytes = compressedBody.length; let numBytes = compressedBody.length;
let ui8Data = new Uint8Array(numBytes); let ui8Data = new Uint8Array(numBytes);
for (let i = 0; i < numBytes; i++) { for (let i = 0; i < numBytes; i++) {
ui8Data[i] = compressedBody.charCodeAt(i) & 0xff; ui8Data[i] = compressedBody.charCodeAt(i) & 0xff;
} }
xmlhttp.send(ui8Data); body = ui8Data;
} }
// Send regular request // Send regular request
else { 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; 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 * 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; 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) { if (document) {
try { try {
await document.cleanup(); await document.cleanup();
if (!Zotero.Integration.currentSession._dontActivateDocument) { if (!Zotero.Integration.currentSession?._dontActivateDocument) {
await document.activate(); await document.activate();
} }
@ -898,7 +898,8 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu
// Perform noteType or fieldType conversion // Perform noteType or fieldType conversion
let fields = yield this._session.getFields(); 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 convertBibliographies = oldData.prefs.fieldType != this._session.data.prefs.fieldType;
var convertItems = convertBibliographies var convertItems = convertBibliographies
|| oldData.prefs.noteType != this._session.data.prefs.noteType; || 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._fields = null;
this._session.ignoreEmptyBibliography = true; 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); yield this._session.updateFromDocument(FORCE_CITATIONS_RESET_TEXT);
return this._session.updateDocument(FORCE_CITATIONS_RESET_TEXT, true, true); return this._session.updateDocument(FORCE_CITATIONS_RESET_TEXT, true, true);

View file

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

View file

@ -83,8 +83,8 @@ Zotero.RecognizeDocument = new function () {
throw new Error(); throw new Error();
} }
let res = await _processItem(item); let parentItem = await _processItem(item);
_progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, item.getField('title')); _progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, parentItem.getField('title'));
} }
catch (e) { catch (e) {
Zotero.logError(e); Zotero.logError(e);
@ -250,7 +250,7 @@ Zotero.RecognizeDocument = new function () {
/** /**
* Processes the item and places it as a children of the new item * Processes the item and places it as a children of the new item
* @param itemID * @param itemID
* @return {Promise} * @return {Promise} A promise that resolves to a newly created, recognized parent item
*/ */
async function _processItem(attachment) { async function _processItem(attachment) {
// Make sure the attachment still doesn't have a parent // Make sure the attachment still doesn't have a parent

View file

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

View file

@ -64,7 +64,7 @@ Zotero.Sync.Storage = new function () {
* @param {String} libraryKey * @param {String} libraryKey
* @param {Number|NULL} * @param {Number|NULL}
*/ */
this.setItemDownloadPercentage = function (libraryKey, percentage) { this.setItemDownloadPercentage = Zotero.Utilities.throttle(function (libraryKey, percentage) {
Zotero.debug("Setting image download percentage to " + percentage Zotero.debug("Setting image download percentage to " + percentage
+ " for item " + libraryKey); + " for item " + libraryKey);
@ -93,7 +93,7 @@ Zotero.Sync.Storage = new function () {
} }
Zotero.Notifier.trigger('redraw', 'item', parentItem.id, { column: "hasAttachment" }); Zotero.Notifier.trigger('redraw', 'item', parentItem.id, { column: "hasAttachment" });
} }
} }, 100);
function error(e) { 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 // full check of this library, check only files that were previously modified or opened
// recently // recently
else if (this.background 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.local.lastFullFileCheck[libraryID] && (this.local.lastFullFileCheck[libraryID]
+ (this.maxCheckAge * 1000)) > new Date().getTime()) { + (this.maxCheckAge * 1000)) > new Date().getTime()) {

View file

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

View file

@ -49,7 +49,7 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
* @param {Zotero.Sync.Storage.Request} request * @param {Zotero.Sync.Storage.Request} request
* @return {Promise<Zotero.Sync.Storage.Result>} * @return {Promise<Zotero.Sync.Storage.Result>}
*/ */
downloadFile: Zotero.Promise.coroutine(function* (request) { downloadFile: async function (request) {
var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request); var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request);
if (!item) { if (!item) {
throw new Error("Item '" + request.name + "' not found"); 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'); var destPath = OS.Path.join(Zotero.getTempDirectory().path, item.key + '.tmp');
// saveURI() below appears not to create empty files for Content-Length: 0, // Create an empty file to check file access
// so we create one here just in case, which also lets us check file access
try { try {
yield IOUtils.write(destPath, new Uint8Array()); await IOUtils.write(destPath, new Uint8Array());
} }
catch (e) { catch (e) {
Zotero.File.checkFileAccessError(e, destPath, 'create'); Zotero.File.checkFileAccessError(e, destPath, 'create');
} }
var deferred = Zotero.Promise.defer();
var requestData = {item}; 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 params = this._getRequestParams(item.libraryID, `items/${item.key}/file`);
var uri = this.apiClient.buildRequestURI(params); var uri = this.apiClient.buildRequestURI(params);
var headers = this.apiClient.getHeaders();
Zotero.debug('Saving ' + uri); return new Promise(async (resolve, reject) => {
const nsIWBP = Components.interfaces.nsIWebBrowserPersist; try {
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] let req = await Zotero.HTTP.download(
.createInstance(nsIWBP); uri,
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE; destPath,
wbp.progressListener = listener; {
Zotero.Utilities.Internal.saveURI(wbp, uri, destPath, headers); successCodes: [200, 404],
headers: this.apiClient.getHeaders(),
return deferred.promise; 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) { uploadFile: Zotero.Promise.coroutine(function* (request) {
@ -1076,7 +1028,9 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
var filename = item.attachmentFilename; var filename = item.attachmentFilename;
var fileSize = (await OS.File.stat(item.getFilePath())).size; 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( var e = new Zotero.Error(
text, text,

View file

@ -933,6 +933,22 @@ Zotero.Sync.Data.Local = {
['mtime', 'md5', 'dateAdded', 'dateModified'] ['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 no changes, just update local version number and mark as synced
if (!result.changes.length && !result.conflicts.length) { if (!result.changes.length && !result.conflicts.length) {
Zotero.debug("No remote changes to apply to local " Zotero.debug("No remote changes to apply to local "
@ -1542,6 +1558,24 @@ Zotero.Sync.Data.Local = {
Zotero.Libraries.get(obj.libraryID).storageDownloadNeeded = true; 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) { catch (e) {
// For now, allow sync to proceed after all errors // For now, allow sync to proceed after all errors

View file

@ -230,7 +230,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
} }
}.bind(this), }.bind(this),
background: !!options.background, background: !!options.background,
firstInSession: _firstInSession, firstInSession: options.firstInSession,
resetMode: options.resetMode 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; _syncInProgress = false;
yield this.checkErrors(_errors, options); await this.checkErrors(_errors, options);
if (!options.restartSync) { if (!options.restartSync) {
this.updateIcons(_errors); 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 = []; _errors = [];
}); };
/** /**
@ -1253,6 +1264,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
options.restartSync = true; options.restartSync = true;
}); });
e.dialogButtonImmediate = true;
} }
else { else {
// Note too long // 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 // Show warning for unknown data that couldn't be saved

View file

@ -468,6 +468,8 @@ Zotero.Utilities.Internal = {
* @param {Zotero.CookieSandbox} [cookieSandbox] * @param {Zotero.CookieSandbox} [cookieSandbox]
*/ */
saveURI: function (wbp, uri, target, headers, cookieSandbox) { saveURI: function (wbp, uri, target, headers, cookieSandbox) {
Zotero.warn("Zotero.Utilities.Internal.saveURI() is deprecated -- use Zotero.HTTP.download()");
// Handle gzip encoding // Handle gzip encoding
wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
// If not explicitly using cache, skip it // If not explicitly using cache, skip it
@ -938,11 +940,13 @@ Zotero.Utilities.Internal = {
} }
// Tags // Tags
for (let i = 0; i < item.tags.length; i++) { if (item.tags) {
if (!item.tags[i].type) { for (let i = 0; i < item.tags.length; i++) {
item.tags[i].type = 0; 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) // "related" was never used (array of itemIDs)
@ -2442,13 +2446,17 @@ Zotero.Utilities.Internal = {
let markup = this._titleMarkup[token]; let markup = this._titleMarkup[token];
if (markup.beginsTag) { if (markup.beginsTag) {
if (targetNode) { if (targetNode) {
let node = targetNode.ownerDocument.createElement(markup.beginsTag); let node;
if (markup.style) {
Object.assign(node.style, markup.style);
}
if (markup.inverseStyle && markupStack.some(otherMarkup => otherMarkup.beginsTag === markup.beginsTag)) { if (markup.inverseStyle && markupStack.some(otherMarkup => otherMarkup.beginsTag === markup.beginsTag)) {
node = targetNode.ownerDocument.createElement('span');
Object.assign(node.style, markup.inverseStyle); 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); nodeStack.push(node);
} }
markupStack.push({ ...markup, token }); markupStack.push({ ...markup, token });
@ -2461,12 +2469,14 @@ Zotero.Utilities.Internal = {
let discardedNode = nodeStack.pop(); let discardedNode = nodeStack.pop();
if (discardedMarkup.beginsTag === markup.endsTag) { if (discardedMarkup.beginsTag === markup.endsTag) {
nodeStack[nodeStack.length - 1].append(discardedNode); nodeStack[nodeStack.length - 1].append(discardedNode);
break;
} }
else { else {
nodeStack[nodeStack.length - 1].append(discardedMarkup.token, ...discardedNode.childNodes); nodeStack[nodeStack.length - 1].append(discardedMarkup.token, ...discardedNode.childNodes);
} }
} }
if (discardedMarkup.beginsTag === markup.endsTag) {
break;
}
} }
continue; continue;
@ -2906,6 +2916,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 * Base64 encode / decode
* From http://www.webtoolkit.info/ * From http://www.webtoolkit.info/
@ -3048,9 +3137,18 @@ Zotero.Utilities.Internal.OpenURL = {
); );
if (co) { if (co) {
let base = Zotero.Prefs.get("openURL.resolver"); let base = Zotero.Prefs.get("openURL.resolver");
// Add & if there's already a ? if (!base.endsWith('?')) {
let splice = base.indexOf("?") == -1 ? "?" : "&"; if (base.includes('login?url=')) {
return base + splice + co; base += '?';
}
else if (base.includes('?')) {
base += '&';
}
else {
base += '?';
}
}
return base + co;
} }
return false; return false;
}, },

View file

@ -996,6 +996,9 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
*/ */
this.launchFile = function (file) { this.launchFile = function (file) {
file = Zotero.File.pathToFile(file); file = Zotero.File.pathToFile(file);
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
try { try {
Zotero.debug("Launching " + file.path); Zotero.debug("Launching " + file.path);
file.launch(); 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]; args = [filePath];
} }
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
// Async, but we don't want to block // 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) { if (!found.value) {
throw new Error(`Handler not found for '${scheme}' URLs`); 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; return;
} }
} }
var mozCleared = false;
try { try {
if (!Zotero.isWin) {
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
mozCleared = true;
}
var uri = Services.io.newURI(url, null, null); var uri = Services.io.newURI(url, null, null);
var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1'] var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1']
.getService(Components.interfaces.nsIExternalProtocolService) .getService(Components.interfaces.nsIExternalProtocolService)
@ -1123,6 +1150,11 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
+ "check extensions.zotero." + pref + " in about:config"); + "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"] var proc = Components.classes["@mozilla.org/process/util;1"]
.createInstance(Components.interfaces.nsIProcess); .createInstance(Components.interfaces.nsIProcess);
proc.init(exec); proc.init(exec);
@ -1130,6 +1162,11 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
var args = [url]; var args = [url];
proc.runw(false, args, args.length); proc.runw(false, args, args.length);
} }
finally {
if (mozCleared) {
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
}
}
} }
@ -1721,9 +1758,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
return Zotero.Tags.purge(); return Zotero.Tags.purge();
}); });
yield Zotero.Fulltext.purgeUnusedWords(); yield Zotero.Fulltext.purgeUnusedWords();
yield Zotero.DB.executeTransaction(async function () { yield Zotero.Items.purge();
return Zotero.Items.purge();
});
// DEBUG: this might not need to be permanent // DEBUG: this might not need to be permanent
//yield Zotero.DB.executeTransaction(async function () { //yield Zotero.DB.executeTransaction(async function () {
// return Zotero.Relations.purge(); // return Zotero.Relations.purge();

View file

@ -144,7 +144,6 @@ const xpcomFilesLocal = [
'storage/storageRequest', 'storage/storageRequest',
'storage/storageResult', 'storage/storageResult',
'storage/storageUtilities', 'storage/storageUtilities',
'storage/streamListener',
'storage/zfs', 'storage/zfs',
'storage/webdav', 'storage/webdav',
'syncedSettings', 'syncedSettings',

View file

@ -50,8 +50,6 @@ var ZoteroPane = new function()
this.handleKeyPress = handleKeyPress; this.handleKeyPress = handleKeyPress;
this.getSelectedCollection = getSelectedCollection; this.getSelectedCollection = getSelectedCollection;
this.getSelectedSavedSearch = getSelectedSavedSearch; this.getSelectedSavedSearch = getSelectedSavedSearch;
this.getSelectedItems = getSelectedItems;
this.getSortedItems = getSortedItems;
this.getSortField = getSortField; this.getSortField = getSortField;
this.getSortDirection = getSortDirection; this.getSortDirection = getSortDirection;
this.setItemsPaneMessage = setItemsPaneMessage; this.setItemsPaneMessage = setItemsPaneMessage;
@ -1936,7 +1934,7 @@ var ZoteroPane = new function()
// Tab selection observer in standalone.js makes sure that // Tab selection observer in standalone.js makes sure that
// updateQuickCopyCommands is called // updateQuickCopyCommands is called
if (Zotero_Tabs.selectedID == 'zotero-pane') { if (Zotero_Tabs.selectedType == 'library') {
this.updateQuickCopyCommands(selectedItems); this.updateQuickCopyCommands(selectedItems);
} }
@ -2654,21 +2652,10 @@ var ZoteroPane = new function()
this.copySelectedItemsToClipboard = function (asCitations) { this.copySelectedItemsToClipboard = function (asCitations) {
var items = []; var items = [];
if (Zotero_Tabs.selectedID == 'zotero-pane') { let itemIDs = this.getSelectedItems(true);
let itemIDs = this.getSelectedItems(true); // Get selected item IDs in the item tree order
// Get selected item IDs in the item tree order itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id));
itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id)); items = Zotero.Items.get(itemIDs);
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];
}
}
}
if (!items.length) { if (!items.length) {
return; return;
@ -3070,14 +3057,28 @@ var ZoteroPane = new function()
* *
* If asIDs is true, return an array of itemIDs instead * If asIDs is true, return an array of itemIDs instead
*/ */
function getSelectedItems(asIDs) this.getSelectedItems = function (asIDs) {
{ switch (Zotero_Tabs.selectedType) {
if (!this.itemsView) { case 'library':
return []; 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 +3086,18 @@ var ZoteroPane = new function()
* *
* If asIDs is true, return an array of itemIDs instead * If asIDs is true, return an array of itemIDs instead
*/ */
function getSortedItems(asIDs) { this.getSortedItems = function (asIDs) {
if (!this.itemsView) { switch (Zotero_Tabs.selectedType) {
return []; 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() { function getSortField() {
@ -4510,22 +4516,14 @@ var ZoteroPane = new function()
/** /**
* @param {Boolean} [link] * @param {Boolean} [link]
* @param {Number} [parentItemID] * @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>} * @returns {Promise<Zotero.Item[] | null>}
*/ */
this.addAttachmentFromDialog = async function (link, parentItemID, files = null) { this.addAttachmentFromDialog = async function (link, parentItemID, files = null) {
if (!this.canEdit()) { var libraryID;
this.displayCannotEditLibraryMessage(); if (Zotero_Tabs.selectedType === 'library') {
return null; let collectionTreeRow = this.getCollectionTreeRow();
} if (link && collectionTreeRow.isPublications()) {
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()) {
Zotero.alert( Zotero.alert(
null, null,
Zotero.getString('general.error'), Zotero.getString('general.error'),
@ -4533,15 +4531,25 @@ var ZoteroPane = new function()
); );
return null; return null;
} }
libraryID = collectionTreeRow.ref.libraryID;
}
else {
libraryID = Zotero.Items.get(parentItemID).libraryID;
} }
if (!this.canEdit()) {
this.displayCannotEditLibraryMessage();
return null;
}
// TODO: disable in menu // TODO: disable in menu
if (!this.canEditFiles()) { if (!this.canEditFiles()) {
this.displayCannotEditLibraryFilesMessage(); 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) { if (!files) {
var fp = new FilePicker(); var fp = new FilePicker();
@ -5369,36 +5377,48 @@ var ZoteroPane = new function()
/** /**
* Test if the user can edit the currently selected view * Test if the user can edit the currently selected view
* *
* @param {Integer} [row] * @param {Integer} [row] Row index - ignored if not in library tab
* * @return {Boolean} TRUE if user can edit, FALSE if not
* @return {Boolean} TRUE if user can edit, FALSE if not
*/ */
this.canEdit = function (row) { this.canEdit = function (row) {
// Currently selected row switch (Zotero_Tabs.selectedType) {
if (row === undefined) { case 'library':
row = this.collectionsView.selection.focused; // 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 * Test if the user can edit the parent library of the selected view
* *
* @param {Integer} [row] * @param {Integer} [row] Row index - ignored if not in library tab
* @return {Boolean} TRUE if user can edit, FALSE if not * @return {Boolean} TRUE if user can edit, FALSE if not
*/ */
this.canEditLibrary = function (row) { this.canEditLibrary = function (row) {
// Currently selected row switch (Zotero_Tabs.selectedType) {
if (row === undefined) { case 'library': // Currently selected row
row = this.collectionsView.selection.focused; 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 +5429,24 @@ var ZoteroPane = new function()
* @return {Boolean} TRUE if user can edit, FALSE if not * @return {Boolean} TRUE if user can edit, FALSE if not
*/ */
this.canEditFiles = function (row) { this.canEditFiles = function (row) {
// Currently selected row switch (Zotero_Tabs.selectedType) {
if (row === undefined) { case 'library':
row = this.collectionsView.selection.focused; // 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 () { this.displayCannotEditLibraryMessage = function () {
@ -6814,13 +6844,14 @@ var ZoteroPane = new function()
}; };
this.buildFieldTransformMenu = function ({ target, onTransform }) { this.buildFieldTransformMenu = function ({ target, onTransform }) {
let doc = target.ownerDocument;
let value = target.value; let value = target.value;
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true); let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
let valueSentenceCased = Zotero.Utilities.sentenceCase(value); 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.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
titleCase.addEventListener('command', () => { titleCase.addEventListener('command', () => {
onTransform(valueTitleCased); onTransform(valueTitleCased);
@ -6828,7 +6859,7 @@ var ZoteroPane = new function()
titleCase.disabled = valueTitleCased == value; titleCase.disabled = valueTitleCased == value;
menupopup.append(titleCase); menupopup.append(titleCase);
let sentenceCase = document.createXULElement('menuitem'); let sentenceCase = doc.createXULElement('menuitem');
sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase')); sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
sentenceCase.addEventListener('command', () => { sentenceCase.addEventListener('command', () => {
onTransform(valueSentenceCased); onTransform(valueSentenceCased);

View file

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

View file

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

View file

@ -48,18 +48,18 @@ pdfReader-import-from-epub =
.label = Tuo e-kirjan huomautukset... .label = Tuo e-kirjan huomautukset...
pdfReader-import-from-epub-prompt-title = Tuo e-kirjan huomautukset pdfReader-import-from-epub-prompt-title = Tuo e-kirjan huomautukset
pdfReader-import-from-epub-prompt-text = pdfReader-import-from-epub-prompt-text =
{ -app-name } found { $count -> { -app-name } löysi { $count ->
[1] { $count } { $tool } annotation [1] { $count } { $tool } huomautuksen
*[other] { $count } { $tool } annotations *[other] { $count } { $tool } huomautusta
}, last edited { $lastModifiedRelative }. }, 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 = 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 = 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. Jos tämän e-kirjan huomautukset on tehty KOReaderilla, koita valita “metadata.epub.lua”-tiedosto suoraan.
pdfReader-import-from-epub-select-other = Select Other File… pdfReader-import-from-epub-select-other = Valitse toinen tiedosto…

View file

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

View file

@ -1,96 +1,96 @@
<!ENTITY preferencesCmdMac.label "Preferences..."> <!ENTITY preferencesCmdMac.label "Postavke…">
<!ENTITY preferencesCmdMac.commandkey ","> <!ENTITY preferencesCmdMac.commandkey ",">
<!ENTITY servicesMenuMac.label "Services"> <!ENTITY servicesMenuMac.label "Servisi">
<!ENTITY hideThisAppCmdMac.label "Hide &brandShortName;"> <!ENTITY hideThisAppCmdMac.label "Sakrij &brandShortName;">
<!ENTITY hideThisAppCmdMac.commandkey "H"> <!ENTITY hideThisAppCmdMac.commandkey "H">
<!ENTITY hideOtherAppsCmdMac.label "Hide Others"> <!ENTITY hideOtherAppsCmdMac.label "Sakrij ostalo">
<!ENTITY hideOtherAppsCmdMac.commandkey "H"> <!ENTITY hideOtherAppsCmdMac.commandkey "H">
<!ENTITY showAllAppsCmdMac.label "Show All"> <!ENTITY showAllAppsCmdMac.label "Prikaži sve">
<!ENTITY quitApplicationCmdMac.label "Quit Zotero"> <!ENTITY quitApplicationCmdMac.label "Prekini Zotero">
<!ENTITY quitApplicationCmdMac.key "Q"> <!ENTITY quitApplicationCmdMac.key "Q">
<!ENTITY fileMenu.label "File"> <!ENTITY fileMenu.label "Datoteka">
<!ENTITY fileMenu.accesskey "F"> <!ENTITY fileMenu.accesskey "F">
<!ENTITY saveCmd.label "Save…"> <!ENTITY saveCmd.label "Spremi…">
<!ENTITY saveCmd.key "S"> <!ENTITY saveCmd.key "S">
<!ENTITY saveCmd.accesskey "A"> <!ENTITY saveCmd.accesskey "A">
<!ENTITY pageSetupCmd.label "Page Setup…"> <!ENTITY pageSetupCmd.label "Postava stranice…">
<!ENTITY pageSetupCmd.accesskey "U"> <!ENTITY pageSetupCmd.accesskey "U">
<!ENTITY printCmd.label "Print…"> <!ENTITY printCmd.label "Printaj…">
<!ENTITY printCmd.key "P"> <!ENTITY printCmd.key "P">
<!ENTITY printCmd.accesskey "P"> <!ENTITY printCmd.accesskey "P">
<!ENTITY closeCmd.label "Close"> <!ENTITY closeCmd.label "Zatvori">
<!ENTITY closeCmd.key "W"> <!ENTITY closeCmd.key "W">
<!ENTITY closeCmd.accesskey "C"> <!ENTITY closeCmd.accesskey "C">
<!ENTITY manageAttachments.label "Manage Attachments"> <!ENTITY manageAttachments.label "Upravljanje prilozima">
<!ENTITY convertToStored.label "Convert Linked Files to Stored Files…"> <!ENTITY convertToStored.label "Pretvori povezane u pohranjene datoteke…">
<!ENTITY importCmd.label "Import..."> <!ENTITY importCmd.label "Uvoz…">
<!ENTITY importCmd.key "I"> <!ENTITY importCmd.key "I">
<!ENTITY importFromClipboardCmd.label "Import from Clipboard"> <!ENTITY importFromClipboardCmd.label "Uvoz iz međuspremnika">
<!ENTITY quitApplicationCmdWin.label "Exit"> <!ENTITY quitApplicationCmdWin.label "Izlaz">
<!ENTITY quitApplicationCmdWin.accesskey "x"> <!ENTITY quitApplicationCmdWin.accesskey "x">
<!ENTITY quitApplicationCmd.label "Quit"> <!ENTITY quitApplicationCmd.label "Prekid">
<!ENTITY quitApplicationCmd.accesskey "Q"> <!ENTITY quitApplicationCmd.accesskey "Q">
<!ENTITY copyCitationCmd.label "Copy Citation"> <!ENTITY copyCitationCmd.label "Kopiraj citat">
<!ENTITY copyBibliographyCmd.label "Copy Bibliography"> <!ENTITY copyBibliographyCmd.label "Kopiraj biblioteku">
<!ENTITY copyNoteCmd.label "Copy Note"> <!ENTITY copyNoteCmd.label "Kopiraj bilješku">
<!ENTITY bidiSwitchPageDirectionItem.label "Switch Page Direction"> <!ENTITY bidiSwitchPageDirectionItem.label "Promjena smjera stranice">
<!ENTITY bidiSwitchPageDirectionItem.accesskey "g"> <!ENTITY bidiSwitchPageDirectionItem.accesskey "g">
<!ENTITY bidiSwitchTextDirectionItem.label "Switch Text Direction"> <!ENTITY bidiSwitchTextDirectionItem.label "Promjena smjera teksta">
<!ENTITY bidiSwitchTextDirectionItem.accesskey "w"> <!ENTITY bidiSwitchTextDirectionItem.accesskey "w">
<!ENTITY bidiSwitchTextDirectionItem.commandkey "X"> <!ENTITY bidiSwitchTextDirectionItem.commandkey "X">
<!ENTITY goMenu.label "Go"> <!ENTITY goMenu.label "Idi">
<!ENTITY goMenu.accesskey "G"> <!ENTITY goMenu.accesskey "G">
<!ENTITY viewMenu.label "View"> <!ENTITY viewMenu.label "Pogled">
<!ENTITY viewMenu.accesskey "V"> <!ENTITY viewMenu.accesskey "V">
<!ENTITY layout.label "Layout"> <!ENTITY layout.label "Izgled">
<!ENTITY standardView.label "Standard View"> <!ENTITY standardView.label "Standardni pogled">
<!ENTITY stackedView.label "Stacked View"> <!ENTITY stackedView.label "Složeni prikaz">
<!ENTITY collectionsPane.label "Collections Pane"> <!ENTITY collectionsPane.label "Okno zbirke">
<!ENTITY itemPane.label "Item Pane"> <!ENTITY itemPane.label "Okno stavke">
<!ENTITY tagSelector.label "Tag Selector"> <!ENTITY tagSelector.label "Odabir oznaka">
<!ENTITY recursiveCollections.label "Show Items from Subcollections"> <!ENTITY recursiveCollections.label "Prikaži stavke iz podzbirki">
<!ENTITY fontSize.label "Font Size"> <!ENTITY fontSize.label "Veličina slova">
<!ENTITY noteFontSize.label "Note Font Size"> <!ENTITY noteFontSize.label "Veličina slova bilješke">
<!ENTITY columns.label "Columns"> <!ENTITY columns.label "Stupac">
<!ENTITY sortBy.label "Sort By"> <!ENTITY sortBy.label "Sortiraj po">
<!ENTITY toolsMenu.label "Tools"> <!ENTITY toolsMenu.label "Alati">
<!ENTITY toolsMenu.accesskey "T"> <!ENTITY toolsMenu.accesskey "T">
<!ENTITY installConnector.label "Install Browser Connector"> <!ENTITY installConnector.label "Instalirajte Browser Connector…">
<!ENTITY installConnector.accesskey "C"> <!ENTITY installConnector.accesskey "C">
<!ENTITY addons.label "Add-ons"> <!ENTITY addons.label "Dodaci">
<!ENTITY developer.label "Developer"> <!ENTITY developer.label "Programer">
<!ENTITY bringAllToFront.label "Bring All to Front"> <!ENTITY bringAllToFront.label "Dovedite sve naprijed">
<!ENTITY helpMenu.label "Help"> <!ENTITY helpMenu.label "Pomoć">
<!ENTITY helpMenu.accesskey "H"> <!ENTITY helpMenu.accesskey "H">
<!ENTITY helpMenuWin.label "Help"> <!ENTITY helpMenuWin.label "Pomoć">
<!ENTITY helpMenuWin.accesskey "H"> <!ENTITY helpMenuWin.accesskey "H">
<!ENTITY helpMac.commandkey "?"> <!ENTITY helpMac.commandkey "?">
<!ENTITY aboutProduct.label "About &brandShortName;"> <!ENTITY aboutProduct.label "O &brandShortName;">
<!ENTITY aboutProduct.accesskey "A"> <!ENTITY aboutProduct.accesskey "A">
<!ENTITY productHelp.label "Support and Documentation"> <!ENTITY productHelp.label "Potpora i dokumentacija">
<!ENTITY productHelp.accesskey "D"> <!ENTITY productHelp.accesskey "D">
<!ENTITY helpTroubleshootingInfo.label "Troubleshooting Information"> <!ENTITY helpTroubleshootingInfo.label "Informacije o rješavanju problema">
<!ENTITY helpTroubleshootingInfo.accesskey "T"> <!ENTITY helpTroubleshootingInfo.accesskey "T">
<!ENTITY helpFeedbackPage.label "Discussion Forums"> <!ENTITY helpFeedbackPage.label "Forumi za raspravu">
<!ENTITY helpFeedbackPage.accesskey "F"> <!ENTITY helpFeedbackPage.accesskey "F">
<!ENTITY helpReportErrors.label "Report Errors..."> <!ENTITY helpReportErrors.label "Prijavi pogreške…">
<!ENTITY helpReportErrors.accesskey "R"> <!ENTITY helpReportErrors.accesskey "R">
<!ENTITY debugOutputLogging.label "Debug Output Logging"> <!ENTITY debugOutputLogging.label "Zapisivanje izlaza otklanjanja pogrešaka">
<!ENTITY debugOutputLogging.accesskey "L"> <!ENTITY debugOutputLogging.accesskey "L">
<!ENTITY debugOutputLogging.submit "Submit Output"> <!ENTITY debugOutputLogging.submit "Pošalji izlaz">
<!ENTITY debugOutputLogging.view "View Output"> <!ENTITY debugOutputLogging.view "Pregled izlaza">
<!ENTITY debugOutputLogging.clear "Clear Output"> <!ENTITY debugOutputLogging.clear "Očisti izlaz">
<!ENTITY debugOutputLogging.restartWithLoggingEnabled "Restart with Logging Enabled…"> <!ENTITY debugOutputLogging.restartWithLoggingEnabled "Ponovo pokreni s omogućenim zapisivanjem…">
<!ENTITY helpCheckForUpdates.label "Check for Updates…"> <!ENTITY helpCheckForUpdates.label "Provjera nadopuna…">
<!ENTITY helpCheckForUpdates.accesskey "U"> <!ENTITY helpCheckForUpdates.accesskey "U">

View file

@ -74,9 +74,9 @@ preferences-advanced-language-and-region-title = Kalba ir regonas
preferences-advanced-enable-bidi-ui = preferences-advanced-enable-bidi-ui =
.label = Įgalinti abikrypčius teksto redagavimo įrankius .label = Įgalinti abikrypčius teksto redagavimo įrankius
preferences-advanced-reset-data-dir = preferences-advanced-reset-data-dir =
.label = Revert to Default Location… .label = Atstatyti numatytąją vietą…
preferences-advanced-custom-data-dir = preferences-advanced-custom-data-dir =
.label = Use Custom Location… .label = Naudoti savitą vietą...
preferences-advanced-default-data-dir = preferences-advanced-default-data-dir =
.value = (Default: { $directory }) .value = (Numatytoji: { $directory })
.aria-label = Default location .aria-label = Numatytoji vieta

View file

@ -5,8 +5,8 @@ pdfReader-selectArea = Pažymėti sritį
pdfReader-draw = Piešti pdfReader-draw = Piešti
pdfReader-highlightAnnotation = Paryškinimas pdfReader-highlightAnnotation = Paryškinimas
pdfReader-underlineAnnotation = Pabraukimas pdfReader-underlineAnnotation = Pabraukimas
pdfReader-noteAnnotation = Note Annotation pdfReader-noteAnnotation = Pastaba
pdfReader-textAnnotation = Text Annotation pdfReader-textAnnotation = Tekstinė anotacija
pdfReader-imageAnnotation = Image Annotation pdfReader-imageAnnotation = Image Annotation
pdfReader-find-in-document = Paieška dokumente pdfReader-find-in-document = Paieška dokumente
pdfReader-move-annotation-start-key = pdfReader-move-annotation-start-key =
@ -14,13 +14,13 @@ pdfReader-move-annotation-start-key =
[macos] { general-key-command } [macos] { general-key-command }
*[other] { general-key-alt } *[other] { general-key-alt }
} }
pdfReader-a11yMoveAnnotation = Use the arrow keys to move the annotation. pdfReader-a11yMoveAnnotation = Spausdami rodyklių klavišus galite pastumti anotaciją.
pdfReader-a11yEditTextAnnotation = To move the end of the text annotation, hold { general-key-shift } and use the left/right arrow keys. To move the start of the annotation, hold { general-key-shift }-{ pdfReader-move-annotation-start-key } and use the arrow keys. pdfReader-a11yEditTextAnnotation = Norėdami pakeisti tekstinės anotacijos plotį pagal dešinįjį kraštą (pabaigą), laikykite { general-key-shift } ir spauskite rodyklių klavišus kairėn arba dešinėn. Norėdami pakeisti tekstinės anotacijos plotį pagal kairįjį kraštą (pradžią), laikykite { general-key-shift }-{ pdfReader-move-annotation-start-key } ir spauskite rodyklių klavišus.
pdfReader-a11yResizeAnnotation = To resize the annotation, hold { general-key-shift } and use the arrow keys. pdfReader-a11yResizeAnnotation = Norėdami pakeisti tekstinės anotacijos dydį, laikykite { general-key-shift } ir spauskite rodyklių klavišus aukštyn arba žemyn.
pdfReader-a11yAnnotationPopupAppeared = Use Tab to navigate the annotation popup. pdfReader-a11yAnnotationPopupAppeared = Use Tab to navigate the annotation popup.
pdfReader-a11yAnnotationCreated = { $type } created. pdfReader-a11yAnnotationCreated = { $type } sukurtas.
pdfReader-a11yAnnotationSelected = { $type } selected. pdfReader-a11yAnnotationSelected = { $type } pasirinktas.
-pdfReader-a11yTextualAnnotationInstruction = To annotate text via the keyboard, first use “{ pdfReader-find-in-document }” to locate the phrase, and then press { general-key-control }-{ option-or-alt }-{ $number } to turn the search result into an annotation. -pdfReader-a11yTextualAnnotationInstruction = Norėdami sukurti tekstinę anotaciją naudodamiesi klaviatūra, pirmiausia raskite tekstą naudodamiesi „{ pdfReader-find-in-document }“, tuomet spauskite { general-key-control }-{ option-or-alt }-{ $number } pažymėtam tekstui paversti anotacija.
-pdfReader-a11yAnnotationInstruction = To add this annotation into the document, focus the document and press { general-key-control }-{ option-or-alt }-{ $number }. -pdfReader-a11yAnnotationInstruction = To add this annotation into the document, focus the document and press { general-key-control }-{ option-or-alt }-{ $number }.
pdfReader-toolbar-highlight = pdfReader-toolbar-highlight =
.aria-description = { -pdfReader-a11yTextualAnnotationInstruction(number: 1) } .aria-description = { -pdfReader-a11yTextualAnnotationInstruction(number: 1) }
@ -38,15 +38,15 @@ pdfReader-toolbar-area =
.aria-description = { -pdfReader-a11yAnnotationInstruction(number: 5) } .aria-description = { -pdfReader-a11yAnnotationInstruction(number: 5) }
.title = { pdfReader-selectArea } .title = { pdfReader-selectArea }
pdfReader-toolbar-draw = pdfReader-toolbar-draw =
.aria-description = This annotation type cannot be created via the keyboard. .aria-description = Šio anotacijų tipo negalite sukurti naudodamiesi klaviatūra.
.title = { pdfReader-draw } .title = { pdfReader-draw }
pdfReader-findInDocumentInput = pdfReader-findInDocumentInput =
.title = Ieškoti .title = Ieškoti
.placeholder = { pdfReader-find-in-document } .placeholder = { pdfReader-find-in-document }
.aria-description = To turn a search result into a highlight annotation, press { general-key-control }-{ option-or-alt }-1. To turn a search result into an underline annotation, press { general-key-control }-{ option-or-alt }-2. .aria-description = To turn a search result into a highlight annotation, press { general-key-control }-{ option-or-alt }-1. To turn a search result into an underline annotation, press { general-key-control }-{ option-or-alt }-2.
pdfReader-import-from-epub = pdfReader-import-from-epub =
.label = Import Ebook Annotations… .label = Importuoti el. knygos anotacijas…
pdfReader-import-from-epub-prompt-title = Import Ebook Annotations pdfReader-import-from-epub-prompt-title = Importuoti el. knygos anotacijas
pdfReader-import-from-epub-prompt-text = pdfReader-import-from-epub-prompt-text =
{ -app-name } found { $count -> { -app-name } found { $count ->
[1] { $count } { $tool } annotation [1] { $count } { $tool } annotation
@ -62,4 +62,4 @@ pdfReader-import-from-epub-no-annotations-other-file =
“{ $filename }” does not appear to contain any Calibre or KOReader annotations. “{ $filename }” does not appear to contain any Calibre or KOReader annotations.
If this ebook has been annotated with KOReader, try selecting a “metadata.epub.lua” file directly. 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… pdfReader-import-from-epub-select-other = Pasirinkti kitą rinkmeną…

View file

@ -14,7 +14,7 @@
<!ENTITY saveCmd.label "Įrašyti..."> <!ENTITY saveCmd.label "Įrašyti...">
<!ENTITY saveCmd.key "S"> <!ENTITY saveCmd.key "S">
<!ENTITY saveCmd.accesskey "r"> <!ENTITY saveCmd.accesskey "r">
<!ENTITY pageSetupCmd.label "Puslapio nuostatos..."> <!ENTITY pageSetupCmd.label "Puslapio nuostatos…">
<!ENTITY pageSetupCmd.accesskey "n"> <!ENTITY pageSetupCmd.accesskey "n">
<!ENTITY printCmd.label "Spausdinti..."> <!ENTITY printCmd.label "Spausdinti...">
<!ENTITY printCmd.key "P"> <!ENTITY printCmd.key "P">
@ -23,8 +23,8 @@
<!ENTITY closeCmd.key "W"> <!ENTITY closeCmd.key "W">
<!ENTITY closeCmd.accesskey "U"> <!ENTITY closeCmd.accesskey "U">
<!ENTITY manageAttachments.label "Tvarkyti pridėtas rinkmenas"> <!ENTITY manageAttachments.label "Tvarkyti pridėtas rinkmenas">
<!ENTITY convertToStored.label "Nuorodas pakeisti tikromis rinkmenomis..."> <!ENTITY convertToStored.label "Nuorodas pakeisti tikromis rinkmenomis…">
<!ENTITY importCmd.label "Importuoti..."> <!ENTITY importCmd.label "Importuoti…">
<!ENTITY importCmd.key "I"> <!ENTITY importCmd.key "I">
<!ENTITY importFromClipboardCmd.label "Importuoti iškarpinės turinį"> <!ENTITY importFromClipboardCmd.label "Importuoti iškarpinės turinį">
<!ENTITY quitApplicationCmdWin.label "Išeiti"> <!ENTITY quitApplicationCmdWin.label "Išeiti">
@ -61,7 +61,7 @@
<!ENTITY toolsMenu.label "Priemonės"> <!ENTITY toolsMenu.label "Priemonės">
<!ENTITY toolsMenu.accesskey "P"> <!ENTITY toolsMenu.accesskey "P">
<!ENTITY installConnector.label "Install Browser Connector…"> <!ENTITY installConnector.label "Įdiegti papildinį naršyklei…">
<!ENTITY installConnector.accesskey "C"> <!ENTITY installConnector.accesskey "C">
<!ENTITY addons.label "Priedai"> <!ENTITY addons.label "Priedai">
<!ENTITY developer.label "Programuotojams"> <!ENTITY developer.label "Programuotojams">
@ -82,7 +82,7 @@
<!ENTITY helpFeedbackPage.label "Diskusijų forumai"> <!ENTITY helpFeedbackPage.label "Diskusijų forumai">
<!ENTITY helpFeedbackPage.accesskey "I"> <!ENTITY helpFeedbackPage.accesskey "I">
<!ENTITY helpReportErrors.label "Pranešti apie klaidas..."> <!ENTITY helpReportErrors.label "Pranešti apie klaidas…">
<!ENTITY helpReportErrors.accesskey "r"> <!ENTITY helpReportErrors.accesskey "r">
<!ENTITY debugOutputLogging.label "Derinimo pranešimų išvedimas"> <!ENTITY debugOutputLogging.label "Derinimo pranešimų išvedimas">
@ -90,7 +90,7 @@
<!ENTITY debugOutputLogging.submit "Siųsti išvedimą"> <!ENTITY debugOutputLogging.submit "Siųsti išvedimą">
<!ENTITY debugOutputLogging.view "Rodyti išvedimą"> <!ENTITY debugOutputLogging.view "Rodyti išvedimą">
<!ENTITY debugOutputLogging.clear "Išvalyti išvedimą"> <!ENTITY debugOutputLogging.clear "Išvalyti išvedimą">
<!ENTITY debugOutputLogging.restartWithLoggingEnabled "Paleisti iš naujo įgalinus derinimo pranešimus..."> <!ENTITY debugOutputLogging.restartWithLoggingEnabled "Paleisti iš naujo įgalinus derinimo pranešimus…">
<!ENTITY helpCheckForUpdates.label "Tikrinti, ar yra atnaujinimų..."> <!ENTITY helpCheckForUpdates.label "Tikrinti, ar yra atnaujinimų…">
<!ENTITY helpCheckForUpdates.accesskey "T"> <!ENTITY helpCheckForUpdates.accesskey "T">

View file

@ -1,7 +1,7 @@
general-key-control = Control general-key-control = Vald
general-key-shift = Shift general-key-shift = Lyg2
general-key-alt = Alt general-key-alt = Alt
general-key-option = Option general-key-option = Parinktis
general-key-command = Command general-key-command = Command
option-or-alt = option-or-alt =
{ PLATFORM() -> { PLATFORM() ->
@ -24,7 +24,7 @@ general-help = Žinynas
general-tag = Gairė general-tag = Gairė
general-done = Atlikta general-done = Atlikta
general-view-troubleshooting-instructions = Peržiūrėti nesklandumų sprendimo instrukcijas general-view-troubleshooting-instructions = Peržiūrėti nesklandumų sprendimo instrukcijas
general-go-back = Go Back general-go-back = Atgal
citation-style-label = Citavimo stilius: citation-style-label = Citavimo stilius:
language-label = Kalba: language-label = Kalba:
menu-file-show-in-finder = menu-file-show-in-finder =
@ -66,9 +66,9 @@ menu-deletePermanently =
menu-tools-plugins = menu-tools-plugins =
.label = Papildiniai .label = Papildiniai
menu-view-columns-move-left = menu-view-columns-move-left =
.label = Move Column Left .label = Perkelti stulpelį kairėn
menu-view-columns-move-right = menu-view-columns-move-right =
.label = Move Column Right .label = Perkelti stulpelį dešinėn
main-window-command = main-window-command =
.label = Library .label = Library
main-window-key = main-window-key =
@ -91,9 +91,9 @@ collections-menu-rename-collection =
collections-menu-edit-saved-search = collections-menu-edit-saved-search =
.label = Taisyti įsimintąją paiešką .label = Taisyti įsimintąją paiešką
collections-menu-move-collection = collections-menu-move-collection =
.label = Move To .label = Perkelti į
collections-menu-copy-collection = collections-menu-copy-collection =
.label = Copy To .label = Kopijuoti į
item-creator-moveDown = item-creator-moveDown =
.label = Nuleisti žemyn .label = Nuleisti žemyn
item-creator-moveToTop = item-creator-moveToTop =
@ -129,7 +129,7 @@ item-menu-add-linked-file =
item-menu-add-url = item-menu-add-url =
.label = Tinklalapio nuoroda .label = Tinklalapio nuoroda
item-menu-change-parent-item = item-menu-change-parent-item =
.label = Change Parent Item… .label = Priskirti kitam aukštesniam įrašui…
view-online = Atverti tinklalapį view-online = Atverti tinklalapį
item-menu-option-view-online = item-menu-option-view-online =
.label = { view-online } .label = { view-online }
@ -286,7 +286,7 @@ integration-editBibliography-add-button =
integration-editBibliography-remove-button = integration-editBibliography-remove-button =
.aria-label = { general-remove } .aria-label = { general-remove }
integration-editBibliography-editor = integration-editBibliography-editor =
.aria-label = Edit reference .aria-label = Keisti nuorodą
-integration-editBibliography-include-uncited = To include an uncited item in your bibliography, select it from the items list and press { general-add }. -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-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-edit-reference = To change how a reference is formatted, use the text editor.
@ -304,7 +304,7 @@ styleEditor-locatorInput = Locator input
styleEditor-citationStyle = { citation-style-label } styleEditor-citationStyle = { citation-style-label }
styleEditor-locale = { language-label } styleEditor-locale = { language-label }
styleEditor-editor = styleEditor-editor =
.aria-label = Style editor .aria-label = Stilių rengyklė
styleEditor-preview = styleEditor-preview =
.aria-label = Peržiūra .aria-label = Peržiūra
integration-prefs-displayAs-label = Citavimo rodymo pavidalas: integration-prefs-displayAs-label = Citavimo rodymo pavidalas:
@ -620,9 +620,11 @@ item-pane-message-unselected =
} }
item-pane-message-objects-unselected = item-pane-message-objects-unselected =
{ $count -> { $count ->
[0] No objects in this view [0] Šiame rodinyje objektų nėra
[one] { $count } object in this view [one] { $count } objektas šiame rodinyje
*[other] { $count } objects in this view [few] { $count } objektai šiame rodinyje
[many] { $count } objektų šiame rodinyje
*[other] { $count } objektas šiame rodinyje
} }
item-pane-duplicates-merge-items = item-pane-duplicates-merge-items =
.label = .label =
@ -696,9 +698,9 @@ post-upgrade-done =
.label = { general-done } .label = { general-done }
text-action-paste-and-search = text-action-paste-and-search =
.label = Įdėti ir ieškoti .label = Įdėti ir ieškoti
mac-word-plugin-install-message = Zotero needs access to Word data to install the Word plugin. mac-word-plugin-install-message = Zotero turi galėti prieiti prie „Word“ duomenų tam, kad galėtų įdiegti papildinį „Word“ programai.
mac-word-plugin-install-action-button = mac-word-plugin-install-action-button =
.label = Install Word plugin .label = Įdiegti papildinį „Word“ programai
mac-word-plugin-install-remind-later-button = mac-word-plugin-install-remind-later-button =
.label = { general-remind-me-later } .label = { general-remind-me-later }
mac-word-plugin-install-dont-ask-again-button = mac-word-plugin-install-dont-ask-again-button =

View file

@ -1358,7 +1358,7 @@ noteEditor.deleteTable=Ištrinti lentelę
pdfReader.annotations=Anotacijos pdfReader.annotations=Anotacijos
pdfReader.showAnnotations=Rodyti anotacijas pdfReader.showAnnotations=Rodyti anotacijas
pdfReader.searchAnnotations=Ieškoti anotacijų pdfReader.searchAnnotations=Ieškoti anotacijų
pdfReader.searchOutline=Search Outline pdfReader.searchOutline=Ieškoti žymelės
pdfReader.noAnnotations=Sukurkite anotaciją ir ją galėsite matyti šoniniame skydelyje. pdfReader.noAnnotations=Sukurkite anotaciją ir ją galėsite matyti šoniniame skydelyje.
pdfReader.noExtractedText=Tekstas neišgautas pdfReader.noExtractedText=Tekstas neišgautas
pdfReader.addComment=Pridėti komentarą pdfReader.addComment=Pridėti komentarą
@ -1448,8 +1448,8 @@ pdfReader.epubAppearance.pageWidth.revert=Naudoti numatytąjį puslapio plotį
pdfReader.convertToHighlight=Paversti paryškinimu pdfReader.convertToHighlight=Paversti paryškinimu
pdfReader.convertToUnderline=Paversti pabraukimu pdfReader.convertToUnderline=Paversti pabraukimu
pdfReader.size=Dydis pdfReader.size=Dydis
pdfReader.merge=Merge pdfReader.merge=Apjungti
pdfReader.copyLink=Copy Link pdfReader.copyLink=Kopijuoti nuorodą
spellCheck.checkSpelling=Rašybos tikrinimas spellCheck.checkSpelling=Rašybos tikrinimas
spellCheck.addRemoveDictionaries=Pridėti / šalinti žodynus... spellCheck.addRemoveDictionaries=Pridėti / šalinti žodynus...

View file

@ -5,11 +5,11 @@
<!ENTITY zotero.preferences.default "Standard:"> <!ENTITY zotero.preferences.default "Standard:">
<!ENTITY zotero.preferences.items "items"> <!ENTITY zotero.preferences.items "items">
<!ENTITY zotero.preferences.period "."> <!ENTITY zotero.preferences.period ".">
<!ENTITY zotero.preferences.custom "Custom…"> <!ENTITY zotero.preferences.custom "Eigendefinert…">
<!ENTITY zotero.preferences.prefpane.general "Generelt"> <!ENTITY zotero.preferences.prefpane.general "Generelt">
<!ENTITY zotero.preferences.interface "Interface"> <!ENTITY zotero.preferences.interface "Grensesnitt">
<!ENTITY zotero.preferences.fileHandling "File Handling"> <!ENTITY zotero.preferences.fileHandling "File Handling">
<!ENTITY zotero.preferences.automaticSnapshots "Ta snapshot automatisk når du genererer element frå nettsider"> <!ENTITY zotero.preferences.automaticSnapshots "Ta snapshot automatisk når du genererer element frå nettsider">
<!ENTITY zotero.preferences.downloadAssociatedFiles "Lagra tilknytte PDF-filer og andre filer ved lagring av element"> <!ENTITY zotero.preferences.downloadAssociatedFiles "Lagra tilknytte PDF-filer og andre filer ved lagring av element">
@ -29,7 +29,7 @@
<!ENTITY zotero.preferences.reader.tabsTitle.label "Show tabs as"> <!ENTITY zotero.preferences.reader.tabsTitle.label "Show tabs as">
<!ENTITY zotero.preferences.reader.tabsTitle.creatorYearTitle "Creator - Year - Title"> <!ENTITY zotero.preferences.reader.tabsTitle.creatorYearTitle "Creator - Year - Title">
<!ENTITY zotero.preferences.reader.tabsTitle.titleCreatorYear "Title - Creator - Year"> <!ENTITY zotero.preferences.reader.tabsTitle.titleCreatorYear "Title - Creator - Year">
<!ENTITY zotero.preferences.reader.tabsTitle.filename "Filename"> <!ENTITY zotero.preferences.reader.tabsTitle.filename "Filnamn">
<!ENTITY zotero.preferences.groups "Grupper"> <!ENTITY zotero.preferences.groups "Grupper">
<!ENTITY zotero.preferences.groups.whenCopyingInclude "Ta med når element vert kopierte mellom bibliotek:"> <!ENTITY zotero.preferences.groups.whenCopyingInclude "Ta med når element vert kopierte mellom bibliotek:">
@ -84,7 +84,7 @@
<!ENTITY zotero.preferences.sync.reset.resetFileSyncHistory.desc "Compare all attachment files with the storage service"> <!ENTITY zotero.preferences.sync.reset.resetFileSyncHistory.desc "Compare all attachment files with the storage service">
<!ENTITY zotero.preferences.sync.reset "Nullstill"> <!ENTITY zotero.preferences.sync.reset "Nullstill">
<!ENTITY zotero.preferences.sync.reset.showResetOptions "Show Reset Options…"> <!ENTITY zotero.preferences.sync.reset.showResetOptions "Show Reset Options…">
<!ENTITY zotero.preferences.sync.reset.button "Reset…"> <!ENTITY zotero.preferences.sync.reset.button "Nullstill…">
<!ENTITY zotero.preferences.prefpane.search "Søk"> <!ENTITY zotero.preferences.prefpane.search "Søk">
@ -111,7 +111,7 @@
<!ENTITY zotero.preferences.quickCopy.copyAsHTML "Kopier som HTML"> <!ENTITY zotero.preferences.quickCopy.copyAsHTML "Kopier som HTML">
<!ENTITY zotero.preferences.quickCopy.siteEditor.setings "Innstillingar"> <!ENTITY zotero.preferences.quickCopy.siteEditor.setings "Innstillingar">
<!ENTITY zotero.preferences.quickCopy.siteEditor.domainPath "Domene/stig"> <!ENTITY zotero.preferences.quickCopy.siteEditor.domainPath "Domene/stig">
<!ENTITY zotero.preferences.quickCopy.siteEditor.domainPath.example "(e.g., wikipedia.org)"> <!ENTITY zotero.preferences.quickCopy.siteEditor.domainPath.example "(t.d. wikipedia.org)">
<!ENTITY zotero.preferences.quickCopy.siteEditor.format "Format"> <!ENTITY zotero.preferences.quickCopy.siteEditor.format "Format">
<!ENTITY zotero.preferences.quickCopy.siteEditor.locale "Språk"> <!ENTITY zotero.preferences.quickCopy.siteEditor.locale "Språk">
<!ENTITY zotero.preferences.quickCopy.dragLimit "Slå av rask kopiering når du drar fleire enn"> <!ENTITY zotero.preferences.quickCopy.dragLimit "Slå av rask kopiering når du drar fleire enn">
@ -126,7 +126,7 @@
<!ENTITY zotero.preferences.cite.styles.styleManager.title "Tittel"> <!ENTITY zotero.preferences.cite.styles.styleManager.title "Tittel">
<!ENTITY zotero.preferences.cite.styles.styleManager.updated "Oppdatert"> <!ENTITY zotero.preferences.cite.styles.styleManager.updated "Oppdatert">
<!ENTITY zotero.preferences.cite.styles.styleManager.csl "CSL"> <!ENTITY zotero.preferences.cite.styles.styleManager.csl "CSL">
<!ENTITY zotero.preferences.export.getAdditionalStyles "Get additional styles…"> <!ENTITY zotero.preferences.export.getAdditionalStyles "Hent fleire stilar…">
<!ENTITY zotero.preferences.keys.saveToZotero "Save to Zotero (address bar icon)"> <!ENTITY zotero.preferences.keys.saveToZotero "Save to Zotero (address bar icon)">
<!ENTITY zotero.preferences.keys.focusLibrariesPane "Focus Libraries Pane"> <!ENTITY zotero.preferences.keys.focusLibrariesPane "Focus Libraries Pane">

View file

@ -513,7 +513,7 @@ new-collection-dialog =
new-collection-name = Namn: new-collection-name = Namn:
new-collection-create-in = Create in: new-collection-create-in = Create in:
attachment-info-title = Tittel attachment-info-title = Tittel
attachment-info-filename = Filename attachment-info-filename = Filnamn
attachment-info-accessed = Lest attachment-info-accessed = Lest
attachment-info-pages = Sidetal attachment-info-pages = Sidetal
attachment-info-modified = Sist endra attachment-info-modified = Sist endra

View file

@ -436,7 +436,7 @@ pane.item.attachments.hasLink=Has link
pane.item.attachments.select=Vel ei fil pane.item.attachments.select=Vel ei fil
pane.item.attachments.PDF.installTools.title=PDF Tools Not Installed pane.item.attachments.PDF.installTools.title=PDF Tools Not Installed
pane.item.attachments.PDF.installTools.text=To use this feature, you must first install the PDF tools in the Search pane of the Zotero preferences. pane.item.attachments.PDF.installTools.text=To use this feature, you must first install the PDF tools in the Search pane of the Zotero preferences.
pane.item.attachments.filename=Filename pane.item.attachments.filename=Filnamn
pane.item.noteEditor.clickHere=Klikk her pane.item.noteEditor.clickHere=Klikk her
pane.item.tags.icon.user=Brukerdefinert tagg: pane.item.tags.icon.user=Brukerdefinert tagg:
pane.item.tags.icon.automatic=Automatisk tagg: pane.item.tags.icon.automatic=Automatisk tagg:
@ -1344,7 +1344,7 @@ noteEditor.removeCitations=Hide Annotation Citations
noteEditor.image=Bilete noteEditor.image=Bilete
noteEditor.math=Math noteEditor.math=Math
noteEditor.table=Table noteEditor.table=Table
noteEditor.citation=Citation noteEditor.citation=Sitering
noteEditor.copyImage=Copy Image noteEditor.copyImage=Copy Image
noteEditor.saveImageAs=Save Image As… noteEditor.saveImageAs=Save Image As…
noteEditor.insertRowBefore=Insert Row Above noteEditor.insertRowBefore=Insert Row Above

View file

@ -76,7 +76,7 @@ preferences-advanced-enable-bidi-ui =
preferences-advanced-reset-data-dir = preferences-advanced-reset-data-dir =
.label = Revert to Default Location… .label = Revert to Default Location…
preferences-advanced-custom-data-dir = preferences-advanced-custom-data-dir =
.label = Use Custom Location… .label = 使用自定义位置
preferences-advanced-default-data-dir = preferences-advanced-default-data-dir =
.value = (Default: { $directory }) .value = (Default: { $directory })
.aria-label = Default location .aria-label = 默认位置

View file

@ -45,8 +45,8 @@ pdfReader-findInDocumentInput =
.placeholder = { pdfReader-find-in-document } .placeholder = { pdfReader-find-in-document }
.aria-description = 要将搜索结果转换为高亮注释,请按 { general-key-control }-{ option-or-alt }-1。要将搜索结果转换为下划线注释,请按 { general-key-control }-{ option-or-alt }-2。 .aria-description = 要将搜索结果转换为高亮注释,请按 { general-key-control }-{ option-or-alt }-1。要将搜索结果转换为下划线注释,请按 { general-key-control }-{ option-or-alt }-2。
pdfReader-import-from-epub = pdfReader-import-from-epub =
.label = Import Ebook Annotations… .label = 导入电子书注释…
pdfReader-import-from-epub-prompt-title = Import Ebook Annotations pdfReader-import-from-epub-prompt-title = 导入电子书注释
pdfReader-import-from-epub-prompt-text = pdfReader-import-from-epub-prompt-text =
{ -app-name } found { $count -> { -app-name } found { $count ->
[1] { $count } { $tool } annotation [1] { $count } { $tool } annotation
@ -62,4 +62,4 @@ pdfReader-import-from-epub-no-annotations-other-file =
“{ $filename }” does not appear to contain any Calibre or KOReader annotations. “{ $filename }” does not appear to contain any Calibre or KOReader annotations.
If this ebook has been annotated with KOReader, try selecting a “metadata.epub.lua” file directly. 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… pdfReader-import-from-epub-select-other = 选择其他文件…

View file

@ -61,7 +61,7 @@
<!ENTITY toolsMenu.label "工具"> <!ENTITY toolsMenu.label "工具">
<!ENTITY toolsMenu.accesskey "T"> <!ENTITY toolsMenu.accesskey "T">
<!ENTITY installConnector.label "Install Browser Connector…"> <!ENTITY installConnector.label "安装浏览器扩展…">
<!ENTITY installConnector.accesskey "C"> <!ENTITY installConnector.accesskey "C">
<!ENTITY addons.label "插件"> <!ENTITY addons.label "插件">
<!ENTITY developer.label "开发者"> <!ENTITY developer.label "开发者">

View file

@ -24,7 +24,7 @@ general-help = 帮助
general-tag = 标签 general-tag = 标签
general-done = 完成 general-done = 完成
general-view-troubleshooting-instructions = 查看故障排除说明 general-view-troubleshooting-instructions = 查看故障排除说明
general-go-back = Go Back general-go-back = 返回
citation-style-label = 参考文献样式: citation-style-label = 参考文献样式:
language-label = 语言: language-label = 语言:
menu-file-show-in-finder = menu-file-show-in-finder =
@ -66,9 +66,9 @@ menu-deletePermanently =
menu-tools-plugins = menu-tools-plugins =
.label = 插件 .label = 插件
menu-view-columns-move-left = menu-view-columns-move-left =
.label = Move Column Left .label = 将列移至左侧
menu-view-columns-move-right = menu-view-columns-move-right =
.label = Move Column Right .label = 将列移至右侧
main-window-command = main-window-command =
.label = Library .label = Library
main-window-key = main-window-key =
@ -283,7 +283,7 @@ integration-editBibliography-add-button =
integration-editBibliography-remove-button = integration-editBibliography-remove-button =
.aria-label = { general-remove } .aria-label = { general-remove }
integration-editBibliography-editor = integration-editBibliography-editor =
.aria-label = Edit reference .aria-label = 编辑参考文献表
-integration-editBibliography-include-uncited = To include an uncited item in your bibliography, select it from the items list and press { general-add }. -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-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-edit-reference = To change how a reference is formatted, use the text editor.

View file

@ -1358,7 +1358,7 @@ noteEditor.deleteTable=删除表格
pdfReader.annotations=注释 pdfReader.annotations=注释
pdfReader.showAnnotations=显示注释 pdfReader.showAnnotations=显示注释
pdfReader.searchAnnotations=搜索注释 pdfReader.searchAnnotations=搜索注释
pdfReader.searchOutline=Search Outline pdfReader.searchOutline=搜索大纲
pdfReader.noAnnotations=创建注释使其在侧栏可见 pdfReader.noAnnotations=创建注释使其在侧栏可见
pdfReader.noExtractedText=无提取的文本 pdfReader.noExtractedText=无提取的文本
pdfReader.addComment=添加评论 pdfReader.addComment=添加评论

View file

@ -38,6 +38,7 @@ pref("extensions.zotero.autoRenameFiles.fileTypes", "application/pdf,application
pref("extensions.zotero.attachmentRenameTemplate", "{{ firstCreator suffix=\" - \" }}{{ year suffix=\" - \" }}{{ title truncate=\"100\" }}"); pref("extensions.zotero.attachmentRenameTemplate", "{{ firstCreator suffix=\" - \" }}{{ year suffix=\" - \" }}{{ title truncate=\"100\" }}");
pref("extensions.zotero.capitalizeTitles", false); pref("extensions.zotero.capitalizeTitles", false);
pref("extensions.zotero.launchNonNativeFiles", false); pref("extensions.zotero.launchNonNativeFiles", false);
pref("extensions.zotero.naturalSorting", true);
pref("extensions.zotero.sortNotesChronologically", false); pref("extensions.zotero.sortNotesChronologically", false);
pref("extensions.zotero.sortNotesChronologically.reader", true); pref("extensions.zotero.sortNotesChronologically.reader", true);
pref("extensions.zotero.sortAttachmentsChronologically", false); pref("extensions.zotero.sortAttachmentsChronologically", false);

@ -1 +1 @@
Subproject commit 0f4f218980ceed99639ae887b084c9b059ebd4d0 Subproject commit cd609cafd3049f4d922c891641ffc7a76eb8ccdf

@ -1 +1 @@
Subproject commit 64c6acb0c06ed9f1d7f090052132292378c5cfe5 Subproject commit f9857be7000bdd90997e7a28abad2383f8d2f88b

2
reader

@ -1 +1 @@
Subproject commit 533ccd38ca0f2f5ab3c5be277516a4dba581629c Subproject commit 51fb33cb8046238d4baee153fd40b2cd035fd7fb

View file

@ -1 +1 @@
2024-11-26 07:33:38 2025-03-13 03:14:07

View file

@ -53,11 +53,6 @@ zoterosearch {
width: 12em; width: 12em;
} }
#condition-tooltips tooltip
{
background: red !important;
}
#condition-tooltips hbox > label #condition-tooltips hbox > label
{ {
font-weight: 600; font-weight: 600;

2
styles

@ -1 +1 @@
Subproject commit 1554b78c316ad2634885a014b0b0b2afa7a476a4 Subproject commit 40b18d8266fba6c740d48218d31ecead786e5e6b

View file

@ -446,7 +446,7 @@ async function waitNoLongerThan(promise, ms = 1000) {
async function waitForScrollToPane(itemDetails, paneID) { async function waitForScrollToPane(itemDetails, paneID) {
await itemDetails._renderPromise; await itemDetails._renderPromise;
itemDetails.scrollToPane(paneID, "instant"); await itemDetails.scrollToPane(paneID, "instant");
// Wait for some frames or up to 150ms to ensure the pane is visible // Wait for some frames or up to 150ms to ensure the pane is visible
await waitForFrames(5); await waitForFrames(5);
} }

View file

@ -925,7 +925,8 @@ describe("Zotero.Attachments", function() {
await item.saveTx(); await item.saveTx();
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 2); // doi.org, publisher, download
assert.equal(requestStub.callCount, 3);
assert.isTrue(requestStub.getCall(0).calledWith('GET', 'https://doi.org/' + doi)); assert.isTrue(requestStub.getCall(0).calledWith('GET', 'https://doi.org/' + doi));
assert.ok(attachment); assert.ok(attachment);
var json = attachment.toJSON(); var json = attachment.toJSON();
@ -961,7 +962,8 @@ describe("Zotero.Attachments", function() {
await item.saveTx(); await item.saveTx();
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 2); // doi.org, publisher, download
assert.equal(requestStub.callCount, 3);
assert.isTrue(requestStub.getCall(0).calledWith('GET', 'https://doi.org/' + doi)); assert.isTrue(requestStub.getCall(0).calledWith('GET', 'https://doi.org/' + doi));
assert.ok(attachment); assert.ok(attachment);
var json = attachment.toJSON(); var json = attachment.toJSON();
@ -979,7 +981,8 @@ describe("Zotero.Attachments", function() {
await item.saveTx(); await item.saveTx();
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 1); // URL, download
assert.equal(requestStub.callCount, 2);
assert.isTrue(requestStub.calledWith('GET', url)); assert.isTrue(requestStub.calledWith('GET', url));
assert.ok(attachment); assert.ok(attachment);
var json = attachment.toJSON(); var json = attachment.toJSON();
@ -996,6 +999,7 @@ describe("Zotero.Attachments", function() {
item.setField('url', url); item.setField('url', url);
await item.saveTx(); await item.saveTx();
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
// URL, redirect target URL
assert.equal(requestStub.callCount, 2); assert.equal(requestStub.callCount, 2);
var call = requestStub.getCall(0); var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', url)); assert.isTrue(call.calledWith('GET', url));
@ -1017,7 +1021,7 @@ describe("Zotero.Attachments", function() {
await item.saveTx(); await item.saveTx();
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 3); assert.equal(requestStub.callCount, 4);
var call1 = requestStub.getCall(0); var call1 = requestStub.getCall(0);
assert.isTrue(call1.calledWith('GET', 'https://doi.org/' + doi)); assert.isTrue(call1.calledWith('GET', 'https://doi.org/' + doi));
var call2 = requestStub.getCall(1); var call2 = requestStub.getCall(1);
@ -1041,7 +1045,7 @@ describe("Zotero.Attachments", function() {
await item.saveTx(); await item.saveTx();
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 4); assert.equal(requestStub.callCount, 5);
// Check the DOI (and get nothing) // Check the DOI (and get nothing)
var call = requestStub.getCall(0); var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi)); assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
@ -1097,8 +1101,9 @@ describe("Zotero.Attachments", function() {
var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2]); var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2]);
assert.equal(requestStub.callCount, 2); // 2 URLs and 2 downloads
assert.isAbove(requestStubCallTimes[1] - requestStubCallTimes[0], 998); assert.equal(requestStub.callCount, 4);
assert.isAbove(requestStubCallTimes[2] - requestStubCallTimes[0], 998);
// Make sure both items have attachments // Make sure both items have attachments
assert.equal(item1.numAttachments(), 1); assert.equal(item1.numAttachments(), 1);
assert.equal(item2.numAttachments(), 1); assert.equal(item2.numAttachments(), 1);
@ -1119,7 +1124,7 @@ describe("Zotero.Attachments", function() {
item2.setField('url', url2); item2.setField('url', url2);
await item2.saveTx(); await item2.saveTx();
// DOI URL resolves to 'website2' domain without PDF // DOI URL resolves to 'website2' domain with PDF
var url3 = doiPrefix + doi6; var url3 = doiPrefix + doi6;
var item3 = createUnsavedDataObject('item', { itemType: 'journalArticle' }); var item3 = createUnsavedDataObject('item', { itemType: 'journalArticle' });
item3.setField('title', 'Test'); item3.setField('title', 'Test');
@ -1128,19 +1133,22 @@ describe("Zotero.Attachments", function() {
var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2, item3]); var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2, item3]);
assert.equal(requestStub.callCount, 6); assert.equal(requestStub.callCount, 8);
assert.equal(requestStub.getCall(0).args[1], doiPrefix + doi1); assert.equal(requestStub.getCall(0).args[1], doiPrefix + doi1);
assert.equal(requestStub.getCall(1).args[1], pageURL1); assert.equal(requestStub.getCall(1).args[1], pageURL1);
assert.equal(requestStub.getCall(2).args[1], doiPrefix + doi4); assert.equal(requestStub.getCall(2).args[1], pdfURL);
assert.equal(requestStub.getCall(3).args[1], doiPrefix + doi4);
// Should skip ahead to the next DOI // Should skip ahead to the next DOI
assert.equal(requestStub.getCall(3).args[1], doiPrefix + doi6); assert.equal(requestStub.getCall(4).args[1], doiPrefix + doi6);
// which is on a new domain // which is on a new domain
assert.equal(requestStub.getCall(4).args[1], pageURL8); assert.equal(requestStub.getCall(5).args[1], pageURL8);
assert.equal(requestStub.getCall(6).args[1], pdfURL);
// and then return to make 'website' request for DOI 4 // and then return to make 'website' request for DOI 4
assert.equal(requestStub.getCall(5).args[1], pageURL4); assert.equal(requestStub.getCall(7).args[1], pageURL4);
// 'website' requests should be a second apart // 'website' requests should be a second apart
assert.isAbove(requestStubCallTimes[5] - requestStubCallTimes[1], 995); assert.isAbove(requestStubCallTimes[7] - requestStubCallTimes[1], 995);
assert.equal(item1.numAttachments(), 1); assert.equal(item1.numAttachments(), 1);
assert.equal(item2.numAttachments(), 0); assert.equal(item2.numAttachments(), 0);
@ -1162,10 +1170,13 @@ describe("Zotero.Attachments", function() {
var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2]); var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2]);
assert.equal(requestStub.callCount, 3); // 429, URL9, download, URL3, download
assert.equal(requestStub.callCount, 5);
assert.equal(requestStub.getCall(0).args[1], pageURL9); assert.equal(requestStub.getCall(0).args[1], pageURL9);
assert.equal(requestStub.getCall(1).args[1], pageURL9); assert.equal(requestStub.getCall(1).args[1], pageURL9);
assert.equal(requestStub.getCall(2).args[1], pageURL3); assert.equal(requestStub.getCall(2).args[1], pdfURL);
assert.equal(requestStub.getCall(3).args[1], pageURL3);
assert.equal(requestStub.getCall(4).args[1], pdfURL);
assert.isAbove(requestStubCallTimes[1] - requestStubCallTimes[0], 1999); assert.isAbove(requestStubCallTimes[1] - requestStubCallTimes[0], 1999);
// Make sure both items have attachments // Make sure both items have attachments
assert.equal(item1.numAttachments(), 1); assert.equal(item1.numAttachments(), 1);
@ -1180,9 +1191,10 @@ describe("Zotero.Attachments", function() {
await item.saveTx(); await item.saveTx();
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 2); assert.equal(requestStub.callCount, 3);
assert.equal(requestStub.getCall(0).args[1], pageURL10) assert.equal(requestStub.getCall(0).args[1], pageURL10)
assert.equal(requestStub.getCall(1).args[1], pageURL1) assert.equal(requestStub.getCall(1).args[1], pageURL1)
assert.equal(requestStub.getCall(2).args[1], pdfURL)
assert.ok(attachment); assert.ok(attachment);
var json = attachment.toJSON(); var json = attachment.toJSON();
assert.equal(json.url, pdfURL); assert.equal(json.url, pdfURL);
@ -1228,7 +1240,7 @@ describe("Zotero.Attachments", function() {
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 4); assert.equal(requestStub.callCount, 5);
var call = requestStub.getCall(0); var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi)); assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
var call = requestStub.getCall(1); var call = requestStub.getCall(1);
@ -1266,7 +1278,7 @@ describe("Zotero.Attachments", function() {
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 4); assert.equal(requestStub.callCount, 5);
var call = requestStub.getCall(0); var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi)); assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
var call = requestStub.getCall(1); var call = requestStub.getCall(1);
@ -1302,7 +1314,7 @@ describe("Zotero.Attachments", function() {
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 4); assert.equal(requestStub.callCount, 5);
var call = requestStub.getCall(0); var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi)); assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
call = requestStub.getCall(1); call = requestStub.getCall(1);
@ -1342,7 +1354,7 @@ describe("Zotero.Attachments", function() {
var attachment = await Zotero.Attachments.addAvailableFile(item); var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 5); assert.equal(requestStub.callCount, 6);
var call = requestStub.getCall(0); var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi)); assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
call = requestStub.getCall(1); call = requestStub.getCall(1);

View file

@ -691,6 +691,23 @@ describe("Zotero.CollectionTree", function() {
assert.equal(cv.selection.focused, 0); assert.equal(cv.selection.focused, 0);
assert.sameMembers(zp.itemsView.getSelectedItems(true), [item1.id, item2.id]); assert.sameMembers(zp.itemsView.getSelectedItems(true), [item1.id, item2.id]);
}); });
it("should switch to trash if item is in trash", async function () {
var item = await createDataObject('item', { deleted: true });
await cv.selectItems([item.id]);
await waitForItemsLoad(win);
assert.isTrue(zp.getCollectionTreeRow().isTrash());
assert.sameMembers(zp.itemsView.getSelectedItems(true), [item.id]);
});
it("should switch to trash if parent of child note is in trash", async function () {
var item = await createDataObject('item', { deleted: true });
var note = await createDataObject('item', { itemType: 'note', parentItemID: item.id });
await cv.selectItems([note.id]);
await waitForItemsLoad(win);
assert.isTrue(zp.getCollectionTreeRow().isTrash());
assert.sameMembers(zp.itemsView.getSelectedItems(true), [note.id]);
});
}); });
describe("#onDrop()", function () { describe("#onDrop()", function () {

View file

@ -2,25 +2,43 @@
{ {
"id": "339d0202-d99f-48a2-aa0d-9b0c5631af26", "id": "339d0202-d99f-48a2-aa0d-9b0c5631af26",
"type": "highlight", "type": "highlight",
"color": "color": {
{
"r": 255, "r": 255,
"g": 245, "g": 245,
"b": 173 "b": 173
}, },
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a", "profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
"positions": "positions": [
[
{ {
"top_left": "top_left": {
{ "x": 191.5049,
"x": 429.74257247464135, "y": 588.5418
"y": 770.1238821199208
}, },
"bottom_right": "bottom_right": {
{ "x": 461.6047999999994,
"x": 572.5401236420382, "y": 576.1542
"y": 680.1292839565452 },
"page": 1
},
{
"top_left": {
"x": 146.3,
"y": 576.8651
},
"bottom_right": {
"x": 461.3467999999992,
"y": 564.6542
},
"page": 1
},
{
"top_left": {
"x": 146.3,
"y": 565.3651
},
"bottom_right": {
"x": 220.06760000000003,
"y": 553.1542
}, },
"page": 1 "page": 1
} }

View file

@ -18,7 +18,7 @@
], ],
"created": "2024-11-04T13:49:03.274Z", "created": "2024-11-04T13:49:03.274Z",
"modified": "2024-11-04T13:49:03.274Z", "modified": "2024-11-04T13:49:03.274Z",
"id": "https://api.mendeley.com/annotations/v2/84f12446-3b49-4052-bbdc-832d28e1e072", "id": "https://api.mendeley.com/annotations/v2/339d0202-d99f-48a2-aa0d-9b0c5631af26",
"body": [ "body": [
{ {
"purpose": "highlighting", "purpose": "highlighting",

View file

@ -0,0 +1,8 @@
[
{
"id": "8041a43b-f740-41ec-be42-a24cc5106d68",
"created": "2024-07-09T09:56:04.349Z",
"modified": "2024-12-06T17:41:37.200Z",
"title": "TEST"
}
]

View file

@ -269,7 +269,7 @@ describe("Item pane", function () {
var menupopup = itemBox.querySelector('#zotero-creator-transform-menu'); var menupopup = itemBox.querySelector('#zotero-creator-transform-menu');
// Fake a right-click // Fake a right-click
doc.popupNode = parent; itemBox._popupNode = parent;
menupopup.openPopup( menupopup.openPopup(
parent, "after_start", 0, 0, true, false, new MouseEvent('click', { button: 2 }) parent, "after_start", 0, 0, true, false, new MouseEvent('click', { button: 2 })
); );
@ -298,7 +298,7 @@ describe("Item pane", function () {
var firstlast = label.closest('.creator-type-value'); var firstlast = label.closest('.creator-type-value');
var menupopup = itemBox.querySelector('#zotero-creator-transform-menu'); var menupopup = itemBox.querySelector('#zotero-creator-transform-menu');
// Fake a right-click // Fake a right-click
doc.popupNode = firstlast; itemBox._popupNode = firstlast;
menupopup.openPopup( menupopup.openPopup(
firstlast, "after_start", 0, 0, true, false, new MouseEvent('click', { button: 2 }) firstlast, "after_start", 0, 0, true, false, new MouseEvent('click', { button: 2 })
); );
@ -1795,4 +1795,47 @@ describe("Item pane", function () {
assert.isTrue(ZoteroContextPane.splitter.hidden); assert.isTrue(ZoteroContextPane.splitter.hidden);
}); });
}); });
describe("Libraries pane", function () {
let paneID = "libraries-collections";
beforeEach(function () {
Zotero.Prefs.set("panes.libraries-collections.open", true);
Zotero_Tabs.select("zotero-pane");
});
afterEach(function () {
Zotero_Tabs.select("zotero-pane");
Zotero_Tabs.closeAll();
});
it("should scroll to pinned pane after selection moves from note to item", async function () {
let itemDetails = ZoteroPane.itemPane._itemDetails;
let pane = itemDetails.getPane(paneID);
let item = await createDataObject('item');
await importPDFAttachment(item);
let note = await createDataObject('item', { itemType: 'note' });
await select(win, item);
await waitForScrollToPane(itemDetails, paneID);
itemDetails.pinnedPane = paneID;
await select(win, note);
await Zotero.Promise.delay(10);
await select(win, item);
await waitForScrollToPane(itemDetails, paneID);
// Should scroll to pinned pane
assert.isTrue(
Math.abs(pane.getBoundingClientRect().top - pane.parentElement.getBoundingClientRect().top) < 3
);
itemDetails.pinnedPane = "";
});
});
}); });

View file

@ -2731,7 +2731,7 @@ describe("Zotero.Item", function () {
}); });
describe("not-strict mode", function () { describe("not-strict mode", function () {
it("should handle Extra in non-strict mode", function () { it("should preserve Extra", function () {
var json = { var json = {
itemType: "journalArticle", itemType: "journalArticle",
title: "Test", title: "Test",
@ -2834,6 +2834,45 @@ describe("Zotero.Item", function () {
assert.equal(item.getField('extra'), ''); assert.equal(item.getField('extra'), '');
}); });
it("should use a Zotero item type stored in Extra", async function () {
var json = {
itemType: "document",
title: "Test",
extra: "Type: dataset\nCitation Key: abc123\nFoo: Bar"
};
var item = new Zotero.Item;
item.fromJSON(json);
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'dataset');
// Move a valid field out of Extra
assert.equal(item.getField('citationKey'), 'abc123');
assert.equal(item.getField('extra'), 'Foo: Bar');
});
it("should use a CSL item type stored in Extra", async function () {
var json = {
itemType: "journalArticle",
title: "Test",
extra: "Type: song"
};
var item = new Zotero.Item;
item.fromJSON(json);
assert.equal(item.itemTypeID, Zotero.ItemTypes.getID('audioRecording'));
});
it("should move a now-invalid existing field to Extra when using Type from Extra", async function () {
var json = {
itemType: "journalArticle",
title: "Test",
pages: "123",
extra: "Type: audioRecording"
};
var item = new Zotero.Item;
item.fromJSON(json);
assert.equal(item.itemTypeID, Zotero.ItemTypes.getID('audioRecording'));
// 'pages' should've been moved to Extra, since it's not valid for the new type
assert.equal(item.getField('extra'), 'Pages: 123');
});
it("should ignore some redundant fields from RDF translator (temporary)", function () { it("should ignore some redundant fields from RDF translator (temporary)", function () {
var json = { var json = {
itemType: "book", itemType: "book",
@ -2941,6 +2980,28 @@ describe("Zotero.Item", function () {
}; };
assert.throws(f, /^Invalid creator type/); assert.throws(f, /^Invalid creator type/);
}); });
it("should ignore item type in Extra", function () {
var json = {
itemType: "document",
title: "",
extra: "Type: preprint"
};
var item = new Zotero.Item;
item.fromJSON(json, { strict: true });
assert.equal(item.getField('extra'), "Type: preprint");
});
it("should ignore valid field in Extra", function () {
var json = {
itemType: "journalArticle",
title: "",
extra: "DOI: 10.1234/abcd"
};
var item = new Zotero.Item;
item.fromJSON(json, { strict: true });
assert.equal(item.getField('extra'), "DOI: 10.1234/abcd");
});
}); });
it("should accept ISO 8601 dates", function* () { it("should accept ISO 8601 dates", function* () {

View file

@ -1640,7 +1640,7 @@ describe("Zotero.ItemTree", function() {
cellText = win.document.querySelector('#zotero-items-tree .row.selected .cell.title .cell-text'); cellText = win.document.querySelector('#zotero-items-tree .row.selected .cell.title .cell-text');
} }
while (!cellText); while (!cellText);
assert.equal(cellText.innerHTML, 'Review of <i xmlns="http://www.w3.org/1999/xhtml">Review of <i style="font-style: normal;">B<sub>oo</sub>k</i> &lt;another-tag/&gt;</i>'); assert.equal(cellText.innerHTML, 'Review of <i xmlns="http://www.w3.org/1999/xhtml">Review of <span style="font-style: normal;">B<sub>oo</sub>k</span> &lt;another-tag/&gt;</i>');
}); });
}); });
}) })

View file

@ -3,10 +3,18 @@
describe('Zotero_Import_Mendeley', function () { describe('Zotero_Import_Mendeley', function () {
var server, httpd, httpdURL, importers; var server, httpd, httpdURL, importers;
const getImporter = () => { const getImporter = ({ importNotes = false } = {}) => {
const importer = new Zotero_Import_Mendeley(); const importer = new Zotero_Import_Mendeley();
importer.mendeleyAuth = { kind: 'direct', tokens: { accessToken: 'access_token', refreshToken: 'refresh_token' } }; importer.mendeleyAuth = { kind: 'direct', tokens: { accessToken: 'access_token', refreshToken: 'refresh_token' } };
importer.skipNotebooks = true; importer.skipNotebooks = !importNotes;
if (importNotes) {
importer._refManagerToken = {
kind: 'referenceManager',
accessToken: 'refman_token',
username: 'username',
password: 'password'
};
}
importers.push(importer); importers.push(importer);
return importer; return importer;
}; };
@ -14,7 +22,7 @@ describe('Zotero_Import_Mendeley', function () {
before(async () => { before(async () => {
Components.utils.import('chrome://zotero/content/import/mendeley/mendeleyImport.js'); Components.utils.import('chrome://zotero/content/import/mendeley/mendeleyImport.js');
// real http server is used to deliver an empty pdf so that annotations can be processed during import // A real HTTP server is used to deliver a Bitcoin PDF so that annotations can be processed during import.
Components.utils.import("resource://zotero-unit/httpd.js"); Components.utils.import("resource://zotero-unit/httpd.js");
const port = 16213; const port = 16213;
httpd = new HttpServer(); httpd = new HttpServer();
@ -22,7 +30,7 @@ describe('Zotero_Import_Mendeley', function () {
httpd.start(port); httpd.start(port);
httpd.registerFile( httpd.registerFile(
'/file1.pdf', '/file1.pdf',
Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'empty.pdf')) Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'recognizePDF_test_title.pdf'))
); );
}); });
@ -84,7 +92,7 @@ describe('Zotero_Import_Mendeley', function () {
setHTTPResponse(server, 'https://api.mendeley.com/', { setHTTPResponse(server, 'https://api.mendeley.com/', {
method: 'GET', method: 'GET',
url: `profiles/v2/me?`, url: `profiles/v2/me`,
status: 200, status: 200,
headers: {}, headers: {},
json: JSON.parse( json: JSON.parse(
@ -104,7 +112,7 @@ describe('Zotero_Import_Mendeley', function () {
setHTTPResponse(server, 'https://api.mendeley.com/', { setHTTPResponse(server, 'https://api.mendeley.com/', {
method: 'GET', method: 'GET',
url: `files/19fb5e5b-1a39-4851-b513-d48441a670e1?`, url: `files/19fb5e5b-1a39-4851-b513-d48441a670e1`,
status: 200, // ideally would be 303 but mock http doesn't like it status: 200, // ideally would be 303 but mock http doesn't like it
headers: { headers: {
Location: `${httpdURL}/file1.pdf` Location: `${httpdURL}/file1.pdf`
@ -526,7 +534,7 @@ describe('Zotero_Import_Mendeley', function () {
annotation.annotationText = 'Highlight text'; annotation.annotationText = 'Highlight text';
annotation.annotationComment = 'Highlight comment'; annotation.annotationComment = 'Highlight comment';
annotation.annotationPageLabel = '57'; annotation.annotationPageLabel = '57';
annotation.addRelation('mendeleyDB:annotationUUID', '84f12446-3b49-4052-bbdc-832d28e1e072'); annotation.addRelation('mendeleyDB:annotationUUID', '339d0202-d99f-48a2-aa0d-9b0c5631af26');
await annotation.saveTx(); await annotation.saveTx();
let mendeleyNotebook = JSON.parse( let mendeleyNotebook = JSON.parse(
@ -544,5 +552,85 @@ describe('Zotero_Import_Mendeley', function () {
await attachment.eraseTx(); await attachment.eraseTx();
await item.eraseTx(); await item.eraseTx();
}); });
it('should import a notebook from Mendeley', async () => {
const importer = getImporter({ importNotes: true });
setHTTPResponse(server, 'https://api.mendeley.com/', {
method: 'GET',
url: `notes/v1?limit=500`,
status: 200,
headers: {},
json: JSON.parse(
await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/notebooks.json')
)
});
setHTTPResponse(server, 'https://api.mendeley.com/', {
method: 'GET',
url: 'notes/v1/8041a43b-f740-41ec-be42-a24cc5106d68',
status: 200,
headers: {},
json: JSON.parse(
await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/notebook.json')
)
});
await importer.translate({
libraryID: Zotero.Libraries.userLibraryID,
collections: null,
linkFiles: false,
});
const note = (await Zotero.Relations
.getByPredicateAndObject('item', 'mendeleyDB:notebookUUID', '8041a43b-f740-41ec-be42-a24cc5106d68'))
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
assert.match(
note.note,
/^<h1>TEST<\/h1>\n<p><span class="highlight" data-annotation="((?:(?:%[0-9A-F]{2}|[^<>'" %])+))">“A purely peer-to-peer version of electronic cash would allow online payments to be sent directly from one party to another without going through a financial institution”<\/span> <span class="citation" data-citation="((?:(?:%[0-9A-F]{2}|[^<>'" %])+))">\(<span class="citation-item">“Item with PDF”, 2005, p. 1<\/span>\)<\/span><\/p>\n<p>Lorem Ipsum<\/p>$/i
);
});
it('should handle incorrect annotations', async () => {
let annotationsResponse = JSON.parse(
await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/annotations.json')
);
delete annotationsResponse.find(a => a.id == '885615a7-170e-4613-af80-0227ea76ae55').type;
delete annotationsResponse.find(a => a.id == 'bfbdb972-171d-4b21-8ae6-f156ac9a2b41').color;
annotationsResponse.push({ type: 'note', id: 'invalid' });
setHTTPResponse(server, 'https://api.mendeley.com/', {
method: 'GET',
url: `annotations?limit=200`,
status: 200,
headers: {},
json: annotationsResponse
});
const importer = getImporter();
await importer.translate({
libraryID: Zotero.Libraries.userLibraryID,
collections: null,
linkFiles: false,
});
const pdf = (await Zotero.Relations
.getByPredicateAndObject('item', 'mendeleyDB:fileHash', 'cc22c6611277df346ff8dc7386ba3880b2bafa15'))
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
const annotations = await pdf.getAnnotations();
// Typeless notes and invalid annotations should be discarded.
assert.lengthOf(annotations, 5);
assert.lengthOf(await Zotero.Relations.getByPredicateAndObject('item', 'mendeleyDB:annotationUUID', '885615a7-170e-4613-af80-0227ea76ae55'), 0);
assert.lengthOf(await Zotero.Relations.getByPredicateAndObject('item', 'mendeleyDB:annotationUUID', 'invalid'), 0);
// Colorless note annotations should have a default colour.
const colorlessNote = (await Zotero.Relations
.getByPredicateAndObject('item', 'mendeleyDB:annotationUUID', 'bfbdb972-171d-4b21-8ae6-f156ac9a2b41'))
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
.shift();
assert.equal(colorlessNote.annotationColor, '#ffd400');
});
}); });
}); });

View file

@ -1354,8 +1354,8 @@ describe("Connector Server", function () {
var origRequest = Zotero.HTTP.request.bind(Zotero.HTTP); var origRequest = Zotero.HTTP.request.bind(Zotero.HTTP);
var called = 0; var called = 0;
var stub = sinon.stub(Zotero.HTTP, 'request').callsFake(function (method, url, options) { var stub = sinon.stub(Zotero.HTTP, 'request').callsFake(function (method, url, options) {
// Forward saveSnapshot request // Forward saveSnapshot and PDF requests
if (url.endsWith('saveSnapshot')) { if (url.endsWith('saveSnapshot') || url.endsWith('test.pdf')) {
return origRequest(...arguments); return origRequest(...arguments);
} }

View file

@ -718,6 +718,33 @@ describe("Zotero.Sync.Data.Local", function() {
} }
}); });
it("should handle local item becoming child and remote being added to a collection", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var collection = await createDataObject('collection');
var parentItem = await createDataObject('item');
var childItem = await createDataObject('item', { itemType: 'note' });
var data = childItem.toJSON();
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [data]);
// Add to parent locally
childItem.parentID = parentItem.id;
await childItem.saveTx();
// Add to collection remotely
data.collections = [collection.key];
var json = {
key: childItem.key,
version: 10,
data
};
await Zotero.Sync.Data.Local.processObjectsFromJSON(
'item', libraryID, [json], { stopOnError: true }
);
assert.isTrue(collection.hasItem(parentItem));
});
it("should automatically resolve collection name conflict", async function () { it("should automatically resolve collection name conflict", async function () {
var libraryID = Zotero.Libraries.userLibraryID; var libraryID = Zotero.Libraries.userLibraryID;

View file

@ -95,6 +95,31 @@ describe("Item Tags Box", function () {
// New empty tag should have focus // New empty tag should have focus
assert.exists(doc.activeElement.closest("[isNew]")); assert.exists(doc.activeElement.closest("[isNew]"));
}); });
it("should save tag edits when another item is selected", async function () {
let notSelectedItem = await createDataObject('item');
var tag = Zotero.Utilities.randomString();
var updatedTag = Zotero.Utilities.randomString();
let selectedItem = await createDataObject('item', { tags: [{ tag }] });
var tagsbox = doc.querySelector('#zotero-editpane-tags');
var rows = tagsbox.querySelectorAll('.row editable-text');
assert.equal(rows.length, 1);
// type something
var firstRow = rows[0];
firstRow.focus();
firstRow.ref.value = updatedTag;
firstRow.ref.dispatchEvent(new Event('input'));
// change the item by clicking on another row in itemTree
let promise = waitForItemEvent('modify');
win.ZoteroPane.selectItem(notSelectedItem.id);
// selectedItem should be modified
await promise;
// make sure that the tag was actually updated
assert.equal(selectedItem.getTags()[0].tag, updatedTag);
});
}); });

View file

@ -738,4 +738,170 @@ describe("Zotero.Utilities.Internal", function () {
assert.equal(html, 'yes1yes2 yes3'); assert.equal(html, 'yes1yes2 yes3');
}); });
}); });
describe("OpenURL", function () {
var item;
before(async function () {
item = await createDataObject('item', { title: 'Foo Bar', date: '2024-12-19' });
})
after(function () {
Zotero.Prefs.clear('openURL.resolver');
});
describe("#resolve()", function () {
it("should add trailing '?' if no query string", async function () {
Zotero.Prefs.set("openURL.resolver", "https://resolver.ebsco.com/c/abcdef/result");
var url = Zotero.Utilities.Internal.OpenURL.resolve(item);
assert.include(url, 'result?url_ver=Z39.88-2004');
});
it("should add trailing '&' if already a query string", async function () {
Zotero.Prefs.set("openURL.resolver", "https://resolver.ebscohost.com/openurl?custid=abcdef&groupid=main&profile=ftf&authtype=ip,uid");
var url = Zotero.Utilities.Internal.OpenURL.resolve(item);
assert.include(url, 'authtype=ip,uid&url_ver=Z39.88-2004');
});
it("should add trailing '?' after /login?url=", async function () {
Zotero.Prefs.set("openURL.resolver", "https://proxy.school.edu/login?url=https://resolver.ebscohost.com/openurl");
var url = Zotero.Utilities.Internal.OpenURL.resolve(item);
assert.include(url, 'openurl?url_ver=Z39.88-2004');
});
it("shouldn't add trailing '?' after /login?url= if URL already ends in '?'", async function () {
Zotero.Prefs.set("openURL.resolver", "https://proxy.school.edu/login?url=https://resolver.ebscohost.com/openurl?");
var url = Zotero.Utilities.Internal.OpenURL.resolve(item);
assert.include(url, 'openurl?url_ver=Z39.88-2004');
});
});
});
describe("#renderItemTitle()", function () {
function renderToElement(title) {
let elem = new DOMParser().parseFromString('<div></div>', 'text/html')
.querySelector('div');
Zotero.Utilities.Internal.renderItemTitle(title, elem);
return elem;
}
function renderToHTML(title) {
return renderToElement(title).innerHTML;
}
function renderToTextViaHTML(title) {
return renderToElement(title).textContent;
}
function renderToText(title) {
return Zotero.Utilities.Internal.renderItemTitle(title);
}
it("should render a title without tags unchanged", function () {
assert.equal(renderToHTML('My Title'), 'My Title');
assert.equal(renderToHTML('1 < 2'), '1 &lt; 2');
assert.equal(renderToHTML('<<ATTENTION>> READ THIS'), '&lt;&lt;ATTENTION&gt;&gt; READ THIS');
});
it("should render single-level tags", function () {
let examples = [
'My <i>Title</i> XYZ',
'My <b>Title</b> XYZ',
'My <sub>Title</sub> XYZ',
'My <sup>Title</sup> XYZ',
];
for (let example of examples) {
assert.equal(renderToHTML(example), example);
}
assert.equal(renderToHTML('My <span class="nocase">Title</span> XYZ'), 'My <span>Title</span> XYZ');
// Citeproc wants no space, DOM output includes space. Confusing!
assert.equal(
renderToHTML('My <span style="font-variant:small-caps;">Title</span> XYZ'),
'My <span style="font-variant: small-caps;">Title</span> XYZ'
);
});
it("should render a tag nested inside a different tag", function () {
let examples = [
'My <i><b>Title</b></i> XYZ',
'My <b><i>Title</i></b> XYZ',
'My <sub><i>Title</i></sub> XYZ',
];
for (let example of examples) {
assert.equal(renderToHTML(example), example);
}
});
it("should preserve mismatched opening and closing tags", function () {
assert.equal(
renderToHTML('My <i><b>Title</i> XYZ'),
'My <i>&lt;b&gt;Title</i> XYZ'
);
assert.equal(
renderToHTML('My <b>Title</i></b> XYZ'),
'My <b>Title&lt;/i&gt;</b> XYZ'
);
assert.equal(
renderToHTML('My <sub><span class="font-variant:small-caps;">Title</sub> XYZ'),
'My <sub>&lt;span class="font-variant:small-caps;"&gt;Title</sub> XYZ'
);
assert.equal(
renderToHTML('My <sub><span class="font-variant:small-caps;">Title</sub></span> XYZ'),
'My <sub>&lt;span class="font-variant:small-caps;"&gt;Title</sub>&lt;/span&gt; XYZ'
);
});
it("should preserve matched but unsupported tags", function () {
assert.equal(
renderToHTML('My <em>Title</em> XYZ'),
'My &lt;em&gt;Title&lt;/em&gt; XYZ'
);
assert.equal(
renderToHTML('My <strong>Title</strong> XYZ'),
'My &lt;strong&gt;Title&lt;/strong&gt; XYZ'
);
assert.equal(
renderToHTML('My <span class="font-variant: small-caps;">Title</span> XYZ'), // Extra space
'My &lt;span class="font-variant: small-caps;"&gt;Title&lt;/span&gt; XYZ'
);
assert.equal(
renderToHTML('My <span>Title</span> XYZ'), // Extra space
'My &lt;span&gt;Title&lt;/span&gt; XYZ'
);
});
it("should invert a tag nested inside itself", function () {
assert.equal(
renderToHTML('<i>A <i>Title</i> in Italics</i>'),
'<i>A <span style="font-style: normal;">Title</span> in Italics</i>'
);
assert.equal(
renderToHTML('<b>A <b>Title</b> in Bold</b>'),
'<b>A <span style="font-weight: normal;">Title</span> in Bold</b>'
);
});
it("should invert a tag nested inside itself, with a different tag in between", function () {
assert.equal(
renderToHTML('<i>A <span class="nocase"><i>Title</i></span> in Italics</i>'),
'<i>A <span><span style="font-style: normal;">Title</span></span> in Italics</i>'
);
});
it("should render the same text directly and via DOM", function () {
let examples = [
'Basic Title',
'Complicated <i>Title</i>',
'Complicated <i>Title</i> With More Text',
'Nested <i>T<b>i</b>tle</i> With More Text',
'Self-Nested <i>T<i>i</i>tle</i> With More Text',
'Fe<sub>1−<i>x</i></sub>O',
'Title with <marquee>INVALID TAGS</marquee> <strong>like this</strong>',
];
for (let example of examples) {
assert.equal(renderToTextViaHTML(example), renderToText(example));
}
});
});
}); });

View file

@ -768,6 +768,50 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
win.close(); win.close();
}); });
it("should show an error for a non-DAV URL", async function () {
Zotero.HTTP.mock = null;
Zotero.Prefs.set("sync.storage.url", davHostPath);
httpd.registerPathHandler(
`${davBasePath}zotero/`,
{
handle: function (request, response) {
// Force Basic Auth
if (!request.hasHeader('Authorization')) {
response.setStatusLine(null, 401, null);
response.setHeader('WWW-Authenticate', 'Basic realm="WebDAV"', false);
return;
}
response.setStatusLine(null, 200, "OK");
response.write("<html><body><p>This is a non-DAV URL.</p></body></html>");
}
}
);
// Begin install procedure
var win = await loadPrefPane('sync');
var button = win.document.getElementById('storage-verify');
var spy = sinon.spy(win.Zotero_Preferences.Sync, "verifyStorageServer");
var promise1 = waitForDialog(function (dialog) {
assert.include(
dialog.document.documentElement.textContent,
Zotero.getString(
'sync.storage.error.webdav.invalidURL',
davScheme + '://' + davHostPath + 'zotero/'
)
);
});
button.click();
await promise1;
var promise2 = spy.returnValues[0];
spy.restore();
await promise2;
win.close();
});
it("should show an error for a 403", function* () { it("should show an error for a 403", function* () {
Zotero.HTTP.mock = null; Zotero.HTTP.mock = null;
httpd.registerPathHandler( httpd.registerPathHandler(

View file

@ -103,6 +103,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
httpd.stop(() => defer.resolve()); httpd.stop(() => defer.resolve());
yield defer.promise; yield defer.promise;
win.close(); win.close();
Zotero.HTTP.disableErrorRetry = false;
}) })
after(function* () { after(function* () {
@ -211,6 +212,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
item.attachmentSyncState = "to_download"; item.attachmentSyncState = "to_download";
yield item.saveTx(); yield item.saveTx();
Zotero.HTTP.disableErrorRetry = true;
httpd.registerPathHandler( httpd.registerPathHandler(
`/users/1/items/${item.key}/file`, `/users/1/items/${item.key}/file`,
{ {

View file

@ -1692,6 +1692,38 @@ describe("ZoteroPane", function() {
assert.equal(parentItem.getAttachments().length, 4); assert.equal(parentItem.getAttachments().length, 4);
assert.equal(epubAttachment.getField('title'), Zotero.getString('file-type-ebook')); assert.equal(epubAttachment.getField('title'), Zotero.getString('file-type-ebook'));
}); });
describe("Linked file renaming", function () {
before(() => {
Zotero.Prefs.set('autoRenameFiles.linked', true);
});
after(() => {
Zotero.Prefs.clear('autoRenameFiles.linked');
});
it("should only rename and change the title of the first PDF attachment", async function () {
let testFile = getTestDataDirectory();
testFile.append('test.pdf');
let tempDir = await getTempDirectory();
let copy1 = PathUtils.join(tempDir, 'copy1.pdf');
let copy2 = PathUtils.join(tempDir, 'copy2.pdf');
await IOUtils.copy(testFile.path, copy1);
await IOUtils.copy(testFile.path, copy2);
let parentItem = await createDataObject('item', { title: 'Foo' });
let [attachment1] = await zp.addAttachmentFromDialog(false, parentItem.id, [copy1]);
assert.equal(attachment1.getField('title'), Zotero.getString('file-type-pdf'));
assert.equal(attachment1.attachmentFilename, 'Foo.pdf');
let [attachment2] = await zp.addAttachmentFromDialog(false, parentItem.id, [copy2]);
assert.equal(attachment2.getField('title'), 'copy2');
assert.equal(attachment2.attachmentFilename, 'copy2.pdf');
});
});
}); });
describe("#createParentItemsFromSelected()", function () { describe("#createParentItemsFromSelected()", function () {

@ -1 +1 @@
Subproject commit 4cadb3e1455e6438b0b7c530d509cddca5f3e1c6 Subproject commit 484110c925d71ec9212709ac420fa03e4f928da8

View file

@ -1 +1 @@
7.0.11.SOURCE 7.0.15.SOURCE