mirror of
https://github.com/zotero/zotero.git
synced 2026-10-16 04:28:45 +00:00
Merge 8dd7c899a2 into e7ed0b2881
This commit is contained in:
commit
fdaf7d8fb3
3 changed files with 226 additions and 6 deletions
|
|
@ -49,9 +49,9 @@ Zotero.Server.Connector.SessionManager = {
|
|||
var ttl = this._sessions.size >= 10 ? 60 : 600;
|
||||
var deleteBefore = new Date() - ttl * 1000;
|
||||
|
||||
for (let session of this._sessions) {
|
||||
for (let [_, session] of this._sessions.entries()) {
|
||||
if (session.created < deleteBefore) {
|
||||
this._session.delete(session.id);
|
||||
this._sessions.delete(session.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ Zotero.Server.Connector.SaveSession = class {
|
|||
constructor(id, action, requestData) {
|
||||
this.id = id;
|
||||
this.created = new Date();
|
||||
this.cancelled = false;
|
||||
this._action = action;
|
||||
this._requestData = requestData;
|
||||
this._items = {};
|
||||
|
|
@ -99,6 +100,8 @@ Zotero.Server.Connector.SaveSession = class {
|
|||
}
|
||||
|
||||
async saveSnapshot(target) {
|
||||
// This should not be called if session is cancelled but just in case
|
||||
if (this.cancelled) return;
|
||||
var { library, collection } = Zotero.Server.Connector.resolveTarget(target);
|
||||
var libraryID = library.libraryID;
|
||||
var data = this._requestData.data;
|
||||
|
|
@ -147,7 +150,14 @@ Zotero.Server.Connector.SaveSession = class {
|
|||
}
|
||||
|
||||
remove() {
|
||||
delete Zotero.Server.Connector.SessionManager._sessions[this.id];
|
||||
Zotero.Server.Connector.SessionManager._sessions.delete(this.id);
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
this.cancelled = true;
|
||||
for (let item of Object.values(this._items)) {
|
||||
await item.eraseTx();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -189,6 +199,8 @@ Zotero.Server.Connector.SaveSession = class {
|
|||
if (Object.values(items).length == 0) {
|
||||
return;
|
||||
}
|
||||
// This should not be called if session is cancelled but just in case
|
||||
if (this.cancelled) return;
|
||||
|
||||
var { library, collection } = Zotero.Server.Connector.resolveTarget(this._currentTargetID);
|
||||
var libraryID = library.libraryID;
|
||||
|
|
|
|||
|
|
@ -320,8 +320,9 @@ Zotero.Server.Connector.SaveItems.prototype = {
|
|||
);
|
||||
}
|
||||
catch (e) {
|
||||
let session = Zotero.Server.Connector.SessionManager.get(data.sessionID);
|
||||
Zotero.debug(e);
|
||||
return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
|
||||
return [409, "application/json", JSON.stringify({ error: session?.cancelled ? "SESSION_CANCELLED" : "SESSION_EXISTS" })];
|
||||
}
|
||||
await session.update(targetID);
|
||||
|
||||
|
|
@ -499,6 +500,10 @@ Zotero.Server.Connector.SaveAttachment.prototype = {
|
|||
return [200, 'text/plain', 'Library files are not editable.'];
|
||||
}
|
||||
|
||||
if (session.cancelled) {
|
||||
Zotero.debug("Session is cancelled: " + session.sessionID, 1);
|
||||
return [409, "application/json", JSON.stringify({ error: "SESSION_CANCELLED" })];
|
||||
}
|
||||
// Save attachment based on provided parent id from stream
|
||||
let parentItem = session.getItemByConnectorKey(metadata.parentItemID);
|
||||
await Zotero.Attachments.importFromNetworkStream({
|
||||
|
|
@ -554,6 +559,10 @@ Zotero.Server.Connector.SaveSingleFile.prototype = {
|
|||
Zotero.debug("Can't find session " + data.sessionID, 1);
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })];
|
||||
}
|
||||
if (session.cancelled) {
|
||||
Zotero.debug("Session is cancelled: " + data.sessionID, 1);
|
||||
return [409, "application/json", JSON.stringify({ error: "SESSION_CANCELLED" })];
|
||||
}
|
||||
|
||||
let { library } = Zotero.Server.Connector.getSaveTarget();
|
||||
if (!library.filesEditable) {
|
||||
|
|
@ -626,8 +635,9 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
|
|||
);
|
||||
}
|
||||
catch (e) {
|
||||
let session = Zotero.Server.Connector.SessionManager.get(data.sessionID);
|
||||
Zotero.debug(e);
|
||||
return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
|
||||
return [409, "application/json", JSON.stringify({ error: session?.cancelled ? "SESSION_CANCELLED" : "SESSION_EXISTS" })];
|
||||
}
|
||||
await session.update(collection ? collection.treeViewID : library.treeViewID);
|
||||
|
||||
|
|
@ -747,6 +757,10 @@ Zotero.Server.Connector.UpdateSession.prototype = {
|
|||
Zotero.debug("Can't find session " + data.sessionID, 1);
|
||||
return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })];
|
||||
}
|
||||
if (session.cancelled) {
|
||||
Zotero.debug("Session is cancelled: " + session.sessionID, 1);
|
||||
return [409, "application/json", JSON.stringify({ error: "SESSION_CANCELLED" })];
|
||||
}
|
||||
|
||||
// Parse treeViewID
|
||||
var [type, id] = [data.target[0], parseInt(data.target.substr(1))];
|
||||
|
|
@ -771,6 +785,49 @@ Zotero.Server.Connector.UpdateSession.prototype = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Accepts:
|
||||
* sessionID - A session ID
|
||||
*
|
||||
* Returns:
|
||||
* 200 response if the session is cancelled
|
||||
* 400 if the session ID is not provided
|
||||
*/
|
||||
Zotero.Server.Connector.CancelSession = function() {};
|
||||
Zotero.Server.Endpoints["/connector/cancelSession"] = Zotero.Server.Connector.CancelSession;
|
||||
Zotero.Server.Connector.CancelSession.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 the session with the specified ID does not exist, create it here so
|
||||
// we can record it as cancelled.
|
||||
if (!session) {
|
||||
session = Zotero.Server.Connector.SessionManager.create(data.sessionID);
|
||||
}
|
||||
// After a session is cancelled, all items added are removed and
|
||||
// subsequent child attachments/notes should no longer be added.
|
||||
// To handle any possible race conditions (e.g. if an item is being added at the same time as
|
||||
// the session is being cancelled), wait a second and try to clear any remaining items again.
|
||||
await session.cancel();
|
||||
setTimeout(() => {
|
||||
session.cancel();
|
||||
}, 1000);
|
||||
|
||||
return [200, "application/json", JSON.stringify({})];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Zotero.Server.Connector.DelaySync = function () {};
|
||||
Zotero.Server.Endpoints["/connector/delaySync"] = Zotero.Server.Connector.DelaySync;
|
||||
|
|
@ -961,7 +1018,8 @@ Zotero.Server.Connector.GetSelectedCollection.prototype = {
|
|||
id: library.treeViewID,
|
||||
name: library.name,
|
||||
filesEditable: library.filesEditable,
|
||||
level: 0
|
||||
level: 0,
|
||||
isUserLibrary: Zotero.Libraries.userLibraryID == library.libraryID,
|
||||
},
|
||||
...Zotero.Collections.getByLibrary(library.libraryID, true).map(c => ({
|
||||
id: c.treeViewID,
|
||||
|
|
@ -1108,6 +1166,7 @@ Zotero.Server.Connector.Ping.prototype = {
|
|||
googleDocsAddNoteEnabled: true,
|
||||
googleDocsAddAnnotationEnabled: true,
|
||||
canUserAddNote: true,
|
||||
supportsSaveCancelling: true,
|
||||
googleDocsCitationExplorerEnabled: false,
|
||||
translatorsHash,
|
||||
sortedTranslatorHash
|
||||
|
|
|
|||
|
|
@ -1937,4 +1937,153 @@ describe("Connector Server", function () {
|
|||
assert.equal(JSON.parse(req.response).status, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/connector/cancelSession', function () {
|
||||
var pdfArrayBuffer;
|
||||
var pdfPath = OS.Path.join(getTestDataDirectory().path, 'test.pdf');
|
||||
before(async () => {
|
||||
await selectLibrary(win, Zotero.Libraries.userLibraryID);
|
||||
pdfArrayBuffer = (await OS.File.read(pdfPath)).buffer;
|
||||
});
|
||||
|
||||
it('should delete saved items if called after everything is saved', async function () {
|
||||
var collection = await createDataObject('collection');
|
||||
let sessionID = Zotero.Utilities.randomString();
|
||||
let bookItemID = Zotero.Utilities.randomString();
|
||||
await select(win, collection);
|
||||
|
||||
let body = {
|
||||
sessionID,
|
||||
items: [
|
||||
{
|
||||
id: bookItemID,
|
||||
itemType: "book",
|
||||
title: "Book Title",
|
||||
}
|
||||
]
|
||||
};
|
||||
let itemAddPromise = waitForItemEvent('add');
|
||||
// Save the item as if by the connector
|
||||
await Zotero.HTTP.request(
|
||||
'POST',
|
||||
connectorServerPath + "/connector/saveItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
);
|
||||
let itemIDs = await itemAddPromise;
|
||||
|
||||
// Add the attachment to the item
|
||||
let attachmentAddPromise = waitForItemEvent('add');
|
||||
let attachmentReq = await httpRequest(
|
||||
'POST',
|
||||
connectorServerPath + "/connector/saveAttachment",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"X-Metadata": JSON.stringify({
|
||||
sessionID,
|
||||
title: "Book Attachment",
|
||||
parentItemID: bookItemID,
|
||||
url: `${testServerPath}/attachment1.pdf`,
|
||||
})
|
||||
},
|
||||
body: pdfArrayBuffer
|
||||
}
|
||||
);
|
||||
assert.equal(attachmentReq.status, 201);
|
||||
let attachmentIds = await attachmentAddPromise;
|
||||
assert.lengthOf(attachmentIds, 1);
|
||||
let attachment1 = Zotero.Items.get(attachmentIds[0]);
|
||||
let bookItem = Zotero.Items.get(itemIDs[0]);
|
||||
assert.equal(attachment1.parentItemID, bookItem.id);
|
||||
|
||||
// Cancel the session
|
||||
let cancelReq = await Zotero.HTTP.request(
|
||||
'POST',
|
||||
connectorServerPath + '/connector/cancelSession',
|
||||
{
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ sessionID })
|
||||
}
|
||||
);
|
||||
assert.equal(cancelReq.status, 200);
|
||||
|
||||
// The item and attachment should be deleted
|
||||
let deletedItem = await Zotero.Items.getAsync(attachment1.parentItemID);
|
||||
let deletedAttachment = await Zotero.Items.getAsync(attachment1.id);
|
||||
assert.isFalse(deletedItem);
|
||||
assert.isFalse(deletedAttachment);
|
||||
});
|
||||
|
||||
it('should not save attachment items if session is cancelled half-way through', async function () {
|
||||
var collection = await createDataObject('collection');
|
||||
let sessionID = Zotero.Utilities.randomString();
|
||||
let bookItemID = Zotero.Utilities.randomString();
|
||||
await select(win, collection);
|
||||
|
||||
let body = {
|
||||
sessionID,
|
||||
items: [
|
||||
{
|
||||
id: bookItemID,
|
||||
itemType: "book",
|
||||
title: "Book Title",
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
let itemAddPromise = waitForItemEvent('add');
|
||||
// Save the item as if by the connector
|
||||
await Zotero.HTTP.request(
|
||||
'POST',
|
||||
connectorServerPath + "/connector/saveItems",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
);
|
||||
let itemIDs = await itemAddPromise;
|
||||
|
||||
// Cancel the session
|
||||
let cancelReq = await Zotero.HTTP.request(
|
||||
'POST',
|
||||
connectorServerPath + '/connector/cancelSession',
|
||||
{
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ sessionID })
|
||||
}
|
||||
);
|
||||
assert.equal(cancelReq.status, 200);
|
||||
|
||||
// Try to add the attachment to the item
|
||||
let attachmentReq = await getPromiseError(httpRequest(
|
||||
'POST',
|
||||
connectorServerPath + "/connector/saveAttachment",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"X-Metadata": JSON.stringify({
|
||||
sessionID,
|
||||
title: "Book Attachment",
|
||||
parentItemID: bookItemID,
|
||||
url: `${testServerPath}/attachment1.pdf`,
|
||||
})
|
||||
},
|
||||
body: pdfArrayBuffer
|
||||
}
|
||||
));
|
||||
// It should be rejected
|
||||
assert.equal(attachmentReq.status, 409);
|
||||
|
||||
// The parent item should also be removed
|
||||
let bookItem = await Zotero.Items.getAsync(itemIDs[0]);
|
||||
assert.isFalse(bookItem);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue