mirror of
https://github.com/zotero/zotero.git
synced 2026-10-10 03:28:10 +00:00
Simple attachment saving via Connector (instead of Zotero) (#5148)
This commit is contained in:
parent
d781151b9a
commit
5e18337cc6
8 changed files with 1118 additions and 683 deletions
|
|
@ -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");
|
||||
|
|
@ -977,6 +977,90 @@ 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 (!title) {
|
||||
// TODO Better attachment name
|
||||
title = Zotero.getString('itemFields.attachmentPDF');
|
||||
}
|
||||
|
||||
if (parentItemID && collections) {
|
||||
throw new Error("parentItemID and collections cannot both be provided");
|
||||
}
|
||||
|
||||
let tmpDirectory = (await this.createTemporaryStorageDirectory()).path;
|
||||
let destDirectory;
|
||||
let attachmentItem;
|
||||
try {
|
||||
let filename = Zotero.File.truncateFileName(this._getFileNameFromURL(url, contentType), 100);
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -446,6 +446,68 @@ 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;
|
||||
|
||||
|
|
|
|||
312
chrome/content/zotero/xpcom/server/saveSession.js
Normal file
312
chrome/content/zotero/xpcom/server/saveSession.js
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
/*
|
||||
***** 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);
|
||||
}
|
||||
|
||||
// If moving from a non-filesEditable library to a filesEditable library, resave from
|
||||
// original data, since there might be files that weren't saved or were removed
|
||||
if (previousTargetID && previousTargetID != targetID) {
|
||||
let { library: oldLibrary } = Zotero.Server.Connector.resolveTarget(previousTargetID);
|
||||
let { library: newLibrary } = Zotero.Server.Connector.resolveTarget(targetID);
|
||||
if (oldLibrary != newLibrary && !oldLibrary.filesEditable && newLibrary.filesEditable) {
|
||||
// TODO
|
||||
throw new Error("Changing from non-filesEditables to editable libraries not supported")
|
||||
Zotero.debug("Resaving items to filesEditable library");
|
||||
if (this._action == 'saveItems' || this._action == 'saveSnapshot') {
|
||||
// Delete old items
|
||||
for (let item of Object.values(this._items)) {
|
||||
await item.eraseTx();
|
||||
}
|
||||
let actionUC = Zotero.Utilities.capitalize(this._action);
|
||||
// saveItems has a different signature with the session as the first argument
|
||||
let params = [targetID, this._requestData];
|
||||
if (this._action == 'saveItems') {
|
||||
params.unshift(this);
|
||||
}
|
||||
let newItems = await Zotero.Server.Connector[actionUC].prototype[this._action].apply(
|
||||
Zotero.Server.Connector[actionUC], params
|
||||
);
|
||||
// saveSnapshot only returns a single item
|
||||
if (this._action == 'saveSnapshot') {
|
||||
newItems = [newItems];
|
||||
}
|
||||
this._items = new Set(newItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (item.libraryID != libraryID) {
|
||||
let newItem = await item.moveToLibrary(libraryID);
|
||||
this._items[key] = newItem;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -127,7 +127,7 @@ Zotero.Server.Headers = class {
|
|||
if (typeof name !== 'string') {
|
||||
return Reflect.has(target, name, receiver);
|
||||
}
|
||||
return Reflect.has(target, name.toLowerCase(), receiver);
|
||||
return Reflect.has(target, name.toLowerCase(), receiver);
|
||||
},
|
||||
set(target, name, value, receiver) {
|
||||
return Reflect.set(target, name.toLowerCase(), value, receiver);
|
||||
|
|
@ -137,6 +137,12 @@ Zotero.Server.Headers = class {
|
|||
};
|
||||
|
||||
|
||||
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;
|
||||
|
|
@ -159,8 +165,7 @@ Zotero.Server.RequestHandler.prototype._bodyData = function () {
|
|||
let data = null;
|
||||
if (this.bodyLength > 0) {
|
||||
if (PLAIN_TEXT_CONTENT_TYPES.has(this.contentType)) {
|
||||
data = NetUtil.readInputStreamToString(this.request.bodyInputStream, this.bodyLength);
|
||||
data = this.body = Zotero.Utilities.Internal.decodeUTF8(this.body);
|
||||
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);
|
||||
|
|
@ -227,19 +232,23 @@ Zotero.Server.RequestHandler.prototype.handleRequest = async function () {
|
|||
// 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;
|
||||
this.query = request.queryString;
|
||||
|
||||
// get content-type
|
||||
var contentType = this.headers['content-type'];
|
||||
|
|
@ -340,7 +349,7 @@ Zotero.Server.RequestHandler.prototype._processEndpoint = async function (method
|
|||
return;
|
||||
}
|
||||
|
||||
var decodedData = null;
|
||||
var data = null;
|
||||
if (method === 'POST' && this.contentType) {
|
||||
// check that endpoint supports contentType
|
||||
var supportedDataTypes = endpoint.supportedDataTypes;
|
||||
|
|
@ -353,7 +362,7 @@ Zotero.Server.RequestHandler.prototype._processEndpoint = async function (method
|
|||
// decode content-type post data
|
||||
if (this.contentType === "application/json") {
|
||||
try {
|
||||
decodedData = JSON.parse(postData);
|
||||
data = JSON.parse(postData);
|
||||
}
|
||||
catch(e) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid JSON provided\n"));
|
||||
|
|
@ -361,13 +370,13 @@ Zotero.Server.RequestHandler.prototype._processEndpoint = async function (method
|
|||
}
|
||||
}
|
||||
else if (this.contentType === "application/x-www-form-urlencoded") {
|
||||
decodedData = Zotero.Server.decodeQueryString(postData);
|
||||
data = Zotero.Server.decodeQueryString(postData);
|
||||
}
|
||||
else if (postData) {
|
||||
decodedData = postData;
|
||||
data = postData;
|
||||
}
|
||||
else {
|
||||
decodedData = this.request.bodyInputStream;
|
||||
data = this.request.bodyInputStream;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -393,9 +402,9 @@ Zotero.Server.RequestHandler.prototype._processEndpoint = async function (method
|
|||
method,
|
||||
pathname: this.pathname,
|
||||
pathParams: this.pathParams,
|
||||
searchParams: new URLSearchParams(this.query ? this.query.substring(1) : ''),
|
||||
searchParams: new URLSearchParams(this.query || ''),
|
||||
headers: this.headers,
|
||||
data: decodedData
|
||||
data
|
||||
});
|
||||
let result;
|
||||
if (maybePromise.then) {
|
||||
|
|
@ -413,16 +422,16 @@ Zotero.Server.RequestHandler.prototype._processEndpoint = async function (method
|
|||
}
|
||||
// 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 {
|
||||
var url = {
|
||||
pathname: this.pathname,
|
||||
searchParams: new URLSearchParams(this.query ? this.query.substring(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);
|
||||
|
|
|
|||
|
|
@ -110,14 +110,15 @@ Zotero.Server.Connector = {
|
|||
return { library, collection, editable };
|
||||
}
|
||||
};
|
||||
/*
|
||||
|
||||
Zotero.Server.Connector.SessionManager = {
|
||||
_sessions: new Map(),
|
||||
|
||||
|
||||
get: function (id) {
|
||||
return this._sessions.get(id);
|
||||
},
|
||||
|
||||
|
||||
create: function (id, action, requestData) {
|
||||
// Legacy connector
|
||||
if (!id) {
|
||||
|
|
@ -133,12 +134,12 @@ Zotero.Server.Connector.SessionManager = {
|
|||
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);
|
||||
|
|
@ -156,7 +157,7 @@ Zotero.Server.Connector.SaveSession = function (id, action, requestData) {
|
|||
this._action = action;
|
||||
this._requestData = requestData;
|
||||
this._items = new Set();
|
||||
|
||||
|
||||
this._progressItems = {};
|
||||
this._orderedProgressItems = [];
|
||||
};
|
||||
|
|
@ -171,7 +172,7 @@ Zotero.Server.Connector.SaveSession.prototype.onProgress = function (item, progr
|
|||
if (item.id === null || item.id === undefined) {
|
||||
throw new Error("ID not provided");
|
||||
}
|
||||
|
||||
|
||||
// Child item
|
||||
if (item.parent) {
|
||||
let progressItem = this._progressItems[item.parent];
|
||||
|
|
@ -191,7 +192,7 @@ Zotero.Server.Connector.SaveSession.prototype.onProgress = function (item, progr
|
|||
a.progress = progress;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Top-level item
|
||||
var o = this._progressItems[item.id];
|
||||
if (!o) {
|
||||
|
|
@ -239,7 +240,7 @@ Zotero.Server.Connector.SaveSession.prototype.addItems = async function (items)
|
|||
for (let item of items) {
|
||||
this._items.add(item);
|
||||
}
|
||||
|
||||
|
||||
// Update the items with the current target data, in case it changed since the save began
|
||||
await this._updateItems(items);
|
||||
};
|
||||
|
|
@ -248,14 +249,14 @@ Zotero.Server.Connector.SaveSession.prototype.remove = function () {
|
|||
delete Zotero.Server.Connector.SessionManager._sessions[this.id];
|
||||
}
|
||||
|
||||
/**
|
||||
/!**
|
||||
* Change the target data for this session and update any items that have already been saved
|
||||
*/
|
||||
*!/
|
||||
Zotero.Server.Connector.SaveSession.prototype.update = async function (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) {
|
||||
|
|
@ -265,7 +266,7 @@ Zotero.Server.Connector.SaveSession.prototype.update = async function (targetID,
|
|||
else {
|
||||
Zotero.Prefs.set('lastViewedFolder', targetID);
|
||||
}
|
||||
|
||||
|
||||
// If moving from a non-filesEditable library to a filesEditable library, resave from
|
||||
// original data, since there might be files that weren't saved or were removed
|
||||
if (previousTargetID && previousTargetID != targetID) {
|
||||
|
|
@ -295,9 +296,9 @@ Zotero.Server.Connector.SaveSession.prototype.update = async function (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 && this._items.size == 1) {
|
||||
let item = Array.from(this._items)[0];
|
||||
|
|
@ -309,25 +310,25 @@ Zotero.Server.Connector.SaveSession.prototype.update = async function (targetID,
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
/!**
|
||||
* Update the passed items with the current target and tags
|
||||
*/
|
||||
*!/
|
||||
Zotero.Server.Connector.SaveSession.prototype._updateItems = Zotero.serial(async function (items) {
|
||||
if (items.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(this._currentTargetID);
|
||||
var libraryID = library.libraryID;
|
||||
|
||||
|
||||
var tags = this._currentTags.trim();
|
||||
tags = tags ? tags.split(/\s*,\s*/).filter(x => x) : [];
|
||||
|
||||
tags = tags ? tags.split(/\s*,\s*!/).filter(x => x) : [];
|
||||
|
||||
Zotero.debug("Updating items for connector save session " + this.id);
|
||||
|
||||
|
||||
for (let item of items) {
|
||||
let newLibrary = Zotero.Libraries.get(library.libraryID);
|
||||
|
||||
|
||||
if (item.libraryID != libraryID) {
|
||||
let newItem = await item.moveToLibrary(libraryID);
|
||||
// Check pending attachments and switch parent ID
|
||||
|
|
@ -340,7 +341,7 @@ Zotero.Server.Connector.SaveSession.prototype._updateItems = Zotero.serial(async
|
|||
this._items.delete(item);
|
||||
this._items.add(newItem);
|
||||
}
|
||||
|
||||
|
||||
// If the item is now a child item (e.g., from Retrieve Metadata), update the
|
||||
// parent item instead
|
||||
if (!item.isTopLevelItem()) {
|
||||
|
|
@ -357,7 +358,7 @@ Zotero.Server.Connector.SaveSession.prototype._updateItems = Zotero.serial(async
|
|||
item.setCollections(collection ? [collection.id] : []);
|
||||
await item.saveTx();
|
||||
}
|
||||
|
||||
|
||||
this._updateRecents();
|
||||
});
|
||||
|
||||
|
|
@ -403,6 +404,7 @@ Zotero.Server.Connector.SaveSession.prototype._updateRecents = function () {
|
|||
}
|
||||
};
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lists all available translators, including code for translators that should be run on every page
|
||||
|
|
@ -726,7 +728,7 @@ Zotero.Server.Connector.SaveItems.prototype = {
|
|||
* Either loads HTML into a hidden browser and initiates translation, or saves items directly
|
||||
* to the database
|
||||
*/
|
||||
init: Zotero.Promise.coroutine(function* (requestData) {
|
||||
init: async function (requestData) {
|
||||
var data = requestData.data;
|
||||
|
||||
var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
|
||||
|
|
@ -741,9 +743,10 @@ Zotero.Server.Connector.SaveItems.prototype = {
|
|||
);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(e);
|
||||
return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
|
||||
}
|
||||
yield session.update(targetID);
|
||||
await session.update(targetID);
|
||||
|
||||
// Shouldn't happen as long as My Library exists
|
||||
if (!library.editable) {
|
||||
|
|
@ -751,161 +754,193 @@ Zotero.Server.Connector.SaveItems.prototype = {
|
|||
return [500, "application/json", JSON.stringify({ libraryEditable: false })];
|
||||
}
|
||||
|
||||
return new Zotero.Promise((resolve) => {
|
||||
try {
|
||||
this.saveItems(
|
||||
session,
|
||||
targetID,
|
||||
requestData,
|
||||
function (jsonItems, items) {
|
||||
session.addItems(items);
|
||||
// Only return the properties the connector needs
|
||||
jsonItems = jsonItems.map((item) => {
|
||||
let o = {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
itemType: item.itemType,
|
||||
contentType: item.mimeType,
|
||||
mimeType: item.mimeType, // TODO: Remove
|
||||
};
|
||||
if (item.attachments) {
|
||||
o.attachments = item.attachments.map((attachment) => {
|
||||
return {
|
||||
id: session.id + '_' + attachment.id, // TODO: Remove prefix
|
||||
title: attachment.title,
|
||||
contentType: attachment.contentType,
|
||||
mimeType: attachment.mimeType, // TODO: Remove
|
||||
};
|
||||
});
|
||||
};
|
||||
return o;
|
||||
});
|
||||
resolve([201, "application/json", JSON.stringify({ items: jsonItems })]);
|
||||
}
|
||||
)
|
||||
// Add items to session once all attachments have been saved
|
||||
.then(function (items) {
|
||||
session.addItems(items);
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
session.remove();
|
||||
resolve(500);
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
saveItems: async function (session, target, requestData, onTopLevelItemsDone) {
|
||||
var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(target);
|
||||
var data = requestData.data;
|
||||
var cookieSandbox = data.uri
|
||||
? new Zotero.CookieSandbox(
|
||||
null,
|
||||
data.uri,
|
||||
data.detailedCookies ? "" : data.cookie || "",
|
||||
requestData.headers["User-Agent"]
|
||||
)
|
||||
: null;
|
||||
if (cookieSandbox && data.detailedCookies) {
|
||||
cookieSandbox.addCookiesFromHeader(data.detailedCookies);
|
||||
try {
|
||||
await session.saveItems(targetID);
|
||||
return [201, "application/json"];
|
||||
}
|
||||
|
||||
var id = 1;
|
||||
for (let item of data.items) {
|
||||
if (!item.id) {
|
||||
item.id = id++;
|
||||
}
|
||||
|
||||
if (item.attachments) {
|
||||
for (let attachment of item.attachments) {
|
||||
attachment.id = id++;
|
||||
attachment.parent = item.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Add parent item to session progress without attachments, which are added later if
|
||||
// they're saved.
|
||||
let progressItem = Object.assign(
|
||||
{},
|
||||
item,
|
||||
{
|
||||
attachments: []
|
||||
}
|
||||
);
|
||||
session.onProgress(progressItem, 0);
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
session.remove();
|
||||
return 500;
|
||||
}
|
||||
|
||||
var proxy = data.proxy && new Zotero.Proxy(data.proxy);
|
||||
|
||||
// Save items
|
||||
var itemSaver = new Zotero.Translate.ItemSaver({
|
||||
libraryID: library.libraryID,
|
||||
collections: collection ? [collection.id] : undefined,
|
||||
attachmentMode: Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD,
|
||||
forceTagType: 1,
|
||||
referrer: data.uri,
|
||||
cookieSandbox,
|
||||
proxy
|
||||
});
|
||||
// This is a bit tricky. When saving items, the callback `onTopLevelItemsDone` will
|
||||
// return the HTTP request to the connector. Then it may spend some time fetching
|
||||
// PDFs. In the meantime, the connector will create a snapshot and send it along to
|
||||
// the `saveSingleFile` endpoint, which quickly adds the data to the session and
|
||||
// then saves the pending attachments, without removing them (we need them in case
|
||||
// the session switches libraries and we need to save again). So the pending
|
||||
// attachments exist and have already been saved by the time this `saveItems`
|
||||
// promise resolves and we continue executing. So we save the number of existing
|
||||
// attachments before that to prevent double saving.
|
||||
let hadPendingAttachments = session.pendingAttachments.length > 0;
|
||||
if (hadPendingAttachments) {
|
||||
// If we have pending attachments then we are saving again by switching to
|
||||
// a `filesEditable` library. So we clear the pendingAttachments since they
|
||||
// get added again right below here in `saveItems`
|
||||
session.pendingAttachments = [];
|
||||
}
|
||||
let items = await itemSaver.saveItems(
|
||||
data.items,
|
||||
function (attachment, progress, error) {
|
||||
session.onProgress(attachment, progress, error);
|
||||
},
|
||||
(itemsJSON, items) => {
|
||||
itemsJSON.forEach(item => session.onProgress(item, 100));
|
||||
if (onTopLevelItemsDone) onTopLevelItemsDone(itemsJSON, items);
|
||||
},
|
||||
function (parentItemID, attachment) {
|
||||
session.pendingAttachments.push([parentItemID, attachment]);
|
||||
}
|
||||
);
|
||||
if (hadPendingAttachments) {
|
||||
// If the session has snapshotContent already (from switching to a `filesEditable` library
|
||||
// then we can save `pendingAttachments` now
|
||||
if (data.snapshotContent) {
|
||||
await itemSaver.saveSnapshotAttachments(
|
||||
session.pendingAttachments,
|
||||
data.snapshotContent,
|
||||
function (attachment, progress, error) {
|
||||
session.onProgress(attachment, progress, error);
|
||||
},
|
||||
);
|
||||
}
|
||||
// This means SingleFile in the Connector failed and we need to just go
|
||||
// ahead and do our fallback save
|
||||
else if (data.singleFile === false) {
|
||||
itemSaver.saveSnapshotAttachments(
|
||||
session.pendingAttachments,
|
||||
false,
|
||||
function (attachment, progress, error) {
|
||||
session.onProgress(attachment, progress, error);
|
||||
},
|
||||
);
|
||||
}
|
||||
// Otherwise we are still waiting for SingleFile in Connector to finish
|
||||
}
|
||||
return items;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the top-level item created for a standalone attachment
|
||||
*
|
||||
* Accepts:
|
||||
* sessionID - A session ID previously passed to /saveItems
|
||||
* Returns:
|
||||
* 200
|
||||
*/
|
||||
Zotero.Server.Connector.GetRecognizedItem = function () {};
|
||||
Zotero.Server.Endpoints["/connector/getRecognizedItem"] = Zotero.Server.Connector.GetRecognizedItem;
|
||||
Zotero.Server.Connector.GetRecognizedItem.prototype = {
|
||||
supportedMethods: ["POST"],
|
||||
supportedDataTypes: ["*"],
|
||||
permitBookmarklet: true,
|
||||
|
||||
init: async function (requestData) {
|
||||
const sessionID = requestData.data.sessionID;
|
||||
if (!sessionID) {
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_ID_NOT_PROVIDED" })];
|
||||
}
|
||||
|
||||
const session = Zotero.Server.Connector.SessionManager.get(sessionID);
|
||||
if (!session) {
|
||||
Zotero.debug("Can't find session " + sessionID, 1);
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })];
|
||||
}
|
||||
|
||||
await session.autoRecognizePromise;
|
||||
let item = session.getRecognizedItem();
|
||||
if (!item) {
|
||||
return 204;
|
||||
}
|
||||
let jsonItem = {
|
||||
title: item.getDisplayTitle(),
|
||||
itemType: item.itemType,
|
||||
};
|
||||
return [200, "application/json", JSON.stringify({ ...jsonItem })];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Saves a standalone attachment
|
||||
*
|
||||
* URI params:
|
||||
* sessionID
|
||||
* Expected headers:
|
||||
* X-Metadata:
|
||||
* - parentItemID
|
||||
* - title
|
||||
* - url
|
||||
* Returns:
|
||||
* 400 - Bad params
|
||||
* 200 - Non-writable library
|
||||
* 201 - Created
|
||||
*/
|
||||
Zotero.Server.Connector.SaveStandaloneAttachment = function () {};
|
||||
Zotero.Server.Endpoints["/connector/saveStandaloneAttachment"] = Zotero.Server.Connector.SaveStandaloneAttachment;
|
||||
Zotero.Server.Connector.SaveStandaloneAttachment.prototype = {
|
||||
supportedMethods: ["POST"],
|
||||
supportedDataTypes: ["*"],
|
||||
permitBookmarklet: true,
|
||||
|
||||
init: async function (requestData) {
|
||||
// Retrieve payload
|
||||
if (!requestData.headers['X-Metadata']) {
|
||||
return [400, "application/json", JSON.stringify({ error: "METADATA_NOT_PROVIDED" })];
|
||||
}
|
||||
const metadata = JSON.parse(requestData.headers['X-Metadata']);
|
||||
|
||||
const sessionID = metadata.sessionID || requestData.searchParams.get('sessionID');
|
||||
if (!sessionID) {
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_ID_NOT_PROVIDED" })];
|
||||
}
|
||||
var { library, collection } = Zotero.Server.Connector.getSaveTarget();
|
||||
if (!library.filesEditable) {
|
||||
return [200, 'text/plain', 'Library is not editable.'];
|
||||
}
|
||||
var libraryID = library.libraryID;
|
||||
var targetID = collection ? collection.treeViewID : library.treeViewID;
|
||||
|
||||
try {
|
||||
var session = Zotero.Server.Connector.SessionManager.create(
|
||||
sessionID,
|
||||
'saveStandaloneAttachment',
|
||||
requestData
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
|
||||
}
|
||||
await session.update(targetID);
|
||||
|
||||
// Save standalone attachment from stream
|
||||
let item = await Zotero.Attachments.importFromNetworkStream({
|
||||
url: metadata.url,
|
||||
libraryID,
|
||||
collections: collection ? [collection.id] : undefined,
|
||||
title: metadata.title,
|
||||
contentType: requestData.headers['Content-Type'],
|
||||
stream: requestData.data,
|
||||
byteCount: requestData.headers['Content-Length'],
|
||||
});
|
||||
session.addItem(metadata.url, item);
|
||||
|
||||
let canRecognize = Zotero.RecognizeDocument.canRecognize(item);
|
||||
if (canRecognize) {
|
||||
// Automatically recognize PDF/EPUB
|
||||
session.autoRecognizePromise = Zotero.RecognizeDocument.autoRecognizeItems([item]);
|
||||
}
|
||||
return [201, "application/json", JSON.stringify({ canRecognize })];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches an PDF/EPUB attachment to an item saved with /saveItems or /saveSnapshot
|
||||
*
|
||||
* URI params:
|
||||
* sessionID
|
||||
* Expected headers:
|
||||
* X-Metadata:
|
||||
* - parentItemID
|
||||
* - title
|
||||
* - url
|
||||
* Returns:
|
||||
* 400 - Bad params
|
||||
* 200 - Non-writable library
|
||||
* 201 - Created
|
||||
*/
|
||||
Zotero.Server.Connector.SaveAttachment = function () {};
|
||||
Zotero.Server.Endpoints["/connector/saveAttachment"] = Zotero.Server.Connector.SaveAttachment;
|
||||
Zotero.Server.Connector.SaveAttachment.prototype = {
|
||||
supportedMethods: ["POST"],
|
||||
supportedDataTypes: ["*"],
|
||||
permitBookmarklet: true,
|
||||
|
||||
init: async function (requestData) {
|
||||
// Retrieve payload
|
||||
if (!requestData.headers['X-Metadata']) {
|
||||
return [400, "application/json", JSON.stringify({ error: "METADATA_NOT_PROVIDED" })];
|
||||
}
|
||||
const metadata = JSON.parse(requestData.headers['X-Metadata']);
|
||||
|
||||
const sessionID = metadata.sessionID || requestData.searchParams.get('sessionID');
|
||||
if (!sessionID) {
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_ID_NOT_PROVIDED" })];
|
||||
}
|
||||
|
||||
let session = Zotero.Server.Connector.SessionManager.get(sessionID);
|
||||
if (!session) {
|
||||
Zotero.debug("Can't find session " + sessionID, 1);
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })];
|
||||
}
|
||||
|
||||
let { library } = Zotero.Server.Connector.getSaveTarget();
|
||||
if (!library.filesEditable) {
|
||||
return [200, 'text/plain', 'Library files are not editable.'];
|
||||
}
|
||||
|
||||
// Save attachment based on provided parent id from stream
|
||||
let parentItem = session.getItemByConnectorKey(metadata.parentItemID);
|
||||
await Zotero.Attachments.importFromNetworkStream({
|
||||
url: metadata.url,
|
||||
parentItemID: parentItem.id,
|
||||
title: metadata.title,
|
||||
contentType: requestData.headers['Content-Type'],
|
||||
stream: requestData.data,
|
||||
byteCount: requestData.headers['Content-Length'],
|
||||
});
|
||||
|
||||
return 201;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Attaches a singlefile attachment to an item saved with /saveItems or /saveSnapshot
|
||||
* If data.snapshotContent is empty, it means the save failed in the Connector
|
||||
|
|
@ -933,8 +968,6 @@ Zotero.Server.Connector.SaveSingleFile.prototype = {
|
|||
* Save SingleFile snapshot to pending attachments
|
||||
*/
|
||||
init: async function (requestData) {
|
||||
const { HiddenBrowser } = ChromeUtils.import('chrome://zotero/content/HiddenBrowser.jsm');
|
||||
|
||||
// Retrieve payload
|
||||
let data = requestData.data;
|
||||
|
||||
|
|
@ -948,78 +981,33 @@ Zotero.Server.Connector.SaveSingleFile.prototype = {
|
|||
return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })];
|
||||
}
|
||||
|
||||
let snapshotContent = data.snapshotContent;
|
||||
|
||||
if (!snapshotContent) {
|
||||
// Connector SingleFile has failed so if we re-save attachments (via
|
||||
// updateSession) then we want to inform saveItems and saveSnapshot that they
|
||||
// do not need to use pendingAttachments because those have failed.
|
||||
session._requestData.data.singleFile = false;
|
||||
|
||||
for (let [_parentItemID, attachment] of session.pendingAttachments) {
|
||||
session.onProgress(attachment, false);
|
||||
}
|
||||
|
||||
return [200, 'text/plain', 'No snapshot content attached.'];
|
||||
}
|
||||
|
||||
// Add to session data, in case `saveSnapshot` is called again by the session
|
||||
session.addSnapshotContent(snapshotContent);
|
||||
|
||||
// We do this after adding to session because if we switch to a `filesEditable`
|
||||
// library we need to have access to the snapshotContent.
|
||||
let { library, collection } = Zotero.Server.Connector.getSaveTarget();
|
||||
let { library } = Zotero.Server.Connector.getSaveTarget();
|
||||
if (!library.filesEditable) {
|
||||
session.savingDone = true;
|
||||
|
||||
return [200, 'text/plain', 'Library is not editable.'];
|
||||
return [200, 'text/plain', 'Library files are not editable.'];
|
||||
}
|
||||
|
||||
// Retrieve all items in the session that need a snapshot
|
||||
// We only save the snapshot in single-item cases
|
||||
if (session._action === 'saveSnapshot') {
|
||||
await Zotero.Promise.all(
|
||||
session.pendingAttachments.map((pendingAttachment) => {
|
||||
return Zotero.Attachments.importFromSnapshotContent({
|
||||
title: data.title,
|
||||
url: data.url,
|
||||
parentItemID: pendingAttachment[0],
|
||||
snapshotContent
|
||||
});
|
||||
})
|
||||
);
|
||||
const parentItemID = session.getItemByConnectorKey(data.url).id;
|
||||
// Just saves the snapshot straight up
|
||||
await Zotero.Attachments.importFromSnapshotContent({
|
||||
title: data.title,
|
||||
url: data.url,
|
||||
parentItemID,
|
||||
snapshotContent: data.snapshotContent
|
||||
});
|
||||
}
|
||||
else if (session._action === 'saveItems') {
|
||||
var cookieSandbox = data.uri
|
||||
? new Zotero.CookieSandbox(
|
||||
null,
|
||||
data.uri,
|
||||
data.detailedCookies ? "" : data.cookie || "",
|
||||
requestData.headers["User-Agent"]
|
||||
)
|
||||
: null;
|
||||
if (cookieSandbox && data.detailedCookies) {
|
||||
cookieSandbox.addCookiesFromHeader(data.detailedCookies);
|
||||
}
|
||||
|
||||
let proxy = data.proxy && new Zotero.Proxy(data.proxy);
|
||||
|
||||
let itemSaver = new Zotero.Translate.ItemSaver({
|
||||
libraryID: library.libraryID,
|
||||
collections: collection ? [collection.id] : undefined,
|
||||
attachmentMode: Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD,
|
||||
forceTagType: 1,
|
||||
referrer: data.uri,
|
||||
cookieSandbox,
|
||||
proxy
|
||||
const parentItemID = session.getItemByConnectorKey(data.items[0].id).id;
|
||||
// Deproxifies and does some other attachment preprocessing
|
||||
await session.itemSaver.saveSnapshotAttachments({
|
||||
title: data.title,
|
||||
url: data.url,
|
||||
parentItemID,
|
||||
snapshotContent: data.snapshotContent
|
||||
});
|
||||
|
||||
await itemSaver.saveSnapshotAttachments(
|
||||
session.pendingAttachments,
|
||||
snapshotContent,
|
||||
function (attachment, progress, error) {
|
||||
session.onProgress(attachment, progress, error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return 201;
|
||||
|
|
@ -1027,7 +1015,7 @@ Zotero.Server.Connector.SaveSingleFile.prototype = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Either creates a webpage item or a PDF/EPUB top-level item in Zotero
|
||||
* Creates a webpage item top-level item in Zotero
|
||||
* Called by the Connector when no translators are detected on the page
|
||||
*
|
||||
* Accepts:
|
||||
|
|
@ -1050,7 +1038,7 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
|
|||
init: async function (requestData) {
|
||||
var data = requestData.data;
|
||||
|
||||
var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
|
||||
var { library, collection } = Zotero.Server.Connector.getSaveTarget();
|
||||
var targetID = collection ? collection.treeViewID : library.treeViewID;
|
||||
|
||||
try {
|
||||
|
|
@ -1061,6 +1049,7 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
|
|||
);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(e);
|
||||
return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
|
||||
}
|
||||
await session.update(collection ? collection.treeViewID : library.treeViewID);
|
||||
|
|
@ -1072,125 +1061,85 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
|
|||
}
|
||||
|
||||
try {
|
||||
var item = await this.saveSnapshot(targetID, requestData);
|
||||
await session.addItem(item);
|
||||
await session.saveSnapshot(targetID);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
return 500;
|
||||
}
|
||||
|
||||
let attachments = [];
|
||||
let hasAttachments = !item.isAttachment() && item.getAttachments().length;
|
||||
if (hasAttachments) {
|
||||
attachments = [{mimeType: "text/html", title: data.title, url: data.url}];
|
||||
}
|
||||
|
||||
return [201,
|
||||
"application/json",
|
||||
JSON.stringify({ saveSingleFile: !data.skipSnapshot && !data.pdf && data.singleFile, attachments })];
|
||||
},
|
||||
|
||||
/*
|
||||
* Perform saving the snapshot
|
||||
*
|
||||
* Note: this function signature cannot change because it can also be called by
|
||||
* updateSession (`Zotero.Server.Connector.SaveSession.prototype.update`).
|
||||
*/
|
||||
saveSnapshot: async function (target, requestData) {
|
||||
var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(target);
|
||||
var libraryID = library.libraryID;
|
||||
var data = requestData.data;
|
||||
|
||||
var cookieSandbox = data.url
|
||||
? new Zotero.CookieSandbox(
|
||||
null,
|
||||
data.url,
|
||||
data.detailedCookies ? "" : data.cookie || "",
|
||||
requestData.headers["User-Agent"]
|
||||
)
|
||||
: null;
|
||||
if (cookieSandbox && data.detailedCookies) {
|
||||
cookieSandbox.addCookiesFromHeader(data.detailedCookies);
|
||||
}
|
||||
|
||||
if (data.pdf && library.filesEditable) {
|
||||
let item = await Zotero.Attachments.importFromURL({
|
||||
libraryID,
|
||||
url: data.url,
|
||||
referrer: data.referrer,
|
||||
collections: collection ? [collection.id] : undefined,
|
||||
contentType: "application/pdf",
|
||||
cookieSandbox
|
||||
});
|
||||
|
||||
// Automatically recognize PDF/EPUB
|
||||
Zotero.RecognizeDocument.autoRecognizeItems([item]);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
if (data.html) {
|
||||
var parser = new DOMParser();
|
||||
var doc = parser.parseFromString(`<html>${data.html}</html>`, 'text/html');
|
||||
doc = Zotero.HTTP.wrapDocument(doc, data.url);
|
||||
var title = doc.title;
|
||||
} else {
|
||||
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]);
|
||||
}
|
||||
var itemID = await item.saveTx();
|
||||
|
||||
// Save snapshot
|
||||
if (!data.skipSnapshot) {
|
||||
// If called from session update, requestData may already have SingleFile data
|
||||
if (library.filesEditable && data.snapshotContent) {
|
||||
await Zotero.Attachments.importFromSnapshotContent({
|
||||
title: data.title,
|
||||
url: data.url,
|
||||
parentItemID: itemID,
|
||||
snapshotContent: data.snapshotContent
|
||||
});
|
||||
}
|
||||
// Otherwise, connector will POST SingleFile data at later time
|
||||
// We want this data regardless of `library.filesEditable` because if we
|
||||
// start on a non-filesEditable library and switch to one, we won't have a
|
||||
// pending attachment
|
||||
else if (data.hasOwnProperty('singleFile')) {
|
||||
let session = Zotero.Server.Connector.SessionManager.get(data.sessionID);
|
||||
session.pendingAttachments = [
|
||||
[itemID, { title: data.title, url: data.url }]
|
||||
];
|
||||
}
|
||||
else if (library.filesEditable) {
|
||||
// Old connector will not use SingleFile so importFromURL now
|
||||
await Zotero.Attachments.importFromURL({
|
||||
libraryID,
|
||||
url: data.url,
|
||||
referrer: data.referrer,
|
||||
title,
|
||||
parentItemID: itemID,
|
||||
contentType: "text/html",
|
||||
cookieSandbox
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
return [201, "application/json"];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the item has OA attachments (in case PDF saving in connector failed).
|
||||
* Also checks custom resolvers.
|
||||
*
|
||||
* Accepts:
|
||||
* sessionID - A session ID previously passed to /saveItems
|
||||
* itemID - The ID of the item to save alternative attachment for
|
||||
*/
|
||||
Zotero.Server.Connector.HasOAAttachments = function () {};
|
||||
Zotero.Server.Endpoints["/connector/hasOAAttachments"] = Zotero.Server.Connector.HasOAAttachments;
|
||||
Zotero.Server.Connector.HasOAAttachments.prototype = {
|
||||
supportedMethods: ["POST"],
|
||||
supportedDataTypes: ["application/json"],
|
||||
permitBookmarklet: true,
|
||||
init: async function (requestData) {
|
||||
let data = requestData.data;
|
||||
let session = Zotero.Server.Connector.SessionManager.get(data.sessionID);
|
||||
if (!session) {
|
||||
Zotero.debug("Can't find session " + data.sessionID, 1);
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })];
|
||||
}
|
||||
let item = session.getItemByConnectorKey(data.itemID);
|
||||
let resolvers = Zotero.Attachments.getFileResolvers(item, ['oa', 'custom'], true);
|
||||
return [200, "application/json", JSON.stringify(resolvers.length > 0)];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Accepts:
|
||||
* sessionID - A session ID previously passed to /saveItems
|
||||
* itemID - The ID of the item to save alternative attachment for
|
||||
*
|
||||
* Returns:
|
||||
* 400 - Bad params
|
||||
* 201 - Created and attachment title
|
||||
* 503 - Failed to save
|
||||
*/
|
||||
Zotero.Server.Connector.SaveOAAttachment = function () {};
|
||||
Zotero.Server.Endpoints["/connector/saveOAAttachment"] = Zotero.Server.Connector.SaveOAAttachment;
|
||||
Zotero.Server.Connector.SaveOAAttachment.prototype = {
|
||||
supportedMethods: ["POST"],
|
||||
supportedDataTypes: ["application/json"],
|
||||
permitBookmarklet: true,
|
||||
init: async function (requestData) {
|
||||
let data = requestData.data;
|
||||
let session = Zotero.Server.Connector.SessionManager.get(data.sessionID);
|
||||
if (!session) {
|
||||
Zotero.debug("Can't find session " + data.sessionID, 1);
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })];
|
||||
}
|
||||
let item = session.getItemByConnectorKey(data.itemID);
|
||||
let resolvers = Zotero.Attachments.getFileResolvers(item, ['oa', 'custom'], true);
|
||||
|
||||
let attachment = await Zotero.Attachments.addFileFromURLs(item, resolvers);
|
||||
|
||||
if (attachment) {
|
||||
return [201, "text/plain", attachment.getDisplayTitle()];
|
||||
}
|
||||
else {
|
||||
return [503, "text/plain", "Failed to save an attachment"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Accepts:
|
||||
* sessionID - A session ID previously passed to /saveItems
|
||||
|
|
@ -1238,55 +1187,6 @@ Zotero.Server.Connector.UpdateSession.prototype = {
|
|||
}
|
||||
};
|
||||
|
||||
Zotero.Server.Connector.SessionProgress = function() {};
|
||||
Zotero.Server.Endpoints["/connector/sessionProgress"] = Zotero.Server.Connector.SessionProgress;
|
||||
Zotero.Server.Connector.SessionProgress.prototype = {
|
||||
supportedMethods: ["POST"],
|
||||
supportedDataTypes: ["application/json"],
|
||||
permitBookmarklet: true,
|
||||
|
||||
init: async function (requestData) {
|
||||
var data = requestData.data
|
||||
|
||||
if (!data.sessionID) {
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_ID_NOT_PROVIDED" })];
|
||||
}
|
||||
|
||||
var session = Zotero.Server.Connector.SessionManager.get(data.sessionID);
|
||||
if (!session) {
|
||||
Zotero.debug("Can't find session " + data.sessionID, 1);
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })];
|
||||
}
|
||||
|
||||
return [
|
||||
200,
|
||||
"application/json",
|
||||
JSON.stringify({
|
||||
items: session.getAllProgress()
|
||||
.map((item) => {
|
||||
var newItem = Object.assign({}, item);
|
||||
if (item.attachments) {
|
||||
newItem.attachments = item.attachments.map((attachment) => {
|
||||
return Object.assign(
|
||||
{},
|
||||
attachment,
|
||||
// Prefix id with 'sessionID_'
|
||||
// TODO: Remove this once support for /attachmentProgress is
|
||||
// removed and we stop prefixing the ids in the /saveItems
|
||||
// response
|
||||
{
|
||||
id: session.id + '_' + attachment.id
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
return newItem;
|
||||
}),
|
||||
done: session.isSavingDone()
|
||||
})
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
Zotero.Server.Connector.DelaySync = function () {};
|
||||
Zotero.Server.Endpoints["/connector/delaySync"] = Zotero.Server.Connector.DelaySync;
|
||||
|
|
@ -1315,8 +1215,12 @@ Zotero.Server.Connector.Import.prototype = {
|
|||
permitBookmarklet: false,
|
||||
|
||||
init: async function (requestData) {
|
||||
let dataString = requestData.data;
|
||||
if (requestData.data instanceof Ci.nsIInputStream) {
|
||||
dataString = Zotero.Server.networkStreamToString(dataString, requestData.headers['content-length']);
|
||||
}
|
||||
let translate = new Zotero.Translate.Import();
|
||||
translate.setString(requestData.data);
|
||||
translate.setString(dataString);
|
||||
let translators = await translate.getTranslators();
|
||||
if (!translators || !translators.length) {
|
||||
return 400;
|
||||
|
|
@ -1335,6 +1239,7 @@ Zotero.Server.Connector.Import.prototype = {
|
|||
var session = Zotero.Server.Connector.SessionManager.create(requestData.searchParams.get('session'));
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(e);
|
||||
return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
|
||||
}
|
||||
await session.update(collection ? collection.treeViewID : library.treeViewID);
|
||||
|
|
@ -1348,7 +1253,9 @@ Zotero.Server.Connector.Import.prototype = {
|
|||
skipSelect: false
|
||||
}
|
||||
});
|
||||
session.addItems(items);
|
||||
items.forEach((item, index) => {
|
||||
session.addItem(items[index].id, item);
|
||||
});
|
||||
|
||||
return [201, "application/json", JSON.stringify(items)];
|
||||
}
|
||||
|
|
@ -1369,9 +1276,13 @@ Zotero.Server.Connector.InstallStyle.prototype = {
|
|||
permitBookmarklet: false,
|
||||
|
||||
init: Zotero.Promise.coroutine(function* (requestData) {
|
||||
let dataString = requestData.data;
|
||||
if (requestData.data instanceof Ci.nsIInputStream) {
|
||||
dataString = Zotero.Server.networkStreamToString(dataString, requestData.headers['content-length']);
|
||||
}
|
||||
try {
|
||||
var { styleTitle, styleID } = yield Zotero.Styles.install(
|
||||
requestData.data, requestData.searchParams.get('origin') || null, true
|
||||
var { styleTitle } = yield Zotero.Styles.install(
|
||||
dataString, requestData.searchParams.get('origin') || null, true
|
||||
);
|
||||
} catch (e) {
|
||||
return [400, "text/plain", e.message];
|
||||
|
|
@ -1409,7 +1320,8 @@ Zotero.Server.Connector.GetTranslatorCode.prototype = {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get selected collection
|
||||
* Returns the full serialized collection tree (excluding non-editable libraries)
|
||||
* and the selected collection tree item.
|
||||
*
|
||||
* Accepts:
|
||||
* Nothing
|
||||
|
|
@ -1437,6 +1349,7 @@ Zotero.Server.Connector.GetSelectedCollection.prototype = {
|
|||
libraryID: library.libraryID,
|
||||
libraryName: library.name,
|
||||
libraryEditable: library.editable,
|
||||
filesEditable: library.filesEditable,
|
||||
editable
|
||||
};
|
||||
|
||||
|
|
@ -1460,11 +1373,13 @@ Zotero.Server.Connector.GetSelectedCollection.prototype = {
|
|||
{
|
||||
id: library.treeViewID,
|
||||
name: library.name,
|
||||
filesEditable: library.filesEditable,
|
||||
level: 0
|
||||
},
|
||||
...Zotero.Collections.getByLibrary(library.libraryID, true).map(c => ({
|
||||
id: c.treeViewID,
|
||||
name: c.name,
|
||||
filesEditable: library.filesEditable,
|
||||
level: c.level + 1 || 1 // Added by Zotero.Collections._getByContainer()
|
||||
}))
|
||||
);
|
||||
|
|
@ -1599,6 +1514,8 @@ Zotero.Server.Connector.Ping.prototype = {
|
|||
let response = {
|
||||
prefs: {
|
||||
automaticSnapshots: Zotero.Prefs.get('automaticSnapshots'),
|
||||
downloadAssociatedFiles: Zotero.Prefs.get("downloadAssociatedFiles"),
|
||||
supportsAttachmentUpload: true,
|
||||
googleDocsAddNoteEnabled: true,
|
||||
googleDocsCitationExplorerEnabled: false,
|
||||
translatorsHash,
|
||||
|
|
|
|||
|
|
@ -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,231 @@ 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;
|
||||
}
|
||||
new Promise(async (resolve, reject) => {
|
||||
await this._saveAttachment(
|
||||
attachment,
|
||||
parentItemID,
|
||||
attachmentCallback
|
||||
(attachment, progress, e) => {
|
||||
if (e) reject(e);
|
||||
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,15 +579,22 @@ 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 {
|
||||
}
|
||||
else {
|
||||
Zotero.debug('Translate: Ignoring attachment due to ATTACHMENT_MODE_IGNORE');
|
||||
}
|
||||
|
||||
|
|
@ -821,6 +873,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 +935,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 +953,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 +969,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,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ const xpcomFilesLocal = [
|
|||
'server/server_connector',
|
||||
'server/server_connectorIntegration',
|
||||
'server/server_localAPI',
|
||||
'server/saveSession',
|
||||
'session',
|
||||
'streamer',
|
||||
'style',
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ describe("Zotero.Server", function () {
|
|||
assert.equal(req.status, 204);
|
||||
});
|
||||
});
|
||||
describe("application/octet-stream", function () {
|
||||
describe("application/pdf", function () {
|
||||
it('should provide a stream', async function () {
|
||||
let called = false;
|
||||
let endpoint = "/test/" + Zotero.Utilities.randomString();
|
||||
|
|
@ -298,14 +298,13 @@ describe("Zotero.Server", function () {
|
|||
Zotero.Server.Endpoints[endpoint] = function () {};
|
||||
Zotero.Server.Endpoints[endpoint].prototype = {
|
||||
supportedMethods: ["POST"],
|
||||
supportedDataTypes: ["application/octet-stream"],
|
||||
supportedDataTypes: ["application/pdf"],
|
||||
|
||||
init: function (options) {
|
||||
called = true;
|
||||
assert.isObject(options);
|
||||
assert.property(options.headers, "Content-Type");
|
||||
assert(options.headers["Content-Type"].startsWith("application/octet-stream"));
|
||||
assert.equal(options.headers['content-disposition'], "attachment; filename=\"test.pdf\"");
|
||||
assert(options.headers["Content-Type"].startsWith("application/pdf"));
|
||||
assert.isFunction(options.data.available);
|
||||
let data = NetUtil.readInputStreamToString(options.data, options.headers['content-length']);
|
||||
assert.equal(data, contents);
|
||||
|
|
@ -321,8 +320,7 @@ describe("Zotero.Server", function () {
|
|||
serverPath + endpoint,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Disposition": "attachment; filename=\"test.pdf\""
|
||||
"Content-Type": "application/pdf",
|
||||
},
|
||||
body: pdf
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue