mirror of
https://github.com/zotero/zotero.git
synced 2026-10-12 03:48:16 +00:00
Compare commits
109 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a611179d1 | ||
|
|
7dc8d13543 | ||
|
|
244487b4de | ||
|
|
0b97822e90 | ||
|
|
3aef6cddcb | ||
|
|
4cc52ec928 | ||
|
|
0d5d43f930 | ||
|
|
7ffd1d4504 | ||
|
|
1c2ec3fd75 | ||
|
|
7eeb666894 | ||
|
|
bd451196b0 | ||
|
|
427e8daaf9 | ||
|
|
a7e1035b77 | ||
|
|
ec59f43a68 | ||
|
|
76b8556c16 | ||
|
|
79feeaecb1 | ||
|
|
bfe28b5918 | ||
|
|
730d3b1f34 | ||
|
|
9fb961fd63 | ||
|
|
ea4f5fee60 | ||
|
|
e700f5ca98 | ||
|
|
ebecf0f4cc | ||
|
|
9129625dd0 | ||
|
|
a2b7d0674e | ||
|
|
e6f15d006f | ||
|
|
ce851ae42f | ||
|
|
bb79e10851 | ||
|
|
226ba8b489 | ||
|
|
cd856efef8 | ||
|
|
1dad1ae0f8 | ||
|
|
3dfe2a5321 | ||
|
|
c8c752663f | ||
|
|
bbf4692b99 | ||
|
|
56f182f816 | ||
|
|
6053321766 | ||
|
|
9828edc2e2 | ||
|
|
35064caddd | ||
|
|
601358f24b | ||
|
|
f4b531e657 | ||
|
|
c93a0e0e80 | ||
|
|
096cc2e27b | ||
|
|
5ae1986f79 | ||
|
|
66d83ec4c8 | ||
|
|
b250aed79d | ||
|
|
85b97f82e6 | ||
|
|
64b444752a | ||
|
|
0542010659 | ||
|
|
4b78b2c130 | ||
|
|
7f6a07b665 | ||
|
|
fbd3fd5ceb | ||
|
|
6ddc360145 | ||
|
|
63b06f4346 | ||
|
|
71375b15a5 | ||
|
|
c6b7829501 | ||
|
|
1bad050d7f | ||
|
|
239530c515 | ||
|
|
828e0c942a | ||
|
|
cf37dfb330 | ||
|
|
6af5a484fd | ||
|
|
7de623accf | ||
|
|
10b3b5f915 | ||
|
|
051280fc43 | ||
|
|
264f55545a | ||
|
|
184671f919 | ||
|
|
24c7f146b3 | ||
|
|
707c71506f | ||
|
|
569bd1ffae | ||
|
|
62d46d337a | ||
|
|
ce0f7c0378 | ||
|
|
d2c9a310a8 | ||
|
|
b4bf86915b | ||
|
|
8804effba4 | ||
|
|
dfde08852a | ||
|
|
006a787272 | ||
|
|
0424faee42 | ||
|
|
3c9dcc94bc | ||
|
|
4bc61273a0 | ||
|
|
59b3268d01 | ||
|
|
ddb1daabbc | ||
|
|
d0debc0bed | ||
|
|
b5b59062a7 | ||
|
|
65461de4f5 | ||
|
|
b9e46ec660 | ||
|
|
22541ff8d8 | ||
|
|
f7b4cf907b | ||
|
|
36838e6a9a | ||
|
|
5b427ac332 | ||
|
|
65c77ddaae | ||
|
|
489d747b44 | ||
|
|
09813be7e0 | ||
|
|
85b111de4a | ||
|
|
788c48cc26 | ||
|
|
e3ae991bb3 | ||
|
|
e4a21b174f | ||
|
|
06fe4ad346 | ||
|
|
2019ac39a1 | ||
|
|
02b48318d7 | ||
|
|
f836e74996 | ||
|
|
1321447374 | ||
|
|
16221ac348 | ||
|
|
c0c00752e7 | ||
|
|
9fc00782de | ||
|
|
b9d1f50559 | ||
|
|
77bdb0b4cb | ||
|
|
320399dca9 | ||
|
|
8c54b98483 | ||
|
|
a0009f0fa3 | ||
|
|
cb0b2f9d58 | ||
|
|
a74117d824 |
178 changed files with 6171 additions and 11253 deletions
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,9 +1,9 @@
|
|||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
# Version of Gecko to build with
|
||||
GECKO_VERSION_MAC="115.9.1esr"
|
||||
GECKO_VERSION_LINUX="115.14.0esr"
|
||||
GECKO_VERSION_WIN="115.14.0esr"
|
||||
GECKO_VERSION_MAC="115.24.0esr"
|
||||
GECKO_VERSION_LINUX="115.24.0esr"
|
||||
GECKO_VERSION_WIN="115.24.0esr"
|
||||
RUST_VERSION=1.69.0
|
||||
|
||||
# URL prefix for custom builds of Firefox components
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 13c92c090c12f6555bf6bab98780025e93d75077
|
||||
Subproject commit 01b4a2abfcdaa35f478168af805df627cfedcb6a
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -333,6 +333,7 @@ function modify_omni {
|
|||
replace_line 'Manage Your Extensions' 'Manage Your Plugins' localization/en-US/toolkit/about/aboutAddons.ftl
|
||||
replace_line '= Extensions' '= Plugins' localization/en-US/toolkit/about/aboutAddons.ftl
|
||||
replace_line 'Add-on' 'Plugin' localization/en-US/toolkit/about/aboutAddons.ftl
|
||||
replace_line 'Select add-on to install' 'Select plugin to install' localization/en-US/toolkit/about/aboutAddons.ftl
|
||||
|
||||
# Hide Recommendations tab in sidebar and recommendations in main pane
|
||||
replace_line 'function isDiscoverEnabled\(\) \{' 'function isDiscoverEnabled() {return false;' chrome/toolkit/content/mozapps/extensions/aboutaddonsCommon.js
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
|
|
|
|||
|
|
@ -969,99 +969,6 @@ function ZoteroProtocolHandler() {
|
|||
}
|
||||
};
|
||||
|
||||
var ConnectorChannel = function(uri, data) {
|
||||
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
|
||||
.getService(Components.interfaces.nsIScriptSecurityManager);
|
||||
|
||||
this.name = uri;
|
||||
this.URI = ios.newURI(uri, "UTF-8", null);
|
||||
this.owner = (secMan.getCodebasePrincipal || secMan.getSimpleCodebasePrincipal)(this.URI);
|
||||
this._isPending = true;
|
||||
|
||||
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
|
||||
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
this._stream = converter.convertToInputStream(data);
|
||||
this.contentLength = this._stream.available();
|
||||
}
|
||||
|
||||
ConnectorChannel.prototype.contentCharset = "UTF-8";
|
||||
ConnectorChannel.prototype.contentType = "text/html";
|
||||
ConnectorChannel.prototype.notificationCallbacks = null;
|
||||
ConnectorChannel.prototype.securityInfo = null;
|
||||
ConnectorChannel.prototype.status = 0;
|
||||
ConnectorChannel.prototype.loadGroup = null;
|
||||
ConnectorChannel.prototype.loadFlags = 393216;
|
||||
|
||||
ConnectorChannel.prototype.__defineGetter__("originalURI", function() { return this.URI });
|
||||
ConnectorChannel.prototype.__defineSetter__("originalURI", function() { });
|
||||
|
||||
ConnectorChannel.prototype.asyncOpen = function(streamListener) {
|
||||
if(this.loadGroup) this.loadGroup.addRequest(this, null);
|
||||
streamListener.onStartRequest(this);
|
||||
streamListener.onDataAvailable(this, this._stream, 0, this.contentLength);
|
||||
streamListener.onStopRequest(this, this.status);
|
||||
this._isPending = false;
|
||||
if(this.loadGroup) this.loadGroup.removeRequest(this, null, 0);
|
||||
}
|
||||
|
||||
ConnectorChannel.prototype.isPending = function() {
|
||||
return this._isPending;
|
||||
}
|
||||
|
||||
ConnectorChannel.prototype.cancel = function(status) {
|
||||
this.status = status;
|
||||
this._isPending = false;
|
||||
if(this._stream) this._stream.close();
|
||||
}
|
||||
|
||||
ConnectorChannel.prototype.suspend = function() {}
|
||||
|
||||
ConnectorChannel.prototype.resume = function() {}
|
||||
|
||||
ConnectorChannel.prototype.open = function() {
|
||||
return this._stream;
|
||||
}
|
||||
|
||||
ConnectorChannel.prototype.QueryInterface = function(iid) {
|
||||
if (!iid.equals(Components.interfaces.nsIChannel) && !iid.equals(Components.interfaces.nsIRequest) &&
|
||||
!iid.equals(Components.interfaces.nsISupports)) {
|
||||
throw Components.results.NS_ERROR_NO_INTERFACE;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* zotero://connector/
|
||||
*
|
||||
* URI spoofing for transferring page data across boundaries
|
||||
*/
|
||||
var ConnectorExtension = new function() {
|
||||
this.loadAsChrome = false;
|
||||
|
||||
this.newChannel = function(uri) {
|
||||
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
|
||||
.getService(Components.interfaces.nsIScriptSecurityManager);
|
||||
var Zotero = Components.classes["@zotero.org/Zotero;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
|
||||
try {
|
||||
var originalURI = uri.pathQueryRef.substr('zotero://connector/'.length);
|
||||
originalURI = decodeURIComponent(originalURI);
|
||||
if(!Zotero.Server.Connector.Data[originalURI]) {
|
||||
return null;
|
||||
} else {
|
||||
return new ConnectorChannel(originalURI, Zotero.Server.Connector.Data[originalURI]);
|
||||
}
|
||||
} catch(e) {
|
||||
Zotero.debug(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
zotero://pdf.js/viewer.html
|
||||
zotero://pdf.js/pdf/1/ABCD5678
|
||||
|
|
@ -1249,7 +1156,6 @@ function ZoteroProtocolHandler() {
|
|||
this._extensions[ZOTERO_SCHEME + "://timeline"] = TimelineExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://select"] = SelectExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://debug"] = DebugExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://connector"] = ConnectorExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://pdf.js"] = PDFJSExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://open"] = OpenExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://open-pdf"] = OpenExtension;
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ var ZoteroAdvancedSearch = new function() {
|
|||
isTrash: () => false
|
||||
};
|
||||
|
||||
this.itemsView.changeCollectionTreeRow(collectionTreeRow);
|
||||
// Focus the first field in the window
|
||||
Services.focus.moveFocus(window, null, Services.focus.MOVEFOCUS_FORWARD, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ window.Zotero_File_Interface_Bibliography = new function () {
|
|||
}
|
||||
}
|
||||
|
||||
if (_io.supportedNotes?.length < 1) {
|
||||
if (_io.supportedNotes?.length < 2) {
|
||||
styleConfigurator.toggleAttribute("no-multi-notes", true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -736,9 +736,9 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
return numSelected;
|
||||
}
|
||||
|
||||
// If there's a single item and it's in the trash, switch to that
|
||||
if (items.length == 1 && items[0].deleted) {
|
||||
Zotero.debug("Item is deleted; switching to trash");
|
||||
// If there's a single item and it or an ancestor are in the trash, switch to that
|
||||
if (items.length == 1 && items[0].isInTrash()) {
|
||||
Zotero.debug("Item is in trash; switching to trash");
|
||||
await this.selectTrash(libraryID);
|
||||
}
|
||||
else {
|
||||
|
|
@ -1326,6 +1326,13 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
return this.getRow(this.selection.focused).editable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the underlying view is editable
|
||||
*/
|
||||
get filesEditable() {
|
||||
return this.getRow(this.selection.focused).filesEditable;
|
||||
}
|
||||
|
||||
getRowString(index) {
|
||||
// During filtering, context rows return an empty string to not be selectable
|
||||
// with key-based navigation
|
||||
|
|
|
|||
|
|
@ -1692,6 +1692,16 @@ var Columns = class {
|
|||
}
|
||||
}
|
||||
this._columns.sort((a, b) => a.ordinal - b.ordinal);
|
||||
|
||||
// Recover from scenarios where a plugin disables the "title" column and
|
||||
// does not provide its own primary column, or is later removed and the
|
||||
// "title" column is never restored.
|
||||
let hasPrimaryColumn = this._columns.some(c => c.primary && !c.hidden);
|
||||
if (!hasPrimaryColumn) {
|
||||
Zotero.debug(`VirtualizedTable: Missing primary column, re-enabling the "title" column.`);
|
||||
this._columns.find(c => c.dataKey === 'title').hidden = false;
|
||||
}
|
||||
|
||||
this._adjustColumnWidths();
|
||||
this.onResize(Object.fromEntries(this._columns.map(c => [c.dataKey, c.width])));
|
||||
this._storePrefs(prefs);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -221,6 +221,12 @@
|
|||
if (!(val instanceof Zotero.Item)) {
|
||||
throw new Error("'item' must be a Zotero.Item");
|
||||
}
|
||||
// Blur editable-text of old attachment. Otherwise, _handleTitleBlur() would fire after
|
||||
// the new item is set but before updateInfo() sets the title field to have the new
|
||||
// item's value, causing the old item's title to be set on the new item.
|
||||
if (this._item && val.id != this._item.id && document.activeElement.closest("editable-text")) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
if (val.isAttachment()) {
|
||||
this._item = val;
|
||||
this.hidden = false;
|
||||
|
|
@ -322,7 +328,6 @@
|
|||
|
||||
async asyncRender() {
|
||||
if (!this.item) return;
|
||||
if (this._asyncRendering) return;
|
||||
if (!this._section.open) return;
|
||||
if (this._isAlreadyRendered("async")) {
|
||||
if (this._previewDiscarded) {
|
||||
|
|
|
|||
|
|
@ -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")];
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -129,8 +129,6 @@
|
|||
|
||||
_contextMenuTarget = null;
|
||||
|
||||
_disableScrollHandler = false;
|
||||
|
||||
get container() {
|
||||
return this._container;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@
|
|||
let removeAllItemTags = this._id('remove-all-item-tags');
|
||||
this._id('remove-all-item-tags').addEventListener('command', this.removeAll);
|
||||
this.querySelector('.body').addEventListener('contextmenu', (event) => {
|
||||
removeAllItemTags.disabled = !this.count;
|
||||
removeAllItemTags.disabled = !this.editable || !this.count;
|
||||
this._id('tags-context-menu').openPopupAtScreen(event.screenX, event.screenY, true);
|
||||
});
|
||||
// Register our observer with priority 101 (after Zotero.Tags) so we get updated tag colors
|
||||
|
|
@ -82,6 +82,9 @@
|
|||
if (this._item == val) {
|
||||
return;
|
||||
}
|
||||
// Blur events do not fire when item is changed by clicking on an itemTree row.
|
||||
// Make sure any changes to the tags get saved before item is updated.
|
||||
this.blurOpenField();
|
||||
this._item = val;
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +135,7 @@
|
|||
|
||||
this._tagColors = Zotero.Tags.getColors(this.item.libraryID);
|
||||
|
||||
let focusedTag = this._id('rows').querySelector('editable-text:focus')?.value;
|
||||
let focusedTag = this._id('rows').querySelector('editable-text:focus-within')?.value;
|
||||
|
||||
let tagRows = this._id('rows');
|
||||
tagRows.replaceChildren();
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -656,13 +656,22 @@ var Zotero_QuickFormat = new function () {
|
|||
}).map(t => t.data.itemID);
|
||||
if (!itemIDs.length) return [];
|
||||
|
||||
let items = itemIDs.map((itemID) => {
|
||||
let item = Zotero.Items.get(itemID);
|
||||
// Fetch top-most items and load necessary data, in case tabs belong to an unloaded library
|
||||
let items = [];
|
||||
for (let itemID of itemIDs) {
|
||||
let item = await Zotero.Items.getAsync(itemID);
|
||||
if (item && item.parentItemID) {
|
||||
itemID = item.parentItemID;
|
||||
item = await Zotero.Items.getAsync(item.parentItemID);
|
||||
}
|
||||
return Zotero.Cite.getItem(itemID);
|
||||
});
|
||||
// Ignore tabs from libraries that are not specified
|
||||
if (io.filterLibraryIDs) {
|
||||
let itemInLibrary = io.filterLibraryIDs.some(id => id === item.libraryID);
|
||||
if (!itemInLibrary) continue;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
await Zotero.Items.loadDataTypes(items);
|
||||
|
||||
let matchedItems = new Set(items);
|
||||
if (options.searchString) {
|
||||
Zotero.debug("QuickFormat: Searching open tabs");
|
||||
|
|
@ -2287,7 +2296,8 @@ var Zotero_QuickFormat = new function () {
|
|||
}
|
||||
}
|
||||
// Arrow up closes the popup
|
||||
if (event.key === "ArrowUp") {
|
||||
// (except on locator type dropdown to allow for default handling)
|
||||
if (event.key === "ArrowUp" && event.target.tagName !== "menulist") {
|
||||
skipInputRefocus = true;
|
||||
itemPopover.hidePopup();
|
||||
return stopEvent();
|
||||
|
|
|
|||
|
|
@ -3097,7 +3097,10 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
// Accessibility
|
||||
div.setAttribute('role', 'treeitem');
|
||||
div.setAttribute('aria-level', this.getLevel(index) + 1);
|
||||
if (!this.isContainerEmpty(index)) {
|
||||
if (this.isContainerEmpty(index)) {
|
||||
div.removeAttribute('aria-expanded');
|
||||
}
|
||||
else {
|
||||
div.setAttribute('aria-expanded', this.isContainerOpen(index));
|
||||
}
|
||||
if (rowData.contextRow) {
|
||||
|
|
|
|||
|
|
@ -59,13 +59,13 @@
|
|||
|
||||
<separator class="thin"/>
|
||||
|
||||
<hbox id="button-bar" align="center">
|
||||
<hbox pack="start" flex="1">
|
||||
<button label="Toggle" onclick="toggleLocateEngines()" flex="1"/>
|
||||
<button id="locateManager-restoreDefaults" label="&zotero.preferences.locate.restoreDefaults;" oncommand="restoreDefaultLocateEngines()" flex="1"/>
|
||||
<hbox id="button-bar">
|
||||
<hbox pack="start">
|
||||
<button label="Toggle" onclick="toggleLocateEngines()" />
|
||||
<button id="locateManager-restoreDefaults" label="&zotero.preferences.locate.restoreDefaults;" oncommand="restoreDefaultLocateEngines()" />
|
||||
</hbox>
|
||||
<hbox pack="end" flex="2">
|
||||
<button disabled="true" id="locateManager-delete" label="-" oncommand="deleteLocateEngine()" flex="0.5"/>
|
||||
<hbox pack="end">
|
||||
<button disabled="true" id="locateManager-delete" label="-" oncommand="deleteLocateEngine()" />
|
||||
</hbox>
|
||||
</hbox>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
|
|
|||
|
|
@ -593,7 +593,7 @@ Zotero_Preferences.Attachment_Base_Directory = {
|
|||
},
|
||||
|
||||
|
||||
changePath: Zotero.Promise.coroutine(function* (basePath) {
|
||||
changePath: async function (basePath) {
|
||||
Zotero.debug(`New base directory is ${basePath}`);
|
||||
|
||||
if (Zotero.File.directoryContains(Zotero.DataDirectory.dir, basePath)) {
|
||||
|
|
@ -605,38 +605,37 @@ Zotero_Preferences.Attachment_Base_Directory = {
|
|||
);
|
||||
}
|
||||
|
||||
// Find all current attachments with relative attachment paths
|
||||
var sql = "SELECT itemID FROM itemAttachments WHERE linkMode=? AND path LIKE ?";
|
||||
var params = [
|
||||
Zotero.Attachments.LINK_MODE_LINKED_FILE,
|
||||
Zotero.Attachments.BASE_PATH_PLACEHOLDER + "%"
|
||||
];
|
||||
var oldRelativeAttachmentIDs = yield Zotero.DB.columnQueryAsync(sql, params);
|
||||
|
||||
//Find all attachments on the new base path
|
||||
// Find all attachments on the new base path
|
||||
var sql = "SELECT itemID FROM itemAttachments WHERE linkMode=?";
|
||||
var params = [Zotero.Attachments.LINK_MODE_LINKED_FILE];
|
||||
var allAttachments = yield Zotero.DB.columnQueryAsync(sql, params);
|
||||
var allAttachments = await Zotero.DB.columnQueryAsync(sql, params);
|
||||
var newAttachmentPaths = {};
|
||||
var numNewAttachments = 0;
|
||||
var numOldAttachments = 0;
|
||||
for (let i=0; i<allAttachments.length; i++) {
|
||||
let attachmentID = allAttachments[i];
|
||||
for (let attachmentID of allAttachments) {
|
||||
let attachmentPath;
|
||||
let relPath = false
|
||||
let relPath;
|
||||
|
||||
try {
|
||||
let attachment = yield Zotero.Items.getAsync(attachmentID);
|
||||
let attachment = await Zotero.Items.getAsync(attachmentID);
|
||||
// This will return FALSE for relative paths if base directory
|
||||
// isn't currently set
|
||||
attachmentPath = attachment.getFilePath();
|
||||
// Get existing relative path
|
||||
let storedPath = attachment.attachmentPath;
|
||||
if (storedPath.startsWith(Zotero.Attachments.BASE_PATH_PLACEHOLDER)) {
|
||||
relPath = storedPath.substr(Zotero.Attachments.BASE_PATH_PLACEHOLDER.length);
|
||||
relPath = storedPath.substring(Zotero.Attachments.BASE_PATH_PLACEHOLDER.length);
|
||||
// Use platform-specific slashes, which PathUtils.joinRelative() requires below
|
||||
relPath = Zotero.Attachments.fixPathSlashes(relPath);
|
||||
}
|
||||
|
||||
// If a file with the same relative path exists within the new base directory,
|
||||
// don't touch the attachment, since it will continue to work
|
||||
if (await IOUtils.exists(PathUtils.joinRelative(basePath, relPath))) {
|
||||
Zotero.debug(`${relPath} found within new base path -- skipping`);
|
||||
numNewAttachments++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
// Don't deal with bad attachment paths. Just skip them.
|
||||
|
|
@ -644,16 +643,6 @@ Zotero_Preferences.Attachment_Base_Directory = {
|
|||
continue;
|
||||
}
|
||||
|
||||
// If a file with the same relative path exists within the new base directory,
|
||||
// don't touch the attachment, since it will continue to work
|
||||
if (relPath) {
|
||||
if (yield IOUtils.exists(PathUtils.joinRelative(basePath, relPath))) {
|
||||
Zotero.debug(`${relPath} found within new base path -- skipping`);
|
||||
numNewAttachments++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Files within the new base directory need to be updated to use
|
||||
// relative paths (or, if the new base directory is an ancestor or
|
||||
// descendant of the old one, new relative paths)
|
||||
|
|
@ -674,7 +663,7 @@ Zotero_Preferences.Attachment_Base_Directory = {
|
|||
}
|
||||
}
|
||||
|
||||
//Confirm change of the base path
|
||||
// Confirm change of the base path
|
||||
var ps = Services.prompt;
|
||||
|
||||
var chooseStrPrefix = 'attachmentBasePath.chooseNewPath.';
|
||||
|
|
@ -730,7 +719,7 @@ Zotero_Preferences.Attachment_Base_Directory = {
|
|||
Zotero.Prefs.set('saveRelativeAttachmentPath', true);
|
||||
// Resave all attachments on base path (so that their paths become relative)
|
||||
// and all other relative attachments (so that their paths become absolute)
|
||||
yield Zotero.Utilities.Internal.forEachChunkAsync(
|
||||
await Zotero.Utilities.Internal.forEachChunkAsync(
|
||||
Object.keys(newAttachmentPaths),
|
||||
100,
|
||||
function (chunk) {
|
||||
|
|
@ -747,12 +736,12 @@ Zotero_Preferences.Attachment_Base_Directory = {
|
|||
skipDateModifiedUpdate: true
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}),
|
||||
},
|
||||
|
||||
|
||||
clearPath: Zotero.Promise.coroutine(function* () {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}),
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<html id="run-js">
|
||||
<html id="run-js" windowtype="zotero:run-js">
|
||||
<head>
|
||||
<title data-l10n-id="runJS-title"></title>
|
||||
<link href="chrome://zotero-platform/content/zotero.css" rel="stylesheet"/>
|
||||
|
|
@ -28,8 +28,9 @@
|
|||
</div>
|
||||
|
||||
<div class="textbox-container">
|
||||
<div class="textbox-header">
|
||||
<label id="result-label" class="textbox-label" for="result" data-l10n-id="runJS-result"/>
|
||||
<div class="textbox-header result-header">
|
||||
<label id="result-label" class="textbox-label" for="result" data-l10n-id="runJS-result"></label>
|
||||
<div id="loading-spinner" class="zotero-spinner-16"></div>
|
||||
</div>
|
||||
<textarea id="result" readonly aria-disabled="true"></textarea>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ async function run() {
|
|||
var isAsync = document.getElementById('run-as-async').checked;
|
||||
var result;
|
||||
var resultTextbox = document.getElementById('result');
|
||||
var spinner = document.getElementById("loading-spinner");
|
||||
spinner.setAttribute("status", "animate");
|
||||
try {
|
||||
if (isAsync) {
|
||||
code = '(async function () {' + code + '})()';
|
||||
|
|
@ -26,10 +28,27 @@ async function run() {
|
|||
catch (e) {
|
||||
resultTextbox.classList.add('error');
|
||||
resultTextbox.textContent = e;
|
||||
spinner.removeAttribute("status");
|
||||
return;
|
||||
}
|
||||
// Hide the spinner after a small delay so it briefly appears even
|
||||
// if the code runs fast to indicate that everything did run
|
||||
setTimeout(() => {
|
||||
spinner.removeAttribute("status");
|
||||
}, 100);
|
||||
|
||||
resultTextbox.classList.remove('error');
|
||||
resultTextbox.textContent = typeof result == 'string' ? result : Zotero.Utilities.varDump(result);
|
||||
if (typeof result == 'string') {
|
||||
resultTextbox.textContent = result;
|
||||
}
|
||||
else if (result !== undefined) {
|
||||
resultTextbox.textContent = Zotero.Utilities.varDump(result);
|
||||
}
|
||||
else {
|
||||
// when nothing is returned, log undefined as the return value but
|
||||
// for clarity also add a note that the JS run was successful
|
||||
resultTextbox.textContent = `===>undefined<=== (${Zotero.getString("runJS-completed")})`;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -228,7 +228,8 @@ var Zotero_Tabs = new function () {
|
|||
});
|
||||
};
|
||||
|
||||
this.restoreState = function (tabs) {
|
||||
this.restoreState = async function (tabs) {
|
||||
let itemIDs = [];
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
let tab = tabs[i];
|
||||
if (tab.type === 'library') {
|
||||
|
|
@ -250,11 +251,17 @@ var Zotero_Tabs = new function () {
|
|||
data: tab.data,
|
||||
select: tab.selected
|
||||
});
|
||||
itemIDs.push(tab.data.itemID);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unset the previously selected tab id, because it was set when restoring tabs
|
||||
this._prevSelectedID = null;
|
||||
// Some items may belong to groups that are not yet loaded. Load them here so that
|
||||
// every component related to tabs (e.g. tabs menu) does not have to handle
|
||||
// potentially not-yet-loaded items.
|
||||
let items = await Zotero.Items.getAsync(itemIDs);
|
||||
await Zotero.Items.loadDataTypes(items);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -399,7 +406,8 @@ var Zotero_Tabs = new function () {
|
|||
null,
|
||||
{
|
||||
tabIndex: tab.index,
|
||||
openInBackground: true
|
||||
openInBackground: true,
|
||||
allowDuplicate: true
|
||||
}
|
||||
));
|
||||
if (tab.index > maxIndex) {
|
||||
|
|
|
|||
|
|
@ -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;">
|
||||
|
||||
|
|
|
|||
|
|
@ -681,7 +681,7 @@ Zotero.Attachments = new function () {
|
|||
*/
|
||||
this.createURLAttachmentFromTemporaryStorageDirectory = async function (options) {
|
||||
if (!options.directory) throw new Error("'directory' not provided");
|
||||
if (!options.libraryID) throw new Error("'libraryID' not provided");
|
||||
if (!options.libraryID && !options.parentItemID) throw new Error("'libraryID' or 'parentItemID' not provided");
|
||||
if (!options.filename) throw new Error("'filename' not provided");
|
||||
if (!options.url) throw new Error("'directory' not provided");
|
||||
if (!options.contentType) throw new Error("'contentType' not provided");
|
||||
|
|
@ -921,20 +921,7 @@ Zotero.Attachments = new function () {
|
|||
}
|
||||
}
|
||||
else {
|
||||
Zotero.debug("Saving file with saveURI()");
|
||||
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
|
||||
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
|
||||
.createInstance(nsIWBP);
|
||||
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_FROM_CACHE;
|
||||
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService);
|
||||
var nsIURL = ioService.newURI(url, null, null);
|
||||
var deferred = Zotero.Promise.defer();
|
||||
wbp.progressListener = new Zotero.WebProgressFinishListener(function () {
|
||||
deferred.resolve();
|
||||
});
|
||||
Zotero.Utilities.Internal.saveURI(wbp, nsIURL, tmpFile);
|
||||
yield deferred.promise;
|
||||
yield Zotero.HTTP.download(url, tmpFile);
|
||||
}
|
||||
|
||||
var attachmentItem;
|
||||
|
|
@ -990,6 +977,96 @@ Zotero.Attachments = new function () {
|
|||
|
||||
return attachmentItem;
|
||||
});
|
||||
|
||||
/**
|
||||
* Save an attachment from a nsIInputStream
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {String} options.url
|
||||
* @param {nsIStream} options.stream - Stream with data
|
||||
* @param {Integer} options.byteCount - Number of bytes in the stream, usually from the
|
||||
* 'Content-Length' HTTP header.
|
||||
* @param {String} options.contentType - Expected content type
|
||||
* @param {Integer} [options.libraryID] Parent item ID if child attachment
|
||||
* @param {Integer} [options.parentItemID] Parent item ID if child attachment
|
||||
* Either options.libraryID or options.parentItemID are mandatory
|
||||
* @param {Array<String|Integer>} [options.collections] Collection ids or keys
|
||||
* @param {String} [options.title]
|
||||
* @param {Object} [options.saveOptions] - Options to pass to Zotero.Item::save()
|
||||
* @return {Promise<Zotero.Item>} - A promise for the created attachment item
|
||||
*/
|
||||
this.importFromNetworkStream = async (options) => {
|
||||
if (!options.url) throw new Error("'url' not provided");
|
||||
if (!options.stream) throw new Error("'stream' not provided");
|
||||
if (!options.byteCount) throw new Error("'byteCount' not provided");
|
||||
if (!options.contentType) throw new Error("'contentType' not provided");
|
||||
Zotero.debug("Importing attachment item from network stream");
|
||||
|
||||
let url = options.url;
|
||||
let stream = options.stream;
|
||||
let contentType = options.contentType;
|
||||
let libraryID = options.libraryID;
|
||||
let parentItemID = options.parentItemID;
|
||||
let collections = options.collections;
|
||||
let title = options.title;
|
||||
let saveOptions = options.saveOptions;
|
||||
|
||||
if (parentItemID && collections) {
|
||||
throw new Error("parentItemID and collections cannot both be provided");
|
||||
}
|
||||
|
||||
// Create a temporary file
|
||||
let filename;
|
||||
if (parentItemID) {
|
||||
let parentItem = Zotero.Items.get(parentItemID);
|
||||
let fileBaseName = this.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
|
||||
let ext = this._getExtensionFromURL(url, contentType);
|
||||
filename = fileBaseName + (ext != '' ? '.' + ext : '');
|
||||
}
|
||||
else {
|
||||
filename = Zotero.File.truncateFileName(this._getFileNameFromURL(url, contentType), 100);
|
||||
}
|
||||
|
||||
let tmpDirectory = (await this.createTemporaryStorageDirectory()).path;
|
||||
let destDirectory;
|
||||
let attachmentItem;
|
||||
try {
|
||||
let tmpFile = OS.Path.join(tmpDirectory, filename);
|
||||
await Zotero.File.putNetworkStream(tmpFile, stream, options.byteCount);
|
||||
|
||||
attachmentItem = await this.createURLAttachmentFromTemporaryStorageDirectory({
|
||||
directory: tmpDirectory,
|
||||
libraryID,
|
||||
parentItemID,
|
||||
title,
|
||||
filename,
|
||||
url,
|
||||
contentType,
|
||||
collections,
|
||||
saveOptions
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(e, 1);
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
if (tmpDirectory) {
|
||||
await OS.File.removeDir(tmpDirectory, { ignoreAbsent: true });
|
||||
}
|
||||
if (destDirectory) {
|
||||
await OS.File.removeDir(destDirectory, { ignoreAbsent: true });
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(e, 1);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return attachmentItem;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -1100,20 +1177,18 @@ Zotero.Attachments = new function () {
|
|||
|
||||
let enforcingFileType = false;
|
||||
try {
|
||||
await new Zotero.Promise(function (resolve) {
|
||||
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
|
||||
.createInstance(Components.interfaces.nsIWebBrowserPersist);
|
||||
if (options.cookieSandbox) {
|
||||
options.cookieSandbox.attachToInterfaceRequestor(wbp);
|
||||
let headers = {};
|
||||
if (options.referrer) {
|
||||
headers.Referer = options.referrer;
|
||||
}
|
||||
await Zotero.HTTP.download(
|
||||
url,
|
||||
path,
|
||||
{
|
||||
headers,
|
||||
cookieSandbox: options.cookieSandbox
|
||||
}
|
||||
|
||||
wbp.progressListener = new Zotero.WebProgressFinishListener(() => resolve());
|
||||
var headers = {};
|
||||
if (options.referrer) {
|
||||
headers.Referer = options.referrer;
|
||||
}
|
||||
Zotero.Utilities.Internal.saveURI(wbp, url, path, headers);
|
||||
});
|
||||
);
|
||||
|
||||
if (options.enforceFileType) {
|
||||
enforcingFileType = true;
|
||||
|
|
@ -2348,7 +2423,11 @@ Zotero.Attachments = new function () {
|
|||
value = value.toLowerCase().replace(/\s+/g, '_');
|
||||
break;
|
||||
case 'camel':
|
||||
value = value.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase());
|
||||
value = value.toLowerCase().replace(/[^\p{L}\d]+(.)/gu, (m, chr) => chr.toUpperCase());
|
||||
break;
|
||||
case 'pascal':
|
||||
value = value.toLowerCase().replace(/[^\p{L}\d]+(.)/gu, (m, chr) => chr.toUpperCase());
|
||||
value = value.slice(0, 1).toUpperCase() + value.slice(1);
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
|
|
@ -2413,6 +2492,9 @@ Zotero.Attachments = new function () {
|
|||
obj[name] = (args) => {
|
||||
return common(commonCreators(name, args), args);
|
||||
};
|
||||
obj[`${name}Count`] = (args) => {
|
||||
return common(getSlicedCreatorsOfType(name, Infinity).length.toString(), args);
|
||||
};
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -208,14 +208,14 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('filesEditable', function ()
|
|||
}
|
||||
var libraryID = this.ref.libraryID;
|
||||
if (this.isGroup()) {
|
||||
return this.ref.filesEditable;
|
||||
return this.ref.editable && this.ref.filesEditable;
|
||||
}
|
||||
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled() || this.isRetracted()) {
|
||||
var type = Zotero.Libraries.get(libraryID).libraryType;
|
||||
if (type == 'group') {
|
||||
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
|
||||
var group = Zotero.Groups.get(groupID);
|
||||
return group.filesEditable;
|
||||
return group.editable && group.filesEditable;
|
||||
}
|
||||
throw ("Unknown library type '" + type + "' in Zotero.CollectionTreeRow.filesEditable");
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -2183,6 +2183,22 @@ Zotero.Item.prototype.setSourceKey = function(sourceItemKey) {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine whether the item or any of its ancestors is in the trash
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Zotero.Item.prototype.isInTrash = function () {
|
||||
if (this.deleted) {
|
||||
return true;
|
||||
}
|
||||
if (this.isTopLevelItem()) {
|
||||
return false;
|
||||
}
|
||||
return this.parentItem.isInTrash();
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
//
|
||||
//
|
||||
|
|
@ -2540,12 +2556,15 @@ Zotero.Item.prototype.numNonHTMLFileAttachments = function () {
|
|||
};
|
||||
|
||||
|
||||
Zotero.Item.prototype.numFileAttachmentsWithContentType = function (contentType) {
|
||||
Zotero.Item.prototype.getFileAttachmentsWithContentType = function (contentType) {
|
||||
this._requireData('childItems');
|
||||
return this.getAttachments()
|
||||
.map(itemID => Zotero.Items.get(itemID))
|
||||
.filter(item => item.isFileAttachment() && item.attachmentContentType == contentType)
|
||||
.length;
|
||||
return Zotero.Items.get(this.getAttachments())
|
||||
.filter(item => item.isFileAttachment() && item.attachmentContentType == contentType);
|
||||
};
|
||||
|
||||
|
||||
Zotero.Item.prototype.numFileAttachmentsWithContentType = function (contentType) {
|
||||
return this.getFileAttachmentsWithContentType(contentType).length;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -3958,7 +3977,8 @@ Zotero.Item.prototype.setAutoAttachmentTitle = function ({ ignoreAutoRenamePrefs
|
|||
// If this is the only attachment of its type on the parent item and the
|
||||
// file is being renamed, give it a default title ("PDF", "Webpage", etc.)
|
||||
let isFirstOfType = this.parentItemID
|
||||
&& this.parentItem.numFileAttachmentsWithContentType(this.attachmentContentType) <= 1;
|
||||
&& this.parentItem.getFileAttachmentsWithContentType(this.attachmentContentType)
|
||||
.every(item => item === this);
|
||||
let isBeingRenamed = ignoreAutoRenamePrefs || Zotero.Attachments.shouldAutoRenameAttachment(this);
|
||||
if (isFirstOfType && isBeingRenamed) {
|
||||
let defaultTitle = this._getDefaultTitleForAttachmentContentType();
|
||||
|
|
@ -4046,7 +4066,7 @@ for (let name of ['type', 'authorName', 'text', 'comment', 'color', 'pageLabel',
|
|||
break;
|
||||
|
||||
case 'sortIndex':
|
||||
if (!/^(\d{5}\|\d{6}\|\d{5}|\d{5}\|\d{8}|\d{8})$/.test(value)) {
|
||||
if (!/^(\d{5}\|\d{6}\|\d{5}|\d{5}\|\d{8}|\d{7,8})$/.test(value)) {
|
||||
throw new Error(`Invalid sortIndex '${value}'`);
|
||||
}
|
||||
break;
|
||||
|
|
@ -5124,6 +5144,7 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
|
|||
*/
|
||||
Zotero.Item.prototype.fromJSON = function (json, options = {}) {
|
||||
var strict = !!options.strict;
|
||||
var migrateExtra = !strict;
|
||||
|
||||
if (!json.itemType && !this._itemTypeID) {
|
||||
throw new Error("itemType property not provided");
|
||||
|
|
@ -5139,29 +5160,34 @@ Zotero.Item.prototype.fromJSON = function (json, options = {}) {
|
|||
|
||||
var isValidForType = {};
|
||||
var setFields = new Set();
|
||||
var { itemType, fields: extraFields, creators: extraCreators, extra } =
|
||||
Zotero.Utilities.Internal.extractExtraFields(
|
||||
json.extra || '',
|
||||
this,
|
||||
Object.keys(json)
|
||||
// TEMP until we move creator lines to real creators
|
||||
.concat('creators')
|
||||
);
|
||||
// If a different item type was parsed out of Extra, use that instead
|
||||
if (itemType && json.itemType != itemType) {
|
||||
itemTypeID = Zotero.ItemTypes.getID(itemType);
|
||||
this.setType(itemTypeID);
|
||||
|
||||
var extraFields = new Map();
|
||||
var extraCreators = [];
|
||||
var extra = json.extra || '';
|
||||
// Transfer valid fields from Extra to regular fields
|
||||
if (migrateExtra) {
|
||||
let itemType;
|
||||
({ itemType, fields: extraFields, creators: extraCreators, extra } =
|
||||
Zotero.Utilities.Internal.extractExtraFields(
|
||||
extra,
|
||||
this,
|
||||
Object.keys(json)
|
||||
// TEMP until we move creator lines to real creators
|
||||
.concat('creators')
|
||||
));
|
||||
// If a different item type was parsed out of Extra, use that instead
|
||||
if (itemType && json.itemType != itemType) {
|
||||
itemTypeID = Zotero.ItemTypes.getID(itemType);
|
||||
this.setType(itemTypeID);
|
||||
}
|
||||
for (let [field, value] of extraFields) {
|
||||
this.setField(field, value);
|
||||
setFields.add(field);
|
||||
extraFields.delete(field);
|
||||
}
|
||||
}
|
||||
var invalidFieldLogLines = new Map();
|
||||
|
||||
// Transfer valid fields from Extra to regular fields
|
||||
// Currently disabled
|
||||
/*for (let [field, value] of extraFields) {
|
||||
this.setField(field, value);
|
||||
setFields.add(field);
|
||||
extraFields.delete(field);
|
||||
}*/
|
||||
|
||||
for (let field in json) {
|
||||
let val = json[field];
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -662,17 +662,9 @@ Zotero.SearchConditions = new function(){
|
|||
continue;
|
||||
}
|
||||
|
||||
let localized = self.getLocalizedName(i);
|
||||
// Hack to use a different name for "issue" in French locale,
|
||||
// where 'number' and 'issue' are translated the same
|
||||
// https://forums.zotero.org/discussion/14942/
|
||||
if (Zotero.ItemFields.getName(fieldID) == 'issue' && locale.substr(0, 2) == 'fr') {
|
||||
localized = "Num\u00E9ro (p\u00E9riodique)";
|
||||
}
|
||||
|
||||
_standardConditions.push({
|
||||
name: i,
|
||||
localized: localized,
|
||||
localized: self.getLocalizedName(i),
|
||||
operators: _conditions[i]['operators'],
|
||||
flags: _conditions[i]['flags']
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -446,18 +446,79 @@ Zotero.File = new function(){
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously writes data from an nsIAsyncInputStream to a file.
|
||||
*
|
||||
* Designed to handle input streams where data may not be
|
||||
* immediately or fully available, such as network streams.
|
||||
*
|
||||
* @param {nsIInputStream} inputStream - The input stream to read from. This
|
||||
* stream should implement nsIAsyncInputStream.
|
||||
* @param {string} path - The file path where the data will be written.
|
||||
* @param {number} byteCount - The expected number of bytes to write.
|
||||
*
|
||||
* @returns {Promise<number>} A promise that resolves with the number of bytes
|
||||
* written when the operation is complete, or rejects with an error
|
||||
* if any issues occur during reading or writing.
|
||||
*/
|
||||
this.putNetworkStream = async function (path, stream, byteCount) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let bytesRead = 0;
|
||||
var os = FileUtils.openSafeFileOutputStream(new FileUtils.File(path));
|
||||
|
||||
let binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
|
||||
binaryInputStream.setInputStream(stream);
|
||||
|
||||
let readNextChunk = () => {
|
||||
stream.asyncWait({
|
||||
onInputStreamReady: (input) => {
|
||||
try {
|
||||
// Check available data in the stream
|
||||
let available = input.available();
|
||||
if (available > 0) {
|
||||
os.write(binaryInputStream.readBytes(available), available);
|
||||
bytesRead += available;
|
||||
|
||||
if (bytesRead < byteCount) {
|
||||
// Continue reading
|
||||
readNextChunk();
|
||||
}
|
||||
else {
|
||||
// Finished writing all expected bytes
|
||||
FileUtils.closeSafeFileOutputStream(os);
|
||||
resolve(bytesRead);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No more data, finish the stream
|
||||
FileUtils.closeSafeFileOutputStream(os);
|
||||
resolve(bytesRead);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
os.close();
|
||||
reject(new Components.Exception("File write operation failed", e));
|
||||
}
|
||||
}
|
||||
}, 0, 0, null);
|
||||
};
|
||||
|
||||
// Start reading the first chunk of data
|
||||
readNextChunk();
|
||||
});
|
||||
};
|
||||
|
||||
this.download = async function (uri, path) {
|
||||
var uriStr = uri.spec || uri;
|
||||
|
||||
const isHTTP = uriStr.startsWith('http');
|
||||
if (uriStr.startsWith('http')) {
|
||||
Zotero.warn("Zotero.File.download() is deprecated for HTTP(S) URLs -- use Zotero.HTTP.download()");
|
||||
return Zotero.HTTP.download(uri, path);
|
||||
}
|
||||
|
||||
Zotero.debug(`Saving ${uriStr} to ${path.pathQueryRef || path}`);
|
||||
|
||||
if (isHTTP && Zotero.HTTP.browserIsOffline()) {
|
||||
let msg = `Download failed: ${Zotero.appName} is currently offline`;
|
||||
Zotero.debug(msg, 2);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
var deferred = Zotero.Promise.defer();
|
||||
const inputChannel = NetUtil.newChannel({
|
||||
uri,
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ Zotero.FileHandlers = {
|
|||
handler = handler || systemHandler;
|
||||
if (handler) {
|
||||
if (Zotero.isMac) {
|
||||
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||
try {
|
||||
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', handler, path]);
|
||||
return true;
|
||||
|
|
@ -156,6 +157,9 @@ Zotero.FileHandlers = {
|
|||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
finally {
|
||||
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -309,11 +313,13 @@ Zotero.FileHandlers = {
|
|||
switch (appPath.toLowerCase()) {
|
||||
case 'okular':
|
||||
appPath = '/usr/bin/okular';
|
||||
break;
|
||||
|
||||
// It's "Document Viewer" on stock Ubuntu
|
||||
case 'document viewer':
|
||||
case 'evince':
|
||||
appPath = '/usr/bin/evince';
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (await IOUtils.exists('/usr/bin/okular')) {
|
||||
|
|
@ -498,8 +504,13 @@ Zotero.FileHandlers = {
|
|||
if (!Zotero.File.pathToFile(command).isExecutable()) {
|
||||
throw new Error(`${command} is not an executable`);
|
||||
}
|
||||
|
||||
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||
|
||||
// Do not await
|
||||
Zotero.Utilities.Internal.exec(command, args);
|
||||
var promise = Zotero.Utilities.Internal.exec(command, args);
|
||||
|
||||
promise.finally(() => Zotero.Utilities.Internal.Environment.restoreMozillaVariables());
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@ Zotero.Plugins = new function () {
|
|||
* https://searchfox.org/mozilla-esr60/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm#4233
|
||||
*/
|
||||
function _loadScope(addon) {
|
||||
if (scopes.has(addon.id)) {
|
||||
return;
|
||||
}
|
||||
var scope = new Cu.Sandbox(
|
||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
{
|
||||
|
|
@ -662,6 +665,7 @@ Zotero.Plugins = new function () {
|
|||
return;
|
||||
}
|
||||
Zotero.debug("Enabling plugin " + addon.id);
|
||||
_loadScope(addon);
|
||||
setDefaultPrefs(addon);
|
||||
await registerLocales(addon);
|
||||
await _callMethod(addon, 'startup', REASONS.ADDON_ENABLE);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ Zotero.Prefs = new function() {
|
|||
if (Zotero.addShutdownListener) {
|
||||
Zotero.addShutdownListener(this.unregister.bind(this));
|
||||
}
|
||||
|
||||
this._checkUserJS();
|
||||
|
||||
// Process pref version updates
|
||||
var fromVersion = this.get('prefVersion');
|
||||
|
|
@ -510,4 +512,53 @@ Zotero.Prefs = new function() {
|
|||
return `{{ ${[field, truncate, prefix, suffix].filter(f => f !== null).join(' ')} }}`;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Preferences set in <profile>/user.js override prefs.js, so they can't
|
||||
* be changed via the UI and may be hard to locate and remove. Warn if
|
||||
* user.js contains user_pref() directives.
|
||||
*/
|
||||
this._checkUserJS = async function () {
|
||||
let userJSPath = PathUtils.join(Zotero.Profile.dir, 'user.js');
|
||||
let userJS;
|
||||
try {
|
||||
userJS = await Zotero.File.getContentsAsync(userJSPath);
|
||||
}
|
||||
catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const DISALLOWED_PREF_RE = /^\s*user_pref\s*\(\s*['"]extensions\.zotero\.fileHandler\..+$/g;
|
||||
|
||||
let updatedUserJS = userJS
|
||||
.split('\n')
|
||||
.filter((prefLine) => {
|
||||
if (DISALLOWED_PREF_RE.test(prefLine)) {
|
||||
Zotero.debug('user.js contains disallowed pref: ' + prefLine);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
if (updatedUserJS === userJS) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Zotero.File.putContentsAsync(userJSPath, updatedUserJS);
|
||||
|
||||
Zotero.alert(null,
|
||||
Zotero.getString('general-error'),
|
||||
Zotero.getString('userjs-pref-warning')
|
||||
);
|
||||
Services.startup.quit(
|
||||
Components.interfaces.nsIAppStartup.eAttemptQuit
|
||||
| Components.interfaces.nsIAppStartup.eRestart
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ Zotero.Prompt = {
|
|||
Zotero.warn("Zotero.Prompt.confirm() option 'delayButtons' is deprecated -- use 'buttonDelay'");
|
||||
buttonDelay = true;
|
||||
}
|
||||
let flags = (buttonDelay && !Zotero.automatedTest) ? Services.prompt.BUTTON_DELAY_ENABLE : 0;
|
||||
let flags = (buttonDelay && !Zotero.test) ? Services.prompt.BUTTON_DELAY_ENABLE : 0;
|
||||
if (typeof button0 == 'number') flags += Services.prompt.BUTTON_POS_0 * button0;
|
||||
else if (typeof button0 == 'string') flags += Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING;
|
||||
if (typeof button1 == 'number') flags += Services.prompt.BUTTON_POS_1 * button1;
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ Zotero.RecognizeDocument = new function () {
|
|||
async function _processQueue() {
|
||||
await Zotero.Schema.schemaUpdatePromise;
|
||||
|
||||
if (_queueProcessing) return;
|
||||
_queueProcessing = true;
|
||||
if (_queueProcessing) return _queueProcessing.promise;
|
||||
_queueProcessing = Zotero.Promise.defer();
|
||||
|
||||
while (1) {
|
||||
// While all current progress queue usages are related with
|
||||
|
|
@ -83,8 +83,8 @@ Zotero.RecognizeDocument = new function () {
|
|||
throw new Error();
|
||||
}
|
||||
|
||||
let res = await _processItem(item);
|
||||
_progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, item.getField('title'));
|
||||
let parentItem = await _processItem(item);
|
||||
_progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, parentItem.getField('title'));
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
|
|
@ -99,6 +99,7 @@ Zotero.RecognizeDocument = new function () {
|
|||
}
|
||||
}
|
||||
|
||||
_queueProcessing.resolve();
|
||||
_queueProcessing = false;
|
||||
_processingItemID = null;
|
||||
}
|
||||
|
|
@ -250,9 +251,10 @@ Zotero.RecognizeDocument = new function () {
|
|||
/**
|
||||
* Processes the item and places it as a children of the new item
|
||||
* @param itemID
|
||||
* @return {Promise}
|
||||
* @return {Promise} A promise that resolves to a newly created, recognized parent item
|
||||
*/
|
||||
async function _processItem(attachment) {
|
||||
Zotero.debug(`RecognizeDocument: Recognizing attachment ${attachment.getDisplayTitle()}`);
|
||||
// Make sure the attachment still doesn't have a parent
|
||||
if (attachment.parentItemID) {
|
||||
throw new Error('Already has parent');
|
||||
|
|
@ -268,10 +270,12 @@ Zotero.RecognizeDocument = new function () {
|
|||
}
|
||||
}
|
||||
|
||||
let parentItem = await _recognize(attachment);
|
||||
let parentItem = await Zotero.RecognizeDocument._recognize(attachment);
|
||||
if (!parentItem) {
|
||||
Zotero.debug(`RecognizeDocument: No matches for attachment ${attachment.getDisplayTitle()}`);
|
||||
throw new Zotero.Exception.Alert("recognizePDF.noMatches");
|
||||
}
|
||||
Zotero.debug(`RecognizeDocument: Recognized attachment ${attachment.getDisplayTitle()}`);
|
||||
|
||||
// Put new item in same collections as the old one
|
||||
let collections = attachment.getCollections();
|
||||
|
|
@ -388,11 +392,7 @@ Zotero.RecognizeDocument = new function () {
|
|||
* @param {Zotero.Item} item
|
||||
* @return {Promise<Zotero.Item>} - New item
|
||||
*/
|
||||
async function _recognize(item) {
|
||||
if (Zotero.RecognizeDocument.recognizeStub) {
|
||||
return Zotero.RecognizeDocument.recognizeStub(item);
|
||||
}
|
||||
|
||||
this._recognize = async function (item) {
|
||||
let filePath = await item.getFilePath();
|
||||
|
||||
if (!filePath || !await OS.File.exists(filePath)) throw new Zotero.Exception.Alert('recognizePDF.fileNotFound');
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
283
chrome/content/zotero/xpcom/server/saveSession.js
Normal file
283
chrome/content/zotero/xpcom/server/saveSession.js
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2024 Corporation for Digital Scholarship
|
||||
Vienna, Virginia, USA
|
||||
http://zotero.org
|
||||
|
||||
This file is part of Zotero.
|
||||
|
||||
Zotero is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Zotero is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
Zotero.Server.Connector.SessionManager = {
|
||||
_sessions: new Map(),
|
||||
|
||||
get: function (id) {
|
||||
return this._sessions.get(id);
|
||||
},
|
||||
|
||||
create: function (id, action, requestData) {
|
||||
if (typeof id === 'undefined') {
|
||||
id = Zotero.Utilities.randomString();
|
||||
}
|
||||
if (this._sessions.has(id)) {
|
||||
throw new Error(`Session ID ${id} exists`);
|
||||
}
|
||||
Zotero.debug(`Creating connector save session ${id}`);
|
||||
var session = new Zotero.Server.Connector.SaveSession(id, action, requestData);
|
||||
this._sessions.set(id, session);
|
||||
this.gc();
|
||||
return session;
|
||||
},
|
||||
|
||||
gc: function () {
|
||||
// Delete sessions older than 10 minutes, or older than 1 minute if more than 10 sessions
|
||||
var ttl = this._sessions.size >= 10 ? 60 : 600;
|
||||
var deleteBefore = new Date() - ttl * 1000;
|
||||
|
||||
for (let session of this._sessions) {
|
||||
if (session.created < deleteBefore) {
|
||||
this._session.delete(session.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
Zotero.Server.Connector.SaveSession = class {
|
||||
constructor(id, action, requestData) {
|
||||
this.id = id;
|
||||
this.created = new Date();
|
||||
this._action = action;
|
||||
this._requestData = requestData;
|
||||
this._items = {};
|
||||
|
||||
this._progressItems = {};
|
||||
this._orderedProgressItems = [];
|
||||
}
|
||||
|
||||
async saveItems(target) {
|
||||
var { library, collection } = Zotero.Server.Connector.resolveTarget(target);
|
||||
var data = this._requestData.data;
|
||||
var headers = this._requestData.headers;
|
||||
var cookieSandbox = data.uri
|
||||
? new Zotero.CookieSandbox(
|
||||
null,
|
||||
data.uri,
|
||||
data.detailedCookies ? "" : data.cookie || "",
|
||||
headers["User-Agent"]
|
||||
)
|
||||
: null;
|
||||
if (cookieSandbox && data.detailedCookies) {
|
||||
cookieSandbox.addCookiesFromHeader(data.detailedCookies);
|
||||
}
|
||||
|
||||
var proxy = data.proxy && new Zotero.Proxy(data.proxy);
|
||||
|
||||
this.itemSaver = new Zotero.Translate.ItemSaver({
|
||||
libraryID: library.libraryID,
|
||||
collections: collection ? [collection.id] : undefined,
|
||||
// All attachments come from the Connector
|
||||
attachmentMode: Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE,
|
||||
forceTagType: 1,
|
||||
referrer: data.uri,
|
||||
cookieSandbox,
|
||||
proxy
|
||||
});
|
||||
let items = await this.itemSaver.saveItems(data.items, () => 0, () => 0);
|
||||
// If more itemSaver calls are made, it means we are saving attachments explicitly (like
|
||||
// a snapshot) and we don't want to ignore those.
|
||||
this.itemSaver.attachmentMode = Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD;
|
||||
items.forEach((item, index) => {
|
||||
this.addItem(data.items[index].id, item);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
async saveSnapshot(target) {
|
||||
var { library, collection } = Zotero.Server.Connector.resolveTarget(target);
|
||||
var libraryID = library.libraryID;
|
||||
var data = this._requestData.data;
|
||||
|
||||
let title = data.title || data.url;
|
||||
|
||||
// Create new webpage item
|
||||
let item = new Zotero.Item("webpage");
|
||||
item.libraryID = libraryID;
|
||||
item.setField("title", title);
|
||||
item.setField("url", data.url);
|
||||
item.setField("accessDate", "CURRENT_TIMESTAMP");
|
||||
if (collection) {
|
||||
item.setCollections([collection.id]);
|
||||
}
|
||||
await item.saveTx();
|
||||
|
||||
// SingleFile snapshot may be coming later
|
||||
this.addItem(data.url, item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
async addItem(key, item) {
|
||||
return this.addItems({ [key]: item });
|
||||
}
|
||||
|
||||
async addItems(items) {
|
||||
this._items = Object.assign(this._items, items);
|
||||
|
||||
// Update the items with the current target data, in case it changed since the save began
|
||||
await this._updateItems(items);
|
||||
}
|
||||
|
||||
getItemByConnectorKey(key) {
|
||||
return this._items[key];
|
||||
}
|
||||
|
||||
// documentRecognizer doesn't return recognized items and it's complicated to make it
|
||||
// do it, so we just retrieve the parent item which is a little hacky but does the job
|
||||
getRecognizedItem() {
|
||||
try {
|
||||
return Object.values(this._items)[0].parentItem;
|
||||
}
|
||||
catch (_) {}
|
||||
}
|
||||
|
||||
remove() {
|
||||
delete Zotero.Server.Connector.SessionManager._sessions[this.id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the target data for this session and update any items that have already been saved
|
||||
*/
|
||||
async update(targetID, tags) {
|
||||
var previousTargetID = this._currentTargetID;
|
||||
this._currentTargetID = targetID;
|
||||
this._currentTags = tags || "";
|
||||
|
||||
// Select new destination in collections pane
|
||||
var zp = Zotero.getActiveZoteroPane();
|
||||
if (zp && zp.collectionsView) {
|
||||
await zp.collectionsView.selectByID(targetID);
|
||||
}
|
||||
// If window is closed, select target collection re-open
|
||||
else {
|
||||
Zotero.Prefs.set('lastViewedFolder', targetID);
|
||||
}
|
||||
|
||||
await this._updateItems(this._items);
|
||||
|
||||
// If a single item was saved, select it (or its parent, if it now has one)
|
||||
if (zp && zp.collectionsView && Object.values(this._items).length == 1) {
|
||||
let item = Object.values(this._items)[0];
|
||||
item = item.isTopLevelItem() ? item : item.parentItem;
|
||||
// Don't select if in trash
|
||||
if (!item.deleted) {
|
||||
await zp.selectItem(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the passed items with the current target and tags
|
||||
*/
|
||||
_updateItems = Zotero.serial(async function (items) {
|
||||
if (Object.values(items).length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var { library, collection } = Zotero.Server.Connector.resolveTarget(this._currentTargetID);
|
||||
var libraryID = library.libraryID;
|
||||
|
||||
var tags = this._currentTags.trim();
|
||||
tags = tags ? tags.split(/\s*,\s*/).filter(x => x) : [];
|
||||
|
||||
Zotero.debug("Updating items for connector save session " + this.id);
|
||||
|
||||
for (let key in items) {
|
||||
let item = items[key];
|
||||
|
||||
// If the item is now a child item (e.g., from Retrieve Metadata), update the
|
||||
// parent item instead
|
||||
if (!item.isTopLevelItem()) {
|
||||
item = item.parentItem;
|
||||
}
|
||||
|
||||
// Skip deleted items
|
||||
if (!Zotero.Items.exists(item.id)) {
|
||||
Zotero.debug(`Item ${item.id} in save session no longer exists`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.libraryID != libraryID) {
|
||||
let newItem = await item.moveToLibrary(libraryID);
|
||||
this._items[key] = newItem;
|
||||
}
|
||||
|
||||
// Keep automatic tags
|
||||
let originalTags = item.getTags().filter(tag => tag.type == 1);
|
||||
item.setTags(originalTags.concat(tags));
|
||||
item.setCollections(collection ? [collection.id] : []);
|
||||
await item.saveTx();
|
||||
}
|
||||
|
||||
this._updateRecents();
|
||||
});
|
||||
|
||||
|
||||
_updateRecents() {
|
||||
var targetID = this._currentTargetID;
|
||||
try {
|
||||
let numRecents = 7;
|
||||
let recents = Zotero.Prefs.get('recentSaveTargets') || '[]';
|
||||
recents = JSON.parse(recents);
|
||||
// If there's already a target from this session in the list, update it
|
||||
for (let recent of recents) {
|
||||
if (recent.sessionID == this.id) {
|
||||
recent.id = targetID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If a session is found with the same target, move it to the end without changing
|
||||
// the sessionID. This could be the current session that we updated above or a different
|
||||
// one. (We need to leave the old sessionID for the same target or we'll end up removing
|
||||
// the previous target from the history if it's changed in the current one.)
|
||||
let pos = recents.findIndex(r => r.id == targetID);
|
||||
if (pos != -1) {
|
||||
recents = [
|
||||
...recents.slice(0, pos),
|
||||
...recents.slice(pos + 1),
|
||||
recents[pos]
|
||||
];
|
||||
}
|
||||
// Otherwise just add this one to the end
|
||||
else {
|
||||
recents = recents.concat([{
|
||||
id: targetID,
|
||||
sessionID: this.id
|
||||
}]);
|
||||
}
|
||||
recents = recents.slice(-1 * numRecents);
|
||||
Zotero.Prefs.set('recentSaveTargets', JSON.stringify(recents));
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
Zotero.Prefs.clear('recentSaveTargets');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -23,6 +23,9 @@
|
|||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");
|
||||
Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
Zotero.Server = new function() {
|
||||
var _onlineObserverRegistered, serv;
|
||||
this.responseCodes = {
|
||||
|
|
@ -42,50 +45,55 @@ Zotero.Server = new function() {
|
|||
504:"Gateway Timeout"
|
||||
};
|
||||
|
||||
Object.defineProperty(this, 'port', {
|
||||
get() {
|
||||
if (!serv) {
|
||||
throw new Error('Server not initialized');
|
||||
}
|
||||
return serv.identity.primaryPort;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* initializes a very rudimentary web server
|
||||
*/
|
||||
this.init = function(port, bindAllAddr, maxConcurrentConnections) {
|
||||
if (Zotero.HTTP.browserIsOffline()) {
|
||||
Zotero.debug('Browser is offline -- not initializing HTTP server');
|
||||
_registerOnlineObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
this.init = function (port) {
|
||||
if(serv) {
|
||||
Zotero.debug("Already listening on port " + serv.port);
|
||||
return;
|
||||
}
|
||||
|
||||
// start listening on socket
|
||||
serv = Components.classes["@mozilla.org/network/server-socket;1"]
|
||||
.createInstance(Components.interfaces.nsIServerSocket);
|
||||
port = port || Zotero.Prefs.get('httpServer.port');
|
||||
try {
|
||||
// bind to a random port on loopback only
|
||||
serv.init(port ? port : Zotero.Prefs.get('httpServer.port'), !bindAllAddr, -1);
|
||||
serv.asyncListen(Zotero.Server.SocketListener);
|
||||
|
||||
Zotero.debug("HTTP server listening on "+(bindAllAddr ? "*": " 127.0.0.1")+":"+serv.port);
|
||||
serv = new HttpServer();
|
||||
serv.registerPrefixHandler('/', this.handleRequest)
|
||||
serv.start(port);
|
||||
|
||||
Zotero.debug(`HTTP server listening on 127.0.0.1:${serv.identity.primaryPort}`);
|
||||
|
||||
// Close port on Zotero shutdown (doesn't apply to translation-server)
|
||||
if (Zotero.addShutdownListener) {
|
||||
Zotero.addShutdownListener(this.close.bind(this));
|
||||
}
|
||||
} catch(e) {
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
Zotero.debug("Not initializing HTTP server");
|
||||
serv = undefined;
|
||||
}
|
||||
|
||||
_registerOnlineObserver()
|
||||
};
|
||||
|
||||
this.handleRequest = function (request, response) {
|
||||
let requestHandler = new Zotero.Server.RequestHandler(request, response);
|
||||
return requestHandler.handleRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* releases bound port
|
||||
*/
|
||||
this.close = function() {
|
||||
if(!serv) return;
|
||||
serv.close();
|
||||
this.close = function () {
|
||||
if (!serv) return;
|
||||
serv.stop();
|
||||
serv = undefined;
|
||||
};
|
||||
|
||||
|
|
@ -102,294 +110,100 @@ Zotero.Server = new function() {
|
|||
}
|
||||
return decodedData;
|
||||
}
|
||||
|
||||
function _registerOnlineObserver() {
|
||||
if (_onlineObserverRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Observer to enable the integration when we go online
|
||||
var observer = {
|
||||
observe: function(subject, topic, data) {
|
||||
if (data == 'online') {
|
||||
Zotero.Server.init();
|
||||
}
|
||||
|
||||
|
||||
// A proxy headers class to make header retrieval case-insensitive
|
||||
Zotero.Server.Headers = class {
|
||||
constructor() {
|
||||
return new Proxy(this, {
|
||||
get(target, name, receiver) {
|
||||
if (typeof name !== 'string') {
|
||||
return Reflect.get(target, name, receiver);
|
||||
}
|
||||
return Reflect.get(target, name.toLowerCase(), receiver);
|
||||
},
|
||||
has(target, name, receiver) {
|
||||
if (typeof name !== 'string') {
|
||||
return Reflect.has(target, name, receiver);
|
||||
}
|
||||
return Reflect.has(target, name.toLowerCase(), receiver);
|
||||
},
|
||||
set(target, name, value, receiver) {
|
||||
return Reflect.set(target, name.toLowerCase(), value, receiver);
|
||||
}
|
||||
};
|
||||
|
||||
var observerService =
|
||||
Components.classes["@mozilla.org/observer-service;1"]
|
||||
.getService(Components.interfaces.nsIObserverService);
|
||||
observerService.addObserver(observer, "network:offline-status-changed", false);
|
||||
|
||||
_onlineObserverRegistered = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Zotero.Server.SocketListener = new function() {
|
||||
this.onSocketAccepted = onSocketAccepted;
|
||||
this.onStopListening = onStopListening;
|
||||
|
||||
/*
|
||||
* called when a socket is opened
|
||||
*/
|
||||
function onSocketAccepted(socket, transport) {
|
||||
// get an input stream
|
||||
var iStream = transport.openInputStream(0, 0, 0);
|
||||
var oStream = transport.openOutputStream(Components.interfaces.nsITransport.OPEN_BLOCKING, 0, 0);
|
||||
|
||||
var dataListener = new Zotero.Server.DataListener(iStream, oStream);
|
||||
var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"]
|
||||
.createInstance(Components.interfaces.nsIInputStreamPump);
|
||||
try {
|
||||
pump.init(iStream, 0, 0, false);
|
||||
}
|
||||
catch (e) {
|
||||
pump.init(iStream, -1, -1, 0, 0, false);
|
||||
}
|
||||
pump.asyncRead(dataListener, null);
|
||||
}
|
||||
|
||||
function onStopListening(serverSocket, status) {
|
||||
Zotero.debug("HTTP server going offline");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* handles the actual acquisition of data
|
||||
*/
|
||||
Zotero.Server.DataListener = function(iStream, oStream) {
|
||||
Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
||||
this.header = "";
|
||||
this.headerFinished = false;
|
||||
|
||||
Zotero.Server.networkStreamToString = function (stream, length) {
|
||||
let data = NetUtil.readInputStreamToString(stream, length);
|
||||
return Zotero.Utilities.Internal.decodeUTF8(data);
|
||||
};
|
||||
|
||||
|
||||
Zotero.Server.RequestHandler = function (request, response) {
|
||||
this.body = "";
|
||||
this.bodyLength = 0;
|
||||
|
||||
this.iStream = iStream;
|
||||
this.oStream = oStream;
|
||||
|
||||
this.foundReturn = false;
|
||||
}
|
||||
|
||||
/*
|
||||
* called when a request begins (although the request should have begun before
|
||||
* the DataListener was generated)
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype.onStartRequest = function(request) {}
|
||||
|
||||
/*
|
||||
* called when a request stops
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype.onStopRequest = function(request, status) {
|
||||
this.iStream.close();
|
||||
this.oStream.close();
|
||||
}
|
||||
|
||||
/*
|
||||
* called when new data is available
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype.onDataAvailable = function (request, inputStream, offset, count) {
|
||||
var readData = NetUtil.readInputStreamToString(inputStream, count);
|
||||
|
||||
if(this.headerFinished) { // reading body
|
||||
this.body += readData;
|
||||
// check to see if data is done
|
||||
this._bodyData();
|
||||
} else { // reading header
|
||||
// see if there's a magic double return
|
||||
var lineBreakIndex = readData.indexOf("\r\n\r\n");
|
||||
if(lineBreakIndex != -1) {
|
||||
if(lineBreakIndex != 0) {
|
||||
this.header += readData.substr(0, lineBreakIndex+4);
|
||||
this.body = readData.substr(lineBreakIndex+4);
|
||||
}
|
||||
|
||||
this._headerFinished();
|
||||
return;
|
||||
}
|
||||
var lineBreakIndex = readData.indexOf("\n\n");
|
||||
if(lineBreakIndex != -1) {
|
||||
if(lineBreakIndex != 0) {
|
||||
this.header += readData.substr(0, lineBreakIndex+2);
|
||||
this.body = readData.substr(lineBreakIndex+2);
|
||||
}
|
||||
|
||||
this._headerFinished();
|
||||
return;
|
||||
}
|
||||
if(this.header && this.header[this.header.length-1] == "\n" &&
|
||||
(readData[0] == "\n" || readData[0] == "\r")) {
|
||||
if(readData.length > 1 && readData[1] == "\n") {
|
||||
this.header += readData.substr(0, 2);
|
||||
this.body = readData.substr(2);
|
||||
} else {
|
||||
this.header += readData[0];
|
||||
this.body = readData.substr(1);
|
||||
}
|
||||
|
||||
this._headerFinished();
|
||||
return;
|
||||
}
|
||||
this.header += readData;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* processes an HTTP header and decides what to do
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype._headerFinished = function() {
|
||||
this.headerFinished = true;
|
||||
|
||||
Zotero.debug(this.header, 5);
|
||||
|
||||
// Parse headers into this.headers with lowercase names
|
||||
this.headers = {};
|
||||
var headerLines = this.header.trim().split(/\r\n/);
|
||||
for (let line of headerLines) {
|
||||
line = line.trim();
|
||||
let pos = line.indexOf(':');
|
||||
if (pos == -1) {
|
||||
continue;
|
||||
}
|
||||
let k = line.substr(0, pos).toLowerCase();
|
||||
let v = line.substr(pos + 1).trim();
|
||||
this.headers[k] = v;
|
||||
}
|
||||
|
||||
if (this.headers.origin) {
|
||||
this.origin = this.headers.origin;
|
||||
}
|
||||
else if (this.headers['zotero-bookmarklet']) {
|
||||
this.origin = "https://www.zotero.org";
|
||||
}
|
||||
|
||||
if (!Zotero.isServer) {
|
||||
// Make sure the Host header is set to localhost/127.0.0.1 to prevent DNS rebinding attacks
|
||||
const hostRe = /^(localhost|127\.0\.0\.1)(:[0-9]+)?$/i;
|
||||
if (!hostRe.test(this.headers.host)) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid Host header\n"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// get first line of request
|
||||
const methodRe = /^([A-Z]+) ([^ \r\n?]+)(\?[^ \r\n]+)?/;
|
||||
var method = methodRe.exec(this.header);
|
||||
|
||||
// get content-type
|
||||
var contentType = this.headers['content-type'];
|
||||
if (contentType) {
|
||||
let splitContentType = contentType.split(/\s*;/);
|
||||
this.contentType = splitContentType[0];
|
||||
}
|
||||
|
||||
if(!method) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid method specified\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.pathParams = {};
|
||||
if (Zotero.Server.Endpoints[method[2]]) {
|
||||
this.endpoint = Zotero.Server.Endpoints[method[2]];
|
||||
}
|
||||
else {
|
||||
let router = new Zotero.Router(this.pathParams);
|
||||
for (let [potentialTemplate, endpoint] of Object.entries(Zotero.Server.Endpoints)) {
|
||||
if (!potentialTemplate.includes(':')) continue;
|
||||
router.add(potentialTemplate, () => {
|
||||
this.pathParams._endpoint = endpoint;
|
||||
}, true, /* Do not allow missing params */ false);
|
||||
}
|
||||
if (router.run(method[2].split('?')[0])) { // Don't let parser handle query params - we do that already
|
||||
this.endpoint = this.pathParams._endpoint;
|
||||
delete this.pathParams._endpoint;
|
||||
delete this.pathParams.url;
|
||||
}
|
||||
else {
|
||||
this._requestFinished(this._generateResponse(404, "text/plain", "No endpoint found\n"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.pathname = method[2];
|
||||
this.query = method[3];
|
||||
|
||||
if(method[1] == "HEAD" || method[1] == "OPTIONS") {
|
||||
this._requestFinished(this._generateResponse(200));
|
||||
} else if(method[1] == "GET") {
|
||||
this._processEndpoint("GET", null); // async
|
||||
} else if(method[1] == "POST") {
|
||||
const contentLengthRe = /^([0-9]+)$/;
|
||||
|
||||
// parse content length
|
||||
var m = contentLengthRe.exec(this.headers['content-length']);
|
||||
if(!m) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Content-length not provided\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.bodyLength = parseInt(m[1]);
|
||||
this._bodyData();
|
||||
} else {
|
||||
this._requestFinished(this._generateResponse(501, "text/plain", "Method not implemented\n"));
|
||||
return;
|
||||
}
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
/*
|
||||
* checks to see if Content-Length bytes of body have been read and, if so, processes the body
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype._bodyData = function() {
|
||||
if(this.body.length >= this.bodyLength) {
|
||||
let logContentTypes = [
|
||||
'text/plain',
|
||||
'application/json'
|
||||
];
|
||||
Zotero.Server.RequestHandler.prototype._bodyData = function () {
|
||||
const PLAIN_TEXT_CONTENT_TYPES = new Set([
|
||||
'text/plain',
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded'
|
||||
]);
|
||||
|
||||
let data = null;
|
||||
if (this.bodyLength > 0) {
|
||||
if (PLAIN_TEXT_CONTENT_TYPES.has(this.contentType)) {
|
||||
this.body = data = Zotero.Server.networkStreamToString(this.request.bodyInputStream, this.bodyLength);
|
||||
}
|
||||
else if (this.contentType === 'multipart/form-data') {
|
||||
data = NetUtil.readInputStreamToString(this.request.bodyInputStream, this.bodyLength);
|
||||
try {
|
||||
data = this._decodeMultipartData(data);
|
||||
}
|
||||
catch (e) {
|
||||
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.body.length >= this.bodyLength) {
|
||||
let noLogEndpoints = [
|
||||
'/connector/saveSingleFile'
|
||||
];
|
||||
if (this.body != '{}'
|
||||
&& logContentTypes.includes(this.contentType)
|
||||
&& PLAIN_TEXT_CONTENT_TYPES.has(this.contentType)
|
||||
&& !noLogEndpoints.includes(this.pathname)) {
|
||||
Zotero.debug(Zotero.Utilities.ellipsize(this.body, 1000, false, true), 5);
|
||||
}
|
||||
// handle envelope
|
||||
this._processEndpoint("POST", this.body); // async
|
||||
}
|
||||
// handle envelope
|
||||
this._processEndpoint("POST", data); // async
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates the response to an HTTP request
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype._generateResponse = function (status, contentTypeOrHeaders, body) {
|
||||
Zotero.Server.RequestHandler.prototype._generateResponse = function (status, contentTypeOrHeaders, body) {
|
||||
var response = "HTTP/1.0 "+status+" "+Zotero.Server.responseCodes[status]+"\r\n";
|
||||
|
||||
// Translation server
|
||||
if (Zotero.isServer) {
|
||||
// Add CORS headers if Origin header matches the allowed origins
|
||||
if (this.origin) {
|
||||
let allowedOrigins = Zotero.Prefs.get('httpServer.allowedOrigins')
|
||||
.split(/, */).filter(x => x);
|
||||
let allAllowed = allowedOrigins.includes('*');
|
||||
if (allAllowed || allowedOrigins.includes(this.origin)) {
|
||||
response += "Access-Control-Allow-Origin: " + (allAllowed ? '*' : this.origin) + "\r\n";
|
||||
response += "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n";
|
||||
response += "Access-Control-Allow-Headers: Content-Type\r\n";
|
||||
response += "Access-Control-Expose-Headers: Link\r\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
// Client
|
||||
else {
|
||||
response += "X-Zotero-Version: "+Zotero.version+"\r\n";
|
||||
response += "X-Zotero-Connector-API-Version: "+CONNECTOR_API_VERSION+"\r\n";
|
||||
response += "X-Zotero-Version: "+Zotero.version+"\r\n";
|
||||
response += "X-Zotero-Connector-API-Version: "+CONNECTOR_API_VERSION+"\r\n";
|
||||
|
||||
if (this.origin === ZOTERO_CONFIG.BOOKMARKLET_ORIGIN) {
|
||||
response += "Access-Control-Allow-Origin: " + this.origin + "\r\n";
|
||||
response += "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n";
|
||||
response += "Access-Control-Allow-Headers: Content-Type,X-Zotero-Connector-API-Version,X-Zotero-Version\r\n";
|
||||
}
|
||||
if (this.origin === ZOTERO_CONFIG.BOOKMARKLET_ORIGIN) {
|
||||
response += "Access-Control-Allow-Origin: " + this.origin + "\r\n";
|
||||
response += "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n";
|
||||
response += "Access-Control-Allow-Headers: Content-Type,X-Zotero-Connector-API-Version,X-Zotero-Version\r\n";
|
||||
}
|
||||
|
||||
if (contentTypeOrHeaders) {
|
||||
|
|
@ -403,7 +217,7 @@ Zotero.Server.DataListener.prototype._generateResponse = function (status, conte
|
|||
}
|
||||
}
|
||||
|
||||
if(body) {
|
||||
if (body) {
|
||||
response += "\r\n"+body;
|
||||
} else {
|
||||
response += "Content-Length: 0\r\n\r\n";
|
||||
|
|
@ -412,34 +226,98 @@ Zotero.Server.DataListener.prototype._generateResponse = function (status, conte
|
|||
return response;
|
||||
}
|
||||
|
||||
Zotero.Server.RequestHandler.prototype.handleRequest = async function () {
|
||||
const request = this.request;
|
||||
const response = this.response;
|
||||
// Tell httpd that we will be constructing our own response
|
||||
// without its custom methods, asynchronously
|
||||
response.seizePower();
|
||||
|
||||
let requestDebug = `${request.method} ${request.path} HTTP/${request.httpVersion}\n`
|
||||
// Parse headers into this.headers with lowercase names
|
||||
this.headers = new Zotero.Server.Headers();
|
||||
for (let { data: name } of request.headers) {
|
||||
requestDebug += `${name}: ${request.getHeader(name)}\n`;
|
||||
this.headers[name.toLowerCase()] = request.getHeader(name);
|
||||
}
|
||||
|
||||
Zotero.debug(requestDebug, 5);
|
||||
|
||||
if (this.headers.origin) {
|
||||
this.origin = this.headers.origin;
|
||||
}
|
||||
|
||||
this.pathname = request.path;
|
||||
this.query = request.queryString;
|
||||
|
||||
// get content-type
|
||||
var contentType = this.headers['content-type'];
|
||||
if (contentType) {
|
||||
let splitContentType = contentType.split(/\s*;/);
|
||||
this.contentType = splitContentType[0];
|
||||
}
|
||||
|
||||
this.pathParams = {};
|
||||
if (Zotero.Server.Endpoints[this.pathname]) {
|
||||
this.endpoint = Zotero.Server.Endpoints[this.pathname];
|
||||
}
|
||||
else {
|
||||
let router = new Zotero.Router(this.pathParams);
|
||||
for (let [potentialTemplate, endpoint] of Object.entries(Zotero.Server.Endpoints)) {
|
||||
if (!potentialTemplate.includes(':')) continue;
|
||||
router.add(potentialTemplate, () => {
|
||||
this.pathParams._endpoint = endpoint;
|
||||
}, true, /* Do not allow missing params */ false);
|
||||
}
|
||||
if (router.run(this.pathname)) {
|
||||
this.endpoint = this.pathParams._endpoint;
|
||||
delete this.pathParams._endpoint;
|
||||
delete this.pathParams.url;
|
||||
}
|
||||
else {
|
||||
this._requestFinished(this._generateResponse(404, "text/plain", "No endpoint found\n"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (request.method == "HEAD" || request.method == "OPTIONS") {
|
||||
this._requestFinished(this._generateResponse(200));
|
||||
}
|
||||
else if (request.method == "GET") {
|
||||
this._processEndpoint("GET", null); // async
|
||||
}
|
||||
else if (request.method == "POST") {
|
||||
const contentLengthRe = /^([0-9]+)$/;
|
||||
|
||||
// parse content length
|
||||
var m = contentLengthRe.exec(this.headers['content-length']);
|
||||
if(!m) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Content-length not provided\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.bodyLength = parseInt(m[1]);
|
||||
this._bodyData();
|
||||
} else {
|
||||
this._requestFinished(this._generateResponse(501, "text/plain", "Method not implemented\n"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a response based on calling the function associated with the endpoint
|
||||
*
|
||||
* Note: postData contains raw bytes and should be decoded before use
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine(function* (method, postData) {
|
||||
Zotero.Server.RequestHandler.prototype._processEndpoint = async function (method, postData) {
|
||||
try {
|
||||
var endpoint = new this.endpoint;
|
||||
|
||||
// Check that endpoint supports method
|
||||
if(endpoint.supportedMethods && endpoint.supportedMethods.indexOf(method) === -1) {
|
||||
if (endpoint.supportedMethods && endpoint.supportedMethods.indexOf(method) === -1) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Endpoint does not support method\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that endpoint supports bookmarklet
|
||||
if(this.origin) {
|
||||
var isBookmarklet = this.origin === "https://www.zotero.org" || this.origin === "http://www.zotero.org";
|
||||
// Disallow bookmarklet origins to access endpoints without permitBookmarklet
|
||||
// set. We allow other origins to access these endpoints because they have to
|
||||
// be privileged to avoid being blocked by our headers.
|
||||
if(isBookmarklet && !endpoint.permitBookmarklet) {
|
||||
this._requestFinished(this._generateResponse(403, "text/plain", "Access forbidden to bookmarklet\n"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Reject browser-based requests that don't require a CORS preflight request [1] if they
|
||||
// don't come from the connector or include Zotero-Allowed-Request
|
||||
//
|
||||
|
|
@ -471,53 +349,44 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
|
|||
return;
|
||||
}
|
||||
|
||||
var decodedData = null;
|
||||
if(postData && this.contentType) {
|
||||
var data = null;
|
||||
if (method === 'POST' && this.contentType) {
|
||||
// check that endpoint supports contentType
|
||||
var supportedDataTypes = endpoint.supportedDataTypes;
|
||||
if(supportedDataTypes && supportedDataTypes != '*'
|
||||
if (supportedDataTypes && supportedDataTypes != '*'
|
||||
&& supportedDataTypes.indexOf(this.contentType) === -1) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Endpoint does not support content-type\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
// decode content-type post data
|
||||
if(this.contentType === "application/json") {
|
||||
if (this.contentType === "application/json") {
|
||||
try {
|
||||
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
|
||||
decodedData = JSON.parse(postData);
|
||||
} catch(e) {
|
||||
data = JSON.parse(postData);
|
||||
}
|
||||
catch(e) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid JSON provided\n"));
|
||||
return;
|
||||
}
|
||||
} else if(this.contentType === "application/x-www-form-urlencoded") {
|
||||
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
|
||||
decodedData = Zotero.Server.decodeQueryString(postData);
|
||||
} else if(this.contentType === "multipart/form-data") {
|
||||
let boundary = /boundary=([^\s]*)/i.exec(this.header);
|
||||
if (!boundary) {
|
||||
Zotero.debug('Invalid boundary: ' + this.header, 1);
|
||||
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
|
||||
}
|
||||
boundary = '--' + boundary[1];
|
||||
try {
|
||||
decodedData = this._decodeMultipartData(postData, boundary);
|
||||
} catch(e) {
|
||||
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
|
||||
}
|
||||
} else {
|
||||
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
|
||||
decodedData = postData;
|
||||
}
|
||||
else if (this.contentType === "application/x-www-form-urlencoded") {
|
||||
data = Zotero.Server.decodeQueryString(postData);
|
||||
}
|
||||
else if (postData) {
|
||||
data = postData;
|
||||
}
|
||||
else {
|
||||
data = this.request.bodyInputStream;
|
||||
}
|
||||
}
|
||||
|
||||
// set up response callback
|
||||
var sendResponseCallback = function (code, contentTypeOrHeaders, arg, options) {
|
||||
var sendResponseCallback = (code, contentTypeOrHeaders, arg, options) => {
|
||||
this._requestFinished(
|
||||
this._generateResponse(code, contentTypeOrHeaders, arg),
|
||||
options
|
||||
);
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
// Pass to endpoint
|
||||
//
|
||||
|
|
@ -528,30 +397,18 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
|
|||
if (endpoint.init.length === 1
|
||||
// Return value from Zotero.Promise.coroutine()
|
||||
|| endpoint.init.length === 0) {
|
||||
let headers = {};
|
||||
let headerLines = this.header.trim().split(/\r\n/);
|
||||
for (let line of headerLines) {
|
||||
line = line.trim();
|
||||
let pos = line.indexOf(':');
|
||||
if (pos == -1) {
|
||||
continue;
|
||||
}
|
||||
let k = line.substr(0, pos);
|
||||
let v = line.substr(pos + 1).trim();
|
||||
headers[k] = v;
|
||||
}
|
||||
|
||||
let maybePromise = endpoint.init({
|
||||
method,
|
||||
pathname: this.pathname,
|
||||
pathParams: this.pathParams,
|
||||
searchParams: new URLSearchParams(this.query ? this.query.substring(1) : ''),
|
||||
headers,
|
||||
data: decodedData
|
||||
searchParams: new URLSearchParams(this.query || ''),
|
||||
headers: this.headers,
|
||||
data
|
||||
});
|
||||
let result;
|
||||
if (maybePromise.then) {
|
||||
result = yield maybePromise;
|
||||
result = await maybePromise;
|
||||
}
|
||||
else {
|
||||
result = maybePromise;
|
||||
|
|
@ -565,71 +422,75 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
|
|||
}
|
||||
// Two-parameter endpoint takes data and a callback
|
||||
else if (endpoint.init.length === 2) {
|
||||
endpoint.init(decodedData, sendResponseCallback);
|
||||
endpoint.init(data, sendResponseCallback);
|
||||
}
|
||||
// Three-parameter endpoint takes a URL, data, and a callback
|
||||
else {
|
||||
const uaRe = /[\r\n]User-Agent: +([^\r\n]+)/i;
|
||||
var m = uaRe.exec(this.header);
|
||||
var url = {
|
||||
pathname: this.pathname,
|
||||
searchParams: new URLSearchParams(this.query ? this.query.substring(1) : ''),
|
||||
userAgent: m && m[1]
|
||||
searchParams: new URLSearchParams(this.query || ''),
|
||||
userAgent: this.headers['user-agent']
|
||||
};
|
||||
endpoint.init(url, decodedData, sendResponseCallback);
|
||||
endpoint.init(url, data, sendResponseCallback);
|
||||
}
|
||||
} catch(e) {
|
||||
Zotero.debug(e);
|
||||
this._requestFinished(this._generateResponse(500), "text/plain", "An error occurred\n");
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* returns HTTP data from a request
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype._requestFinished = function (response, options) {
|
||||
if(this._responseSent) {
|
||||
Zotero.Server.RequestHandler.prototype._requestFinished = function (responseBody, options) {
|
||||
if (this._responseSent) {
|
||||
Zotero.debug("Request already finished; not sending another response");
|
||||
return;
|
||||
}
|
||||
this._responseSent = true;
|
||||
|
||||
// close input stream
|
||||
this.iStream.close();
|
||||
|
||||
// open UTF-8 converter for output stream
|
||||
var intlStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
|
||||
.createInstance(Components.interfaces.nsIConverterOutputStream);
|
||||
|
||||
// write
|
||||
try {
|
||||
intlStream.init(this.oStream, "UTF-8", 1024, "?".charCodeAt(0));
|
||||
intlStream.init(this.response.bodyOutputStream, "UTF-8", 1024, "?".charCodeAt(0));
|
||||
|
||||
// Filter logged response
|
||||
if (Zotero.Debug.enabled) {
|
||||
let maxLogLength = 2000;
|
||||
let str = response;
|
||||
let str = responseBody;
|
||||
if (options && options.logFilter) {
|
||||
str = options.logFilter(str);
|
||||
}
|
||||
if (str.length > maxLogLength) {
|
||||
str = str.substr(0, maxLogLength) + `\u2026 (${response.length} chars)`;
|
||||
str = str.substr(0, maxLogLength) + `\u2026 (${responseBody.length} chars)`;
|
||||
}
|
||||
Zotero.debug(str, 5);
|
||||
}
|
||||
|
||||
intlStream.writeString(response);
|
||||
} finally {
|
||||
intlStream.close();
|
||||
intlStream.writeString(responseBody);
|
||||
}
|
||||
finally {
|
||||
this.response.finish();
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.Server.DataListener.prototype._decodeMultipartData = function(data, boundary) {
|
||||
var contentDispositionRe = /^Content-Disposition:\s*(.*)$/i;
|
||||
let contentTypeRe = /^Content-Type:\s*(.*)$/i
|
||||
Zotero.Server.RequestHandler.prototype._decodeMultipartData = function(data) {
|
||||
const contentDispositionRe = /^Content-Disposition:\s*(.*)$/i;
|
||||
const contentTypeRe = /^Content-Type:\s*(.*)$/i
|
||||
|
||||
var results = [];
|
||||
let results = [];
|
||||
|
||||
let boundary = /boundary=([^\s]*)/i.exec(this.headers['content-type']);
|
||||
if (!boundary) {
|
||||
Zotero.debug('Invalid boundary: ' + this.headers['content-type'], 1);
|
||||
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
|
||||
}
|
||||
boundary = '--' + boundary[1];
|
||||
|
||||
data = data.split(boundary);
|
||||
// Ignore pre first boundary and post last boundary
|
||||
data = data.slice(1, data.length-1);
|
||||
1240
chrome/content/zotero/xpcom/server/server_connector.js
Normal file
1240
chrome/content/zotero/xpcom/server/server_connector.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -330,21 +330,21 @@ Zotero.Sync.Storage.Local = {
|
|||
var fileName = PathUtils.filename(path);
|
||||
|
||||
try {
|
||||
// If file is already marked for upload, skip check. Even if the file was changed
|
||||
// both locally and remotely, conflicts are checked at upload time, so we don't need
|
||||
// to worry about it here.
|
||||
//
|
||||
// This is after open() so that a missing file is properly marked for download.
|
||||
if (item.attachmentSyncState == this.SYNC_STATE_TO_UPLOAD) {
|
||||
Zotero.debug("File is already marked for upload");
|
||||
return false;
|
||||
}
|
||||
|
||||
let { lastModified: fmtime } = yield IOUtils.stat(path);
|
||||
//Zotero.debug("Memory usage: " + memmgr.resident);
|
||||
|
||||
//Zotero.debug("File modification time for item " + lk + " is " + fmtime);
|
||||
|
||||
// If file is already marked for upload, skip check. Even if the file was changed
|
||||
// both locally and remotely, conflicts are checked at upload time, so we don't need
|
||||
// to worry about it here.
|
||||
//
|
||||
// This is after stat() so that a missing file is properly marked for download.
|
||||
if (item.attachmentSyncState == this.SYNC_STATE_TO_UPLOAD) {
|
||||
Zotero.debug("File is already marked for upload");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fmtime < 0) {
|
||||
Zotero.debug("File mod time " + fmtime + " is less than 0 -- interpreting as 0", 2);
|
||||
fmtime = 0;
|
||||
|
|
@ -550,6 +550,10 @@ Zotero.Sync.Storage.Local = {
|
|||
}
|
||||
sql = "UPDATE itemAttachments SET syncState=? WHERE itemID IN (" + sql + ")";
|
||||
await Zotero.DB.queryAsync(sql, [this.SYNC_STATE_TO_UPLOAD].concat(params));
|
||||
|
||||
var library = Zotero.Libraries.get(libraryID);
|
||||
library.storageVersion = -1;
|
||||
await library.save();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
};
|
||||
|
|
@ -31,10 +31,10 @@ if (!Zotero.Sync.Storage.Mode) {
|
|||
Zotero.Sync.Storage.Mode.WebDAV = function (options) {
|
||||
this.options = options;
|
||||
|
||||
this.VerificationError = function (error, uri) {
|
||||
this.VerificationError = function (error, url) {
|
||||
this.message = `WebDAV verification error (${error})`;
|
||||
this.error = error;
|
||||
this.uri = uri;
|
||||
this.url = url;
|
||||
}
|
||||
this.VerificationError.prototype = Object.create(Error.prototype);
|
||||
}
|
||||
|
|
@ -196,14 +196,23 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
throw new this.VerificationError("NO_PASSWORD");
|
||||
}
|
||||
|
||||
url = scheme + '://'
|
||||
var spec = scheme + '://'
|
||||
+ encodeURIComponent(username) + ':' + encodeURIComponent(password) + '@'
|
||||
+ url
|
||||
+ (url.endsWith('/') ? '' : '/');
|
||||
|
||||
var io = Services.io;
|
||||
this._parentURI = io.newURI(url, null, null);
|
||||
this._rootURI = io.newURI(url + "zotero/", null, null);
|
||||
try {
|
||||
this._parentURI = io.newURI(spec, null, null);
|
||||
}
|
||||
catch (e) {
|
||||
if (e.message.includes('NS_ERROR_MALFORMED_URI')) {
|
||||
let displayURL = scheme + '://' + url + (url.endsWith('/') ? '' : '/');
|
||||
throw new this.VerificationError("INVALID_URL", displayURL);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
this._rootURI = io.newURI(spec + "zotero/", null, null);
|
||||
Zotero.HTTP.CookieBlocker.addURL(this._rootURI.spec);
|
||||
},
|
||||
|
||||
|
|
@ -258,7 +267,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
* @param {Zotero.Sync.Storage.Request} request
|
||||
* @return {Promise<Zotero.Sync.Storage.Result>}
|
||||
*/
|
||||
downloadFile: Zotero.Promise.coroutine(function* (request) {
|
||||
downloadFile: async function (request) {
|
||||
var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request);
|
||||
if (!item) {
|
||||
throw new Error("Item '" + request.name + "' not found");
|
||||
|
|
@ -272,7 +281,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
}
|
||||
|
||||
// Retrieve modification time from server
|
||||
var metadata = yield this._getStorageFileMetadata(item, request);
|
||||
var metadata = await this._getStorageFileMetadata(item, request);
|
||||
|
||||
if (!request.isRunning()) {
|
||||
Zotero.debug("Download request '" + request.name
|
||||
|
|
@ -285,7 +294,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
return new Zotero.Sync.Storage.Result;
|
||||
}
|
||||
|
||||
var fileModTime = yield item.attachmentModificationTime;
|
||||
var fileModTime = await item.attachmentModificationTime;
|
||||
if (metadata.mtime == fileModTime) {
|
||||
Zotero.debug("File mod time matches remote file -- skipping download of "
|
||||
+ item.libraryKey);
|
||||
|
|
@ -293,10 +302,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
var updateItem = item.attachmentSyncState != 1
|
||||
item.attachmentSyncedModificationTime = metadata.mtime;
|
||||
item.attachmentSyncState = "in_sync";
|
||||
yield item.saveTx({ skipAll: true });
|
||||
await item.saveTx({ skipAll: true });
|
||||
// DEBUG: Necessary?
|
||||
if (updateItem) {
|
||||
yield item.updateSynced(false);
|
||||
await item.updateSynced(false);
|
||||
}
|
||||
|
||||
return new Zotero.Sync.Storage.Result({
|
||||
|
|
@ -307,9 +316,8 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
var uri = this._getItemURI(item);
|
||||
|
||||
var destPath = OS.Path.join(Zotero.getTempDirectory().path, item.key + '.tmp');
|
||||
yield Zotero.File.removeIfExists(destPath);
|
||||
await Zotero.File.removeIfExists(destPath);
|
||||
|
||||
var deferred = Zotero.Promise.defer();
|
||||
var requestData = {
|
||||
item,
|
||||
mtime: metadata.mtime,
|
||||
|
|
@ -317,88 +325,71 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
compressed: true
|
||||
};
|
||||
|
||||
var listener = new Zotero.Sync.Storage.StreamListener(
|
||||
{
|
||||
onStart: function (req) {
|
||||
if (request.isFinished()) {
|
||||
Zotero.debug("Download request " + request.name
|
||||
+ " stopped before download started -- closing channel");
|
||||
req.cancel(0x804b0002); // NS_BINDING_ABORTED
|
||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let req = await Zotero.HTTP.download(
|
||||
uri,
|
||||
destPath,
|
||||
{
|
||||
successCodes: [200, 404],
|
||||
noCache: true,
|
||||
notificationCallbacks: {
|
||||
onProgress: function (req, progress, progressMax) {
|
||||
request.onProgress(progress, progressMax)
|
||||
},
|
||||
},
|
||||
errorDelayIntervals: this.ERROR_DELAY_INTERVALS,
|
||||
errorDelayMax: this.ERROR_DELAY_MAX,
|
||||
}
|
||||
},
|
||||
onProgress: function (a, b, c) {
|
||||
request.onProgress(a, b, c)
|
||||
},
|
||||
onStop: Zotero.Promise.coroutine(function* (req, status, res) {
|
||||
request.setChannel(false);
|
||||
|
||||
if (status == 404) {
|
||||
let msg = "Remote ZIP file not found for item " + item.libraryKey;
|
||||
Zotero.debug(msg, 2);
|
||||
Components.utils.reportError(msg);
|
||||
|
||||
// Delete the orphaned prop file
|
||||
try {
|
||||
yield this._deleteStorageFiles([item.key + ".prop"]);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
|
||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
||||
return;
|
||||
}
|
||||
else if (status != 200) {
|
||||
try {
|
||||
this._throwFriendlyError("GET", dispURL, status);
|
||||
}
|
||||
catch (e) {
|
||||
deferred.reject(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't try to process if the request has been cancelled
|
||||
if (request.isFinished()) {
|
||||
Zotero.debug("Download request " + request.name
|
||||
+ " is no longer running after file download");
|
||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
||||
return;
|
||||
}
|
||||
|
||||
Zotero.debug("Finished download of " + destPath);
|
||||
);
|
||||
|
||||
if (req.status == 404) {
|
||||
let msg = "Remote ZIP file not found for item " + item.libraryKey;
|
||||
Zotero.debug(msg, 2);
|
||||
Cu.reportError(msg);
|
||||
|
||||
// Delete the orphaned prop file
|
||||
try {
|
||||
deferred.resolve(
|
||||
Zotero.Sync.Storage.Local.processDownload(requestData)
|
||||
);
|
||||
await this._deleteStorageFiles([item.key + ".prop"]);
|
||||
}
|
||||
catch (e) {
|
||||
deferred.reject(e);
|
||||
}
|
||||
}.bind(this)),
|
||||
onCancel: function (req, status) {
|
||||
Zotero.debug("Request cancelled");
|
||||
if (deferred.promise.isPending()) {
|
||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
||||
Zotero.logError(e);
|
||||
}
|
||||
|
||||
resolve(new Zotero.Sync.Storage.Result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't try to process if the request has been cancelled
|
||||
if (request.isFinished()) {
|
||||
Zotero.debug("Download request " + request.name
|
||||
+ " is no longer running after file download");
|
||||
resolve(new Zotero.Sync.Storage.Result);
|
||||
return;
|
||||
}
|
||||
|
||||
Zotero.debug("Finished download of " + destPath);
|
||||
|
||||
resolve(
|
||||
Zotero.Sync.Storage.Local.processDownload(requestData)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Don't display password in console
|
||||
var dispURL = Zotero.HTTP.getDisplayURI(uri).spec;
|
||||
Zotero.debug('Saving ' + dispURL);
|
||||
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
|
||||
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
|
||||
.createInstance(nsIWBP);
|
||||
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
|
||||
wbp.progressListener = listener;
|
||||
Zotero.Utilities.Internal.saveURI(wbp, uri, destPath);
|
||||
|
||||
return deferred.promise;
|
||||
}),
|
||||
catch (e) {
|
||||
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
|
||||
try {
|
||||
let dispURL = Zotero.HTTP.getDisplayURI(uri).spec;
|
||||
this._throwFriendlyError("GET", dispURL, e.xmlhttp.status);
|
||||
}
|
||||
catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Zotero.logError(e);
|
||||
reject(new Error(Zotero.Sync.Storage.defaultError));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
uploadFile: Zotero.Promise.coroutine(function* (request) {
|
||||
|
|
@ -641,7 +632,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
|
||||
var dav = req.getResponseHeader("DAV");
|
||||
if (dav == null) {
|
||||
throw new this.VerificationError("NOT_DAV", uri);
|
||||
throw new this.VerificationError("NOT_DAV", Zotero.HTTP.getDisplayURI(uri, true).spec);
|
||||
}
|
||||
|
||||
var headers = { Depth: 0 };
|
||||
|
|
@ -683,7 +674,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
catch (e) {
|
||||
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
|
||||
if (e.status >= 200 && e.status < 300) {
|
||||
throw new this.VerificationError("NONEXISTENT_FILE_NOT_MISSING", uri);
|
||||
throw new this.VerificationError(
|
||||
"NONEXISTENT_FILE_NOT_MISSING",
|
||||
Zotero.HTTP.getDisplayURI(uri, true).spec
|
||||
);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
|
|
@ -730,7 +724,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
// This can also be from IIS 6+, which is configured not to serve .prop files.
|
||||
// http://support.microsoft.com/kb/326965
|
||||
else if (req.status == 404) {
|
||||
throw new this.VerificationError("FILE_MISSING_AFTER_UPLOAD", uri);
|
||||
throw new this.VerificationError(
|
||||
"FILE_MISSING_AFTER_UPLOAD",
|
||||
Zotero.HTTP.getDisplayURI(uri, true).spec
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (req.status == 404) {
|
||||
|
|
@ -751,10 +748,16 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
});
|
||||
|
||||
if (req.status == 207) {
|
||||
throw new this.VerificationError("ZOTERO_DIR_NOT_FOUND", uri);
|
||||
throw new this.VerificationError(
|
||||
"ZOTERO_DIR_NOT_FOUND",
|
||||
Zotero.HTTP.getDisplayURI(uri, true).spec
|
||||
);
|
||||
}
|
||||
else if (req.status == 404) {
|
||||
throw new this.VerificationError("PARENT_DIR_NOT_FOUND", uri);
|
||||
throw new this.VerificationError(
|
||||
"PARENT_DIR_NOT_FOUND",
|
||||
Zotero.HTTP.getDisplayURI(uri, true).spec
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -805,10 +808,6 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
}
|
||||
}
|
||||
else if (err instanceof this.VerificationError) {
|
||||
let spec;
|
||||
if (err.uri) {
|
||||
spec = err.uri.scheme + '://' + err.uri.hostPort + err.uri.pathQueryRef;
|
||||
}
|
||||
switch (err.error) {
|
||||
case "NO_URL":
|
||||
errorMsg = Zotero.getString('sync.storage.error.webdav.enterURL');
|
||||
|
|
@ -822,21 +821,22 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
|||
errorMsg = Zotero.getString('sync.error.enterPassword');
|
||||
break;
|
||||
|
||||
case "INVALID_URL":
|
||||
case "NOT_DAV":
|
||||
errorMsg = Zotero.getString('sync.storage.error.webdav.invalidURL', spec);
|
||||
errorMsg = Zotero.getString('sync.storage.error.webdav.invalidURL', err.url);
|
||||
break;
|
||||
|
||||
case "PARENT_DIR_NOT_FOUND":
|
||||
errorTitle = Zotero.getString('sync.storage.error.directoryNotFound');
|
||||
var parentSpec = spec.replace(/zotero\/$/, "");
|
||||
errorMsg = Zotero.getString('sync.storage.error.doesNotExist', parentSpec);
|
||||
var parentURL = err.url.replace(/zotero\/$/, "");
|
||||
errorMsg = Zotero.getString('sync.storage.error.doesNotExist', parentURL);
|
||||
break;
|
||||
|
||||
case "ZOTERO_DIR_NOT_FOUND":
|
||||
var create = promptService.confirmEx(
|
||||
window,
|
||||
Zotero.getString('sync.storage.error.directoryNotFound'),
|
||||
Zotero.getString('sync.storage.error.doesNotExist', spec) + "\n\n"
|
||||
Zotero.getString('sync.storage.error.doesNotExist', err.url) + "\n\n"
|
||||
+ Zotero.getString('sync.storage.error.createNow'),
|
||||
promptService.BUTTON_POS_0
|
||||
* promptService.BUTTON_TITLE_IS_STRING
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ Zotero.Translate.ItemSaver = function(options) {
|
|||
this._referrer = options.referrer;
|
||||
this._cookieSandbox = options.cookieSandbox;
|
||||
this._proxy = options.proxy;
|
||||
this._itemToJSONItem = new Map();
|
||||
|
||||
// the URI to which other URIs are assumed to be relative
|
||||
if(typeof options.baseURI === "object" && options.baseURI instanceof Components.interfaces.nsIURI) {
|
||||
|
|
@ -82,6 +83,7 @@ Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES = new Set([
|
|||
]);
|
||||
|
||||
Zotero.Translate.ItemSaver.prototype = {
|
||||
|
||||
/**
|
||||
* Saves items to Standalone or the server
|
||||
* @param {Object[]} jsonItems - Items in Zotero.Item.toArray() format
|
||||
|
|
@ -90,21 +92,17 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
|
||||
* @param {Function} [itemsDoneCallback] A callback that is called once all top-level items are
|
||||
* done saving with a list of items. Will include saved notes, but exclude attachments.
|
||||
* @param {Function} [pendingAttachmentsCallback] A callback that is called for every
|
||||
* pending attachment to an item. pendingAttachmentsCallback(parentItemID, jsonAttachment)
|
||||
*/
|
||||
saveItems: async function (jsonItems, attachmentCallback, itemsDoneCallback, pendingAttachmentsCallback) {
|
||||
saveItems: async function (jsonItems, attachmentCallback, itemsDoneCallback) {
|
||||
var items = [];
|
||||
var standaloneAttachments = [];
|
||||
var childAttachments = [];
|
||||
var jsonByItem = new Map();
|
||||
|
||||
await Zotero.DB.executeTransaction(async function () {
|
||||
for (let jsonItem of jsonItems) {
|
||||
jsonItem = Object.assign({}, jsonItem);
|
||||
|
||||
let item;
|
||||
let itemID;
|
||||
// Type defaults to "webpage"
|
||||
let type = jsonItem.itemType || "webpage";
|
||||
|
||||
|
|
@ -121,84 +119,25 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
continue;
|
||||
}
|
||||
else {
|
||||
item = new Zotero.Item(type);
|
||||
item.libraryID = this._libraryID;
|
||||
if (jsonItem.creators) this._cleanCreators(jsonItem.creators);
|
||||
if (jsonItem.tags) jsonItem.tags = this._cleanTags(jsonItem.tags);
|
||||
|
||||
if (jsonItem.accessDate == 'CURRENT_TIMESTAMP') {
|
||||
jsonItem.accessDate = Zotero.Date.dateToISO(new Date());
|
||||
}
|
||||
|
||||
item.fromJSON(this._copyJSONItemForImport(jsonItem));
|
||||
|
||||
// deproxify url
|
||||
if (this._proxy && jsonItem.url) {
|
||||
let url = this._proxy.toProper(jsonItem.url);
|
||||
Zotero.debug(`Deproxifying item url ${jsonItem.url} with scheme ${this._proxy.scheme} to ${url}`, 5);
|
||||
item.setField('url', url);
|
||||
}
|
||||
|
||||
if (this._collections) {
|
||||
item.setCollections(this._collections);
|
||||
}
|
||||
|
||||
// save item
|
||||
itemID = await item.save(this._saveOptions);
|
||||
|
||||
// handle notes
|
||||
if (jsonItem.notes) {
|
||||
for (let note of jsonItem.notes) {
|
||||
await this._saveNote(note, itemID);
|
||||
}
|
||||
}
|
||||
item = await this._saveItem(jsonItem, type);
|
||||
|
||||
// handle attachments
|
||||
if (jsonItem.attachments) {
|
||||
let attachmentsToSave = [];
|
||||
let foundPrimary = false;
|
||||
for (let jsonAttachment of jsonItem.attachments) {
|
||||
if (!this._canSaveAttachment(jsonAttachment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The first PDF/EPUB is the primary one. If that one fails to download,
|
||||
// we might check for an open-access PDF below.
|
||||
if (Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(jsonAttachment.mimeType)
|
||||
&& !foundPrimary) {
|
||||
jsonAttachment.isPrimary = true;
|
||||
foundPrimary = true;
|
||||
}
|
||||
attachmentsToSave.push(jsonAttachment);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
if (jsonAttachment.singleFile) {
|
||||
// SingleFile attachments are saved in 'saveSingleFile'
|
||||
// connector endpoint
|
||||
if (pendingAttachmentsCallback) {
|
||||
pendingAttachmentsCallback(itemID, jsonAttachment);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
childAttachments.push([jsonAttachment, itemID]);
|
||||
}
|
||||
jsonItem.attachments = attachmentsToSave;
|
||||
}
|
||||
|
||||
// handle see also
|
||||
this._handleRelated(jsonItem, item);
|
||||
// process attachments
|
||||
let attachments = this._processChildAttachments(jsonItem, attachmentCallback);
|
||||
attachments.forEach(attachment => childAttachments.push([attachment, item.id]));
|
||||
}
|
||||
|
||||
// Add to new item list
|
||||
items.push(item);
|
||||
jsonByItem.set(item, jsonItem);
|
||||
this._itemToJSONItem.set(item, jsonItem);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
// Done saving top-level items. Call the callback so that UI code can update
|
||||
if (itemsDoneCallback) {
|
||||
itemsDoneCallback(items.map(item => jsonByItem.get(item)), items);
|
||||
itemsDoneCallback(items.map(item => this._itemToJSONItem.get(item)), items);
|
||||
}
|
||||
|
||||
// Save standalone attachments
|
||||
// Download standalone attachments
|
||||
for (let jsonItem of standaloneAttachments) {
|
||||
let item = await this._saveAttachment(jsonItem, null, attachmentCallback);
|
||||
if (item) {
|
||||
|
|
@ -210,45 +149,21 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
// open-access PDFs. There's no guarantee that either translated PDFs or OA PDFs will
|
||||
// successfully download, but this lets us update the progress window sooner with
|
||||
// possible downloads.
|
||||
//
|
||||
this._openAccessPDFURLs = new Map();
|
||||
|
||||
// TODO: Separate pref?
|
||||
var shouldDownloadOAPDF = this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD
|
||||
&& Zotero.Prefs.get('downloadAssociatedFiles');
|
||||
var openAccessPDFURLs = new Map();
|
||||
&& Zotero.Prefs.get('downloadAssociatedFiles');
|
||||
if (shouldDownloadOAPDF) {
|
||||
for (let item of items) {
|
||||
let jsonItem = jsonByItem.get(item);
|
||||
|
||||
// Skip items with translated PDF attachments
|
||||
if (jsonItem.attachments
|
||||
&& jsonItem.attachments.some(x => Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(x.mimeType))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
let resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
|
||||
if (!resolvers.length) {
|
||||
openAccessPDFURLs.set(item, []);
|
||||
continue;
|
||||
}
|
||||
let urlObjects = await resolvers[0]();
|
||||
openAccessPDFURLs.set(item, urlObjects);
|
||||
// If there are possible URLs, create a status line for the PDF
|
||||
if (urlObjects.length) {
|
||||
let title = Zotero.getString('findPDF.openAccessPDF');
|
||||
let jsonAttachment = this._makeJSONAttachment(jsonItem.id, title);
|
||||
if (!jsonItem.attachments) jsonItem.attachments = [];
|
||||
jsonItem.attachments.push(jsonAttachment);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
let urlObjects = await this._getOpenAccessPDFURLs(item, attachmentCallback);
|
||||
if (urlObjects) {
|
||||
this._openAccessPDFURLs.set(item, urlObjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save translated child attachments, and keep track of whether the save was successful
|
||||
// Save translated child attachments
|
||||
var itemIDsWithPrimaryAttachments = new Set();
|
||||
for (let [jsonAttachment, parentItemID] of childAttachments) {
|
||||
let attachment = await this._saveAttachment(
|
||||
|
|
@ -267,93 +182,15 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
}
|
||||
}
|
||||
|
||||
// If a translated PDF attachment wasn't saved successfully, either because there wasn't
|
||||
// one or there was but it failed, look for another PDF (if enabled)
|
||||
if (shouldDownloadOAPDF) {
|
||||
// If a translated PDF attachment wasn't saved successfully, either because there wasn't
|
||||
// one or there was but it failed, look for another PDF (if enabled)
|
||||
for (let item of items) {
|
||||
// Already have a primary attachment from translation
|
||||
if (itemIDsWithPrimaryAttachments.has(item.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let jsonItem = jsonByItem.get(item);
|
||||
// Reuse the existing status line if there is one. This could be a failed
|
||||
// translator attachment or a possible OA PDF found above.
|
||||
// Explicitly check that the attachment is a PDF, not just any primary type,
|
||||
// since we're reusing it for a PDF attachment.
|
||||
let jsonAttachment = jsonItem.attachments && jsonItem.attachments.find(
|
||||
x => x.mimeType == 'application/pdf' && x.isPrimary
|
||||
);
|
||||
|
||||
// If no translated, no OA, and no custom, don't show a line
|
||||
// If no translated and potential OA, show "Open-Access PDF"
|
||||
// If no translated, no OA, but custom, show custom when it starts
|
||||
// If translated fails and potential OA, show "Open-Access PDF"
|
||||
// If translated fails, no OA, no custom, fail original
|
||||
// If translated fails, no OA, but custom, change to custom when it starts
|
||||
let resolvers = openAccessPDFURLs.get(item);
|
||||
// No translated PDF, so we checked for OA PDFs above
|
||||
if (resolvers) {
|
||||
// Add custom resolvers
|
||||
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
|
||||
|
||||
// No translated, no OA, no custom, no status line
|
||||
if (!resolvers.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// No translated, no OA, just potential custom, so create a status line
|
||||
if (!jsonAttachment) {
|
||||
jsonAttachment = this._makeJSONAttachment(
|
||||
jsonItem.id, Zotero.getString('findPDF.searchingForAvailableFiles')
|
||||
);
|
||||
}
|
||||
}
|
||||
// There was a translated PDF, so we didn't check for OA PDFs yet and didn't
|
||||
// update the status line
|
||||
else {
|
||||
// Look for OA PDFs now
|
||||
resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
|
||||
if (resolvers.length) {
|
||||
resolvers = await resolvers[0]();
|
||||
}
|
||||
|
||||
// Add custom resolvers
|
||||
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
|
||||
|
||||
// Failed translated, no OA, no custom, so fail the existing translator line
|
||||
if (!resolvers.length) {
|
||||
attachmentCallback(jsonAttachment, false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let attachment;
|
||||
try {
|
||||
attachment = await Zotero.Attachments.addFileFromURLs(
|
||||
item,
|
||||
resolvers,
|
||||
{
|
||||
// When a new access method starts, update the status line
|
||||
onAccessMethodStart: (method) => {
|
||||
jsonAttachment.title = this._getPDFTitleForAccessMethod(method);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
attachmentCallback(jsonAttachment, false, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
attachmentCallback(jsonAttachment, 100);
|
||||
}
|
||||
else {
|
||||
attachmentCallback(jsonAttachment, false, "PDF not found");
|
||||
}
|
||||
await this.saveOpenAccessAttachment(item, attachmentCallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -364,23 +201,233 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
/**
|
||||
* Save pending snapshot attachments to disk and library
|
||||
*
|
||||
* @param {Array} pendingAttachments - A list of snapshot attachments
|
||||
* @param {Object} content - Snapshot content from SingleFile
|
||||
* @param {Function} attachmentCallback - Callback with progress of attachments
|
||||
* @param {Object} options - A list of snapshot attachments
|
||||
* - title {String}
|
||||
* - url {String}
|
||||
* - parentItemID {Number}
|
||||
* - snapshotContent {String}
|
||||
*/
|
||||
saveSnapshotAttachments: Zotero.Promise.coroutine(function* (pendingAttachments, snapshotContent, attachmentCallback) {
|
||||
for (let [parentItemID, attachment] of pendingAttachments) {
|
||||
Zotero.debug('Saving pending attachment: ' + JSON.stringify(attachment));
|
||||
if (snapshotContent) {
|
||||
attachment.snapshotContent = snapshotContent;
|
||||
}
|
||||
yield this._saveAttachment(
|
||||
saveSnapshotAttachments: async function (options) {
|
||||
let { title, url, parentItemID, snapshotContent } = options;
|
||||
let attachment = { title, url };
|
||||
Zotero.debug('Saving pending attachment: ' + JSON.stringify(attachment));
|
||||
if (snapshotContent) {
|
||||
attachment.snapshotContent = snapshotContent;
|
||||
}
|
||||
await new Promise(async (resolve, reject) => {
|
||||
await this._saveAttachment(
|
||||
attachment,
|
||||
parentItemID,
|
||||
attachmentCallback
|
||||
(attachment, progress, e) => {
|
||||
if (e) reject(e);
|
||||
if (progress === 100) {
|
||||
resolve(progress);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
async _saveItem(jsonItem, type) {
|
||||
let itemID;
|
||||
let item = new Zotero.Item(type);
|
||||
item.libraryID = this._libraryID;
|
||||
if (jsonItem.creators) this._cleanCreators(jsonItem.creators);
|
||||
if (jsonItem.tags) jsonItem.tags = this._cleanTags(jsonItem.tags);
|
||||
|
||||
if (jsonItem.accessDate == 'CURRENT_TIMESTAMP') {
|
||||
jsonItem.accessDate = Zotero.Date.dateToISO(new Date());
|
||||
}
|
||||
|
||||
item.fromJSON(this._copyJSONItemForImport(jsonItem));
|
||||
|
||||
// deproxify url
|
||||
if (this._proxy && jsonItem.url) {
|
||||
let url = this._proxy.toProper(jsonItem.url);
|
||||
Zotero.debug(`Deproxifying item url ${jsonItem.url} with scheme ${this._proxy.scheme} to ${url}`, 5);
|
||||
item.setField('url', url);
|
||||
}
|
||||
|
||||
// save item
|
||||
if (this._collections) {
|
||||
item.setCollections(this._collections);
|
||||
}
|
||||
itemID = await item.save(this._saveOptions);
|
||||
|
||||
// handle notes
|
||||
if (jsonItem.notes) {
|
||||
for (let note of jsonItem.notes) {
|
||||
await this._saveNote(note, itemID);
|
||||
}
|
||||
}
|
||||
|
||||
// handle see also
|
||||
this._handleRelated(jsonItem, item);
|
||||
return item;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Processes attachments to be saved either via Zotero or externally (Connector)
|
||||
*
|
||||
* Calls attachment callbacks for initial attachment progress (0)
|
||||
*/
|
||||
_processChildAttachments(jsonItem, attachmentCallback) {
|
||||
let childAttachments = [];
|
||||
|
||||
let foundPrimary = false;
|
||||
// Attachments to be saved within Zotero
|
||||
if (jsonItem.attachments) {
|
||||
let attachmentsToSave = [];
|
||||
for (let jsonAttachment of jsonItem.attachments) {
|
||||
if (!this._canSaveAttachment(jsonAttachment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The first PDF/EPUB is the primary one. If that one fails to download,
|
||||
// we might check for an open-access PDF below.
|
||||
if (Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(jsonAttachment.mimeType)
|
||||
&& !foundPrimary) {
|
||||
jsonAttachment.isPrimary = true;
|
||||
foundPrimary = true;
|
||||
}
|
||||
attachmentsToSave.push(jsonAttachment);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
childAttachments.push(jsonAttachment);
|
||||
}
|
||||
|
||||
jsonItem.attachments = attachmentsToSave;
|
||||
}
|
||||
return childAttachments;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a list of OA PDF URLs for items that did not receive a PDF attachment
|
||||
* from the translator
|
||||
*
|
||||
* Calls attachmentCallback to update UI
|
||||
* @param items
|
||||
* @param attachmentCallback
|
||||
* @returns {Promise<Map<any, any>>}
|
||||
* @private
|
||||
*/
|
||||
async _getOpenAccessPDFURLs(item, attachmentCallback) {
|
||||
let jsonItem = this._itemToJSONItem.get(item);
|
||||
let urlObjects = [];
|
||||
|
||||
// Has a primary attachment or a pending (from Connector) primary attachment
|
||||
if (jsonItem.attachments?.some(x => Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(x.mimeType))
|
||||
|| jsonItem.pendingPrimaryAttachment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If no primary attachments available look for an OA one and call attachmentCallback to update UI
|
||||
try {
|
||||
let resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
|
||||
if (!resolvers.length) {
|
||||
return urlObjects;
|
||||
}
|
||||
urlObjects = await resolvers[0]();
|
||||
// If there are possible URLs, create a status line for the PDF
|
||||
if (urlObjects.length) {
|
||||
let title = Zotero.getString('findPDF.openAccessPDF');
|
||||
let jsonAttachment = this._makeJSONAttachment(jsonItem.id, title);
|
||||
if (!jsonItem.attachments) jsonItem.attachments = [];
|
||||
jsonItem.attachments.push(jsonAttachment);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
return urlObjects;
|
||||
},
|
||||
|
||||
async saveOpenAccessAttachment(item, attachmentCallback) {
|
||||
let jsonItem = this._itemToJSONItem.get(item);
|
||||
// Reuse the existing status line if there is one. This could be a failed
|
||||
// translator attachment or a possible OA PDF found above.
|
||||
// Explicitly check that the attachment is a PDF, not just any primary type,
|
||||
// since we're reusing it for a PDF attachment.
|
||||
let jsonAttachment = jsonItem.attachments && jsonItem.attachments.find(
|
||||
x => x.mimeType == 'application/pdf' && x.isPrimary
|
||||
);
|
||||
|
||||
|
||||
// If no translated, no OA, no custom, don't show a line
|
||||
// If translated fails, no OA, no custom, fail original
|
||||
|
||||
// If no translated, potential OA, show "Open-Access PDF" (set in _getOpenAccessPDFURLs())
|
||||
// If translated fails, potential OA, show "Open-Access PDF" (set here)
|
||||
|
||||
// If no translated
|
||||
// or translated fails, no OA, but custom, show custom when it starts
|
||||
|
||||
let resolvers = this._openAccessPDFURLs.get(item);
|
||||
// We checked for OA PDFs in _getOpenAccessPDFURLs() so there was no translated pdf
|
||||
if (resolvers) {
|
||||
// Add custom resolvers
|
||||
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
|
||||
|
||||
// No translated, no OA, no custom, no status line
|
||||
if (!resolvers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No translated, no OA, just potential custom, so create a status line
|
||||
if (!jsonAttachment) {
|
||||
jsonAttachment = this._makeJSONAttachment(
|
||||
jsonItem.id, Zotero.getString('findPDF.searchingForAvailableFiles')
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Translated attachment failed, so we didn't check for OA PDFs yet and didn't
|
||||
// update the status line
|
||||
// Look for OA PDFs now
|
||||
resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
|
||||
if (resolvers.length) {
|
||||
resolvers = await resolvers[0]();
|
||||
}
|
||||
|
||||
// Add custom resolvers
|
||||
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
|
||||
|
||||
// Failed translated, no OA, no custom, so fail the existing translator line
|
||||
if (!resolvers.length) {
|
||||
attachmentCallback(jsonAttachment, false);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let attachment;
|
||||
try {
|
||||
attachment = await Zotero.Attachments.addFileFromURLs(
|
||||
item,
|
||||
resolvers,
|
||||
{
|
||||
// When a new access method starts, update the status line
|
||||
onAccessMethodStart: (method) => {
|
||||
jsonAttachment.title = this._getPDFTitleForAccessMethod(method);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}),
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
attachmentCallback(jsonAttachment, false, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
attachmentCallback(jsonAttachment, 100);
|
||||
}
|
||||
else {
|
||||
attachmentCallback(jsonAttachment, false, "PDF not found");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
_makeJSONAttachment: function (parentID, title) {
|
||||
|
|
@ -534,16 +581,23 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
try {
|
||||
let newAttachment;
|
||||
|
||||
const isSinglefileSnapshot = !!attachment.snapshotContent;
|
||||
// determine whether to save files and attachments
|
||||
var isLink = Zotero.MIME.isWebPageType(attachment.mimeType)
|
||||
// .snapshot coming from most translators, .linkMode coming from RDF
|
||||
&& (attachment.snapshot === false || attachment.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL);
|
||||
if (isLink || this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) {
|
||||
// .snapshot coming from most translators, .linkMode coming from RDF
|
||||
var isLink = attachment.snapshot === false
|
||||
|| attachment.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL;
|
||||
|
||||
if (isLink || this.attachmentMode === Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE) {
|
||||
newAttachment = yield this._saveAttachmentLink.apply(this, arguments);
|
||||
}
|
||||
else if (isSinglefileSnapshot || this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) {
|
||||
newAttachment = yield this._saveAttachmentDownload.apply(this, arguments);
|
||||
} else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) {
|
||||
}
|
||||
else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) {
|
||||
newAttachment = yield this._saveAttachmentFile.apply(this, arguments);
|
||||
} else {
|
||||
Zotero.debug('Translate: Ignoring attachment due to ATTACHMENT_MODE_IGNORE');
|
||||
}
|
||||
else {
|
||||
Zotero.debug(`Translate: Ignoring attachment ${attachment.title} due to ATTACHMENT_MODE_IGNORE`);
|
||||
}
|
||||
|
||||
if (!newAttachment) return false; // attachmentCallback should not have been called in this case
|
||||
|
|
@ -821,6 +875,50 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
return false;
|
||||
},
|
||||
|
||||
_saveAttachmentLink: async function(attachment, parentItemID, attachmentCallback) {
|
||||
attachment.linkMode = "linked_url";
|
||||
let url, mimeType;
|
||||
if(attachment.document) {
|
||||
url = attachment.document.location.href;
|
||||
mimeType = attachment.mimeType || attachment.document.contentType;
|
||||
} else {
|
||||
url = attachment.url
|
||||
mimeType = attachment.mimeType || undefined;
|
||||
}
|
||||
|
||||
// If no title provided, use "Attachment" as title for progress UI (but not for item)
|
||||
let title = attachment.title || null;
|
||||
if(!attachment.title) {
|
||||
attachment.title = Zotero.getString("itemTypes.attachment");
|
||||
}
|
||||
|
||||
if(!mimeType || !title) {
|
||||
Zotero.debug("Translate: mimeType or title is missing; attaching link to URL will be slower");
|
||||
}
|
||||
|
||||
let cleanURI = Zotero.Attachments.cleanAttachmentURI(url);
|
||||
if (!cleanURI) {
|
||||
throw new Error("Translate: Invalid attachment URL specified <" + url + ">");
|
||||
}
|
||||
url = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService)
|
||||
.newURI(cleanURI, null, null); // This cannot fail, since we check above
|
||||
|
||||
// Only HTTP/HTTPS links are allowed
|
||||
if(url.scheme != "http" && url.scheme != "https") {
|
||||
throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators.");
|
||||
}
|
||||
|
||||
return Zotero.Attachments.linkFromURL({
|
||||
url: cleanURI,
|
||||
parentItemID,
|
||||
contentType: mimeType,
|
||||
title,
|
||||
collections: !parentItemID ? this._collections : undefined,
|
||||
saveOptions: this._saveOptions,
|
||||
});
|
||||
},
|
||||
|
||||
_saveAttachmentDownload: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) {
|
||||
Zotero.debug("Translate: Adding attachment", 4);
|
||||
|
||||
|
|
@ -839,51 +937,11 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
// Commit to saving
|
||||
attachmentCallback(attachment, 0);
|
||||
|
||||
var isLink = attachment.snapshot === false
|
||||
|| attachment.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL;
|
||||
if (isLink || this.attachmentMode === Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE) {
|
||||
// if snapshot is explicitly set to false, attach as link
|
||||
attachment.linkMode = "linked_url";
|
||||
let url, mimeType;
|
||||
if(attachment.document) {
|
||||
url = attachment.document.location.href;
|
||||
mimeType = attachment.mimeType || attachment.document.contentType;
|
||||
} else {
|
||||
url = attachment.url
|
||||
mimeType = attachment.mimeType || undefined;
|
||||
}
|
||||
|
||||
if(!mimeType || !title) {
|
||||
Zotero.debug("Translate: mimeType or title is missing; attaching link to URL will be slower");
|
||||
}
|
||||
|
||||
let cleanURI = Zotero.Attachments.cleanAttachmentURI(url);
|
||||
if (!cleanURI) {
|
||||
throw new Error("Translate: Invalid attachment URL specified <" + url + ">");
|
||||
}
|
||||
url = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService)
|
||||
.newURI(cleanURI, null, null); // This cannot fail, since we check above
|
||||
|
||||
// Only HTTP/HTTPS links are allowed
|
||||
if(url.scheme != "http" && url.scheme != "https") {
|
||||
throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators.");
|
||||
}
|
||||
|
||||
return Zotero.Attachments.linkFromURL({
|
||||
url: cleanURI,
|
||||
parentItemID,
|
||||
contentType: mimeType,
|
||||
title,
|
||||
collections: !parentItemID ? this._collections : undefined,
|
||||
saveOptions: this._saveOptions,
|
||||
});
|
||||
}
|
||||
|
||||
// Snapshot is not explicitly set to false, import as file attachment
|
||||
attachment.linkMode = "imported_url";
|
||||
|
||||
// Import from document
|
||||
if(attachment.document) {
|
||||
if (attachment.document) {
|
||||
Zotero.debug('Importing attachment from document');
|
||||
attachment.linkMode = "imported_url";
|
||||
|
||||
|
|
@ -897,17 +955,6 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
});
|
||||
}
|
||||
|
||||
let mimeType = attachment.mimeType ? attachment.mimeType : null;
|
||||
let fileBaseName;
|
||||
if (parentItemID) {
|
||||
let parentItem = yield Zotero.Items.getAsync(parentItemID);
|
||||
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
|
||||
}
|
||||
|
||||
attachment.linkMode = "imported_url";
|
||||
|
||||
attachmentCallback(attachment, 0);
|
||||
|
||||
// Import from SingleFile content
|
||||
if (attachment.snapshotContent) {
|
||||
Zotero.debug('Importing attachment from SingleFile');
|
||||
|
|
@ -924,6 +971,13 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
}
|
||||
|
||||
// Import from URL
|
||||
let mimeType = attachment.mimeType ? attachment.mimeType : null;
|
||||
let fileBaseName;
|
||||
if (parentItemID) {
|
||||
let parentItem = yield Zotero.Items.getAsync(parentItemID);
|
||||
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
|
||||
}
|
||||
|
||||
Zotero.debug('Importing attachment from URL');
|
||||
return Zotero.Attachments.importFromURL({
|
||||
libraryID: this._libraryID,
|
||||
|
|
|
|||
|
|
@ -468,6 +468,8 @@ Zotero.Utilities.Internal = {
|
|||
* @param {Zotero.CookieSandbox} [cookieSandbox]
|
||||
*/
|
||||
saveURI: function (wbp, uri, target, headers, cookieSandbox) {
|
||||
Zotero.warn("Zotero.Utilities.Internal.saveURI() is deprecated -- use Zotero.HTTP.download()");
|
||||
|
||||
// Handle gzip encoding
|
||||
wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
|
||||
// If not explicitly using cache, skip it
|
||||
|
|
@ -938,11 +940,13 @@ Zotero.Utilities.Internal = {
|
|||
}
|
||||
|
||||
// Tags
|
||||
for (let i = 0; i < item.tags.length; i++) {
|
||||
if (!item.tags[i].type) {
|
||||
item.tags[i].type = 0;
|
||||
if (item.tags) {
|
||||
for (let i = 0; i < item.tags.length; i++) {
|
||||
if (!item.tags[i].type) {
|
||||
item.tags[i].type = 0;
|
||||
}
|
||||
// No translator ever used "primary", "fields", or "linkedItems" objects
|
||||
}
|
||||
// No translator ever used "primary", "fields", or "linkedItems" objects
|
||||
}
|
||||
|
||||
// "related" was never used (array of itemIDs)
|
||||
|
|
@ -2178,14 +2182,32 @@ Zotero.Utilities.Internal = {
|
|||
return [operator, args];
|
||||
};
|
||||
|
||||
// We allow unquoted numbers in conditions, e.g. `a == 1` or `a == 1.0` but not `a == 1.0.0` or `a == 1st edition`
|
||||
const asNumber = (string) => {
|
||||
if (typeof string === 'number') {
|
||||
return string;
|
||||
}
|
||||
const number = parseFloat(string);
|
||||
if (!Number.isNaN(number) && string?.trim().match(/^[+-]?\d+(\.\d+)?$/)) {
|
||||
return number;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// evaluates a condition (e.g. `a == "b"`) into a boolean value
|
||||
const evaluateCondition = (condition) => {
|
||||
const comparators = ['==', '!='];
|
||||
const comparators = ['==', '!=', "<=", ">=", '<', '>'];
|
||||
condition = condition.trim();
|
||||
|
||||
// match[1] if left is statement, match[3] if left is literal, match[4] if left is identifier
|
||||
// match[6] if right is statement, match[8] if right is literal, match[9] if right is identifier
|
||||
// match[2] and match[7] are used to match the quotes around the literal (and then check that the other quote is the same)
|
||||
|
||||
// Regular expression breakdown for condition matching:
|
||||
// - `match[1]`: Left operand if it's a statement enclosed in `{{...}}`.
|
||||
// - `match[3]`: Left operand if it's a string literal, extracted without quotes.
|
||||
// - `match[4]`: Left operand if it's a standalone identifier (e.g., a variable) or a number
|
||||
// - `match[6]`: Right operand if it's a statement enclosed in `{{...}}`.
|
||||
// - `match[8]`: Right operand if it's a string literal, extracted without quotes.
|
||||
// - `match[9]`: Right operand if it's a standalone identifier or a number.
|
||||
// - `match[2]` and `match[7]`: Captured quotes around string literals, used to ensure matching pairs.
|
||||
// - `match[5]`: The comparator (e.g., `==`, `!=`, `<`, `>`, etc.), extracted from `comparators.join('|')`.
|
||||
const match = condition.match(new RegExp(String.raw`(?:{{(.*?)}}|(?:(['"])(.*?)\2)|([^ ]+)) *(${comparators.join('|')}) *(?:{{(.*?)}}|(?:(['"])(.*?)\7)|([^ ]+))`));
|
||||
|
||||
if (!match) {
|
||||
|
|
@ -2197,16 +2219,24 @@ Zotero.Utilities.Internal = {
|
|||
return !!evaluateIdentifier(condition);
|
||||
}
|
||||
|
||||
const left = match[1] ? evaluateStatement(match[1]) : match[3] ?? evaluateIdentifier(match[4]) ?? '';
|
||||
const left = match[1] ? evaluateStatement(match[1]) : match[3] ?? asNumber(match[4]) ?? evaluateIdentifier(match[4]) ?? '';
|
||||
const comparator = match[5];
|
||||
const right = match[6] ? evaluateStatement(match[6]) : match[8] ?? evaluateIdentifier(match[9]) ?? '';
|
||||
const right = match[6] ? evaluateStatement(match[6]) : match[8] ?? asNumber(match[9]) ?? evaluateIdentifier(match[9]) ?? '';
|
||||
|
||||
switch (comparator) {
|
||||
default:
|
||||
case '==':
|
||||
return left.toLowerCase() == right.toLowerCase();
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() == right.toLowerCase() : asNumber(left) == asNumber(right);
|
||||
case '!=':
|
||||
return left.toLowerCase() != right.toLowerCase();
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() != right.toLowerCase() : asNumber(left) != asNumber(right);
|
||||
case ">=":
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() >= right.toLowerCase() : asNumber(left) >= asNumber(right);
|
||||
case "<=":
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() <= right.toLowerCase() : asNumber(left) <= asNumber(right);
|
||||
case '>':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() > right.toLowerCase() : asNumber(left) > asNumber(right);
|
||||
case '<':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() < right.toLowerCase() : asNumber(left) < asNumber(right);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -2442,13 +2472,17 @@ Zotero.Utilities.Internal = {
|
|||
let markup = this._titleMarkup[token];
|
||||
if (markup.beginsTag) {
|
||||
if (targetNode) {
|
||||
let node = targetNode.ownerDocument.createElement(markup.beginsTag);
|
||||
if (markup.style) {
|
||||
Object.assign(node.style, markup.style);
|
||||
}
|
||||
let node;
|
||||
if (markup.inverseStyle && markupStack.some(otherMarkup => otherMarkup.beginsTag === markup.beginsTag)) {
|
||||
node = targetNode.ownerDocument.createElement('span');
|
||||
Object.assign(node.style, markup.inverseStyle);
|
||||
}
|
||||
else {
|
||||
node = targetNode.ownerDocument.createElement(markup.beginsTag);
|
||||
if (markup.style) {
|
||||
Object.assign(node.style, markup.style);
|
||||
}
|
||||
}
|
||||
nodeStack.push(node);
|
||||
}
|
||||
markupStack.push({ ...markup, token });
|
||||
|
|
@ -2461,12 +2495,14 @@ Zotero.Utilities.Internal = {
|
|||
let discardedNode = nodeStack.pop();
|
||||
if (discardedMarkup.beginsTag === markup.endsTag) {
|
||||
nodeStack[nodeStack.length - 1].append(discardedNode);
|
||||
break;
|
||||
}
|
||||
else {
|
||||
nodeStack[nodeStack.length - 1].append(discardedMarkup.token, ...discardedNode.childNodes);
|
||||
}
|
||||
}
|
||||
if (discardedMarkup.beginsTag === markup.endsTag) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
|
|
@ -2906,6 +2942,85 @@ Zotero.Utilities.Internal.getProcessID = function () {
|
|||
};
|
||||
|
||||
|
||||
Zotero.Utilities.Internal.Environment = {
|
||||
/**
|
||||
* Unset an environment variable
|
||||
*
|
||||
* Since nsIEnvironment doesn't have an unset method
|
||||
*/
|
||||
unset: function (varName) {
|
||||
Components.utils.import("resource://gre/modules/ctypes.jsm");
|
||||
|
||||
let lib;
|
||||
let result;
|
||||
let success;
|
||||
// Windows
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-setenvironmentvariablew
|
||||
if (Zotero.isWin) {
|
||||
lib = ctypes.open("kernel32.dll");
|
||||
let SetEnvironmentVariable = lib.declare(
|
||||
"SetEnvironmentVariableW",
|
||||
ctypes.winapi_abi,
|
||||
ctypes.int, // Return type
|
||||
ctypes.char16_t.ptr, // Argument type: LPCTSTR (variable name)
|
||||
ctypes.char16_t.ptr // Argument type: LPCTSTR (value, NULL to unset)
|
||||
);
|
||||
result = SetEnvironmentVariable(varName, null);
|
||||
success = result != 0; // "If the function succeeds, the return value is nonzero."
|
||||
}
|
||||
// macOS or Linux
|
||||
else {
|
||||
lib = ctypes.open(Zotero.isMac ? "/usr/lib/libSystem.dylib" : "libc.so.6");
|
||||
let unsetenv = lib.declare(
|
||||
"unsetenv",
|
||||
ctypes.default_abi,
|
||||
ctypes.int, // Return type
|
||||
ctypes.char.ptr // Argument type: const char *
|
||||
);
|
||||
result = unsetenv(varName);
|
||||
success = result == 0;
|
||||
}
|
||||
|
||||
// Check the result
|
||||
if (success) {
|
||||
//Zotero.debug(`Unset environment variable ${varName}`);
|
||||
}
|
||||
else {
|
||||
Zotero.logError(`Failed to unset environment variable ${varName} (${result})`);
|
||||
}
|
||||
|
||||
lib.close();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the Mozilla environment variables that we changed in the launcher
|
||||
*
|
||||
* Call this before launching another process that might be Firefox so that we don't end up
|
||||
* using the wrong Firefox profile when launching URLs or PDFs. On Windows, it's not necessary
|
||||
* to call this when launching URLs, only processes.
|
||||
*
|
||||
* https://github.com/zotero/zotero/issues/4981
|
||||
*/
|
||||
clearMozillaVariables: function () {
|
||||
this.unset("MOZ_ALLOW_DOWNGRADE");
|
||||
this.unset("MOZ_LEGACY_PROFILES");
|
||||
},
|
||||
|
||||
/**
|
||||
* Re-set the Mozilla environment variables that we changed in the launcher
|
||||
*
|
||||
* Call this in a finally() after using unsetMozillaVariables(). This mostly isn't necessary,
|
||||
* since most new launches of Zotero would use the modified launcher, but a restart on Linux
|
||||
* skips our shell script where we set these variables.
|
||||
*/
|
||||
restoreMozillaVariables: function () {
|
||||
var env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||
env.set("MOZ_ALLOW_DOWNGRADE", "1");
|
||||
env.set("MOZ_LEGACY_PROFILES", "1");
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Base64 encode / decode
|
||||
* From http://www.webtoolkit.info/
|
||||
|
|
@ -3048,9 +3163,18 @@ Zotero.Utilities.Internal.OpenURL = {
|
|||
);
|
||||
if (co) {
|
||||
let base = Zotero.Prefs.get("openURL.resolver");
|
||||
// Add & if there's already a ?
|
||||
let splice = base.indexOf("?") == -1 ? "?" : "&";
|
||||
return base + splice + co;
|
||||
if (!base.endsWith('?')) {
|
||||
if (base.includes('login?url=')) {
|
||||
base += '?';
|
||||
}
|
||||
else if (base.includes('?')) {
|
||||
base += '&';
|
||||
}
|
||||
else {
|
||||
base += '?';
|
||||
}
|
||||
}
|
||||
return base + co;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -996,6 +996,9 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
*/
|
||||
this.launchFile = function (file) {
|
||||
file = Zotero.File.pathToFile(file);
|
||||
|
||||
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||
|
||||
try {
|
||||
Zotero.debug("Launching " + file.path);
|
||||
file.launch();
|
||||
|
|
@ -1038,6 +1041,9 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1062,8 +1068,12 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
args = [filePath];
|
||||
}
|
||||
|
||||
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||
|
||||
// Async, but we don't want to block
|
||||
Zotero.Utilities.Internal.exec(applicationPath, args);
|
||||
var promise = Zotero.Utilities.Internal.exec(applicationPath, args);
|
||||
|
||||
promise.finally(() => Zotero.Utilities.Internal.Environment.restoreMozillaVariables());
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1093,12 +1103,29 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
if (!found.value) {
|
||||
throw new Error(`Handler not found for '${scheme}' URLs`);
|
||||
}
|
||||
svc.loadURI(Services.io.newURI(url, null, null));
|
||||
try {
|
||||
if (!Zotero.isWin) {
|
||||
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||
}
|
||||
|
||||
svc.loadURI(Services.io.newURI(url, null, null));
|
||||
}
|
||||
finally {
|
||||
if (!Zotero.isWin) {
|
||||
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var mozCleared = false;
|
||||
try {
|
||||
if (!Zotero.isWin) {
|
||||
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||
mozCleared = true;
|
||||
}
|
||||
|
||||
var uri = Services.io.newURI(url, null, null);
|
||||
var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1']
|
||||
.getService(Components.interfaces.nsIExternalProtocolService)
|
||||
|
|
@ -1123,6 +1150,11 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
+ "check extensions.zotero." + pref + " in about:config");
|
||||
}
|
||||
|
||||
if (!mozCleared) {
|
||||
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||
mozCleared = true;
|
||||
}
|
||||
|
||||
var proc = Components.classes["@mozilla.org/process/util;1"]
|
||||
.createInstance(Components.interfaces.nsIProcess);
|
||||
proc.init(exec);
|
||||
|
|
@ -1130,6 +1162,11 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
var args = [url];
|
||||
proc.runw(false, args, args.length);
|
||||
}
|
||||
finally {
|
||||
if (mozCleared) {
|
||||
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1425,6 +1462,10 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
locale: Zotero.locale,
|
||||
};
|
||||
|
||||
if (Services.appinfo.inSafeMode) {
|
||||
info.safeMode = true;
|
||||
}
|
||||
|
||||
var extensions = await Zotero.getInstalledExtensions();
|
||||
info.extensions = extensions.join(', ');
|
||||
|
||||
|
|
@ -1484,6 +1525,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
((b.appDisabled || b.userDisabled) ? 1 : 0);
|
||||
});
|
||||
var addons = [];
|
||||
var isSafeMode = Services.appinfo.inSafeMode;
|
||||
for (let addon of installed) {
|
||||
if (addon.type == "theme") {
|
||||
continue;
|
||||
|
|
@ -1491,7 +1533,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
|
||||
addons.push(addon.name + " (" + addon.version
|
||||
+ (addon.type != 2 ? ", " + addon.type : "")
|
||||
+ ((addon.appDisabled || addon.userDisabled) ? ", disabled" : "")
|
||||
+ ((addon.appDisabled || addon.userDisabled || isSafeMode) ? ", disabled" : "")
|
||||
+ ")");
|
||||
}
|
||||
|
||||
|
|
@ -1721,9 +1763,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
return Zotero.Tags.purge();
|
||||
});
|
||||
yield Zotero.Fulltext.purgeUnusedWords();
|
||||
yield Zotero.DB.executeTransaction(async function () {
|
||||
return Zotero.Items.purge();
|
||||
});
|
||||
yield Zotero.Items.purge();
|
||||
// DEBUG: this might not need to be permanent
|
||||
//yield Zotero.DB.executeTransaction(async function () {
|
||||
// return Zotero.Relations.purge();
|
||||
|
|
@ -1979,7 +2019,7 @@ Zotero.VersionHeader = {
|
|||
}
|
||||
else {
|
||||
// Use "Firefox/[version]" in user agent if not a proxy check or file sync request
|
||||
let s3RE = /(zoteroproxycheck|zoterofilestorage(test)?)\.s3\.(us-east-1\.)?amazonaws\.com/;
|
||||
let s3RE = /(zoteroproxycheck|zoterofilestorage(test)?)\.s3\.(us-east-1\.)?amazonaws\.com|files\.zotero\.net/;
|
||||
let isAppNameDomain = s3RE.test(domain);
|
||||
if (!isAppNameDomain) {
|
||||
let ua = channel.getRequestHeader('User-Agent');
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ const xpcomFilesLocal = [
|
|||
'feedReader',
|
||||
'fileDragDataProvider',
|
||||
'fulltext',
|
||||
'httpIntegrationClient',
|
||||
'id',
|
||||
'integration',
|
||||
'locale',
|
||||
|
|
@ -124,8 +125,12 @@ const xpcomFilesLocal = [
|
|||
'retractions',
|
||||
'router',
|
||||
'schema',
|
||||
'server',
|
||||
'server_integration',
|
||||
'server/server',
|
||||
'server/server_integration',
|
||||
'server/server_connector',
|
||||
'server/server_connectorIntegration',
|
||||
'server/server_localAPI',
|
||||
'server/saveSession',
|
||||
'session',
|
||||
'streamer',
|
||||
'style',
|
||||
|
|
@ -144,7 +149,6 @@ const xpcomFilesLocal = [
|
|||
'storage/storageRequest',
|
||||
'storage/storageResult',
|
||||
'storage/storageUtilities',
|
||||
'storage/streamListener',
|
||||
'storage/zfs',
|
||||
'storage/webdav',
|
||||
'syncedSettings',
|
||||
|
|
@ -153,10 +157,6 @@ const xpcomFilesLocal = [
|
|||
'users',
|
||||
'translation/translate_item',
|
||||
'translation/translators',
|
||||
'connector/httpIntegrationClient',
|
||||
'connector/server_connector',
|
||||
'connector/server_connectorIntegration',
|
||||
'localAPI/server_localAPI',
|
||||
];
|
||||
|
||||
Components.utils.import("resource://gre/modules/ComponentUtils.jsm");
|
||||
|
|
|
|||
|
|
@ -50,8 +50,6 @@ var ZoteroPane = new function()
|
|||
this.handleKeyPress = handleKeyPress;
|
||||
this.getSelectedCollection = getSelectedCollection;
|
||||
this.getSelectedSavedSearch = getSelectedSavedSearch;
|
||||
this.getSelectedItems = getSelectedItems;
|
||||
this.getSortedItems = getSortedItems;
|
||||
this.getSortField = getSortField;
|
||||
this.getSortDirection = getSortDirection;
|
||||
this.setItemsPaneMessage = setItemsPaneMessage;
|
||||
|
|
@ -701,15 +699,18 @@ var ZoteroPane = new function()
|
|||
localized: Zotero.ItemTypes.getLocalizedString(type.id)
|
||||
};
|
||||
});
|
||||
|
||||
let allItemTypes = [...primaryItemTypes, ...secondaryItemTypes];
|
||||
|
||||
var collation = Zotero.getLocaleCollation();
|
||||
primaryItemTypes.sort(function (a, b) {
|
||||
return collation.compareString(1, a.localized, b.localized);
|
||||
});
|
||||
secondaryItemTypes.sort(function (a, b) {
|
||||
allItemTypes.sort(function (a, b) {
|
||||
return collation.compareString(1, a.localized, b.localized);
|
||||
});
|
||||
let lastPrimaryType = primaryItemTypes[primaryItemTypes.length - 1];
|
||||
let itemTypes = primaryItemTypes.concat(secondaryItemTypes);
|
||||
// The array of all item types with MRU prepended to the top
|
||||
let itemTypes = primaryItemTypes.concat(allItemTypes);
|
||||
for (let itemType of itemTypes) {
|
||||
let menuitem = document.createXULElement("menuitem");
|
||||
menuitem.setAttribute("label", itemType.localized);
|
||||
|
|
@ -720,7 +721,7 @@ var ZoteroPane = new function()
|
|||
});
|
||||
addMenu.appendChild(menuitem);
|
||||
// Add a separator between primary and secondary types
|
||||
if (lastPrimaryType.id == type) {
|
||||
if (addMenu.childElementCount == primaryItemTypes.length) {
|
||||
let separator = document.createXULElement("menuseparator");
|
||||
addMenu.appendChild(separator);
|
||||
}
|
||||
|
|
@ -1936,7 +1937,7 @@ var ZoteroPane = new function()
|
|||
|
||||
// Tab selection observer in standalone.js makes sure that
|
||||
// updateQuickCopyCommands is called
|
||||
if (Zotero_Tabs.selectedID == 'zotero-pane') {
|
||||
if (Zotero_Tabs.selectedType == 'library') {
|
||||
this.updateQuickCopyCommands(selectedItems);
|
||||
}
|
||||
|
||||
|
|
@ -2654,21 +2655,10 @@ var ZoteroPane = new function()
|
|||
|
||||
this.copySelectedItemsToClipboard = function (asCitations) {
|
||||
var items = [];
|
||||
if (Zotero_Tabs.selectedID == 'zotero-pane') {
|
||||
let itemIDs = this.getSelectedItems(true);
|
||||
// Get selected item IDs in the item tree order
|
||||
itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id));
|
||||
items = Zotero.Items.get(itemIDs);
|
||||
}
|
||||
else {
|
||||
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
|
||||
if (reader) {
|
||||
let item = Zotero.Items.get(reader.itemID);
|
||||
if (item.parentItem) {
|
||||
items = [item.parentItem];
|
||||
}
|
||||
}
|
||||
}
|
||||
let itemIDs = this.getSelectedItems(true);
|
||||
// Get selected item IDs in the item tree order
|
||||
itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id));
|
||||
items = Zotero.Items.get(itemIDs);
|
||||
|
||||
if (!items.length) {
|
||||
return;
|
||||
|
|
@ -3070,14 +3060,28 @@ var ZoteroPane = new function()
|
|||
*
|
||||
* If asIDs is true, return an array of itemIDs instead
|
||||
*/
|
||||
function getSelectedItems(asIDs)
|
||||
{
|
||||
if (!this.itemsView) {
|
||||
return [];
|
||||
this.getSelectedItems = function (asIDs) {
|
||||
switch (Zotero_Tabs.selectedType) {
|
||||
case 'library':
|
||||
if (!this.itemsView) {
|
||||
return [];
|
||||
}
|
||||
return this.itemsView.getSelectedItems(asIDs);
|
||||
case 'reader': {
|
||||
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
|
||||
if (reader) {
|
||||
let item = Zotero.Items.get(reader.itemID);
|
||||
if (item.parentItem) {
|
||||
item = item.parentItem;
|
||||
}
|
||||
return asIDs ? [item.id] : [item];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.itemsView.getSelectedItems(asIDs);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
|
|
@ -3085,13 +3089,18 @@ var ZoteroPane = new function()
|
|||
*
|
||||
* If asIDs is true, return an array of itemIDs instead
|
||||
*/
|
||||
function getSortedItems(asIDs) {
|
||||
if (!this.itemsView) {
|
||||
return [];
|
||||
this.getSortedItems = function (asIDs) {
|
||||
switch (Zotero_Tabs.selectedType) {
|
||||
case 'library':
|
||||
if (!this.itemsView) {
|
||||
return [];
|
||||
}
|
||||
return this.itemsView.getSortedItems(asIDs);
|
||||
default:
|
||||
// ALl non-library tabs: Visible items == "selected" items
|
||||
return this.getSelectedItems(asIDs);
|
||||
}
|
||||
|
||||
return this.itemsView.getSortedItems(asIDs);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function getSortField() {
|
||||
|
|
@ -4510,22 +4519,14 @@ var ZoteroPane = new function()
|
|||
/**
|
||||
* @param {Boolean} [link]
|
||||
* @param {Number} [parentItemID]
|
||||
* @param {nsIFile[]} [files] Used instead of showing a file picker - for tests
|
||||
* @param {String[]} [files] Used instead of showing a file picker - for tests
|
||||
* @returns {Promise<Zotero.Item[] | null>}
|
||||
*/
|
||||
this.addAttachmentFromDialog = async function (link, parentItemID, files = null) {
|
||||
if (!this.canEdit()) {
|
||||
this.displayCannotEditLibraryMessage();
|
||||
return null;
|
||||
}
|
||||
|
||||
var collectionTreeRow = this.getCollectionTreeRow();
|
||||
if (link) {
|
||||
if (collectionTreeRow.isWithinGroup()) {
|
||||
Zotero.alert(null, "", "Linked files cannot be added to group libraries.");
|
||||
return null;
|
||||
}
|
||||
else if (collectionTreeRow.isPublications()) {
|
||||
var libraryID;
|
||||
if (Zotero_Tabs.selectedType === 'library') {
|
||||
let collectionTreeRow = this.getCollectionTreeRow();
|
||||
if (link && collectionTreeRow.isPublications()) {
|
||||
Zotero.alert(
|
||||
null,
|
||||
Zotero.getString('general.error'),
|
||||
|
|
@ -4533,15 +4534,25 @@ var ZoteroPane = new function()
|
|||
);
|
||||
return null;
|
||||
}
|
||||
libraryID = collectionTreeRow.ref.libraryID;
|
||||
}
|
||||
else {
|
||||
libraryID = Zotero.Items.get(parentItemID).libraryID;
|
||||
}
|
||||
|
||||
if (!this.canEdit()) {
|
||||
this.displayCannotEditLibraryMessage();
|
||||
return null;
|
||||
}
|
||||
// TODO: disable in menu
|
||||
if (!this.canEditFiles()) {
|
||||
this.displayCannotEditLibraryFilesMessage();
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (link && Zotero.Libraries.get(libraryID).isGroup) {
|
||||
Zotero.alert(null, "", "Linked files cannot be added to group libraries.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var libraryID = collectionTreeRow.ref.libraryID;
|
||||
|
||||
if (!files) {
|
||||
var fp = new FilePicker();
|
||||
|
|
@ -5369,36 +5380,48 @@ var ZoteroPane = new function()
|
|||
/**
|
||||
* Test if the user can edit the currently selected view
|
||||
*
|
||||
* @param {Integer} [row]
|
||||
*
|
||||
* @return {Boolean} TRUE if user can edit, FALSE if not
|
||||
* @param {Integer} [row] Row index - ignored if not in library tab
|
||||
* @return {Boolean} TRUE if user can edit, FALSE if not
|
||||
*/
|
||||
this.canEdit = function (row) {
|
||||
// Currently selected row
|
||||
if (row === undefined) {
|
||||
row = this.collectionsView.selection.focused;
|
||||
switch (Zotero_Tabs.selectedType) {
|
||||
case 'library':
|
||||
// Currently selected row
|
||||
if (row === undefined) {
|
||||
row = this.collectionsView.selection.focused;
|
||||
}
|
||||
return this.collectionsView.getRow(row).editable;
|
||||
case 'reader': {
|
||||
let itemID = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)?.itemID;
|
||||
if (!itemID) {
|
||||
throw new Error('Reader tab has no itemID');
|
||||
}
|
||||
return Zotero.Items.get(itemID).library.editable;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
var collectionTreeRow = this.collectionsView.getRow(row);
|
||||
return collectionTreeRow.editable;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Test if the user can edit the parent library of the selected view
|
||||
*
|
||||
* @param {Integer} [row]
|
||||
* @return {Boolean} TRUE if user can edit, FALSE if not
|
||||
* @param {Integer} [row] Row index - ignored if not in library tab
|
||||
* @return {Boolean} TRUE if user can edit, FALSE if not
|
||||
*/
|
||||
this.canEditLibrary = function (row) {
|
||||
// Currently selected row
|
||||
if (row === undefined) {
|
||||
row = this.collectionsView.selection.focused;
|
||||
switch (Zotero_Tabs.selectedType) {
|
||||
case 'library': // Currently selected row
|
||||
if (row === undefined) {
|
||||
row = this.collectionsView.selection.focused;
|
||||
}
|
||||
return Zotero.Libraries.get(this.collectionsView.getRow(row).ref.libraryID).editable;
|
||||
default:
|
||||
// All non-library tabs: canEditLibrary() == canEdit()
|
||||
return this.canEdit(row);
|
||||
}
|
||||
|
||||
var collectionTreeRow = this.collectionsView.getRow(row);
|
||||
return Zotero.Libraries.get(collectionTreeRow.ref.libraryID).editable;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -5409,14 +5432,24 @@ var ZoteroPane = new function()
|
|||
* @return {Boolean} TRUE if user can edit, FALSE if not
|
||||
*/
|
||||
this.canEditFiles = function (row) {
|
||||
// Currently selected row
|
||||
if (row === undefined) {
|
||||
row = this.collectionsView.selection.focused;
|
||||
switch (Zotero_Tabs.selectedType) {
|
||||
case 'library':
|
||||
// Currently selected row
|
||||
if (row === undefined) {
|
||||
row = this.collectionsView.selection.focused;
|
||||
}
|
||||
return this.collectionsView.getRow(row).filesEditable;
|
||||
case 'reader': {
|
||||
let itemID = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)?.itemID;
|
||||
if (!itemID) {
|
||||
throw new Error('Reader tab has no itemID');
|
||||
}
|
||||
return Zotero.Items.get(itemID).library.filesEditable;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
var collectionTreeRow = this.collectionsView.getRow(row);
|
||||
return collectionTreeRow.filesEditable;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.displayCannotEditLibraryMessage = function () {
|
||||
|
|
@ -6814,13 +6847,14 @@ var ZoteroPane = new function()
|
|||
};
|
||||
|
||||
this.buildFieldTransformMenu = function ({ target, onTransform }) {
|
||||
let doc = target.ownerDocument;
|
||||
let value = target.value;
|
||||
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
|
||||
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value, true);
|
||||
let valueSentenceCased = Zotero.Utilities.sentenceCase(value);
|
||||
|
||||
let menupopup = document.createXULElement('menupopup');
|
||||
let menupopup = doc.createXULElement('menupopup');
|
||||
|
||||
let titleCase = document.createXULElement('menuitem');
|
||||
let titleCase = doc.createXULElement('menuitem');
|
||||
titleCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
|
||||
titleCase.addEventListener('command', () => {
|
||||
onTransform(valueTitleCased);
|
||||
|
|
@ -6828,7 +6862,7 @@ var ZoteroPane = new function()
|
|||
titleCase.disabled = valueTitleCased == value;
|
||||
menupopup.append(titleCase);
|
||||
|
||||
let sentenceCase = document.createXULElement('menuitem');
|
||||
let sentenceCase = doc.createXULElement('menuitem');
|
||||
sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
|
||||
sentenceCase.addEventListener('command', () => {
|
||||
onTransform(valueSentenceCased);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
|
|||
noteEditor.deleteRow=Delete Row
|
||||
noteEditor.deleteColumn=Delete Column
|
||||
noteEditor.deleteTable=Delete Table
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Annotations
|
||||
pdfReader.showAnnotations=Show Annotations
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
|
|||
noteEditor.deleteRow=Delete Row
|
||||
noteEditor.deleteColumn=Delete Column
|
||||
noteEditor.deleteTable=Delete Table
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Annotations
|
||||
pdfReader.showAnnotations=عرض التعليقات
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
|
|||
noteEditor.deleteRow=Delete Row
|
||||
noteEditor.deleteColumn=Delete Column
|
||||
noteEditor.deleteTable=Delete Table
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Annotations
|
||||
pdfReader.showAnnotations=Show Annotations
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Enlakaat ur golonenn a zehoù
|
|||
noteEditor.deleteRow=Delete Row
|
||||
noteEditor.deleteColumn=Delete Column
|
||||
noteEditor.deleteTable=Delete Table
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Ennotadurioù
|
||||
pdfReader.showAnnotations=Diskouez an ennotadurioù
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insereix una columna a la dreta
|
|||
noteEditor.deleteRow=Suprimeix la fila
|
||||
noteEditor.deleteColumn=Suprimeix la columna
|
||||
noteEditor.deleteTable=Suprimeix la taula
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Anotacions
|
||||
pdfReader.showAnnotations=Mostra les anotacions
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Vložit sloupec napravo
|
|||
noteEditor.deleteRow=Smazat řádek
|
||||
noteEditor.deleteColumn=Smazat sloupec
|
||||
noteEditor.deleteTable=Smazat tabulku
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Anotace
|
||||
pdfReader.showAnnotations=Zobrazit anotace
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
|
|||
noteEditor.deleteRow=Delete Row
|
||||
noteEditor.deleteColumn=Delete Column
|
||||
noteEditor.deleteTable=Delete Table
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Annotations
|
||||
pdfReader.showAnnotations=Show Annotations
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Spalte rechts einfügen
|
|||
noteEditor.deleteRow=Zeile löschen
|
||||
noteEditor.deleteColumn=Spalte löschen
|
||||
noteEditor.deleteTable=Tabelle löschen
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Anmerkungen
|
||||
pdfReader.showAnnotations=Anmerkungen anzeigen
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
|
|||
noteEditor.deleteRow=Delete Row
|
||||
noteEditor.deleteColumn=Delete Column
|
||||
noteEditor.deleteTable=Delete Table
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Annotations
|
||||
pdfReader.showAnnotations=Show Annotations
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
|
|||
noteEditor.deleteRow=Delete Row
|
||||
noteEditor.deleteColumn=Delete Column
|
||||
noteEditor.deleteTable=Delete Table
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Annotations
|
||||
pdfReader.showAnnotations=Show Annotations
|
||||
|
|
|
|||
582
chrome/locale/en-US/zotero/mobile.strings
Normal file
582
chrome/locale/en-US/zotero/mobile.strings
Normal file
|
|
@ -0,0 +1,582 @@
|
|||
/*
|
||||
Localizable.strings
|
||||
Zotero
|
||||
|
||||
Created by Michal Rentka on 21/04/2020.
|
||||
Copyright © 2020 Corporation for Digital Scholarship. All rights reserved.
|
||||
*/
|
||||
|
||||
"close" = "Close";
|
||||
"delete" = "Delete";
|
||||
"edit" = "Edit";
|
||||
"last_updated" = "Last Updated";
|
||||
"name" = "Name";
|
||||
"save" = "Save";
|
||||
"creator" = "Creator";
|
||||
"date" = "Date";
|
||||
"date_added" = "Date Added";
|
||||
"date_modified" = "Date Modified";
|
||||
"item_type" = "Item Type";
|
||||
"publication_title" = "Publication Title";
|
||||
"publisher" = "Publisher";
|
||||
"title" = "Title";
|
||||
"year" = "Year";
|
||||
"abstract" = "Abstract";
|
||||
"error" = "Error";
|
||||
"warning" = "Warning";
|
||||
"yes" = "Yes";
|
||||
"no" = "No";
|
||||
"cancel" = "Cancel";
|
||||
"done" = "Done";
|
||||
"ok" = "OK";
|
||||
"add" = "Add";
|
||||
"unknown" = "Unknown";
|
||||
"retry" = "Retry";
|
||||
"page" = "Page";
|
||||
"report" = "Report";
|
||||
"total" = "Total";
|
||||
"restore" = "Restore";
|
||||
"copy" = "Copy";
|
||||
"privacy_policy" = "Privacy Policy";
|
||||
"support_feedback" = "Support and Feedback";
|
||||
"about_zotero" = "About Zotero";
|
||||
"recent" = "Recents";
|
||||
"look_up" = "Look Up";
|
||||
"not_found" = "Not Found";
|
||||
"share" = "Share";
|
||||
"select" = "Select";
|
||||
"create" = "Create";
|
||||
"more_information" = "More Information";
|
||||
"clear" = "Clear";
|
||||
"scan_text" = "Scan Text";
|
||||
"move_to_trash" = "Move to Trash";
|
||||
"size" = "Size";
|
||||
"remove" = "Remove";
|
||||
"keep" = "Keep";
|
||||
"cancel_all" = "Cancel All";
|
||||
"back" = "Back";
|
||||
"forward" = "Forward";
|
||||
"go_to_settings" = "Go to Settings";
|
||||
"close_without_saving" = "Close without saving";
|
||||
"stay" = "Stay";
|
||||
|
||||
"beta_wipe_title" = "Resync Required";
|
||||
"beta_wipe_message" = "Due to a beta update, your data must be redownloaded from zotero.org.";
|
||||
"migration_debug" = "App failed to initialize and can’t function properly. Please report Debug ID %@ in the Zotero Forums.";
|
||||
"login_debug" = "App failed to log in. Please log in again and report Debug ID %@ in the Zotero Forums.";
|
||||
"full_sync_debug" = "Full sync has finished. Please report Debug ID %@ in the Zotero Forums.";
|
||||
|
||||
"day_suffixes" = "st,nd,rd,th";
|
||||
|
||||
"onboarding.access" = "<b>Zotero organizes research</b> however you want. Sort your items into collections and tag them with keywords.";
|
||||
"onboarding.sync" = "<b>Synchronize and collaborate</b> across devices, keeping your reading and notes seamlessly up to date.";
|
||||
"onboarding.annotate" = "<b>Highlight and take notes</b> directly in your PDFs as you read them.";
|
||||
"onboarding.share" = "<b>Tap to collect</b> articles and books directly from the web, including their PDFs and full metadata.";
|
||||
"onboarding.sign_in" = "Sign In";
|
||||
"onboarding.create_account" = "Sign Up";
|
||||
"login.username" = "Username";
|
||||
"login.password" = "Password";
|
||||
"login.email" = "Email";
|
||||
"login.repeat_password" = "Repeat password";
|
||||
"login.forgot_password" = "Forgot Password?";
|
||||
|
||||
"libraries.my_library" = "My Library";
|
||||
"libraries.group_libraries" = "Group Libraries";
|
||||
"libraries.delete_question" = "Do you really want to delete “%@”?";
|
||||
|
||||
"collections.search_title" = "Find Collection";
|
||||
"collections.all_items" = "All Items";
|
||||
"collections.my_publications" = "My Publications";
|
||||
"collections.trash" = "Trash";
|
||||
"collections.unfiled" = "Unfiled Items";
|
||||
"collections.new_subcollection" = "New Subcollection";
|
||||
"collections.edit_title" = "Edit Collection";
|
||||
"collections.delete" = "Delete Collection";
|
||||
"collections.delete_with_items" = "Delete Collection and Items";
|
||||
"collections.create_title" = "Create Collection";
|
||||
"collections.picker_title" = "Choose Parent";
|
||||
"collections.empty_trash" = "Empty Trash";
|
||||
"collections.collapse_all" = "Collapse All";
|
||||
"collections.expand_all" = "Expand All";
|
||||
"collections.create_bibliography" = "Create Bibliography from Collection";
|
||||
"collections.download_attachments" = "Download Attachments";
|
||||
"collections.delete_attachment_files" = "Remove Downloads";
|
||||
|
||||
"sync_toolbar.starting" = "Sync starting";
|
||||
"sync_toolbar.groups" = "Syncing groups";
|
||||
"sync_toolbar.groups_with_data" = "Syncing groups (%d / %d)";
|
||||
"sync_toolbar.library" = "Syncing %@";
|
||||
"sync_toolbar.object" = "Syncing %@ in %@";
|
||||
"sync_toolbar.object_with_data" = "Syncing %@ (%d / %d) in %@";
|
||||
"sync_toolbar.finished" = "Finished sync";
|
||||
"sync_toolbar.deletion" = "Applying remote deletions in %@";
|
||||
"sync_toolbar.aborted" = "Sync failed (%@)";
|
||||
"sync_toolbar.writes" = "Uploading changes (%d / %d)";
|
||||
"sync_toolbar.uploads" = "Uploading attachment (%d / %d)";
|
||||
"sync_toolbar.object.collections" = "collections";
|
||||
"sync_toolbar.object.groups" = "groups";
|
||||
"sync_toolbar.object.items" = "items";
|
||||
"sync_toolbar.object.searches" = "searches";
|
||||
|
||||
"sync.conflict_resolution.changed_item_deleted" = "The item “%@” has been removed. Do you want to keep your changes?";
|
||||
|
||||
"items.search_title" = "Search Items";
|
||||
"items.select_all" = "Select All";
|
||||
"items.deselect_all" = "Deselect All";
|
||||
"items.sort_by" = "Sort By";
|
||||
"items.sort_order" = "Sort Order";
|
||||
"items.ascending" = "Ascending";
|
||||
"items.descending" = "Descending";
|
||||
"items.lookup" = "Add by Identifier";
|
||||
"items.barcode" = "Scan Barcode";
|
||||
"items.new" = "Add Manually";
|
||||
"items.new_note" = "New Standalone Note";
|
||||
"items.new_file" = "Add File";
|
||||
"items.remove_from_collection_title" = "Remove from Collection";
|
||||
"items.action.add_to_collection" = "Add to Collection";
|
||||
"items.action.remove_from_collection" = "Remove from Collection";
|
||||
"items.action.duplicate" = "Duplicate";
|
||||
"items.action.create_parent" = "Create Parent Item";
|
||||
"items.action.download" = "Download";
|
||||
"items.action.remove_download" = "Remove Download";
|
||||
"items.filters.title" = "Filters";
|
||||
"items.filters.downloads" = "Downloaded Files";
|
||||
"items.filters.tags" = "Tags";
|
||||
"items.toolbar_saved" = "Saved %d / %d";
|
||||
"items.toolbar_downloaded" = "Downloaded %d / %d";
|
||||
"items.generating_bib" = "Generating Bibliography";
|
||||
"items.creator_summary.and" = "%@ and %@";
|
||||
"items.creator_summary.etal" = "%@ et al.";
|
||||
|
||||
"lookup.title" = "Enter ISBNs, DOls, PMIDs, arXiv IDs, or ADS Bibcodes to add to your library:";
|
||||
|
||||
"citation.title" = "Citation Preview";
|
||||
"citation.copy_citation" = "Copy Citation";
|
||||
"citation.copy_bibliography" = "Copy Bibliography";
|
||||
"citation.preview" = "Preview";
|
||||
"citation.omit_author" = "Omit Author";
|
||||
"citation.locator_placeholder" = "Number";
|
||||
"citation.locator.page" = "Page";
|
||||
"citation.locator.book" = "Book";
|
||||
"citation.locator.chapter" = "Chapter";
|
||||
"citation.locator.column" = "Column";
|
||||
"citation.locator.figure" = "Figure";
|
||||
"citation.locator.folio" = "Folio";
|
||||
"citation.locator.issue" = "Issue";
|
||||
"citation.locator.line" = "Line";
|
||||
"citation.locator.note" = "Note";
|
||||
"citation.locator.opus" = "Opus";
|
||||
"citation.locator.paragraph" = "Paragraph";
|
||||
"citation.locator.part" = "Part";
|
||||
"citation.locator.section" = "Section";
|
||||
"citation.locator.sub verbo" = "Sub verbo";
|
||||
"citation.locator.volume" = "Volume";
|
||||
"citation.locator.verse" = "Verse";
|
||||
"citation.style" = "Style";
|
||||
"citation.language" = "Language";
|
||||
"citation.output_mode" = "Output Mode";
|
||||
"citation.citations" = "Citations";
|
||||
"citation.bibliography" = "Bibliography";
|
||||
"citation.notes" = "Notes";
|
||||
"citation.output_method" = "Output Method";
|
||||
"citation.save_html" = "Save as HTML";
|
||||
"citation.copy" = "Copy to Clipboard";
|
||||
|
||||
"item_detail.add_creator" = "Add Creator";
|
||||
"item_detail.notes" = "Notes";
|
||||
"item_detail.add_note" = "Add note";
|
||||
"item_detail.tags" = "Tags";
|
||||
"item_detail.add_tag" = "Add tag";
|
||||
"item_detail.attachments" = "Attachments";
|
||||
"item_detail.add_attachment" = "Add attachment";
|
||||
"item_detail.split_name" = "Split name";
|
||||
"item_detail.merge_name" = "Merge name";
|
||||
"item_detail.untitled" = "Untitled";
|
||||
"item_detail.show_more" = "Show more";
|
||||
"item_detail.show_less" = "Show less";
|
||||
"item_detail.view_pdf" = "View PDF";
|
||||
"item_detail.data_reloaded" = "This item has been changed remotely. It will now reload.";
|
||||
"item_detail.delete_attachment_file" = "Remove Download";
|
||||
"item_detail.deleted_title" = "Deleted";
|
||||
"item_detail.deleted_message" = "This item has been deleted. Do you want to restore it?";
|
||||
"item_detail.move_to_standalone_attachment" = "Convert to Standalone Attachment";
|
||||
|
||||
"tag_picker.title" = "%d selected";
|
||||
"tag_picker.placeholder" = "Tag name";
|
||||
"tag_picker.create_tag" = "Create Tag \“%@\”";
|
||||
"tag_picker.confirm_deletion_question" = "Delete Automatic Tags";
|
||||
"tag_picker.search_placeholder" = "Search Tags";
|
||||
"tag_picker.deselect_all" = "Deselect All";
|
||||
"tag_picker.show_auto" = "Show Automatic Tags";
|
||||
"tag_picker.show_all" = "Display All Tags in This Library";
|
||||
"tag_picker.delete_automatic" = "Delete Automatic Tags in This Library";
|
||||
|
||||
"creator_editor.creator" = "Creator Type";
|
||||
"creator_editor.first_name" = "First Name";
|
||||
"creator_editor.last_name" = "Last Name";
|
||||
"creator_editor.switch_to_single" = "Switch to single field";
|
||||
"creator_editor.switch_to_dual" = "Switch to two fields";
|
||||
"creator_editor.delete_confirmation" = "Do you really want to delete this creator?";
|
||||
|
||||
"pdf.annotations_sidebar.search_title" = "Search";
|
||||
"pdf.annotations_sidebar.add_comment" = "Add comment";
|
||||
"pdf.annotations_sidebar.add_tags" = "Add tags";
|
||||
"pdf.annotations_sidebar.merge" = "Merge";
|
||||
"pdf.annotations_sidebar.filter.title" = "Filter Annotations";
|
||||
"pdf.annotations_sidebar.filter.tags_placeholder" = "Select Tags…";
|
||||
"pdf.search.title" = "Search in Document";
|
||||
"pdf.search.failed" = "Search failed";
|
||||
"pdf.search.dismiss" = "Dismiss Search";
|
||||
"pdf.annotation_popover.title" = "Edit Annotation";
|
||||
"pdf.annotation_popover.delete" = "Delete Annotation";
|
||||
"pdf.annotation_popover.update_subsequent_pages" = "Update subsequent pages";
|
||||
"pdf.annotation_popover.page_label_title" = "Edit Page Number";
|
||||
"pdf.annotation_popover.no_comment" = "No comment";
|
||||
"pdf.annotation_popover.line_width" = "Width";
|
||||
"pdf.annotation_popover.size" = "Size";
|
||||
"pdf.deleted_title" = "Deleted";
|
||||
"pdf.deleted_message" = "This document has been deleted. Do you want to restore it?";
|
||||
"pdf.line_width_point" = "%0.1f pt";
|
||||
"pdf.highlight" = "Highlight";
|
||||
"pdf.underline" = "Underline";
|
||||
"pdf.annotation_share.image.share" = "Share image";
|
||||
"pdf.annotation_share.image.medium" = "Medium";
|
||||
"pdf.annotation_share.image.large" = "Large";
|
||||
"pdf.annotation_toolbar.eraser" = "Eraser";
|
||||
"pdf.annotation_toolbar.highlight" = "Highlight";
|
||||
"pdf.annotation_toolbar.note" = "Note";
|
||||
"pdf.annotation_toolbar.image" = "Image";
|
||||
"pdf.annotation_toolbar.ink" = "Ink";
|
||||
"pdf.annotation_toolbar.underline" = "Underline";
|
||||
"pdf.annotation_toolbar.text" = "Text";
|
||||
"pdf.locked.locked" = "Locked";
|
||||
"pdf.locked.enter_password" = "Please enter the password to open this PDF.";
|
||||
"pdf.locked.failed" = "Incorrect password. Please try again.";
|
||||
"pdf.settings.scroll_direction.title" = "Scroll Direction";
|
||||
"pdf.settings.scroll_direction.horizontal" = "Horizontal";
|
||||
"pdf.settings.scroll_direction.vertical" = "Vertical";
|
||||
"pdf.settings.page_transition.title" = "Page Transition";
|
||||
"pdf.settings.page_transition.jump" = "Jump";
|
||||
"pdf.settings.page_transition.continuous" = "Continuous";
|
||||
"pdf.settings.appearance.title" = "Appearance";
|
||||
"pdf.settings.appearance.auto" = "Automatic";
|
||||
"pdf.settings.appearance.light_mode" = "Light";
|
||||
"pdf.settings.appearance.dark_mode" = "Dark";
|
||||
"pdf.settings.appearance.sepia_mode" = "Sepia";
|
||||
"pdf.settings.page_mode.title" = "Page Mode";
|
||||
"pdf.settings.page_mode.single" = "Single";
|
||||
"pdf.settings.page_mode.double" = "Double";
|
||||
"pdf.settings.page_mode.automatic" = "Automatic";
|
||||
"pdf.settings.page_fitting.title" = "Page Fitting";
|
||||
"pdf.settings.page_fitting.fit" = "Fit";
|
||||
"pdf.settings.page_fitting.fill" = "Fill";
|
||||
"pdf.settings.page_spreads.title" = "Page Spreads";
|
||||
"pdf.settings.page_spreads.odd" = "Odd";
|
||||
"pdf.settings.page_spreads.even" = "Even";
|
||||
"pdf.settings.page_fitting.automatic" = "Automatic";
|
||||
"pdf.export.export" = "Export PDF";
|
||||
"pdf.export.export_annotated" = "Export Annotated PDF";
|
||||
"pdf.sidebar.no_annotations" = "No Annotations";
|
||||
"pdf.sidebar.no_outline" = "No Outline";
|
||||
"pdf.delete_annotation" = "Do you really want to delete annotation?";
|
||||
"pdf.previous_page" = "Previous Page";
|
||||
"pdf.next_page" = "Next Page";
|
||||
"pdf.previous_viewport" = "Previous Viewport";
|
||||
"pdf.next_viewport" = "Next Viewport";
|
||||
|
||||
"settings.title" = "Settings";
|
||||
"settings.logout" = "Sign Out";
|
||||
"settings.logout_warning" = "Any local data that was not synced will be deleted. Do you really want to sign out?";
|
||||
"settings.general.title" = "General";
|
||||
"settings.general.show_subcollections_title" = "Show Items from Subcollections";
|
||||
"settings.general.show_collection_item_counts" = "Show collection sizes";
|
||||
"settings.general.open_links_in_external_browser" = "Open links in external browser";
|
||||
"settings.general.autoempty_title" = "Delete Items in Trash";
|
||||
"settings.general.never" = "Never";
|
||||
"settings.item_count" = "Item count";
|
||||
"settings.item_count_subtitle" = "Show item count for all collections.";
|
||||
"settings.sync.title" = "Account";
|
||||
"settings.sync.data_syncing" = "Data Syncing";
|
||||
"settings.sync.file_syncing" = "File Syncing";
|
||||
"settings.sync.file_syncing_type_message" = "Sync attachment files in My Library using";
|
||||
"settings.sync.account" = "Account";
|
||||
"settings.sync.manage_account" = "Manage Account";
|
||||
"settings.sync.delete_account" = "Delete Account";
|
||||
"settings.sync.username" = "Username";
|
||||
"settings.sync.password" = "Password";
|
||||
"settings.sync.verify" = "Verify Server";
|
||||
"settings.sync.directory_not_found.title" = "Directory not found";
|
||||
"settings.sync.directory_not_found.message" = "%@ does not exist.\n\nDo you want to create it now?";
|
||||
"settings.sync.verified" = "Verified";
|
||||
"settings.permission" = "User Permission";
|
||||
"settings.permission_subtitle" = "Ask for user permission for each write action";
|
||||
"settings.translators" = "Translators";
|
||||
"settings.translators_update" = "Update translators";
|
||||
"settings.translators_updating" = "Updating…";
|
||||
"settings.reset_to_bundled" = "Reset to bundled";
|
||||
"settings.debug" = "Debug Output Logging";
|
||||
"settings.logging_title" = "Logging";
|
||||
"settings.start_logging" = "Start Logging";
|
||||
"settings.start_logging_on_launch" = "Start Logging on Next App Launch";
|
||||
"settings.cancel_logging" = "Cancel Logging";
|
||||
"settings.stop_logging" = "Stop Logging";
|
||||
"settings.view_output" = "View Output";
|
||||
"settings.clear_output" = "Clear Output";
|
||||
"settings.logging_desc1" = "To debug a startup issue, force-quit the app and start it again.";
|
||||
"settings.logging_desc2" = "To debug a share extension issue, open the share extension.";
|
||||
"settings.send_manually" = "Send Manually";
|
||||
"settings.log_alert.progress_title" = "Sending Logs";
|
||||
"settings.log_alert.title" = "Logs Sent";
|
||||
"settings.log_alert.message" = "Your Debug ID is %@";
|
||||
"settings.crash_alert.title" = "Crash Log Sent";
|
||||
"settings.crash_alert.message" = "Your Report ID is %@.\n\nPlease post a message to the Zotero Forums (forums.zotero.org) with this Report ID and any steps necessary to reproduce the crash.";
|
||||
"settings.crash_alert.message_with_db" = "Your Report ID is %@.\n\nPlease post a message to the Zotero Forums (forums.zotero.org) with this Report ID and any steps necessary to reproduce the crash.\n\nIf Zotero crashes repeatedly, please tap “Export Database” and send exported files to support@zotero.org.";
|
||||
"settings.crash_alert.submit_forum" = "Submit to Zotero Forums";
|
||||
"settings.crash_alert.export_db" = "Copy and Export Database";
|
||||
"settings.crash_alert.copy" = "Copy to Clipboard";
|
||||
"settings.websocket_title" = "Server Connection";
|
||||
"settings.websocket_connected" = "Connected";
|
||||
"settings.websocket_connecting" = "Connecting";
|
||||
"settings.websocket_disconnected" = "Disconnected";
|
||||
"settings.websocket_disconnect" = "Disconnect from Server";
|
||||
"settings.websocket_connect" = "Connect to Server";
|
||||
"settings.storage" = "Local Storage";
|
||||
"settings.storage.delete_all" = "Delete All Local Attachment Files";
|
||||
"settings.storage.delete_all_question" = "Are you sure you want to delete all attachment files from this device?\n\nOther synced devices will not be affected.";
|
||||
"settings.storage.delete_library_question" = "Are you sure you want to delete all attachment files in %@ from this device?\n\nOther synced devices will not be affected.";
|
||||
"settings.saving.title" = "Saving";
|
||||
"settings.saving.files_title" = "Save Files";
|
||||
"settings.saving.files_message" = "Automatically attach associated PDFs and other files when saving items";
|
||||
"settings.saving.tags_title" = "Save Automatic Tags";
|
||||
"settings.saving.tags_message" = "Automatically tag items with keywords and subject headings";
|
||||
"settings.cite.title" = "Cite";
|
||||
"settings.cite.styles_title" = "Citation Styles";
|
||||
"settings.cite.get_more_styles" = "Get additional styles";
|
||||
"settings.cite_search.search_title" = "Search styles";
|
||||
"settings.export.title" = "Quick Copy";
|
||||
"settings.export.default_format" = "Default Format";
|
||||
"settings.export.language" = "Language";
|
||||
"settings.export.copy_as_html" = "Copy as HTML";
|
||||
"settings.versionAndBuild" = "Version %@ Build %@";
|
||||
"settings.export_db" = "Export Database File";
|
||||
"settings.full_sync_debug" = "Full Sync Debugging";
|
||||
"settings.full_sync.start" = "Start Full Sync Debugging";
|
||||
"settings.full_sync.other_in_progress" = "Sync is already in progress. Wait for it to finish.";
|
||||
"settings.full_sync.in_progress" = "A full sync is in progress. Wait for it to finish and post the Debug ID to the Zotero Forums.";
|
||||
|
||||
"shareext.save" = "Save to Zotero";
|
||||
"shareext.decoding_attachment" = "Searching for items";
|
||||
"shareext.translation.item_selection" = "Choose an item";
|
||||
"shareext.translation.translating_with" = "Saving with %@";
|
||||
"shareext.loading_collections" = "Loading Collections";
|
||||
"shareext.sync_error" = "Can't sync collections";
|
||||
"shareext.collection_other" = "More";
|
||||
"shareext.item_title" = "Item";
|
||||
"shareext.collection_title" = "Collection";
|
||||
"shareext.tags_title" = "Tags";
|
||||
|
||||
"searchbar.placeholder" = "Search";
|
||||
"searchbar.accessibility_cancel" = "Cancel Search";
|
||||
"searchbar.accessibility_clear" = "Clear Search";
|
||||
|
||||
"errors.schema" = "Some data in My Library could not be downloaded. It may have been saved with a newer version of Zotero.\n\nOther data will continue to sync.";
|
||||
"errors.parsing" = "Could not parse some data. Other data will continue to sync.";
|
||||
"errors.api" = "Failed API response: %@";
|
||||
"errors.db" = "Could not connect to database. The device storage might be full.";
|
||||
"errors.versionMismatch" = "A remote change was made during the sync";
|
||||
"errors.unknown" = "Unknown error";
|
||||
"errors.attachments.cant_unzip_snapshot" = "Unable to unzip snapshot";
|
||||
"errors.attachments.incompatible_attachment" = "Linked files are not supported on iOS. You can open them using the Zotero desktop app.";
|
||||
"errors.attachments.cant_open_attachment" = "The attached file could not be found.";
|
||||
"errors.attachments.missing_zotero" = "The attached file is not available in the online library.";
|
||||
"errors.attachments.missing_webdav" = "The attached file is not available on the WebDAV server.";
|
||||
"errors.attachments.missing_additional" = "Please check that the file has synced on the device where it was added.";
|
||||
"errors.attachments.unauthorized_webdav" = "The WebDAV server did not accept the username and password you have saved in settings.";
|
||||
"errors.attachments.forbidden_webdav" = "You don’t have permission to access %@ on the WebDAV server.";
|
||||
"errors.attachments.generic_filename" = "the file";
|
||||
"errors.login.invalid_password" = "Invalid password";
|
||||
"errors.login.invalid_username" = "Invalid username";
|
||||
"errors.login.invalid_credentials" = "Invalid username or password";
|
||||
"errors.libraries.cantLoad" = "Unable to load libraries";
|
||||
"errors.collections.empty_name" = "Please enter a collection name";
|
||||
"errors.collections.save_failed" = "Unable to save collection";
|
||||
"errors.collections.bibliography_failed" = "Could not load items for bibliography.";
|
||||
"errors.sync.group_removed" = "Group “%@” is no longer accessible. What would you like to do?";
|
||||
"errors.sync.metadata_write_denied" = "You can no longer write to the group “%@”. What would you like to do?";
|
||||
"errors.sync.revert_to_original" = "Revert to original";
|
||||
"errors.sync.file_write_denied" = "You no longer have file-editing access for the group “%@”, and files you’ve changed locally cannot be uploaded. If you continue, all group files will be reset to their state on %@.\n\nIf you would like a chance to copy modified files elsewhere or to request file-editing access from a group administrator, you can skip syncing of the group now.";
|
||||
"errors.sync.reset_group_files" = "Reset Group Files and Sync";
|
||||
"errors.sync.skip_group" = "Skip Group";
|
||||
"errors.sync_toolbar.finished_with_errors" = "Finished sync (%@)";
|
||||
"errors.sync_toolbar.attachment_missing" = "Unable to upload attachment: %@. Please try removing and re-adding the attachment.";
|
||||
"errors.sync_toolbar.libraries_missing" = "No libraries found. Please sign out and back in again.";
|
||||
"errors.sync_toolbar.groups_failed" = "Could not sync groups. Please try again.";
|
||||
"errors.sync_toolbar.conflict_retry_limit" = "Remote sync in progress. Please try again in a few minutes.";
|
||||
"errors.sync_toolbar.group_permissions" = "You don’t have permission to edit groups.";
|
||||
"errors.sync_toolbar.internet_connection" = "Unable to connect to the network. Please try again.";
|
||||
"errors.sync_toolbar.personal_quota_reached" = "You have reached your Zotero Storage quota. Some files were not uploaded. Other Zotero data will continue to sync.\n\nSee your zotero.org account settings for additional storage options.";
|
||||
"errors.sync_toolbar.group_quota_reached" = "The group “%@” has reached its Zotero Storage quota. Some files were not uploaded. Other Zotero data will continue to sync.\n\nThe group owner can increase the group’s storage capacity from their storage settings on zotero.org.";
|
||||
"errors.sync_toolbar.insufficient_space" = "You have insufficient space on your WebDAV server. Some files were not uploaded. Other Zotero data will continue to sync.";
|
||||
"errors.sync_toolbar.quota_reached_short" = "Quota Reached.";
|
||||
"errors.sync_toolbar.unavailable" = "Zotero services are temporarily unavailable. Please try again in a few minutes.";
|
||||
"errors.sync_toolbar.webdav_error" = "Could not delete files from your WebDAV server: \"%@\".";
|
||||
"errors.sync_toolbar.webdav_item_prop" = "Invalid prop file: %@";
|
||||
"errors.sync_toolbar.show_item" = "Show Item";
|
||||
"errors.sync_toolbar.show_items" = "Show Items";
|
||||
"errors.sync_toolbar.forbidden" = "Invalid username or password";
|
||||
"errors.sync_toolbar.forbidden_message" = "The Zotero sync server did not accept your username and password.\n\nPlease log out and log in with correct login information.";
|
||||
"errors.sync_toolbar.webdav_request_failed" = "Your WebDAV server returned an HTTP %d error for a %@ request.";
|
||||
"errors.items.loading" = "Could not load items.";
|
||||
"errors.items.deletion" = "Could not remove item.";
|
||||
"errors.items.deletion_from_collection" = "Could not remove item from collection.";
|
||||
"errors.items.add_to_collection" = "Could not add item to collection.";
|
||||
"errors.items.move_item" = "Could not move item.";
|
||||
"errors.items.save_note" = "Could not save note.";
|
||||
"errors.items.add_attachment" = "Could not add attachment.";
|
||||
"errors.items.add_some_attachments" = "Some attachments were not added: %@.";
|
||||
"errors.items.load_duplication" = "Could not load item to duplicate.";
|
||||
"errors.items.generating_bib" = "Could not generate bibliography";
|
||||
"errors.citation_preview" = "Could not generate citation preview";
|
||||
"errors.item_detail.dropped_fields_title" = "Change Item Type";
|
||||
"errors.item_detail.dropped_fields_message" = "Are you sure you want to change the item type?\n\nThe following fields will be lost:\n\n%@";
|
||||
"errors.item_detail.cant_load_data" = "Could not load data. Please try again.";
|
||||
"errors.item_detail.cant_create_attachments_with_names" = "Could not create attachments: %@";
|
||||
"errors.item_detail.cant_create_attachments" = "Could not create attachments.";
|
||||
"errors.item_detail.cant_save_note" = "Could not save note.";
|
||||
"errors.item_detail.cant_save_changes" = "Could not save changes.";
|
||||
"errors.item_detail.cant_trash_item" = "Could not move item to trash.";
|
||||
"errors.item_detail.cant_save_tags" = "Could not save tags.";
|
||||
"errors.item_detail.unsupported_type" = "Type \"%@\" not supported.";
|
||||
"errors.settings.storage" = "Could not collect storage data";
|
||||
"errors.settings.webdav.file_missing_after_upload" = "A potential problem was found with your WebDAV server.\n\nAn uploaded file was not immediately available for download. There may be a short delay between when you upload files and when they become available, particularly if you are using a cloud storage service.\n\nIf Zotero file syncing appears to work normally, you can ignore this message. If you have trouble, please post to the Zotero Forums.";
|
||||
"errors.settings.webdav.invalid_url" = "WebDAV verification error";
|
||||
"errors.settings.webdav.no_password" = "WebDAV verification error";
|
||||
"errors.settings.webdav.no_url" = "WebDAV verification error";
|
||||
"errors.settings.webdav.no_username" = "WebDAV verification error";
|
||||
"errors.settings.webdav.non_existent_file_not_missing" = "WebDAV verification error";
|
||||
"errors.settings.webdav.not_dav" = "Not a valid WebDAV URL";
|
||||
"errors.settings.webdav.parent_dir_not_found" = "WebDAV verification error";
|
||||
"errors.settings.webdav.zotero_dir_not_found" = "WebDAV verification error";
|
||||
"errors.settings.webdav.unauthorized" = "The WebDAV server did not accept the username and password you entered.";
|
||||
"errors.settings.webdav.forbidden" = "You don’t have permission to access the specified folder on the WebDAV server.";
|
||||
"errors.settings.webdav.internet_connection" = "Unable to connect to the network. Please try again.";
|
||||
"errors.settings.webdav.host_not_found" = "Could not connect to WebDAV server";
|
||||
"errors.settings.webdav.ats" = "Due to iOS restrictions, you must use HTTPS to connect to a non-local WebDAV server.";
|
||||
"errors.shareext.logged_out" = "Please log into the app before using this extension.";
|
||||
"errors.shareext.cant_load_data" = "Failed to load data. Please try again.";
|
||||
"errors.shareext.unknown" = "An unknown error occurred.";
|
||||
"errors.shareext.cant_load_schema" = "An error occurred. Please open the Zotero app, sync, and try again.";
|
||||
"errors.shareext.download_failed" = "Could not download file";
|
||||
"errors.shareext.items_not_found" = "No items found on page";
|
||||
"errors.shareext.parsing_error" = "Error parsing translator response";
|
||||
"errors.shareext.schema_error" = "Some data could not be downloaded. It may have been saved with a newer version of Zotero.";
|
||||
"errors.shareext.incompatible_item" = "No data returned";
|
||||
"errors.shareext.javascript_failed" = "JS call failed";
|
||||
"errors.shareext.translation_failed" = "Saving failed";
|
||||
"errors.shareext.missing_base_files" = "Translator missing";
|
||||
"errors.shareext.response_missing_data" = "An error occurred. Please try again.";
|
||||
"errors.shareext.missing_file" = "Could not find file to upload";
|
||||
"errors.shareext.background_uploader_failure" = "Background uploader not initialized";
|
||||
"errors.shareext.failed_additional" = "You can still save this page as a webpage item.";
|
||||
"errors.shareext.file_not_pdf" = "Unable to save PDF";
|
||||
"errors.shareext.api_error" = "Error uploading item. The item was saved to your local library.";
|
||||
"errors.shareext.webdav_error" = "Error uploading attachment to WebDAV server";
|
||||
"errors.shareext.personal_quota_reached" = "You have reached your Zotero Storage quota, and the file could not be uploaded. See your account settings for additional storage options.\n\nThe file was saved to your local library.";
|
||||
"errors.shareext.group_quota_reached" = "The group “%@” has reached its Zotero Storage quota, and the file could not be uploaded. The group owner can view their account settings for additional storage options.\n\nThe file was saved to the local library.";
|
||||
"errors.shareext.forbidden" = "You don’t have necessary rights to submit this item to %@.";
|
||||
"errors.shareext.webdav_not_verified" = "WebDAV verification error";
|
||||
"errors.db_failure" = "Error creating database. Please try logging in again.";
|
||||
"errors.logging.title" = "Debugging Error";
|
||||
"errors.logging.start" = "Unable to start debug logging";
|
||||
"errors.logging.content_reading" = "Log files could not be found";
|
||||
"errors.logging.no_logs_recorded" = "No debug output occurred during logging";
|
||||
"errors.logging.upload" = "Could not upload logs. Please try again.";
|
||||
"errors.logging.response_parsing" = "Unexpected response from server";
|
||||
"errors.translators.bundle_loading" = "Could not update translators from bundle. Would you like to try again?";
|
||||
"errors.translators.bundle_reset" = "Could not load bundled translators.";
|
||||
"errors.styles.addition" = "Could not add style \“%@\”.";
|
||||
"errors.styles.deletion" = "Could not delete style \“%@\”.";
|
||||
"errors.styles.loading" = "Could not load styles.";
|
||||
"errors.styles_search.loading" = "Could not load styles. Do you want to try again?";
|
||||
"errors.citation.generate_citation" = "Could not generate citation.";
|
||||
"errors.citation.generate_bibliography" = "Could not generate bibliography.";
|
||||
"errors.citation.invalid_types" = "Invalid item types selected.";
|
||||
"errors.citation.missing_style" = "No citation style selected. Go to Settings → Quick Copy and select a new style.";
|
||||
"errors.citation.open_settings" = "Open Settings";
|
||||
"errors.lookup.no_identifiers_and_no_lookup_data" = "Zotero could not find any identifiers in your input. Please verify your input and try again.";
|
||||
"errors.lookup.no_identifiers_with_lookup_data" = "Zotero could not find any new identifiers in your input, or they are already being added. Please verify your input and try again.";
|
||||
"errors.pdf.merge_too_big_title" = "Unable to merge annotations";
|
||||
"errors.pdf.merge_too_big" = "The combined annotation would be too large.";
|
||||
"errors.pdf.cant_update_annotation" = "Can’t update annotation.";
|
||||
"errors.pdf.cant_add_annotations" = "Can’t add annotations.";
|
||||
"errors.pdf.cant_delete_annotations" = "Can’t delete annotations.";
|
||||
"errors.pdf.incompatible_document" = "This document is not supported.";
|
||||
"errors.pdf.empty_document" = "This document is empty.";
|
||||
"errors.pdf.page_index_not_int" = "Incorrect format of page stored for this document.";
|
||||
"errors.pdf.document_changed" = "This document has been changed on another device. Please reopen it to continue editing.";
|
||||
|
||||
"accessibility.untitled" = "Untitled";
|
||||
"accessibility.locked" = "Locked";
|
||||
"accessibility.archived" = "Archived";
|
||||
"accessibility.collections.items" = "items";
|
||||
"accessibility.collections.expand" = "Expand";
|
||||
"accessibility.collections.collapse" = "Collapse";
|
||||
"accessibility.collections.create_collection" = "Create collection";
|
||||
"accessibility.collections.search_collections" = "Search collections";
|
||||
"accessibility.collections.expand_all_collections" = "Expand all collections";
|
||||
"accessibility.items.open_item" = "Open item info";
|
||||
"accessibility.items.select_items" = "Select items";
|
||||
"accessibility.items.select_all_items" = "Select All Items";
|
||||
"accessibility.items.deselect_all_items" = "Deselect All Items";
|
||||
"accessibility.items.filter_items" = "Filter items";
|
||||
"accessibility.items.sort_items" = "Sort items";
|
||||
"accessibility.items.add_to_collection" = "Add selected items to collection";
|
||||
"accessibility.items.remove_from_collection" = "Remove selected items from collection";
|
||||
"accessibility.items.trash" = "Move selected items to trash";
|
||||
"accessibility.items.delete" = "Delete selected items";
|
||||
"accessibility.items.restore" = "Restore selected items";
|
||||
"accessibility.items.duplicate" = "Duplicate selected item";
|
||||
"accessibility.items.share" = "Share selected items";
|
||||
"accessibility.items.download_attachments" = "Download attachments for selected items";
|
||||
"accessibility.items.remove_downloads" = "Remove downloads for selected items";
|
||||
"accessibility.items.collection" = "Collection";
|
||||
"accessibility.item_detail.download_and_open" = "Double-tap to download and open";
|
||||
"accessibility.item_detail.open" = "Double-tap to open";
|
||||
"accessibility.pdf.sidebar_open" = "Open sidebar";
|
||||
"accessibility.pdf.sidebar_close" = "Close sidebar";
|
||||
"accessibility.pdf.share" = "Share PDF";
|
||||
"accessibility.pdf.export" = "Export PDF";
|
||||
"accessibility.pdf.export_annotated" = "Export annotated PDF";
|
||||
"accessibility.pdf.settings" = "Settings";
|
||||
"accessibility.pdf.color_picker" = "Color picker";
|
||||
"accessibility.pdf.highlight_annotation_tool" = "Create highlight annotation";
|
||||
"accessibility.pdf.note_annotation_tool" = "Create note annotation";
|
||||
"accessibility.pdf.image_annotation_tool" = "Create image annotation";
|
||||
"accessibility.pdf.ink_annotation_tool" = "Create ink annotation";
|
||||
"accessibility.pdf.underline_annotation_tool" = "Create underline annotation";
|
||||
"accessibility.pdf.text_annotation_tool" = "Create text annotation";
|
||||
"accessibility.pdf.eraser_annotation_tool" = "Eraser";
|
||||
"accessibility.pdf.highlight_annotation" = "Highlight annotation";
|
||||
"accessibility.pdf.note_annotation" = "Note annotation";
|
||||
"accessibility.pdf.image_annotation" = "Image annotation";
|
||||
"accessibility.pdf.ink_annotation" = "Ink annotation";
|
||||
"accessibility.pdf.underline_annotation" = "Underline annotation";
|
||||
"accessibility.pdf.text_annotation" = "Text annotation";
|
||||
"accessibility.pdf.edit_annotation" = "Edit annotation";
|
||||
"accessibility.pdf.share_annotation" = "Share annotation";
|
||||
"accessibility.pdf.share_annotation_image" = "Share annotation image";
|
||||
"accessibility.pdf.eraser_annotation" = "Eraser";
|
||||
"accessibility.pdf.highlighted_text" = "Highlighted text";
|
||||
"accessibility.pdf.underlined_text" = "Underlined text";
|
||||
"accessibility.pdf.author" = "Author";
|
||||
"accessibility.pdf.comment" = "Comment";
|
||||
"accessibility.pdf.tags" = "Tags";
|
||||
"accessibility.pdf.annotation_hint" = "Double-tap to select and edit";
|
||||
"accessibility.pdf.tags_hint" = "Double-tap to edit tags";
|
||||
"accessibility.pdf.selected" = "Selected";
|
||||
"accessibility.pdf.open_reader" = "Open reading mode";
|
||||
"accessibility.pdf.search_pdf" = "Search PDF";
|
||||
"accessibility.pdf.redo" = "Redo";
|
||||
"accessibility.pdf.undo" = "Undo";
|
||||
"accessibility.pdf.toggle_annotation_toolbar" = "Toggle annotation toolbar";
|
||||
"accessibility.pdf.show_more_tools" = "Show more";
|
||||
215
chrome/locale/en-US/zotero/mobile.stringsdict
Normal file
215
chrome/locale/en-US/zotero/mobile.stringsdict
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>items.collections_selected</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@collections_selected@</string>
|
||||
<key>collections_selected</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>zero</key>
|
||||
<string>Select a Collection</string>
|
||||
<key>one</key>
|
||||
<string>1 Collection Selected</string>
|
||||
<key>other</key>
|
||||
<string>%d Collections Selected</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>items.delete_question</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@delete_question@</string>
|
||||
<key>delete_question</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Are you sure you want to delete the selected item?</string>
|
||||
<key>other</key>
|
||||
<string>Are you sure you want to delete the selected items?</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>items.remove_from_collection_question</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@remove_from_collection_question@</string>
|
||||
<key>remove_from_collection_question</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Are you sure you want to remove the selected item from this collection?</string>
|
||||
<key>other</key>
|
||||
<string>Are you sure you want to remove the selected items from this collection?</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>items.toolbar_filter</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@toolbar_filter@</string>
|
||||
<key>toolbar_filter</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Filter: 1 item</string>
|
||||
<key>other</key>
|
||||
<string>Filter: %d items</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>pdf.search.matches</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@matches@</string>
|
||||
<key>matches</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Found 1 match</string>
|
||||
<key>other</key>
|
||||
<string>Found %d matches</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>settings.storage.files_size_and_unit</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@files@ (%.2f %@)</string>
|
||||
<key>files</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>1 file</string>
|
||||
<key>other</key>
|
||||
<string>%d files</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>errors.sync_toolbar.errors</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@errors@</string>
|
||||
<key>errors</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>1 issue</string>
|
||||
<key>other</key>
|
||||
<string>%d issues</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>tag_picker.tags_selected</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@tags_selected@</string>
|
||||
<key>tags_selected</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>1 tag selected</string>
|
||||
<key>other</key>
|
||||
<string>%d tags selected</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>tag_picker.confirm_deletion</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@confirm_deletion@</string>
|
||||
<key>confirm_deletion</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Are you sure you want to delete 1 automatic tag in this library?\n\nThis action cannot be undone.</string>
|
||||
<key>other</key>
|
||||
<string>Are you sure you want to delete %d automatic tags in this library?\n\nThis action cannot be undone.</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>settings.lines_logged</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@lines_logged@</string>
|
||||
<key>lines_logged</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>1 line logged</string>
|
||||
<key>other</key>
|
||||
<string>%d lines logged</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>settings.lines</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@lines@</string>
|
||||
<key>lines</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>1 line</string>
|
||||
<key>other</key>
|
||||
<string>%d lines</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>errors.sync_toolbar.webdav_error2</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@webdav_error2@</string>
|
||||
<key>webdav_error2</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Could not delete 1 file from your WebDAV server</string>
|
||||
<key>other</key>
|
||||
<string>Could not delete %d files from your WebDAV server</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>settings.general.after_x_days</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@after_x_days@</string>
|
||||
<key>after_x_days</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>After 1 Day</string>
|
||||
<key>other</key>
|
||||
<string>After %d Days</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -294,6 +294,7 @@ runJS-title = Run JavaScript
|
|||
runJS-editor-label = Code:
|
||||
runJS-run = Run
|
||||
runJS-help = { general-help }
|
||||
runJS-completed = completed successfully
|
||||
runJS-result = {
|
||||
$type ->
|
||||
[async] Return value:
|
||||
|
|
@ -766,3 +767,5 @@ mac-word-plugin-install-remind-later-button =
|
|||
.label = { general-remind-me-later }
|
||||
mac-word-plugin-install-dont-ask-again-button =
|
||||
.label = { general-dont-ask-again }
|
||||
|
||||
userjs-pref-warning = Some { -app-name } settings have been overridden using an unsupported method. { -app-name } will revert them and restart.
|
||||
|
|
|
|||
|
|
@ -1355,6 +1355,7 @@ noteEditor.insertColumnAfter = Insert Column Right
|
|||
noteEditor.deleteRow = Delete Row
|
||||
noteEditor.deleteColumn = Delete Column
|
||||
noteEditor.deleteTable = Delete Table
|
||||
noteEditor.a11yLinkPopupAppearedAlert = Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations = Annotations
|
||||
pdfReader.showAnnotations = Show Annotations
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insertar columna a la derecha
|
|||
noteEditor.deleteRow=Borrar fila
|
||||
noteEditor.deleteColumn=Borrar columna
|
||||
noteEditor.deleteTable=Borrar tabla
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Anotaciones
|
||||
pdfReader.showAnnotations=Mostrar anotaciones
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
|
|||
noteEditor.deleteRow=Delete Row
|
||||
noteEditor.deleteColumn=Delete Column
|
||||
noteEditor.deleteTable=Delete Table
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Annotations
|
||||
pdfReader.showAnnotations=Show Annotations
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Txertatu zutabea eskubian
|
|||
noteEditor.deleteRow=Ezabatu errenkada
|
||||
noteEditor.deleteColumn=Ezabatu zutabea
|
||||
noteEditor.deleteTable=Ezabatu taula
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Oharpenak
|
||||
pdfReader.showAnnotations=Erakutsi oharpenak
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insert Column Right
|
|||
noteEditor.deleteRow=Delete Row
|
||||
noteEditor.deleteColumn=Delete Column
|
||||
noteEditor.deleteTable=Delete Table
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Annotations
|
||||
pdfReader.showAnnotations=Show Annotations
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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…
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Lisää sarake oikealle
|
|||
noteEditor.deleteRow=Poista rivi
|
||||
noteEditor.deleteColumn=Poista sarake
|
||||
noteEditor.deleteTable=Poista taulukko
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Huomautukset
|
||||
pdfReader.showAnnotations=Näytä huomautukset
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Insérer une colonne à droite
|
|||
noteEditor.deleteRow=Supprimer la ligne
|
||||
noteEditor.deleteColumn=Supprimer la colonne
|
||||
noteEditor.deleteTable=Supprimer le tableau
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Annotations
|
||||
pdfReader.showAnnotations=Afficher les annotations
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=Inserir columna á dereita
|
|||
noteEditor.deleteRow=Eliminar fila
|
||||
noteEditor.deleteColumn=Eliminar columna
|
||||
noteEditor.deleteTable=Eliminar táboa
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=Anotacións
|
||||
pdfReader.showAnnotations=Amosar anotacións
|
||||
|
|
|
|||
|
|
@ -1354,6 +1354,7 @@ noteEditor.insertColumnAfter=הוספת עמודה מימין
|
|||
noteEditor.deleteRow=מחיקת שורה
|
||||
noteEditor.deleteColumn=מחיקת עמודה
|
||||
noteEditor.deleteTable=מחיקת טבלה
|
||||
noteEditor.a11yLinkPopupAppearedAlert=Link popup appeared. Use Shift-Tab to navigate it.
|
||||
|
||||
pdfReader.annotations=הסברים
|
||||
pdfReader.showAnnotations=הצגת הסברים
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue