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

View file

@ -6,6 +6,10 @@ pref("javascript.options.showInConsole", true);
// Don't retrieve unrequested links when performing standalone translation
pref("network.prefetch-next", false);
// Don't make DNS requests for links in snapshots
pref("network.dns.disablePrefetch", true);
// Don't open TCP connection when clicking on a link in a snapshot
pref("network.http.speculative-parallel-limit", 0);
// Let operations run as long as necessary
pref("dom.max_chrome_script_run_time", 0);

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -82,6 +82,9 @@
if (this._item == val) {
return;
}
// Blur events do not fire when item is changed by clicking on an itemTree row.
// Make sure any changes to the tags get saved before item is updated.
this.blurOpenField();
this._item = val;
}
@ -132,7 +135,7 @@
this._tagColors = Zotero.Tags.getColors(this.item.libraryID);
let focusedTag = this._id('rows').querySelector('editable-text:focus')?.value;
let focusedTag = this._id('rows').querySelector('editable-text:focus-within')?.value;
let tagRows = this._id('rows');
tagRows.replaceChildren();

View file

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

View file

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

View file

@ -2287,7 +2287,8 @@ var Zotero_QuickFormat = new function () {
}
}
// Arrow up closes the popup
if (event.key === "ArrowUp") {
// (except on locator type dropdown to allow for default handling)
if (event.key === "ArrowUp" && event.target.tagName !== "menulist") {
skipInputRefocus = true;
itemPopover.hidePopup();
return stopEvent();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -921,20 +921,7 @@ Zotero.Attachments = new function () {
}
}
else {
Zotero.debug("Saving file with saveURI()");
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(nsIWBP);
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_FROM_CACHE;
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var nsIURL = ioService.newURI(url, null, null);
var deferred = Zotero.Promise.defer();
wbp.progressListener = new Zotero.WebProgressFinishListener(function () {
deferred.resolve();
});
Zotero.Utilities.Internal.saveURI(wbp, nsIURL, tmpFile);
yield deferred.promise;
yield Zotero.HTTP.download(url, tmpFile);
}
var attachmentItem;
@ -1100,20 +1087,18 @@ Zotero.Attachments = new function () {
let enforcingFileType = false;
try {
await new Zotero.Promise(function (resolve) {
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
.createInstance(Components.interfaces.nsIWebBrowserPersist);
if (options.cookieSandbox) {
options.cookieSandbox.attachToInterfaceRequestor(wbp);
let headers = {};
if (options.referrer) {
headers.Referer = options.referrer;
}
await Zotero.HTTP.download(
url,
path,
{
headers,
cookieSandbox: options.cookieSandbox
}
wbp.progressListener = new Zotero.WebProgressFinishListener(() => resolve());
var headers = {};
if (options.referrer) {
headers.Referer = options.referrer;
}
Zotero.Utilities.Internal.saveURI(wbp, url, path, headers);
});
);
if (options.enforceFileType) {
enforcingFileType = true;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -448,16 +448,15 @@ Zotero.File = new function(){
this.download = async function (uri, path) {
var uriStr = uri.spec || uri;
const isHTTP = uriStr.startsWith('http');
if (uriStr.startsWith('http')) {
Zotero.warn("Zotero.File.download() is deprecated for HTTP(S) URLs -- use Zotero.HTTP.download()");
return Zotero.HTTP.download(uri, path);
}
Zotero.debug(`Saving ${uriStr} to ${path.pathQueryRef || path}`);
if (isHTTP && Zotero.HTTP.browserIsOffline()) {
let msg = `Download failed: ${Zotero.appName} is currently offline`;
Zotero.debug(msg, 2);
throw new Error(msg);
}
var deferred = Zotero.Promise.defer();
const inputChannel = NetUtil.newChannel({
uri,

View file

@ -149,6 +149,7 @@ Zotero.FileHandlers = {
handler = handler || systemHandler;
if (handler) {
if (Zotero.isMac) {
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
try {
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', handler, path]);
return true;
@ -156,6 +157,9 @@ Zotero.FileHandlers = {
catch (e) {
Zotero.logError(e);
}
finally {
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
}
}
try {
@ -498,8 +502,13 @@ Zotero.FileHandlers = {
if (!Zotero.File.pathToFile(command).isExecutable()) {
throw new Error(`${command} is not an executable`);
}
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
// Do not await
Zotero.Utilities.Internal.exec(command, args);
var promise = Zotero.Utilities.Internal.exec(command, args);
promise.finally(() => Zotero.Utilities.Internal.Environment.restoreMozillaVariables());
},
};

View file

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

View file

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

View file

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

View file

@ -83,8 +83,8 @@ Zotero.RecognizeDocument = new function () {
throw new Error();
}
let res = await _processItem(item);
_progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, item.getField('title'));
let parentItem = await _processItem(item);
_progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, parentItem.getField('title'));
}
catch (e) {
Zotero.logError(e);
@ -250,7 +250,7 @@ Zotero.RecognizeDocument = new function () {
/**
* Processes the item and places it as a children of the new item
* @param itemID
* @return {Promise}
* @return {Promise} A promise that resolves to a newly created, recognized parent item
*/
async function _processItem(attachment) {
// 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) {
await Zotero.DB.executeTransaction(async function () {
await _updateGlobalSchema(schema);
}.bind(this), { disableForeignKeys: true });
await Zotero.DB.queryAsync("PRAGMA foreign_keys=OFF");
try {
await Zotero.DB.executeTransaction(async function () {
await _updateGlobalSchema(schema);
});
}
finally {
await Zotero.DB.queryAsync("PRAGMA foreign_keys=ON");
}
};
@ -733,58 +739,64 @@ Zotero.Schema = new function(){
var itemTypeID = Zotero.ID.get('customItemTypes');
yield Zotero.DB.executeTransaction(async function () {
await Zotero.DB.queryAsync("INSERT INTO customItemTypes VALUES (?, 'nsfReviewer', 'NSF Reviewer', 1, 'chrome://zotero/skin/report_user.png')", itemTypeID);
var fields = [
['name', 'Name'],
['institution', 'Institution'],
['address', 'Address'],
['telephone', 'Telephone'],
['email', 'Email'],
['homepage', 'Webpage'],
['discipline', 'Discipline'],
['nsfID', 'NSF ID'],
['dateSent', 'Date Sent'],
['dateDue', 'Date Due'],
['accepted', 'Accepted'],
['programDirector', 'Program Director']
];
for (var i=0; i<fields.length; i++) {
var fieldID = Zotero.ItemFields.getID(fields[i][0]);
if (!fieldID) {
var fieldID = Zotero.ID.get('customFields');
await Zotero.DB.queryAsync("INSERT INTO customFields VALUES (?, ?, ?)", [fieldID, fields[i][0], fields[i][1]]);
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, NULL, ?, 1, ?)", [itemTypeID, fieldID, i+1]);
}
else {
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, ?, NULL, 1, ?)", [itemTypeID, fieldID, i+1]);
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=OFF");
try {
yield Zotero.DB.executeTransaction(async function () {
await Zotero.DB.queryAsync("INSERT INTO customItemTypes VALUES (?, 'nsfReviewer', 'NSF Reviewer', 1, 'chrome://zotero/skin/report_user.png')", itemTypeID);
var fields = [
['name', 'Name'],
['institution', 'Institution'],
['address', 'Address'],
['telephone', 'Telephone'],
['email', 'Email'],
['homepage', 'Webpage'],
['discipline', 'Discipline'],
['nsfID', 'NSF ID'],
['dateSent', 'Date Sent'],
['dateDue', 'Date Due'],
['accepted', 'Accepted'],
['programDirector', 'Program Director']
];
for (var i=0; i<fields.length; i++) {
var fieldID = Zotero.ItemFields.getID(fields[i][0]);
if (!fieldID) {
var fieldID = Zotero.ID.get('customFields');
await Zotero.DB.queryAsync("INSERT INTO customFields VALUES (?, ?, ?)", [fieldID, fields[i][0], fields[i][1]]);
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, NULL, ?, 1, ?)", [itemTypeID, fieldID, i+1]);
}
else {
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, ?, NULL, 1, ?)", [itemTypeID, fieldID, i+1]);
}
switch (fields[i][0]) {
case 'name':
var baseFieldID = Zotero.ItemFields.getID('title');
break;
case 'dateSent':
var baseFieldID = Zotero.ItemFields.getID('date');
break;
case 'homepage':
var baseFieldID = Zotero.ItemFields.getID('url');
break;
default:
var baseFieldID = null;
}
if (baseFieldID) {
await Zotero.DB.queryAsync("INSERT INTO customBaseFieldMappings VALUES (?, ?, ?)", [itemTypeID, baseFieldID, fieldID]);
}
}
switch (fields[i][0]) {
case 'name':
var baseFieldID = Zotero.ItemFields.getID('title');
break;
case 'dateSent':
var baseFieldID = Zotero.ItemFields.getID('date');
break;
case 'homepage':
var baseFieldID = Zotero.ItemFields.getID('url');
break;
default:
var baseFieldID = null;
}
if (baseFieldID) {
await Zotero.DB.queryAsync("INSERT INTO customBaseFieldMappings VALUES (?, ?, ?)", [itemTypeID, baseFieldID, fieldID]);
}
}
await _reloadSchema();
}, { disableForeignKeys: true });
await _reloadSchema();
});
}
finally {
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=ON");
}
var s = new Zotero.Search;
s.name = "Overdue NSF Reviewers";
@ -814,26 +826,32 @@ Zotero.Schema = new function(){
}
Zotero.debug("Uninstalling nsfReviewer item type");
yield Zotero.DB.executeTransaction(async function () {
await Zotero.DB.queryAsync("DELETE FROM customItemTypeFields WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
await Zotero.DB.queryAsync("DELETE FROM customBaseFieldMappings WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
for (let fieldID of fields) {
if (Zotero.ItemFields.isCustom(fieldID)) {
await Zotero.DB.queryAsync("DELETE FROM customFields WHERE customFieldID=?", fieldID - Zotero.ItemTypes.customIDOffset);
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=OFF");
try {
yield Zotero.DB.executeTransaction(async function () {
await Zotero.DB.queryAsync("DELETE FROM customItemTypeFields WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
await Zotero.DB.queryAsync("DELETE FROM customBaseFieldMappings WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
for (let fieldID of fields) {
if (Zotero.ItemFields.isCustom(fieldID)) {
await Zotero.DB.queryAsync("DELETE FROM customFields WHERE customFieldID=?", fieldID - Zotero.ItemTypes.customIDOffset);
}
}
}
await Zotero.DB.queryAsync("DELETE FROM customItemTypes WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
var searches = Zotero.Searches.getByLibrary(Zotero.Libraries.userLibraryID);
for (let search of searches) {
if (search.name == 'Overdue NSF Reviewers') {
await search.erase();
await Zotero.DB.queryAsync("DELETE FROM customItemTypes WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
var searches = Zotero.Searches.getByLibrary(Zotero.Libraries.userLibraryID);
for (let search of searches) {
if (search.name == 'Overdue NSF Reviewers') {
await search.erase();
}
}
}
await _reloadSchema();
}.bind(this), { disableForeignKeys: true });
await _reloadSchema();
});
}
finally {
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=ON");
}
ps.alert(null, "Zotero Item Type Removed", "The 'NSF Reviewer' item type has been uninstalled.");
}

View file

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

View file

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

View file

@ -550,6 +550,10 @@ Zotero.Sync.Storage.Local = {
}
sql = "UPDATE itemAttachments SET syncState=? WHERE itemID IN (" + sql + ")";
await Zotero.DB.queryAsync(sql, [this.SYNC_STATE_TO_UPLOAD].concat(params));
var library = Zotero.Libraries.get(libraryID);
library.storageVersion = -1;
await library.save();
}.bind(this));
},

View file

@ -1,276 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
/**
* Stream listener that can handle both download and upload requests
*
* Possible properties of data object:
* - onStart: f(request)
* - onProgress: f(request, progress, progressMax)
* - onStop: f(request, status, response)
* - onCancel: f(request, status)
* - streams: array of streams to close on completion
*/
Zotero.Sync.Storage.StreamListener = function (data) {
this._data = data;
}
Zotero.Sync.Storage.StreamListener.prototype = {
_channel: null,
// nsIProgressEventSink
onProgress: function (request, progress, progressMax) {
Zotero.debug("onProgress with " + progress + "/" + progressMax);
this._onProgress(request, progress, progressMax);
},
onStatus: function (request, status, statusArg) {
Zotero.debug('onStatus with ' + status);
},
// nsIRequestObserver
// Note: For uploads, this isn't called until data is done uploading
onStartRequest: function (request) {
Zotero.debug('onStartRequest');
this._response = "";
this._onStart(request);
},
onStopRequest: function (request, status) {
Zotero.debug('onStopRequest with ' + status);
// Some errors from https://developer.mozilla.org/en-US/docs/Table_Of_Errors
var msg = "";
switch (status) {
// Normal
case 0:
break;
// NS_BINDING_ABORTED
case 0x804b0002:
msg = "Request cancelled";
break;
// NS_ERROR_NET_INTERRUPT
case 0x804B0047:
msg = "Request interrupted";
break;
// NS_ERROR_NET_TIMEOUT
case 0x804B000E:
msg = "Request timed out";
break;
default:
msg = "Request failed";
break;
}
if (msg) {
msg += " in Zotero.Sync.Storage.StreamListener.onStopRequest() (" + status + ")";
Components.utils.reportError(msg);
Zotero.debug(msg, 1);
}
this._onStop(request, status);
},
// nsIWebProgressListener
onProgressChange: function (wp, request, curSelfProgress,
maxSelfProgress, curTotalProgress, maxTotalProgress) {
//Zotero.debug("onProgressChange with " + curTotalProgress + "/" + maxTotalProgress);
// onProgress gets called too, so this isn't necessary
//this._onProgress(request, curTotalProgress, maxTotalProgress);
},
onStateChange: function (wp, request, stateFlags, status) {
Zotero.debug("onStateChange with " + stateFlags);
if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_REQUEST) {
if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_START) {
this._onStart(request);
}
else if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP) {
this._onStop(request, status);
}
}
},
onStatusChange: function (progress, request, status, message) {
Zotero.debug("onStatusChange with '" + message + "'");
},
onLocationChange: function () {
Zotero.debug('onLocationChange');
},
onSecurityChange: function () {
Zotero.debug('onSecurityChange');
},
// nsIStreamListener
onDataAvailable: function (request, stream, sourceOffset, length) {
Zotero.debug('onDataAvailable');
var scriptableInputStream =
Components.classes["@mozilla.org/scriptableinputstream;1"]
.createInstance(Components.interfaces.nsIScriptableInputStream);
scriptableInputStream.init(stream);
var data = scriptableInputStream.read(length);
Zotero.debug(data);
this._response += data;
},
// nsIChannelEventSink
//
// If this._data.onChannelRedirect exists, it should return a promise resolving to true to
// follow the redirect or false to cancel it
onChannelRedirect: Zotero.Promise.coroutine(function* (oldChannel, newChannel, flags) {
Zotero.debug('onChannelRedirect');
if (this._data && this._data.onChannelRedirect) {
let result = yield this._data.onChannelRedirect(oldChannel, newChannel, flags);
if (!result) {
oldChannel.cancel(Components.results.NS_BINDING_ABORTED);
newChannel.cancel(Components.results.NS_BINDING_ABORTED);
Zotero.debug("Cancelling redirect");
// TODO: Prevent onStateChange error
return false;
}
}
// if redirecting, store the new channel
this._channel = newChannel;
}),
asyncOnChannelRedirect: function (oldChan, newChan, flags, redirectCallback) {
Zotero.debug('asyncOnRedirect');
this.onChannelRedirect(oldChan, newChan, flags)
.then(function (result) {
redirectCallback.onRedirectVerifyCallback(
result ? Components.results.NS_SUCCEEDED : Components.results.NS_FAILED
);
})
.catch(function (e) {
Zotero.logError(e);
redirectCallback.onRedirectVerifyCallback(Components.results.NS_FAILED);
});
},
// nsIHttpEventSink
onRedirect: function (oldChannel, newChannel) {
Zotero.debug('onRedirect');
var newURL = Zotero.HTTP.getDisplayURI(newChannel.URI).spec;
Zotero.debug("Redirecting to " + newURL);
},
//
// Private methods
//
_onStart: function (request) {
Zotero.debug('Starting request');
if (this._data && this._data.onStart) {
this._data.onStart(request);
}
},
_onProgress: function (request, progress, progressMax) {
if (this._data && this._data.onProgress) {
this._data.onProgress(request, progress, progressMax);
}
},
_onStop: function (request, status) {
var cancelled = status == 0x804b0002; // NS_BINDING_ABORTED
if (!cancelled && status == 0 && request instanceof Components.interfaces.nsIHttpChannel) {
request.QueryInterface(Components.interfaces.nsIHttpChannel);
try {
status = request.responseStatus;
}
catch (e) {
Zotero.debug("Request responseStatus not available", 1);
status = 0;
}
Zotero.debug('Request ended with status code ' + status);
request.QueryInterface(Components.interfaces.nsIRequest);
}
else {
Zotero.debug('Request ended with status ' + status);
status = 0;
}
if (this._data.streams) {
for (let stream of this._data.streams) {
stream.close();
}
}
if (cancelled) {
if (this._data.onCancel) {
this._data.onCancel(request, status);
}
}
else {
if (this._data.onStop) {
this._data.onStop(request, status, this._response);
}
}
this._channel = null;
},
// nsIInterfaceRequestor
getInterface: function (iid) {
try {
return this.QueryInterface(iid);
}
catch (e) {
throw Components.results.NS_NOINTERFACE;
}
},
QueryInterface: function(iid) {
if (iid.equals(Components.interfaces.nsISupports) ||
iid.equals(Components.interfaces.nsIInterfaceRequestor) ||
iid.equals(Components.interfaces.nsIChannelEventSink) ||
iid.equals(Components.interfaces.nsIProgressEventSink) ||
iid.equals(Components.interfaces.nsIHttpEventSink) ||
iid.equals(Components.interfaces.nsIStreamListener) ||
iid.equals(Components.interfaces.nsIWebProgressListener)) {
return this;
}
throw Components.results.NS_NOINTERFACE;
},
_safeSpec: function (uri) {
return uri.scheme + '://' + uri.username + ':********@'
+ uri.hostPort + uri.pathQueryRef
},
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,8 +5,8 @@ pdfReader-selectArea = Pažymėti sritį
pdfReader-draw = Piešti
pdfReader-highlightAnnotation = Paryškinimas
pdfReader-underlineAnnotation = Pabraukimas
pdfReader-noteAnnotation = Note Annotation
pdfReader-textAnnotation = Text Annotation
pdfReader-noteAnnotation = Pastaba
pdfReader-textAnnotation = Tekstinė anotacija
pdfReader-imageAnnotation = Image Annotation
pdfReader-find-in-document = Paieška dokumente
pdfReader-move-annotation-start-key =
@ -14,13 +14,13 @@ pdfReader-move-annotation-start-key =
[macos] { general-key-command }
*[other] { general-key-alt }
}
pdfReader-a11yMoveAnnotation = Use the arrow keys to move the annotation.
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-a11yResizeAnnotation = To resize the annotation, hold { general-key-shift } and use the arrow keys.
pdfReader-a11yMoveAnnotation = Spausdami rodyklių klavišus galite pastumti anotaciją.
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 = 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-a11yAnnotationCreated = { $type } created.
pdfReader-a11yAnnotationSelected = { $type } selected.
-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-a11yAnnotationCreated = { $type } sukurtas.
pdfReader-a11yAnnotationSelected = { $type } pasirinktas.
-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-toolbar-highlight =
.aria-description = { -pdfReader-a11yTextualAnnotationInstruction(number: 1) }
@ -38,15 +38,15 @@ pdfReader-toolbar-area =
.aria-description = { -pdfReader-a11yAnnotationInstruction(number: 5) }
.title = { pdfReader-selectArea }
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 }
pdfReader-findInDocumentInput =
.title = Ieškoti
.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.
pdfReader-import-from-epub =
.label = Import Ebook Annotations…
pdfReader-import-from-epub-prompt-title = Import Ebook Annotations
.label = Importuoti el. knygos anotacijas…
pdfReader-import-from-epub-prompt-title = Importuoti el. knygos anotacijas
pdfReader-import-from-epub-prompt-text =
{ -app-name } found { $count ->
[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.
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.key "S">
<!ENTITY saveCmd.accesskey "r">
<!ENTITY pageSetupCmd.label "Puslapio nuostatos...">
<!ENTITY pageSetupCmd.label "Puslapio nuostatos…">
<!ENTITY pageSetupCmd.accesskey "n">
<!ENTITY printCmd.label "Spausdinti...">
<!ENTITY printCmd.key "P">
@ -23,8 +23,8 @@
<!ENTITY closeCmd.key "W">
<!ENTITY closeCmd.accesskey "U">
<!ENTITY manageAttachments.label "Tvarkyti pridėtas rinkmenas">
<!ENTITY convertToStored.label "Nuorodas pakeisti tikromis rinkmenomis...">
<!ENTITY importCmd.label "Importuoti...">
<!ENTITY convertToStored.label "Nuorodas pakeisti tikromis rinkmenomis…">
<!ENTITY importCmd.label "Importuoti…">
<!ENTITY importCmd.key "I">
<!ENTITY importFromClipboardCmd.label "Importuoti iškarpinės turinį">
<!ENTITY quitApplicationCmdWin.label "Išeiti">
@ -61,7 +61,7 @@
<!ENTITY toolsMenu.label "Priemonės">
<!ENTITY toolsMenu.accesskey "P">
<!ENTITY installConnector.label "Install Browser Connector…">
<!ENTITY installConnector.label "Įdiegti papildinį naršyklei…">
<!ENTITY installConnector.accesskey "C">
<!ENTITY addons.label "Priedai">
<!ENTITY developer.label "Programuotojams">
@ -82,7 +82,7 @@
<!ENTITY helpFeedbackPage.label "Diskusijų forumai">
<!ENTITY helpFeedbackPage.accesskey "I">
<!ENTITY helpReportErrors.label "Pranešti apie klaidas...">
<!ENTITY helpReportErrors.label "Pranešti apie klaidas…">
<!ENTITY helpReportErrors.accesskey "r">
<!ENTITY debugOutputLogging.label "Derinimo pranešimų išvedimas">
@ -90,7 +90,7 @@
<!ENTITY debugOutputLogging.submit "Siųsti išvedimą">
<!ENTITY debugOutputLogging.view "Rodyti 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">

View file

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

View file

@ -1358,7 +1358,7 @@ noteEditor.deleteTable=Ištrinti lentelę
pdfReader.annotations=Anotacijos
pdfReader.showAnnotations=Rodyti anotacijas
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.noExtractedText=Tekstas neišgautas
pdfReader.addComment=Pridėti komentarą
@ -1448,8 +1448,8 @@ pdfReader.epubAppearance.pageWidth.revert=Naudoti numatytąjį puslapio plotį
pdfReader.convertToHighlight=Paversti paryškinimu
pdfReader.convertToUnderline=Paversti pabraukimu
pdfReader.size=Dydis
pdfReader.merge=Merge
pdfReader.copyLink=Copy Link
pdfReader.merge=Apjungti
pdfReader.copyLink=Kopijuoti nuorodą
spellCheck.checkSpelling=Rašybos tikrinimas
spellCheck.addRemoveDictionaries=Pridėti / šalinti žodynus...

View file

@ -5,11 +5,11 @@
<!ENTITY zotero.preferences.default "Standard:">
<!ENTITY zotero.preferences.items "items">
<!ENTITY zotero.preferences.period ".">
<!ENTITY zotero.preferences.custom "Custom…">
<!ENTITY zotero.preferences.custom "Eigendefinert…">
<!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.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">
@ -29,7 +29,7 @@
<!ENTITY zotero.preferences.reader.tabsTitle.label "Show tabs as">
<!ENTITY zotero.preferences.reader.tabsTitle.creatorYearTitle "Creator - Year - Title">
<!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.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 "Nullstill">
<!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">
@ -111,7 +111,7 @@
<!ENTITY zotero.preferences.quickCopy.copyAsHTML "Kopier som HTML">
<!ENTITY zotero.preferences.quickCopy.siteEditor.setings "Innstillingar">
<!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.locale "Språk">
<!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.updated "Oppdatert">
<!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.focusLibrariesPane "Focus Libraries Pane">

View file

@ -513,7 +513,7 @@ new-collection-dialog =
new-collection-name = Namn:
new-collection-create-in = Create in:
attachment-info-title = Tittel
attachment-info-filename = Filename
attachment-info-filename = Filnamn
attachment-info-accessed = Lest
attachment-info-pages = Sidetal
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.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.filename=Filename
pane.item.attachments.filename=Filnamn
pane.item.noteEditor.clickHere=Klikk her
pane.item.tags.icon.user=Brukerdefinert tagg:
pane.item.tags.icon.automatic=Automatisk tagg:
@ -1344,7 +1344,7 @@ noteEditor.removeCitations=Hide Annotation Citations
noteEditor.image=Bilete
noteEditor.math=Math
noteEditor.table=Table
noteEditor.citation=Citation
noteEditor.citation=Sitering
noteEditor.copyImage=Copy Image
noteEditor.saveImageAs=Save Image As…
noteEditor.insertRowBefore=Insert Row Above

View file

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

View file

@ -45,8 +45,8 @@ pdfReader-findInDocumentInput =
.placeholder = { pdfReader-find-in-document }
.aria-description = 要将搜索结果转换为高亮注释,请按 { general-key-control }-{ option-or-alt }-1。要将搜索结果转换为下划线注释,请按 { general-key-control }-{ option-or-alt }-2。
pdfReader-import-from-epub =
.label = Import Ebook Annotations…
pdfReader-import-from-epub-prompt-title = Import Ebook Annotations
.label = 导入电子书注释…
pdfReader-import-from-epub-prompt-title = 导入电子书注释
pdfReader-import-from-epub-prompt-text =
{ -app-name } found { $count ->
[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.
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.accesskey "T">
<!ENTITY installConnector.label "Install Browser Connector…">
<!ENTITY installConnector.label "安装浏览器扩展…">
<!ENTITY installConnector.accesskey "C">
<!ENTITY addons.label "插件">
<!ENTITY developer.label "开发者">

View file

@ -24,7 +24,7 @@ general-help = 帮助
general-tag = 标签
general-done = 完成
general-view-troubleshooting-instructions = 查看故障排除说明
general-go-back = Go Back
general-go-back = 返回
citation-style-label = 参考文献样式:
language-label = 语言:
menu-file-show-in-finder =
@ -66,9 +66,9 @@ menu-deletePermanently =
menu-tools-plugins =
.label = 插件
menu-view-columns-move-left =
.label = Move Column Left
.label = 将列移至左侧
menu-view-columns-move-right =
.label = Move Column Right
.label = 将列移至右侧
main-window-command =
.label = Library
main-window-key =
@ -283,7 +283,7 @@ integration-editBibliography-add-button =
integration-editBibliography-remove-button =
.aria-label = { general-remove }
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-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.

View file

@ -1358,7 +1358,7 @@ noteEditor.deleteTable=删除表格
pdfReader.annotations=注释
pdfReader.showAnnotations=显示注释
pdfReader.searchAnnotations=搜索注释
pdfReader.searchOutline=Search Outline
pdfReader.searchOutline=搜索大纲
pdfReader.noAnnotations=创建注释使其在侧栏可见
pdfReader.noExtractedText=无提取的文本
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.capitalizeTitles", false);
pref("extensions.zotero.launchNonNativeFiles", false);
pref("extensions.zotero.naturalSorting", true);
pref("extensions.zotero.sortNotesChronologically", false);
pref("extensions.zotero.sortNotesChronologically.reader", true);
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;
}
#condition-tooltips tooltip
{
background: red !important;
}
#condition-tooltips hbox > label
{
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) {
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
await waitForFrames(5);
}

View file

@ -925,7 +925,8 @@ describe("Zotero.Attachments", function() {
await item.saveTx();
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.ok(attachment);
var json = attachment.toJSON();
@ -961,7 +962,8 @@ describe("Zotero.Attachments", function() {
await item.saveTx();
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.ok(attachment);
var json = attachment.toJSON();
@ -979,7 +981,8 @@ describe("Zotero.Attachments", function() {
await item.saveTx();
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.ok(attachment);
var json = attachment.toJSON();
@ -996,6 +999,7 @@ describe("Zotero.Attachments", function() {
item.setField('url', url);
await item.saveTx();
var attachment = await Zotero.Attachments.addAvailableFile(item);
// URL, redirect target URL
assert.equal(requestStub.callCount, 2);
var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', url));
@ -1017,7 +1021,7 @@ describe("Zotero.Attachments", function() {
await item.saveTx();
var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 3);
assert.equal(requestStub.callCount, 4);
var call1 = requestStub.getCall(0);
assert.isTrue(call1.calledWith('GET', 'https://doi.org/' + doi));
var call2 = requestStub.getCall(1);
@ -1041,7 +1045,7 @@ describe("Zotero.Attachments", function() {
await item.saveTx();
var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 4);
assert.equal(requestStub.callCount, 5);
// Check the DOI (and get nothing)
var call = requestStub.getCall(0);
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]);
assert.equal(requestStub.callCount, 2);
assert.isAbove(requestStubCallTimes[1] - requestStubCallTimes[0], 998);
// 2 URLs and 2 downloads
assert.equal(requestStub.callCount, 4);
assert.isAbove(requestStubCallTimes[2] - requestStubCallTimes[0], 998);
// Make sure both items have attachments
assert.equal(item1.numAttachments(), 1);
assert.equal(item2.numAttachments(), 1);
@ -1119,7 +1124,7 @@ describe("Zotero.Attachments", function() {
item2.setField('url', url2);
await item2.saveTx();
// DOI URL resolves to 'website2' domain without PDF
// DOI URL resolves to 'website2' domain with PDF
var url3 = doiPrefix + doi6;
var item3 = createUnsavedDataObject('item', { itemType: 'journalArticle' });
item3.setField('title', 'Test');
@ -1128,19 +1133,22 @@ describe("Zotero.Attachments", function() {
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(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
assert.equal(requestStub.getCall(3).args[1], doiPrefix + doi6);
assert.equal(requestStub.getCall(4).args[1], doiPrefix + doi6);
// 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
assert.equal(requestStub.getCall(5).args[1], pageURL4);
assert.equal(requestStub.getCall(7).args[1], pageURL4);
// '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(item2.numAttachments(), 0);
@ -1162,10 +1170,13 @@ describe("Zotero.Attachments", function() {
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(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);
// Make sure both items have attachments
assert.equal(item1.numAttachments(), 1);
@ -1180,9 +1191,10 @@ describe("Zotero.Attachments", function() {
await item.saveTx();
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(1).args[1], pageURL1)
assert.equal(requestStub.getCall(2).args[1], pdfURL)
assert.ok(attachment);
var json = attachment.toJSON();
assert.equal(json.url, pdfURL);
@ -1228,7 +1240,7 @@ describe("Zotero.Attachments", function() {
var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 4);
assert.equal(requestStub.callCount, 5);
var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
var call = requestStub.getCall(1);
@ -1266,7 +1278,7 @@ describe("Zotero.Attachments", function() {
var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 4);
assert.equal(requestStub.callCount, 5);
var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
var call = requestStub.getCall(1);
@ -1302,7 +1314,7 @@ describe("Zotero.Attachments", function() {
var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 4);
assert.equal(requestStub.callCount, 5);
var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
call = requestStub.getCall(1);
@ -1342,7 +1354,7 @@ describe("Zotero.Attachments", function() {
var attachment = await Zotero.Attachments.addAvailableFile(item);
assert.equal(requestStub.callCount, 5);
assert.equal(requestStub.callCount, 6);
var call = requestStub.getCall(0);
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
call = requestStub.getCall(1);

View file

@ -691,6 +691,23 @@ describe("Zotero.CollectionTree", function() {
assert.equal(cv.selection.focused, 0);
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 () {

View file

@ -2,25 +2,43 @@
{
"id": "339d0202-d99f-48a2-aa0d-9b0c5631af26",
"type": "highlight",
"color":
{
"color": {
"r": 255,
"g": 245,
"b": 173
},
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
"positions":
[
"positions": [
{
"top_left":
{
"x": 429.74257247464135,
"y": 770.1238821199208
"top_left": {
"x": 191.5049,
"y": 588.5418
},
"bottom_right":
{
"x": 572.5401236420382,
"y": 680.1292839565452
"bottom_right": {
"x": 461.6047999999994,
"y": 576.1542
},
"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
}

View file

@ -18,7 +18,7 @@
],
"created": "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": [
{
"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');
// Fake a right-click
doc.popupNode = parent;
itemBox._popupNode = parent;
menupopup.openPopup(
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 menupopup = itemBox.querySelector('#zotero-creator-transform-menu');
// Fake a right-click
doc.popupNode = firstlast;
itemBox._popupNode = firstlast;
menupopup.openPopup(
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);
});
});
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 () {
it("should handle Extra in non-strict mode", function () {
it("should preserve Extra", function () {
var json = {
itemType: "journalArticle",
title: "Test",
@ -2834,6 +2834,45 @@ describe("Zotero.Item", function () {
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 () {
var json = {
itemType: "book",
@ -2941,6 +2980,28 @@ describe("Zotero.Item", function () {
};
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* () {

View file

@ -1640,7 +1640,7 @@ describe("Zotero.ItemTree", function() {
cellText = win.document.querySelector('#zotero-items-tree .row.selected .cell.title .cell-text');
}
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 () {
var server, httpd, httpdURL, importers;
const getImporter = () => {
const getImporter = ({ importNotes = false } = {}) => {
const importer = new Zotero_Import_Mendeley();
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);
return importer;
};
@ -14,7 +22,7 @@ describe('Zotero_Import_Mendeley', function () {
before(async () => {
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");
const port = 16213;
httpd = new HttpServer();
@ -22,7 +30,7 @@ describe('Zotero_Import_Mendeley', function () {
httpd.start(port);
httpd.registerFile(
'/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/', {
method: 'GET',
url: `profiles/v2/me?`,
url: `profiles/v2/me`,
status: 200,
headers: {},
json: JSON.parse(
@ -104,7 +112,7 @@ describe('Zotero_Import_Mendeley', function () {
setHTTPResponse(server, 'https://api.mendeley.com/', {
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
headers: {
Location: `${httpdURL}/file1.pdf`
@ -526,7 +534,7 @@ describe('Zotero_Import_Mendeley', function () {
annotation.annotationText = 'Highlight text';
annotation.annotationComment = 'Highlight comment';
annotation.annotationPageLabel = '57';
annotation.addRelation('mendeleyDB:annotationUUID', '84f12446-3b49-4052-bbdc-832d28e1e072');
annotation.addRelation('mendeleyDB:annotationUUID', '339d0202-d99f-48a2-aa0d-9b0c5631af26');
await annotation.saveTx();
let mendeleyNotebook = JSON.parse(
@ -544,5 +552,85 @@ describe('Zotero_Import_Mendeley', function () {
await attachment.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 called = 0;
var stub = sinon.stub(Zotero.HTTP, 'request').callsFake(function (method, url, options) {
// Forward saveSnapshot request
if (url.endsWith('saveSnapshot')) {
// Forward saveSnapshot and PDF requests
if (url.endsWith('saveSnapshot') || url.endsWith('test.pdf')) {
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 () {
var libraryID = Zotero.Libraries.userLibraryID;

View file

@ -95,6 +95,31 @@ describe("Item Tags Box", function () {
// New empty tag should have focus
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');
});
});
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();
});
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* () {
Zotero.HTTP.mock = null;
httpd.registerPathHandler(

View file

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

View file

@ -1692,6 +1692,38 @@ describe("ZoteroPane", function() {
assert.equal(parentItem.getAttachments().length, 4);
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 () {

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

View file

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