Merge pull request #1846 from fletcherhaz/snapshot
Use SingleFile to create snapshots of web pages
This commit is contained in:
commit
20c8cede4d
13 changed files with 1924 additions and 54 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -26,3 +26,6 @@
|
||||||
path = resource/schema/global
|
path = resource/schema/global
|
||||||
url = git://github.com/zotero/zotero-schema.git
|
url = git://github.com/zotero/zotero-schema.git
|
||||||
branch = master
|
branch = master
|
||||||
|
[submodule "resource/SingleFileZ"]
|
||||||
|
path = resource/SingleFileZ
|
||||||
|
url = https://github.com/gildas-lormeau/SingleFileZ.git
|
||||||
|
|
|
@ -762,8 +762,16 @@ Zotero.Attachments = new function(){
|
||||||
if ((contentType === 'text/html' || contentType === 'application/xhtml+xml')
|
if ((contentType === 'text/html' || contentType === 'application/xhtml+xml')
|
||||||
// Documents from XHR don't work here
|
// Documents from XHR don't work here
|
||||||
&& Zotero.Translate.DOMWrapper.unwrap(document) instanceof Ci.nsIDOMDocument) {
|
&& Zotero.Translate.DOMWrapper.unwrap(document) instanceof Ci.nsIDOMDocument) {
|
||||||
Zotero.debug('Saving document with saveDocument()');
|
if (document.defaultView.window) {
|
||||||
yield Zotero.Utilities.Internal.saveDocument(document, tmpFile);
|
// If we have a full hidden browser, use SingleFile
|
||||||
|
Zotero.debug('Saving document with saveHTMLDocument()');
|
||||||
|
yield Zotero.Utilities.Internal.saveHTMLDocument(document, tmpFile);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Fallback to nsIWebBrowserPersist
|
||||||
|
Zotero.debug('Saving document with saveDocument()');
|
||||||
|
yield Zotero.Utilities.Internal.saveDocument(document, tmpFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Zotero.debug("Saving file with saveURI()");
|
Zotero.debug("Saving file with saveURI()");
|
||||||
|
@ -837,6 +845,95 @@ Zotero.Attachments = new function(){
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a snapshot from a page data given by SingleFileZ
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {String} options.url
|
||||||
|
* @param {Object} options.pageData - PageData object from SingleFileZ
|
||||||
|
* @param {Integer} [options.parentItemID]
|
||||||
|
* @param {Integer[]} [options.collections]
|
||||||
|
* @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.importFromPageData = async (options) => {
|
||||||
|
Zotero.debug("Importing attachment item from PageData");
|
||||||
|
|
||||||
|
let url = options.url;
|
||||||
|
let pageData = options.pageData;
|
||||||
|
let parentItemID = options.parentItemID;
|
||||||
|
let collections = options.collections;
|
||||||
|
let title = options.title;
|
||||||
|
let saveOptions = options.saveOptions;
|
||||||
|
|
||||||
|
let contentType = "text/html";
|
||||||
|
|
||||||
|
if (parentItemID && collections) {
|
||||||
|
throw new Error("parentItemID and parentCollectionIDs 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.putContentsAsync(tmpFile, pageData.content);
|
||||||
|
|
||||||
|
await Zotero.Utilities.Internal.saveSingleFileResources(tmpDirectory, pageData.resources, "");
|
||||||
|
|
||||||
|
// If we're using the title from the document, make some adjustments
|
||||||
|
// Remove e.g. " - Scaled (-17%)" from end of images saved from links,
|
||||||
|
// though I'm not sure why it's getting added to begin with
|
||||||
|
if (contentType.indexOf('image/') === 0) {
|
||||||
|
title = title.replace(/(.+ \([^,]+, [0-9]+x[0-9]+[^\)]+\)) - .+/, "$1" );
|
||||||
|
}
|
||||||
|
// If not native type, strip mime type data in parens
|
||||||
|
else if (!Zotero.MIME.hasNativeHandler(contentType, this._getExtensionFromURL(url))) {
|
||||||
|
title = title.replace(/(.+) \([a-z]+\/[^\)]+\)/, "$1" );
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentItem = await _addToDB({
|
||||||
|
file: 'storage:' + fileName,
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
linkMode: Zotero.Attachments.LINK_MODE_IMPORTED_URL,
|
||||||
|
parentItemID,
|
||||||
|
charset: 'utf-8',
|
||||||
|
contentType,
|
||||||
|
collections,
|
||||||
|
saveOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
Zotero.Fulltext.queueItem(attachmentItem);
|
||||||
|
|
||||||
|
destDirectory = this.getStorageDirectory(attachmentItem).path;
|
||||||
|
await OS.File.move(tmpDirectory, destDirectory);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} url
|
* @param {String} url
|
||||||
* @param {String} path
|
* @param {String} path
|
||||||
|
|
|
@ -153,6 +153,7 @@ Zotero.Server.Connector.SaveSession = function (id, action, requestData) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.created = new Date();
|
this.created = new Date();
|
||||||
this.savingDone = false;
|
this.savingDone = false;
|
||||||
|
this.pendingAttachments = [];
|
||||||
this._action = action;
|
this._action = action;
|
||||||
this._requestData = requestData;
|
this._requestData = requestData;
|
||||||
this._items = new Set();
|
this._items = new Set();
|
||||||
|
@ -162,6 +163,11 @@ Zotero.Server.Connector.SaveSession = function (id, action, requestData) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.Server.Connector.SaveSession.prototype.addPageData = function (pageData) {
|
||||||
|
this._requestData.data.pageData = pageData;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
Zotero.Server.Connector.SaveSession.prototype.onProgress = function (item, progress, error) {
|
Zotero.Server.Connector.SaveSession.prototype.onProgress = function (item, progress, error) {
|
||||||
if (item.id === null || item.id === undefined) {
|
if (item.id === null || item.id === undefined) {
|
||||||
throw new Error("ID not provided");
|
throw new Error("ID not provided");
|
||||||
|
@ -264,6 +270,8 @@ Zotero.Server.Connector.SaveSession.prototype.update = async function (targetID,
|
||||||
for (let item of this._items) {
|
for (let item of this._items) {
|
||||||
await item.eraseTx();
|
await item.eraseTx();
|
||||||
}
|
}
|
||||||
|
// Remove pending attachments (will be recreated by calling `save...` below)
|
||||||
|
this.pendingAttachments = [];
|
||||||
let actionUC = Zotero.Utilities.capitalize(this._action);
|
let actionUC = Zotero.Utilities.capitalize(this._action);
|
||||||
// saveItems has a different signature with the session as the first argument
|
// saveItems has a different signature with the session as the first argument
|
||||||
let params = [targetID, this._requestData];
|
let params = [targetID, this._requestData];
|
||||||
|
@ -316,6 +324,12 @@ Zotero.Server.Connector.SaveSession.prototype._updateItems = Zotero.serial(async
|
||||||
|
|
||||||
if (item.libraryID != libraryID) {
|
if (item.libraryID != libraryID) {
|
||||||
let newItem = await item.moveToLibrary(libraryID);
|
let newItem = await item.moveToLibrary(libraryID);
|
||||||
|
// Check pending attachments and switch parent ID
|
||||||
|
for (let i = 0; i < this.pendingAttachments.length; ++i) {
|
||||||
|
if (this.pendingAttachments[i][0] === item.id) {
|
||||||
|
this.pendingAttachments[i][0] = newItem.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Replace item in session
|
// Replace item in session
|
||||||
this._items.delete(item);
|
this._items.delete(item);
|
||||||
this._items.add(newItem);
|
this._items.add(newItem);
|
||||||
|
@ -384,6 +398,41 @@ Zotero.Server.Connector.SaveSession.prototype._updateRecents = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.Server.Connector.Utilities = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to insert form data back into SingleFileZ pageData object
|
||||||
|
*
|
||||||
|
* SingleFileZ creates a single object containing all page data including all
|
||||||
|
* resource files. We turn that into a multipart/form-data request for upload
|
||||||
|
* and here we insert the form resources back into the SingleFileZ object.
|
||||||
|
*
|
||||||
|
* @param {Object} resources - Resources object inside SingleFileZ pageData object
|
||||||
|
* @param {Object} formData - Multipart form data as a keyed object
|
||||||
|
*/
|
||||||
|
insertSnapshotResources: function (resources, formData) {
|
||||||
|
for (let resourceType in resources) {
|
||||||
|
for (let resource of resources[resourceType]) {
|
||||||
|
// Frames have whole new set of resources
|
||||||
|
// We handle these by recursion
|
||||||
|
if (resourceType === "frames") {
|
||||||
|
Zotero.Server.Connector.Utilities.insertSnapshotResources(resource.resources, formData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// UUIDs are marked by a prefix
|
||||||
|
if (resource.content.startsWith('binary-')) {
|
||||||
|
// Replace content with actual content indexed in formData
|
||||||
|
// by the UUID stored in the content
|
||||||
|
resource.content = formData.find(
|
||||||
|
element => element.params.name === resource.content
|
||||||
|
).body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists all available translators, including code for translators that should be run on every page
|
* Lists all available translators, including code for translators that should be run on every page
|
||||||
*
|
*
|
||||||
|
@ -744,6 +793,7 @@ Zotero.Server.Connector.SaveItems.prototype = {
|
||||||
requestData,
|
requestData,
|
||||||
function (jsonItems, items) {
|
function (jsonItems, items) {
|
||||||
session.addItems(items);
|
session.addItems(items);
|
||||||
|
let singleFile = false;
|
||||||
// Only return the properties the connector needs
|
// Only return the properties the connector needs
|
||||||
jsonItems = jsonItems.map((item) => {
|
jsonItems = jsonItems.map((item) => {
|
||||||
let o = {
|
let o = {
|
||||||
|
@ -755,6 +805,9 @@ Zotero.Server.Connector.SaveItems.prototype = {
|
||||||
};
|
};
|
||||||
if (item.attachments) {
|
if (item.attachments) {
|
||||||
o.attachments = item.attachments.map((attachment) => {
|
o.attachments = item.attachments.map((attachment) => {
|
||||||
|
if (attachment.singleFile) {
|
||||||
|
singleFile = true;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: session.id + '_' + attachment.id, // TODO: Remove prefix
|
id: session.id + '_' + attachment.id, // TODO: Remove prefix
|
||||||
title: attachment.title,
|
title: attachment.title,
|
||||||
|
@ -765,14 +818,16 @@ Zotero.Server.Connector.SaveItems.prototype = {
|
||||||
};
|
};
|
||||||
return o;
|
return o;
|
||||||
});
|
});
|
||||||
resolve([201, "application/json", JSON.stringify({items: jsonItems})]);
|
resolve([201, "application/json", JSON.stringify({ items: jsonItems, singleFile: singleFile })]);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// Add items to session once all attachments have been saved
|
// Add items to session once all attachments have been saved
|
||||||
.then(function (items) {
|
.then(function (items) {
|
||||||
session.addItems(items);
|
session.addItems(items);
|
||||||
// Return 'done: true' so the connector stops checking for updates
|
if (session.pendingAttachments.length === 0) {
|
||||||
session.savingDone = true;
|
// Return 'done: true' so the connector stops checking for updates
|
||||||
|
session.savingDone = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
@ -835,16 +890,168 @@ Zotero.Server.Connector.SaveItems.prototype = {
|
||||||
cookieSandbox,
|
cookieSandbox,
|
||||||
proxy
|
proxy
|
||||||
});
|
});
|
||||||
return itemSaver.saveItems(
|
let items = await itemSaver.saveItems(
|
||||||
data.items,
|
data.items,
|
||||||
function (attachment, progress, error) {
|
function (attachment, progress, error) {
|
||||||
session.onProgress(attachment, progress, error);
|
session.onProgress(attachment, progress, error);
|
||||||
},
|
},
|
||||||
onTopLevelItemsDone
|
onTopLevelItemsDone,
|
||||||
|
function (parentItemID, attachment) {
|
||||||
|
session.pendingAttachments.push([parentItemID, attachment]);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
if (session.pendingAttachments.length > 0) {
|
||||||
|
// If the session has pageData already (from switching to a `filesEditable` library
|
||||||
|
// then we can save `pendingAttachments` now
|
||||||
|
if (data.pageData) {
|
||||||
|
await itemSaver.saveSnapshotAttachments(
|
||||||
|
session.pendingAttachments,
|
||||||
|
data.pageData,
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a snapshot to the DB
|
||||||
|
*
|
||||||
|
* Accepts:
|
||||||
|
* uri - The URI of the page to be saved
|
||||||
|
* html - document.innerHTML or equivalent
|
||||||
|
* cookie - document.cookie or equivalent
|
||||||
|
* Returns:
|
||||||
|
* Nothing (200 OK response)
|
||||||
|
*/
|
||||||
|
Zotero.Server.Connector.SaveSingleFile = function () {};
|
||||||
|
Zotero.Server.Endpoints["/connector/saveSingleFile"] = Zotero.Server.Connector.SaveSingleFile;
|
||||||
|
Zotero.Server.Connector.SaveSingleFile.prototype = {
|
||||||
|
supportedMethods: ["POST"],
|
||||||
|
supportedDataTypes: ["multipart/form-data"],
|
||||||
|
permitBookmarklet: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save SingleFile snapshot to pending attachments
|
||||||
|
*/
|
||||||
|
init: async function (requestData) {
|
||||||
|
// Retrieve payload
|
||||||
|
let data = JSON.parse(Zotero.Utilities.Internal.decodeUTF8(
|
||||||
|
requestData.data.find(e => e.params.name === "payload").body
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!data.sessionID) {
|
||||||
|
return [400, "application/json", JSON.stringify({ error: "SESSION_ID_NOT_PROVIDED" })];
|
||||||
|
}
|
||||||
|
|
||||||
|
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" })];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.pageData) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.savingDone = true;
|
||||||
|
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild SingleFile object from multipart/form-data
|
||||||
|
Zotero.Server.Connector.Utilities.insertSnapshotResources(
|
||||||
|
data.pageData.resources,
|
||||||
|
requestData.data
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add to session data, in case `saveSnapshot` is called again by the session
|
||||||
|
session.addPageData(data.pageData);
|
||||||
|
|
||||||
|
// We do this after adding to session because if we switch to a `filesEditable`
|
||||||
|
// library we need to have access to the pageData.
|
||||||
|
let { library, collection } = Zotero.Server.Connector.getSaveTarget();
|
||||||
|
if (!library.filesEditable) {
|
||||||
|
session.savingDone = true;
|
||||||
|
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve all items in the session that need a snapshot
|
||||||
|
if (session._action === 'saveSnapshot') {
|
||||||
|
await Zotero.Promise.all(
|
||||||
|
session.pendingAttachments.map((pendingAttachment) => {
|
||||||
|
return Zotero.Attachments.importFromPageData({
|
||||||
|
title: data.title,
|
||||||
|
url: data.url,
|
||||||
|
parentItemID: pendingAttachment[0],
|
||||||
|
pageData: data.pageData
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
await itemSaver.saveSnapshotAttachments(
|
||||||
|
session.pendingAttachments,
|
||||||
|
data.pageData,
|
||||||
|
function (attachment, progress, error) {
|
||||||
|
session.onProgress(attachment, progress, error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return 'done: true' so the connector stops checking for updates
|
||||||
|
session.savingDone = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 201;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves a snapshot to the DB
|
* Saves a snapshot to the DB
|
||||||
*
|
*
|
||||||
|
@ -898,9 +1105,15 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
|
||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 201;
|
return [201, "application/json", JSON.stringify({ saveSingleFile: !data.skipSnapshot })];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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) {
|
saveSnapshot: async function (target, requestData) {
|
||||||
var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(target);
|
var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(target);
|
||||||
var libraryID = library.libraryID;
|
var libraryID = library.libraryID;
|
||||||
|
@ -939,10 +1152,15 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
|
||||||
var doc = parser.parseFromString(`<html>${data.html}</html>`, 'text/html');
|
var doc = parser.parseFromString(`<html>${data.html}</html>`, 'text/html');
|
||||||
doc = Zotero.HTTP.wrapDocument(doc, data.url);
|
doc = Zotero.HTTP.wrapDocument(doc, data.url);
|
||||||
|
|
||||||
|
let title = doc.title;
|
||||||
|
if (!data.html) {
|
||||||
|
title = data.title;
|
||||||
|
}
|
||||||
|
|
||||||
// Create new webpage item
|
// Create new webpage item
|
||||||
let item = new Zotero.Item("webpage");
|
let item = new Zotero.Item("webpage");
|
||||||
item.libraryID = libraryID;
|
item.libraryID = libraryID;
|
||||||
item.setField("title", doc.title);
|
item.setField("title", title);
|
||||||
item.setField("url", data.url);
|
item.setField("url", data.url);
|
||||||
item.setField("accessDate", "CURRENT_TIMESTAMP");
|
item.setField("accessDate", "CURRENT_TIMESTAMP");
|
||||||
if (collection) {
|
if (collection) {
|
||||||
|
@ -951,11 +1169,35 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
|
||||||
var itemID = await item.saveTx();
|
var itemID = await item.saveTx();
|
||||||
|
|
||||||
// Save snapshot
|
// Save snapshot
|
||||||
if (library.filesEditable && !data.skipSnapshot) {
|
if (!data.skipSnapshot) {
|
||||||
await Zotero.Attachments.importFromDocument({
|
// If called from session update, requestData may already have SingleFile data
|
||||||
document: doc,
|
if (library.filesEditable && data.pageData) {
|
||||||
parentItemID: itemID
|
await Zotero.Attachments.importFromPageData({
|
||||||
});
|
title: data.title,
|
||||||
|
url: data.url,
|
||||||
|
parentItemID: itemID,
|
||||||
|
pageData: data.pageData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 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.push([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,
|
||||||
|
title,
|
||||||
|
parentItemID: itemID,
|
||||||
|
contentType: "text/html",
|
||||||
|
cookieSandbox
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
|
|
|
@ -158,6 +158,7 @@ Zotero.Server.SocketListener = new function() {
|
||||||
* handles the actual acquisition of data
|
* handles the actual acquisition of data
|
||||||
*/
|
*/
|
||||||
Zotero.Server.DataListener = function(iStream, oStream) {
|
Zotero.Server.DataListener = function(iStream, oStream) {
|
||||||
|
Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
||||||
this.header = "";
|
this.header = "";
|
||||||
this.headerFinished = false;
|
this.headerFinished = false;
|
||||||
|
|
||||||
|
@ -166,9 +167,6 @@ Zotero.Server.DataListener = function(iStream, oStream) {
|
||||||
|
|
||||||
this.iStream = iStream;
|
this.iStream = iStream;
|
||||||
this.oStream = oStream;
|
this.oStream = oStream;
|
||||||
this.sStream = Components.classes["@mozilla.org/scriptableinputstream;1"]
|
|
||||||
.createInstance(Components.interfaces.nsIScriptableInputStream);
|
|
||||||
this.sStream.init(iStream);
|
|
||||||
|
|
||||||
this.foundReturn = false;
|
this.foundReturn = false;
|
||||||
}
|
}
|
||||||
|
@ -192,7 +190,7 @@ Zotero.Server.DataListener.prototype.onStopRequest = function(request, context,
|
||||||
*/
|
*/
|
||||||
Zotero.Server.DataListener.prototype.onDataAvailable = function(request, context,
|
Zotero.Server.DataListener.prototype.onDataAvailable = function(request, context,
|
||||||
inputStream, offset, count) {
|
inputStream, offset, count) {
|
||||||
var readData = this.sStream.read(count);
|
var readData = NetUtil.readInputStreamToString(inputStream, count);
|
||||||
|
|
||||||
if(this.headerFinished) { // reading body
|
if(this.headerFinished) { // reading body
|
||||||
this.body += readData;
|
this.body += readData;
|
||||||
|
@ -325,26 +323,12 @@ Zotero.Server.DataListener.prototype._headerFinished = function() {
|
||||||
*/
|
*/
|
||||||
Zotero.Server.DataListener.prototype._bodyData = function() {
|
Zotero.Server.DataListener.prototype._bodyData = function() {
|
||||||
if(this.body.length >= this.bodyLength) {
|
if(this.body.length >= this.bodyLength) {
|
||||||
// convert to UTF-8
|
|
||||||
var dataStream = Components.classes["@mozilla.org/io/string-input-stream;1"]
|
|
||||||
.createInstance(Components.interfaces.nsIStringInputStream);
|
|
||||||
dataStream.setData(this.body, this.bodyLength);
|
|
||||||
|
|
||||||
var utf8Stream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
|
|
||||||
.createInstance(Components.interfaces.nsIConverterInputStream);
|
|
||||||
utf8Stream.init(dataStream, "UTF-8", 4096, "?");
|
|
||||||
|
|
||||||
this.body = "";
|
|
||||||
var string = {};
|
|
||||||
while(utf8Stream.readString(this.bodyLength, string)) {
|
|
||||||
this.body += string.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle envelope
|
// handle envelope
|
||||||
this._processEndpoint("POST", this.body); // async
|
this._processEndpoint("POST", this.body); // async
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the response to an HTTP request
|
* Generates the response to an HTTP request
|
||||||
*/
|
*/
|
||||||
|
@ -400,6 +384,8 @@ Zotero.Server.DataListener.prototype._generateResponse = function (status, conte
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a response based on calling the function associated with the endpoint
|
* Generates a response based on calling the function associated with the endpoint
|
||||||
|
*
|
||||||
|
* Note: postData contains raw bytes and should be decoded before use
|
||||||
*/
|
*/
|
||||||
Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine(function* (method, postData) {
|
Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine(function* (method, postData) {
|
||||||
try {
|
try {
|
||||||
|
@ -468,12 +454,14 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
|
||||||
// decode content-type post data
|
// decode content-type post data
|
||||||
if(this.contentType === "application/json") {
|
if(this.contentType === "application/json") {
|
||||||
try {
|
try {
|
||||||
|
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
|
||||||
decodedData = JSON.parse(postData);
|
decodedData = JSON.parse(postData);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid JSON provided\n"));
|
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid JSON provided\n"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if(this.contentType === "application/x-www-form-urlencoded") {
|
} else if(this.contentType === "application/x-www-form-urlencoded") {
|
||||||
|
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
|
||||||
decodedData = Zotero.Server.decodeQueryString(postData);
|
decodedData = Zotero.Server.decodeQueryString(postData);
|
||||||
} else if(this.contentType === "multipart/form-data") {
|
} else if(this.contentType === "multipart/form-data") {
|
||||||
let boundary = /boundary=([^\s]*)/i.exec(this.header);
|
let boundary = /boundary=([^\s]*)/i.exec(this.header);
|
||||||
|
@ -487,6 +475,7 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
|
||||||
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
|
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
|
||||||
decodedData = postData;
|
decodedData = postData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -606,6 +595,8 @@ Zotero.Server.DataListener.prototype._requestFinished = function (response, opti
|
||||||
|
|
||||||
Zotero.Server.DataListener.prototype._decodeMultipartData = function(data, boundary) {
|
Zotero.Server.DataListener.prototype._decodeMultipartData = function(data, boundary) {
|
||||||
var contentDispositionRe = /^Content-Disposition:\s*(.*)$/i;
|
var contentDispositionRe = /^Content-Disposition:\s*(.*)$/i;
|
||||||
|
let contentTypeRe = /^Content-Type:\s*(.*)$/i
|
||||||
|
|
||||||
var results = [];
|
var results = [];
|
||||||
data = data.split(boundary);
|
data = data.split(boundary);
|
||||||
// Ignore pre first boundary and post last boundary
|
// Ignore pre first boundary and post last boundary
|
||||||
|
@ -626,11 +617,37 @@ Zotero.Server.DataListener.prototype._decodeMultipartData = function(data, bound
|
||||||
throw new Error('Malformed multipart/form-data body');
|
throw new Error('Malformed multipart/form-data body');
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentDisposition = contentDispositionRe.exec(fieldData.header);
|
fieldData.params = {};
|
||||||
if (contentDisposition) {
|
let headers = [];
|
||||||
for (let nameVal of contentDisposition[1].split(';')) {
|
if (fieldData.header.indexOf("\r\n") > -1) {
|
||||||
nameVal.split('=');
|
headers = fieldData.header.split("\r\n");
|
||||||
fieldData[nameVal[0]] = nameVal.length > 1 ? nameVal[1] : null;
|
}
|
||||||
|
else if (fieldData.header.indexOf("\n\n") > -1) {
|
||||||
|
headers = fieldData.header.split("\n\n");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
headers = [fieldData.header];
|
||||||
|
}
|
||||||
|
for (const header of headers) {
|
||||||
|
if (contentDispositionRe.test(header)) {
|
||||||
|
// Example:
|
||||||
|
// Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"
|
||||||
|
let contentDisposition = header.split(';');
|
||||||
|
if (contentDisposition.length > 1) {
|
||||||
|
contentDisposition.shift();
|
||||||
|
for (let param of contentDisposition) {
|
||||||
|
let nameVal = param.trim().split('=');
|
||||||
|
fieldData.params[nameVal[0]] = nameVal[1].trim().slice(1, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (contentTypeRe.test(header)) {
|
||||||
|
// Example:
|
||||||
|
// Content-Type: image/png
|
||||||
|
let contentType = header.split(':');
|
||||||
|
if (contentType.length > 1) {
|
||||||
|
fieldData.params.contentType = contentType[1].trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.push(fieldData);
|
results.push(fieldData);
|
||||||
|
|
|
@ -86,8 +86,10 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
|
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
|
||||||
* @param {Function} [itemsDoneCallback] A callback that is called once all top-level items are
|
* @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.
|
* 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) {
|
saveItems: async function (jsonItems, attachmentCallback, itemsDoneCallback, pendingAttachmentsCallback) {
|
||||||
var items = [];
|
var items = [];
|
||||||
var standaloneAttachments = [];
|
var standaloneAttachments = [];
|
||||||
var childAttachments = [];
|
var childAttachments = [];
|
||||||
|
@ -165,6 +167,14 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
}
|
}
|
||||||
attachmentsToSave.push(jsonAttachment);
|
attachmentsToSave.push(jsonAttachment);
|
||||||
attachmentCallback(jsonAttachment, 0);
|
attachmentCallback(jsonAttachment, 0);
|
||||||
|
if (jsonAttachment.singleFile) {
|
||||||
|
// SingleFile attachments are saved in 'saveSingleFile'
|
||||||
|
// connector endpoint
|
||||||
|
if (pendingAttachmentsCallback) {
|
||||||
|
pendingAttachmentsCallback(itemID, jsonAttachment);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
childAttachments.push([jsonAttachment, itemID]);
|
childAttachments.push([jsonAttachment, itemID]);
|
||||||
}
|
}
|
||||||
jsonItem.attachments = attachmentsToSave;
|
jsonItem.attachments = attachmentsToSave;
|
||||||
|
@ -343,6 +353,27 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save pending snapshot attachments to disk and library
|
||||||
|
*
|
||||||
|
* @param {Array} pendingAttachments - A list of snapshot attachments
|
||||||
|
* @param {Object} pageData - Snapshot data from SingleFile
|
||||||
|
* @param {Function} attachmentCallback - Callback with progress of attachments
|
||||||
|
*/
|
||||||
|
saveSnapshotAttachments: Zotero.Promise.coroutine(function* (pendingAttachments, pageData, attachmentCallback) {
|
||||||
|
for (let [parentItemID, attachment] of pendingAttachments) {
|
||||||
|
if (pageData) {
|
||||||
|
attachment.pageData = pageData;
|
||||||
|
}
|
||||||
|
yield this._saveAttachment(
|
||||||
|
attachment,
|
||||||
|
parentItemID,
|
||||||
|
attachmentCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
_makeJSONAttachment: function (parentID, title) {
|
_makeJSONAttachment: function (parentID, title) {
|
||||||
|
@ -857,7 +888,6 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import from URL
|
|
||||||
let mimeType = attachment.mimeType ? attachment.mimeType : null;
|
let mimeType = attachment.mimeType ? attachment.mimeType : null;
|
||||||
let fileBaseName;
|
let fileBaseName;
|
||||||
if (parentItemID) {
|
if (parentItemID) {
|
||||||
|
@ -865,11 +895,27 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem);
|
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
Zotero.debug('Importing attachment from URL');
|
|
||||||
attachment.linkMode = "imported_url";
|
attachment.linkMode = "imported_url";
|
||||||
|
|
||||||
attachmentCallback(attachment, 0);
|
attachmentCallback(attachment, 0);
|
||||||
|
|
||||||
|
// Import from SingleFileZ Page Data
|
||||||
|
if (attachment.pageData) {
|
||||||
|
Zotero.debug('Importing attachment from SingleFileZ');
|
||||||
|
|
||||||
|
return Zotero.Attachments.importFromPageData({
|
||||||
|
libraryID: this._libraryID,
|
||||||
|
title,
|
||||||
|
url: attachment.url,
|
||||||
|
parentItemID,
|
||||||
|
pageData: attachment.pageData,
|
||||||
|
collections: !parentItemID ? this._collections : undefined,
|
||||||
|
saveOptions: this._saveOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import from URL
|
||||||
|
Zotero.debug('Importing attachment from URL');
|
||||||
return Zotero.Attachments.importFromURL({
|
return Zotero.Attachments.importFromURL({
|
||||||
libraryID: this._libraryID,
|
libraryID: this._libraryID,
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
|
|
|
@ -360,6 +360,35 @@ Zotero.Utilities.Internal = {
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a binary string into a typed Uint8Array
|
||||||
|
*
|
||||||
|
* @param {String} data - Binary string to decode
|
||||||
|
* @return {Uint8Array} Typed array holding data
|
||||||
|
*/
|
||||||
|
_decodeToUint8Array: function (data) {
|
||||||
|
var buf = new ArrayBuffer(data.length);
|
||||||
|
var bufView = new Uint8Array(buf);
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
bufView[i] = data.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bufView;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a binary string to UTF-8 string
|
||||||
|
*
|
||||||
|
* @param {String} data - Binary string to decode
|
||||||
|
* @return {String} UTF-8 encoded string
|
||||||
|
*/
|
||||||
|
decodeUTF8: function (data) {
|
||||||
|
var bufView = Zotero.Utilities.Internal._decodeToUint8Array(data);
|
||||||
|
var decoder = new TextDecoder();
|
||||||
|
return decoder.decode(bufView);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the byte length of a UTF-8 string
|
* Return the byte length of a UTF-8 string
|
||||||
*
|
*
|
||||||
|
@ -519,6 +548,311 @@ Zotero.Utilities.Internal = {
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes in a document, creates a JS Sandbox and executes the SingleFile
|
||||||
|
* extension to save the page as one single file without JavaScript.
|
||||||
|
*
|
||||||
|
* @param {Object} document
|
||||||
|
* @param {String} destFile - Path for file to write to
|
||||||
|
*/
|
||||||
|
saveHTMLDocument: async function (document, destFile) {
|
||||||
|
// Create sandbox for SingleFile
|
||||||
|
var view = document.defaultView;
|
||||||
|
var sandbox = new Components.utils.Sandbox(view, { wantGlobalProperties: ["XMLHttpRequest", "fetch"] });
|
||||||
|
sandbox.window = view.window;
|
||||||
|
sandbox.document = sandbox.window.document;
|
||||||
|
sandbox.browser = false;
|
||||||
|
sandbox.__proto__ = sandbox.window;
|
||||||
|
|
||||||
|
sandbox.Zotero = Components.utils.cloneInto({ HTTP: {} }, sandbox);
|
||||||
|
sandbox.Zotero.debug = Components.utils.exportFunction(Zotero.debug, sandbox);
|
||||||
|
// Mostly copied from:
|
||||||
|
// resources/SingleFileZ/extension/lib/single-file/fetch/bg/fetch.js::fetchResource
|
||||||
|
sandbox.coFetch = Components.utils.exportFunction(
|
||||||
|
function (url, onDone) {
|
||||||
|
const xhrRequest = new XMLHttpRequest();
|
||||||
|
xhrRequest.withCredentials = true;
|
||||||
|
xhrRequest.responseType = "arraybuffer";
|
||||||
|
xhrRequest.onerror = (e) => {
|
||||||
|
let error = new Error(e.detail);
|
||||||
|
onDone(Components.utils.cloneInto(error, sandbox));
|
||||||
|
};
|
||||||
|
xhrRequest.onreadystatechange = () => {
|
||||||
|
if (xhrRequest.readyState == XMLHttpRequest.DONE) {
|
||||||
|
if (xhrRequest.status || xhrRequest.response.byteLength) {
|
||||||
|
let res = {
|
||||||
|
array: new Uint8Array(xhrRequest.response),
|
||||||
|
headers: { "content-type": xhrRequest.getResponseHeader("Content-Type") },
|
||||||
|
status: xhrRequest.status
|
||||||
|
};
|
||||||
|
// Ensure sandbox will have access to response by cloning
|
||||||
|
onDone(Components.utils.cloneInto(res, sandbox));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let error = new Error('Bad Status or Length');
|
||||||
|
onDone(Components.utils.cloneInto(error, sandbox));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhrRequest.open("GET", url, true);
|
||||||
|
xhrRequest.send();
|
||||||
|
},
|
||||||
|
sandbox
|
||||||
|
);
|
||||||
|
|
||||||
|
// First we try regular fetch, then proceed with fetch outside sandbox to evade CORS
|
||||||
|
// restrictions, partly from:
|
||||||
|
// resources/SingleFileZ/extension/lib/single-file/fetch/content/content-fetch.js::fetch
|
||||||
|
Components.utils.evalInSandbox(
|
||||||
|
`
|
||||||
|
ZoteroFetch = async function (url) {
|
||||||
|
try {
|
||||||
|
let response = await fetch(url, { cache: "force-cache" });
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
let response = await new Promise((resolve, reject) => {
|
||||||
|
coFetch(url, (response) => {
|
||||||
|
if (response.status) {
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Zotero.debug("Error retrieving url: " + url);
|
||||||
|
Zotero.debug(response.message);
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
headers: { get: headerName => response.headers[headerName] },
|
||||||
|
arrayBuffer: async () => response.array.buffer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};`,
|
||||||
|
sandbox
|
||||||
|
);
|
||||||
|
|
||||||
|
const SCRIPTS = [
|
||||||
|
// This first script replace in the INDEX_SCRIPTS from the single file cli loader
|
||||||
|
"lib/single-file/index.js",
|
||||||
|
|
||||||
|
// Rest of the scripts (does not include WEB_SCRIPTS, those are handled in build process)
|
||||||
|
"lib/single-file/processors/hooks/content/content-hooks.js",
|
||||||
|
"lib/single-file/processors/hooks/content/content-hooks-frames.js",
|
||||||
|
"lib/single-file/processors/frame-tree/content/content-frame-tree.js",
|
||||||
|
"lib/single-file/processors/lazy/content/content-lazy-loader.js",
|
||||||
|
"lib/single-file/single-file-util.js",
|
||||||
|
"lib/single-file/single-file-helper.js",
|
||||||
|
"lib/single-file/vendor/css-tree.js",
|
||||||
|
"lib/single-file/vendor/html-srcset-parser.js",
|
||||||
|
"lib/single-file/vendor/css-minifier.js",
|
||||||
|
"lib/single-file/vendor/css-font-property-parser.js",
|
||||||
|
"lib/single-file/vendor/css-unescape.js",
|
||||||
|
"lib/single-file/vendor/css-media-query-parser.js",
|
||||||
|
"lib/single-file/modules/html-minifier.js",
|
||||||
|
"lib/single-file/modules/css-fonts-minifier.js",
|
||||||
|
"lib/single-file/modules/css-fonts-alt-minifier.js",
|
||||||
|
"lib/single-file/modules/css-matched-rules.js",
|
||||||
|
"lib/single-file/modules/css-medias-alt-minifier.js",
|
||||||
|
"lib/single-file/modules/css-rules-minifier.js",
|
||||||
|
"lib/single-file/modules/html-images-alt-minifier.js",
|
||||||
|
"lib/single-file/modules/html-serializer.js",
|
||||||
|
"lib/single-file/single-file-core.js",
|
||||||
|
"lib/single-file/single-file.js",
|
||||||
|
|
||||||
|
// Web SCRIPTS
|
||||||
|
"lib/single-file/processors/hooks/content/content-hooks-frames-web.js",
|
||||||
|
"lib/single-file/processors/hooks/content/content-hooks-web.js",
|
||||||
|
];
|
||||||
|
|
||||||
|
const { loadSubScript } = Components.classes['@mozilla.org/moz/jssubscript-loader;1']
|
||||||
|
.getService(Ci.mozIJSSubScriptLoader);
|
||||||
|
|
||||||
|
Zotero.debug('Injecting single file scripts');
|
||||||
|
// Run all the scripts of SingleFile scripts in Sandbox
|
||||||
|
SCRIPTS.forEach(
|
||||||
|
script => loadSubScript('resource://zotero/SingleFileZ/' + script, sandbox)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Zotero.Promise.delay(1500);
|
||||||
|
|
||||||
|
// Use SingleFile to retrieve the html
|
||||||
|
// These are defaults from SingleFileZ
|
||||||
|
// Located in: resources/SingleFileZ/extension/core/bg/config.js
|
||||||
|
// Only change is removeFrames to true (often ads that take a long time)
|
||||||
|
const pageData = await Components.utils.evalInSandbox(
|
||||||
|
`this.singlefile.lib.getPageData({
|
||||||
|
removeHiddenElements: true,
|
||||||
|
removeUnusedStyles: true,
|
||||||
|
removeUnusedFonts: true,
|
||||||
|
removeFrames: true,
|
||||||
|
removeImports: true,
|
||||||
|
removeScripts: true,
|
||||||
|
compressHTML: true,
|
||||||
|
compressCSS: false,
|
||||||
|
loadDeferredImages: true,
|
||||||
|
loadDeferredImagesMaxIdleTime: 1500,
|
||||||
|
loadDeferredImagesBlockCookies: false,
|
||||||
|
loadDeferredImagesBlockStorage: false,
|
||||||
|
loadDeferredImagesKeepZoomLevel: true,
|
||||||
|
filenameTemplate: "{page-title} ({date-iso} {time-locale}).html",
|
||||||
|
infobarTemplate: "",
|
||||||
|
includeInfobar: false,
|
||||||
|
confirmInfobarContent: false,
|
||||||
|
autoClose: false,
|
||||||
|
confirmFilename: false,
|
||||||
|
filenameConflictAction: "uniquify",
|
||||||
|
filenameMaxLength: 192,
|
||||||
|
filenameReplacementCharacter: "_",
|
||||||
|
contextMenuEnabled: true,
|
||||||
|
tabMenuEnabled: true,
|
||||||
|
browserActionMenuEnabled: true,
|
||||||
|
shadowEnabled: true,
|
||||||
|
logsEnabled: true,
|
||||||
|
progressBarEnabled: true,
|
||||||
|
maxResourceSizeEnabled: false,
|
||||||
|
maxResourceSize: 10,
|
||||||
|
removeAudioSrc: true,
|
||||||
|
removeVideoSrc: true,
|
||||||
|
displayInfobar: true,
|
||||||
|
displayStats: false,
|
||||||
|
backgroundSave: true,
|
||||||
|
autoSaveDelay: 1,
|
||||||
|
autoSaveLoad: false,
|
||||||
|
autoSaveUnload: false,
|
||||||
|
autoSaveLoadOrUnload: true,
|
||||||
|
autoSaveRepeat: false,
|
||||||
|
autoSaveRepeatDelay: 10,
|
||||||
|
removeAlternativeFonts: true,
|
||||||
|
removeAlternativeMedias: true,
|
||||||
|
removeAlternativeImages: true,
|
||||||
|
saveRawPage: false,
|
||||||
|
saveToGDrive: false,
|
||||||
|
forceWebAuthFlow: false,
|
||||||
|
extractAuthCode: true,
|
||||||
|
insertTextBody: true,
|
||||||
|
resolveFragmentIdentifierURLs: false,
|
||||||
|
userScriptEnabled: false,
|
||||||
|
saveCreatedBookmarks: false,
|
||||||
|
ignoredBookmarkFolders: [],
|
||||||
|
replaceBookmarkURL: true,
|
||||||
|
saveFavicon: true,
|
||||||
|
includeBOM: false
|
||||||
|
},
|
||||||
|
{ fetch: ZoteroFetch }
|
||||||
|
)`,
|
||||||
|
sandbox
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write main HTML file to disk
|
||||||
|
await Zotero.File.putContentsAsync(destFile, pageData.content);
|
||||||
|
|
||||||
|
// Write resources to disk
|
||||||
|
let tmpDirectory = OS.Path.dirname(destFile);
|
||||||
|
await this.saveSingleFileResources(tmpDirectory, pageData.resources, "");
|
||||||
|
|
||||||
|
Components.utils.nukeSandbox(sandbox);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save all resources to support SingleFile webpage
|
||||||
|
*
|
||||||
|
* @param {String} tmpDirectory - Path to location of attachment root
|
||||||
|
* @param {Object} resources - Resources from SingleFile pageData object
|
||||||
|
* @param {String} prefix - Recursive structure that is initially blank
|
||||||
|
*/
|
||||||
|
saveSingleFileResources: async function (tmpDirectory, resources, prefix) {
|
||||||
|
// This looping/recursion structure comes from:
|
||||||
|
// SingleFileZ/extension/core/bg/compression.js::addPageResources
|
||||||
|
await Zotero.Promise.all(Object.keys(resources).map(
|
||||||
|
(resourceType) => {
|
||||||
|
return Zotero.Promise.all(resources[resourceType].map(
|
||||||
|
async (data) => {
|
||||||
|
// Frames have whole new set of resources
|
||||||
|
// We handle these by recursion
|
||||||
|
if (resourceType === "frames") {
|
||||||
|
// Save frame HTML
|
||||||
|
await Zotero.Utilities.Internal._saveSingleFileResource(
|
||||||
|
data.content,
|
||||||
|
tmpDirectory,
|
||||||
|
prefix + data.name + "index.html",
|
||||||
|
data.binary
|
||||||
|
);
|
||||||
|
// Save frame resources
|
||||||
|
return Zotero.Utilities.Internal.saveSingleFileResources(tmpDirectory, data.resources, prefix + data.name);
|
||||||
|
}
|
||||||
|
return Zotero.Utilities.Internal._saveSingleFileResource(
|
||||||
|
data.content,
|
||||||
|
tmpDirectory,
|
||||||
|
prefix + data.name,
|
||||||
|
data.binary
|
||||||
|
);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
));
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a individual resource from a SingleFile attachment
|
||||||
|
*
|
||||||
|
* @param {String} resource - The actual content to save to file
|
||||||
|
* @param {String} tmpDirectory - Path to location of attachment root
|
||||||
|
* @param {String} fileName - Filename for the piece to save under
|
||||||
|
* @param {Boolean} binary - Whether the resource string is binary or not
|
||||||
|
*/
|
||||||
|
_saveSingleFileResource: async (resource, tmpDirectory, fileName, binary) => {
|
||||||
|
Zotero.debug('Saving resource: ' + fileName);
|
||||||
|
// This seems weird, but it is because SingleFileZ gives us path filenames
|
||||||
|
// (e.g. images/0.png). We want to know if the directory 'images' exists.
|
||||||
|
let filePath = OS.Path.join(tmpDirectory, fileName);
|
||||||
|
let fileDirectory = OS.Path.dirname(filePath);
|
||||||
|
|
||||||
|
// If the directory doesn't exist, make it
|
||||||
|
await OS.File.makeDir(fileDirectory, {
|
||||||
|
unixMode: 0o755,
|
||||||
|
from: tmpDirectory
|
||||||
|
});
|
||||||
|
|
||||||
|
// Binary string from Connector
|
||||||
|
if (typeof resource === "string" && binary) {
|
||||||
|
Components.utils.importGlobalProperties(["Blob"]);
|
||||||
|
let resourceBlob = new Blob([Zotero.Utilities.Internal._decodeToUint8Array(resource)]);
|
||||||
|
await Zotero.File.putContentsAsync(
|
||||||
|
filePath,
|
||||||
|
resourceBlob
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Uint8Array from hidden browser sandbox
|
||||||
|
else if (Object.prototype.toString.call(resource) === "[object Uint8Array]") {
|
||||||
|
let data = Components.utils.waiveXrays(resource);
|
||||||
|
// Write to disk
|
||||||
|
let is = Components.classes["@mozilla.org/io/arraybuffer-input-stream;1"]
|
||||||
|
.createInstance(Components.interfaces.nsIArrayBufferInputStream);
|
||||||
|
is.setData(data.buffer, 0, data.byteLength);
|
||||||
|
// Write to disk
|
||||||
|
await Zotero.File.putContentsAsync(
|
||||||
|
filePath,
|
||||||
|
is
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (resource === undefined) {
|
||||||
|
Zotero.debug('Error saving resource: ' + fileName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise a normal string
|
||||||
|
await Zotero.File.putContentsAsync(
|
||||||
|
filePath,
|
||||||
|
resource
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
1
resource/SingleFileZ
Submodule
1
resource/SingleFileZ
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 7a7073d797c328683c39d0a8672b95b3670e9bef
|
|
@ -47,6 +47,74 @@ async function babelWorker(ev) {
|
||||||
.replace('document.body.appendChild(scrollDiv)', 'document.documentElement.appendChild(scrollDiv)')
|
.replace('document.body.appendChild(scrollDiv)', 'document.documentElement.appendChild(scrollDiv)')
|
||||||
.replace('document.body.removeChild(scrollDiv)', 'document.documentElement.removeChild(scrollDiv)');
|
.replace('document.body.removeChild(scrollDiv)', 'document.documentElement.removeChild(scrollDiv)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note about Single File helper and util patching:
|
||||||
|
// I think this has something to do with the hidden browser being an older version or possibly
|
||||||
|
// it is an issue with the sandbox, but it fails to find addEventListener and the fetch does
|
||||||
|
// not work even if replace it properly in initOptions.
|
||||||
|
|
||||||
|
// Patch single-file-helper
|
||||||
|
else if (sourcefile === 'resource/SingleFileZ/lib/single-file/single-file-helper.js') {
|
||||||
|
transformed = contents.replace('addEventListener("single-filez-user-script-init"',
|
||||||
|
'window.addEventListener("single-filez-user-script-init"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch index.js - This is a SingleFileZ issue. SingleFileZ does not typically use
|
||||||
|
// use this code from SingleFile so the namespace is screwed up.
|
||||||
|
else if (sourcefile === 'resource/SingleFileZ/lib/single-file/index.js') {
|
||||||
|
transformed = contents
|
||||||
|
.replace('this.frameTree.content.frames.getAsync',
|
||||||
|
'this.processors.frameTree.content.frames.getAsync')
|
||||||
|
.replace('this.lazy.content.loader.process',
|
||||||
|
'this.processors.lazy.content.loader.process');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch single-file-core
|
||||||
|
// This style element trick was not working in the hidden browser, so we ignore it
|
||||||
|
else if (sourcefile === 'resource/SingleFileZ/lib/single-file/single-file-core.js') {
|
||||||
|
transformed = contents.replace('if (workStylesheet.sheet.cssRules.length) {', 'if (true) {');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch content-lazy-loader
|
||||||
|
else if (sourcefile === 'resource/SingleFileZ/lib/single-file/processors/lazy/content/content-lazy-loader.js') {
|
||||||
|
transformed = contents
|
||||||
|
.replace(
|
||||||
|
'if (scrollY <= maxScrollY && scrollX <= maxScrollX)',
|
||||||
|
'if (window.scrollY <= maxScrollY && window.scrollX <= maxScrollX)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch single-file
|
||||||
|
else if (sourcefile === 'resource/SingleFileZ/lib/single-file/single-file.js') {
|
||||||
|
// We need to add this bit that is done for the cli implementation of singleFile
|
||||||
|
// See resource/SingleFile/cli/back-ends/common/scripts.js
|
||||||
|
const WEB_SCRIPTS = [
|
||||||
|
"lib/single-file/processors/hooks/content/content-hooks-web.js",
|
||||||
|
"lib/single-file/processors/hooks/content/content-hooks-frames-web.js"
|
||||||
|
];
|
||||||
|
let basePath = 'resource/SingleFileZ/';
|
||||||
|
|
||||||
|
function readScriptFile(path, basePath) {
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
fs.readFile(basePath + path, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(data.toString() + "\n");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const webScripts = {};
|
||||||
|
await Promise.all(
|
||||||
|
WEB_SCRIPTS.map(async path => webScripts[path] = await readScriptFile(path, basePath))
|
||||||
|
);
|
||||||
|
|
||||||
|
transformed = contents + '\n\n'
|
||||||
|
+ "this.singlefile.lib.getFileContent = filename => (" + JSON.stringify(webScripts) + ")[filename];\n";
|
||||||
|
}
|
||||||
|
|
||||||
else if ('ignore' in options && options.ignore.some(ignoreGlob => multimatch(sourcefile, ignoreGlob).length)) {
|
else if ('ignore' in options && options.ignore.some(ignoreGlob => multimatch(sourcefile, ignoreGlob).length)) {
|
||||||
transformed = contents;
|
transformed = contents;
|
||||||
isSkipped = true;
|
isSkipped = true;
|
||||||
|
|
|
@ -32,6 +32,17 @@ const symlinkFiles = [
|
||||||
'!resource/react.js',
|
'!resource/react.js',
|
||||||
'!resource/react-dom.js',
|
'!resource/react-dom.js',
|
||||||
'!resource/react-virtualized.js',
|
'!resource/react-virtualized.js',
|
||||||
|
// Only include lib directory of singleFile
|
||||||
|
// Also do a little bit of manipulation similar to React
|
||||||
|
'!resource/SingleFileZ/**/*',
|
||||||
|
'resource/SingleFileZ/lib/**/*',
|
||||||
|
'resource/SingleFileZ/extension/lib/single-file/fetch/content/content-fetch.js',
|
||||||
|
'resource/SingleFileZ/extension/lib/single-file/index.js',
|
||||||
|
'!resource/SingleFileZ/lib/single-file/single-file-helper.js',
|
||||||
|
'!resource/SingleFileZ/lib/single-file/index.js',
|
||||||
|
'!resource/SingleFileZ/lib/single-file/single-file-core.js',
|
||||||
|
'!resource/SingleFileZ/lib/single-file/processors/lazy/content/content-lazy-loader.js',
|
||||||
|
'!resource/SingleFileZ/lib/single-file/single-file.js',
|
||||||
'update.rdf'
|
'update.rdf'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -84,6 +95,11 @@ const jsFiles = [
|
||||||
'resource/react.js',
|
'resource/react.js',
|
||||||
'resource/react-dom.js',
|
'resource/react-dom.js',
|
||||||
'resource/react-virtualized.js',
|
'resource/react-virtualized.js',
|
||||||
|
'resource/SingleFileZ/lib/single-file/single-file-helper.js',
|
||||||
|
'resource/SingleFileZ/lib/single-file/index.js',
|
||||||
|
'resource/SingleFileZ/lib/single-file/single-file-core.js',
|
||||||
|
'resource/SingleFileZ/lib/single-file/processors/lazy/content/content-lazy-loader.js',
|
||||||
|
'resource/SingleFileZ/lib/single-file/single-file.js'
|
||||||
];
|
];
|
||||||
|
|
||||||
const scssFiles = [
|
const scssFiles = [
|
||||||
|
|
|
@ -306,23 +306,47 @@ describe("Zotero.Attachments", function() {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("#importFromDocument()", function () {
|
describe("#importFromDocument()", function () {
|
||||||
|
Components.utils.import("resource://gre/modules/FileUtils.jsm");
|
||||||
|
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||||
|
var testServerPath, httpd;
|
||||||
|
var testServerPort = 16213;
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(20000);
|
||||||
|
Zotero.Prefs.set("httpServer.enabled", true);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// Alternate ports to prevent exceptions not catchable in JS
|
||||||
|
testServerPort += (testServerPort & 1) ? 1 : -1;
|
||||||
|
testServerPath = 'http://127.0.0.1:' + testServerPort;
|
||||||
|
httpd = new HttpServer();
|
||||||
|
httpd.start(testServerPort);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
var defer = new Zotero.Promise.defer();
|
||||||
|
httpd.stop(() => defer.resolve());
|
||||||
|
await defer.promise;
|
||||||
|
});
|
||||||
|
|
||||||
it("should save a document with embedded files", function* () {
|
it("should save a document with embedded files", function* () {
|
||||||
var item = yield createDataObject('item');
|
var item = yield createDataObject('item');
|
||||||
|
|
||||||
|
var uri = OS.Path.join(getTestDataDirectory().path, "snapshot");
|
||||||
|
httpd.registerDirectory("/", new FileUtils.File(uri));
|
||||||
|
|
||||||
var uri = OS.Path.join(getTestDataDirectory().path, "snapshot", "index.html");
|
|
||||||
var deferred = Zotero.Promise.defer();
|
var deferred = Zotero.Promise.defer();
|
||||||
win.addEventListener('pageshow', () => deferred.resolve());
|
win.addEventListener('pageshow', () => deferred.resolve());
|
||||||
win.loadURI(uri);
|
win.loadURI(testServerPath + "/index.html");
|
||||||
yield deferred.promise;
|
yield deferred.promise;
|
||||||
|
|
||||||
var file = getTestDataDirectory();
|
|
||||||
file.append('test.png');
|
|
||||||
var attachment = yield Zotero.Attachments.importFromDocument({
|
var attachment = yield Zotero.Attachments.importFromDocument({
|
||||||
document: win.content.document,
|
document: win.content.document,
|
||||||
parentItemID: item.id
|
parentItemID: item.id
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(attachment.getField('url'), "file://" + uri);
|
assert.equal(attachment.getField('url'), testServerPath + "/index.html");
|
||||||
|
|
||||||
// Check indexing
|
// Check indexing
|
||||||
var matches = yield Zotero.Fulltext.findTextInItems([attachment.id], 'share your research');
|
var matches = yield Zotero.Fulltext.findTextInItems([attachment.id], 'share your research');
|
||||||
|
@ -333,7 +357,133 @@ describe("Zotero.Attachments", function() {
|
||||||
var storageDir = Zotero.Attachments.getStorageDirectory(attachment).path;
|
var storageDir = Zotero.Attachments.getStorageDirectory(attachment).path;
|
||||||
var file = yield attachment.getFilePathAsync();
|
var file = yield attachment.getFilePathAsync();
|
||||||
assert.equal(OS.Path.basename(file), 'index.html');
|
assert.equal(OS.Path.basename(file), 'index.html');
|
||||||
assert.isTrue(yield OS.File.exists(OS.Path.join(storageDir, 'img.gif')));
|
assert.isTrue(yield OS.File.exists(OS.Path.join(storageDir, 'images', '2.gif')));
|
||||||
|
|
||||||
|
// Check attachment html file contents
|
||||||
|
let path = OS.Path.join(storageDir, 'index.html');
|
||||||
|
assert.isTrue(yield OS.File.exists(path));
|
||||||
|
let contents = yield Zotero.File.getContentsAsync(path);
|
||||||
|
assert.isTrue(contents.startsWith("<html><!--\n Page saved with SingleFileZ"));
|
||||||
|
|
||||||
|
// Check attachment binary file contents
|
||||||
|
path = OS.Path.join(storageDir, 'images', '2.gif');
|
||||||
|
assert.isTrue(yield OS.File.exists(path));
|
||||||
|
contents = yield Zotero.File.getBinaryContentsAsync(path);
|
||||||
|
let expectedPath = getTestDataDirectory();
|
||||||
|
expectedPath.append('snapshot');
|
||||||
|
expectedPath.append('img.gif');
|
||||||
|
let expectedContents = yield Zotero.File.getBinaryContentsAsync(expectedPath);
|
||||||
|
assert.equal(contents, expectedContents);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should save a document with embedded files restricted by CORS", async function () {
|
||||||
|
var item = await createDataObject('item');
|
||||||
|
|
||||||
|
var url = "file://" + OS.Path.join(getTestDataDirectory().path, "snapshot", "img.gif");
|
||||||
|
httpd.registerPathHandler(
|
||||||
|
'/index.html',
|
||||||
|
{
|
||||||
|
handle: function (request, response) {
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
response.write(`<html><head><title>Test</title></head><body><img src="${url}"/>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
var deferred = Zotero.Promise.defer();
|
||||||
|
win.addEventListener('pageshow', () => deferred.resolve());
|
||||||
|
win.loadURI(testServerPath + "/index.html");
|
||||||
|
await deferred.promise;
|
||||||
|
|
||||||
|
var attachment = await Zotero.Attachments.importFromDocument({
|
||||||
|
document: win.content.document,
|
||||||
|
parentItemID: item.id
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(attachment.getField('url'), testServerPath + "/index.html");
|
||||||
|
|
||||||
|
// Check for embedded files
|
||||||
|
var storageDir = Zotero.Attachments.getStorageDirectory(attachment).path;
|
||||||
|
var file = await attachment.getFilePathAsync();
|
||||||
|
assert.equal(OS.Path.basename(file), 'index.html');
|
||||||
|
assert.isTrue(await OS.File.exists(OS.Path.join(storageDir, 'images', '1.gif')));
|
||||||
|
|
||||||
|
// Check attachment html file contents
|
||||||
|
let path = OS.Path.join(storageDir, 'index.html');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
let contents = await Zotero.File.getContentsAsync(path);
|
||||||
|
assert.isTrue(contents.startsWith("<html><!--\n Page saved with SingleFileZ"));
|
||||||
|
|
||||||
|
// Check attachment binary file contents
|
||||||
|
path = OS.Path.join(storageDir, 'images', '1.gif');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
contents = await Zotero.File.getBinaryContentsAsync(path);
|
||||||
|
let expectedPath = getTestDataDirectory();
|
||||||
|
expectedPath.append('snapshot');
|
||||||
|
expectedPath.append('img.gif');
|
||||||
|
let expectedContents = await Zotero.File.getBinaryContentsAsync(expectedPath);
|
||||||
|
assert.equal(contents, expectedContents);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#importFromPageData()", function () {
|
||||||
|
it("should save a SingleFileZ PageData object", async function () {
|
||||||
|
let item = await createDataObject('item');
|
||||||
|
|
||||||
|
let content = getTestDataDirectory();
|
||||||
|
content.append('snapshot');
|
||||||
|
content.append('index.html');
|
||||||
|
|
||||||
|
let image = getTestDataDirectory();
|
||||||
|
image.append('snapshot');
|
||||||
|
image.append('img.gif');
|
||||||
|
|
||||||
|
let pageData = {
|
||||||
|
content: await Zotero.File.getContentsAsync(content),
|
||||||
|
resources: {
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
name: "img.gif",
|
||||||
|
content: await Zotero.File.getBinaryContentsAsync(image),
|
||||||
|
binary: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let attachment = await Zotero.Attachments.importFromPageData({
|
||||||
|
parentItemID: item.id,
|
||||||
|
url: "https://example.com/test.html",
|
||||||
|
title: "Testing Title",
|
||||||
|
pageData
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(attachment.getField('url'), "https://example.com/test.html");
|
||||||
|
|
||||||
|
// Check indexing
|
||||||
|
let matches = await Zotero.Fulltext.findTextInItems([attachment.id], 'share your research');
|
||||||
|
assert.lengthOf(matches, 1);
|
||||||
|
assert.propertyVal(matches[0], 'id', attachment.id);
|
||||||
|
|
||||||
|
// Check for embedded files
|
||||||
|
let storageDir = Zotero.Attachments.getStorageDirectory(attachment).path;
|
||||||
|
let file = await attachment.getFilePathAsync();
|
||||||
|
assert.equal(OS.Path.basename(file), 'test.html');
|
||||||
|
assert.isTrue(await OS.File.exists(OS.Path.join(storageDir, 'img.gif')));
|
||||||
|
|
||||||
|
// Check attachment html file contents
|
||||||
|
let path = OS.Path.join(storageDir, 'test.html');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
let contents = await Zotero.File.getContentsAsync(path);
|
||||||
|
let expectedContents = await Zotero.File.getContentsAsync(file);
|
||||||
|
assert.equal(contents, expectedContents);
|
||||||
|
|
||||||
|
// Check attachment binary file contents
|
||||||
|
path = OS.Path.join(storageDir, 'img.gif');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
contents = await Zotero.File.getBinaryContentsAsync(path);
|
||||||
|
expectedContents = await Zotero.File.getBinaryContentsAsync(image);
|
||||||
|
assert.equal(contents, expectedContents);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -141,6 +141,106 @@ describe("Zotero.Server", function () {
|
||||||
assert.equal(req.responseText, "Test");
|
assert.equal(req.responseText, "Test");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("multipart/form-data", function () {
|
||||||
|
it("should support text", async function () {
|
||||||
|
var called = false;
|
||||||
|
var endpoint = "/test/" + Zotero.Utilities.randomString();
|
||||||
|
|
||||||
|
Zotero.Server.Endpoints[endpoint] = function () {};
|
||||||
|
Zotero.Server.Endpoints[endpoint].prototype = {
|
||||||
|
supportedMethods: ["POST"],
|
||||||
|
supportedDataTypes: ["multipart/form-data"],
|
||||||
|
|
||||||
|
init: function (options) {
|
||||||
|
called = true;
|
||||||
|
assert.isObject(options);
|
||||||
|
assert.property(options.headers, "Content-Type");
|
||||||
|
assert(options.headers["Content-Type"].startsWith("multipart/form-data; boundary="));
|
||||||
|
assert.isArray(options.data);
|
||||||
|
assert.equal(options.data.length, 1);
|
||||||
|
|
||||||
|
let expected = {
|
||||||
|
header: "Content-Disposition: form-data; name=\"foo\"",
|
||||||
|
body: "bar",
|
||||||
|
params: {
|
||||||
|
name: "foo"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert.deepEqual(options.data[0], expected);
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append("foo", "bar");
|
||||||
|
|
||||||
|
let req = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
serverPath + endpoint,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(called);
|
||||||
|
assert.equal(req.status, 204);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support binary", async function () {
|
||||||
|
let called = false;
|
||||||
|
let endpoint = "/test/" + Zotero.Utilities.randomString();
|
||||||
|
let file = getTestDataDirectory();
|
||||||
|
file.append('test.png');
|
||||||
|
let contents = await Zotero.File.getBinaryContentsAsync(file);
|
||||||
|
|
||||||
|
Zotero.Server.Endpoints[endpoint] = function () {};
|
||||||
|
Zotero.Server.Endpoints[endpoint].prototype = {
|
||||||
|
supportedMethods: ["POST"],
|
||||||
|
supportedDataTypes: ["multipart/form-data"],
|
||||||
|
|
||||||
|
init: function (options) {
|
||||||
|
called = true;
|
||||||
|
assert.isObject(options);
|
||||||
|
assert.property(options.headers, "Content-Type");
|
||||||
|
assert(options.headers["Content-Type"].startsWith("multipart/form-data; boundary="));
|
||||||
|
assert.isArray(options.data);
|
||||||
|
assert.equal(options.data.length, 1);
|
||||||
|
assert.equal(options.data[0].header, "Content-Disposition: form-data; name=\"image\"; filename=\"test.png\"\r\nContent-Type: image/png");
|
||||||
|
let expected = {
|
||||||
|
name: "image",
|
||||||
|
filename: "test.png",
|
||||||
|
contentType: "image/png"
|
||||||
|
};
|
||||||
|
assert.deepEqual(options.data[0].params, expected);
|
||||||
|
assert.equal(options.data[0].body, contents);
|
||||||
|
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let image = await File.createFromFileName(OS.Path.join(getTestDataDirectory().path, 'test.png'));
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append("image", image);
|
||||||
|
|
||||||
|
let req = await Zotero.HTTP.request(
|
||||||
|
"POST",
|
||||||
|
serverPath + endpoint,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(called);
|
||||||
|
assert.equal(req.status, 204);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -746,12 +746,231 @@ describe("Connector Server", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("/connector/saveSingleFile", function () {
|
||||||
|
it("should save a webpage item with /saveSnapshot", async function () {
|
||||||
|
var collection = await createDataObject('collection');
|
||||||
|
await waitForItemsLoad(win);
|
||||||
|
|
||||||
|
// Promise for item save
|
||||||
|
let promise = waitForItemEvent('add');
|
||||||
|
|
||||||
|
let testDataDirectory = getTestDataDirectory().path;
|
||||||
|
let indexPath = OS.Path.join(testDataDirectory, 'snapshot', 'index.html');
|
||||||
|
|
||||||
|
let title = Zotero.Utilities.randomString();
|
||||||
|
let sessionID = Zotero.Utilities.randomString();
|
||||||
|
let payload = {
|
||||||
|
sessionID,
|
||||||
|
url: "http://example.com/test",
|
||||||
|
title,
|
||||||
|
singleFile: true
|
||||||
|
};
|
||||||
|
|
||||||
|
await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveSnapshot",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Await item save
|
||||||
|
let parentIDs = await promise;
|
||||||
|
|
||||||
|
// Check parent item
|
||||||
|
assert.lengthOf(parentIDs, 1);
|
||||||
|
var item = Zotero.Items.get(parentIDs[0]);
|
||||||
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage');
|
||||||
|
assert.isTrue(collection.hasItem(item.id));
|
||||||
|
assert.equal(item.getField('title'), title);
|
||||||
|
|
||||||
|
// Promise for attachment save
|
||||||
|
promise = waitForItemEvent('add');
|
||||||
|
|
||||||
|
let body = new FormData();
|
||||||
|
let uuid = 'binary-' + Zotero.Utilities.randomString();
|
||||||
|
body.append("payload", JSON.stringify(Object.assign(payload, {
|
||||||
|
pageData: {
|
||||||
|
content: await Zotero.File.getContentsAsync(indexPath),
|
||||||
|
resources: {
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
name: "img.gif",
|
||||||
|
content: uuid,
|
||||||
|
binary: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
|
||||||
|
let imagePath = OS.Path.join(testDataDirectory, 'snapshot', 'img.gif');
|
||||||
|
body.append(uuid, await File.createFromFileName(imagePath));
|
||||||
|
|
||||||
|
await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveSingleFile",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Await attachment save
|
||||||
|
let attachmentIDs = await promise;
|
||||||
|
|
||||||
|
// Check attachment
|
||||||
|
assert.lengthOf(attachmentIDs, 1);
|
||||||
|
item = Zotero.Items.get(attachmentIDs[0]);
|
||||||
|
assert.isTrue(item.isImportedAttachment());
|
||||||
|
assert.equal(item.getField('title'), title);
|
||||||
|
|
||||||
|
// Check attachment html file
|
||||||
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path;
|
||||||
|
let path = OS.Path.join(attachmentDirectory, 'test.html');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
let contents = await Zotero.File.getContentsAsync(path);
|
||||||
|
let expectedContents = await Zotero.File.getContentsAsync(indexPath);
|
||||||
|
assert.equal(contents, expectedContents);
|
||||||
|
|
||||||
|
// Check attachment binary file
|
||||||
|
path = OS.Path.join(attachmentDirectory, 'img.gif');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
contents = await Zotero.File.getBinaryContentsAsync(path);
|
||||||
|
expectedContents = await Zotero.File.getBinaryContentsAsync(imagePath);
|
||||||
|
assert.equal(contents, expectedContents);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should save a webpage item with /saveItems", async function () {
|
||||||
|
let collection = await createDataObject('collection');
|
||||||
|
await waitForItemsLoad(win);
|
||||||
|
|
||||||
|
let title = Zotero.Utilities.randomString();
|
||||||
|
let sessionID = Zotero.Utilities.randomString();
|
||||||
|
let payload = {
|
||||||
|
sessionID: sessionID,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
itemType: "newspaperArticle",
|
||||||
|
title: title,
|
||||||
|
creators: [
|
||||||
|
{
|
||||||
|
firstName: "First",
|
||||||
|
lastName: "Last",
|
||||||
|
creatorType: "author"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
title: "Snapshot",
|
||||||
|
url: `${testServerPath}/attachment`,
|
||||||
|
mimeType: "text/html",
|
||||||
|
singleFile: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
uri: "http://example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
let promise = waitForItemEvent('add');
|
||||||
|
let req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveItems",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert.equal(req.status, 201);
|
||||||
|
|
||||||
|
// Check parent item
|
||||||
|
let itemIDs = await promise;
|
||||||
|
assert.lengthOf(itemIDs, 1);
|
||||||
|
let item = Zotero.Items.get(itemIDs[0]);
|
||||||
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'newspaperArticle');
|
||||||
|
assert.isTrue(collection.hasItem(item.id));
|
||||||
|
|
||||||
|
// Promise for attachment save
|
||||||
|
promise = waitForItemEvent('add');
|
||||||
|
|
||||||
|
let testDataDirectory = getTestDataDirectory().path;
|
||||||
|
let indexPath = OS.Path.join(testDataDirectory, 'snapshot', 'index.html');
|
||||||
|
|
||||||
|
let body = new FormData();
|
||||||
|
let uuid = 'binary-' + Zotero.Utilities.randomString();
|
||||||
|
body.append("payload", JSON.stringify(Object.assign(payload, {
|
||||||
|
pageData: {
|
||||||
|
content: await Zotero.File.getContentsAsync(indexPath),
|
||||||
|
resources: {
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
name: "img.gif",
|
||||||
|
content: uuid,
|
||||||
|
binary: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
|
||||||
|
let imagePath = OS.Path.join(testDataDirectory, 'snapshot', 'img.gif');
|
||||||
|
body.append(uuid, await File.createFromFileName(imagePath));
|
||||||
|
|
||||||
|
req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveSingleFile",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert.equal(req.status, 201);
|
||||||
|
|
||||||
|
// Await attachment save
|
||||||
|
let attachmentIDs = await promise;
|
||||||
|
|
||||||
|
// Check attachment
|
||||||
|
assert.lengthOf(attachmentIDs, 1);
|
||||||
|
item = Zotero.Items.get(attachmentIDs[0]);
|
||||||
|
assert.isTrue(item.isImportedAttachment());
|
||||||
|
assert.equal(item.getField('title'), 'Snapshot');
|
||||||
|
|
||||||
|
// Check attachment html file
|
||||||
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item).path;
|
||||||
|
let path = OS.Path.join(attachmentDirectory, 'attachment.html');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
let contents = await Zotero.File.getContentsAsync(path);
|
||||||
|
let expectedContents = await Zotero.File.getContentsAsync(indexPath);
|
||||||
|
assert.equal(contents, expectedContents);
|
||||||
|
|
||||||
|
// Check attachment binary file
|
||||||
|
path = OS.Path.join(attachmentDirectory, 'img.gif');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
contents = await Zotero.File.getBinaryContentsAsync(path);
|
||||||
|
expectedContents = await Zotero.File.getBinaryContentsAsync(imagePath);
|
||||||
|
assert.equal(contents, expectedContents);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("/connector/saveSnapshot", function () {
|
describe("/connector/saveSnapshot", function () {
|
||||||
it("should save a webpage item and snapshot to the current selected collection", function* () {
|
it("should save a webpage item and snapshot to the current selected collection", function* () {
|
||||||
var collection = yield createDataObject('collection');
|
var collection = yield createDataObject('collection');
|
||||||
yield waitForItemsLoad(win);
|
yield waitForItemsLoad(win);
|
||||||
|
|
||||||
// saveSnapshot saves parent and child before returning
|
// saveSnapshot saves parent and child before returning
|
||||||
var ids1, ids2;
|
var ids1, ids2;
|
||||||
var promise = waitForItemEvent('add').then(function (ids) {
|
var promise = waitForItemEvent('add').then(function (ids) {
|
||||||
|
@ -760,6 +979,12 @@ describe("Connector Server", function () {
|
||||||
ids2 = ids;
|
ids2 = ids;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var file = getTestDataDirectory();
|
||||||
|
file.append('snapshot');
|
||||||
|
file.append('index.html');
|
||||||
|
httpd.registerFile("/test", file);
|
||||||
|
|
||||||
yield Zotero.HTTP.request(
|
yield Zotero.HTTP.request(
|
||||||
'POST',
|
'POST',
|
||||||
connectorServerPath + "/connector/saveSnapshot",
|
connectorServerPath + "/connector/saveSnapshot",
|
||||||
|
@ -768,21 +993,21 @@ describe("Connector Server", function () {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: "http://example.com",
|
url: `${testServerPath}/test`,
|
||||||
html: "<html><head><title>Title</title><body>Body</body></html>"
|
html: "<html><head><title>Title</title><body>Body</body></html>"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.isTrue(promise.isFulfilled());
|
assert.isTrue(promise.isFulfilled());
|
||||||
|
|
||||||
// Check parent item
|
// Check parent item
|
||||||
assert.lengthOf(ids1, 1);
|
assert.lengthOf(ids1, 1);
|
||||||
var item = Zotero.Items.get(ids1[0]);
|
var item = Zotero.Items.get(ids1[0]);
|
||||||
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage');
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'webpage');
|
||||||
assert.isTrue(collection.hasItem(item.id));
|
assert.isTrue(collection.hasItem(item.id));
|
||||||
assert.equal(item.getField('title'), 'Title');
|
assert.equal(item.getField('title'), 'Title');
|
||||||
|
|
||||||
// Check attachment
|
// Check attachment
|
||||||
assert.lengthOf(ids2, 1);
|
assert.lengthOf(ids2, 1);
|
||||||
item = Zotero.Items.get(ids2[0]);
|
item = Zotero.Items.get(ids2[0]);
|
||||||
|
@ -1319,6 +1544,568 @@ describe("Connector Server", function () {
|
||||||
assert.equal(item3.libraryID, Zotero.Libraries.userLibraryID);
|
assert.equal(item3.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
assert.equal(item3.numAttachments(), 1);
|
assert.equal(item3.numAttachments(), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should save item saved via /saveSnapshot and /saveSingleFile to another library", async function () {
|
||||||
|
let group = await createGroup({ editable: true, filesEditable: false });
|
||||||
|
await selectLibrary(win);
|
||||||
|
await waitForItemsLoad(win);
|
||||||
|
let sessionID = Zotero.Utilities.randomString();
|
||||||
|
|
||||||
|
// Wait for /saveSnapshot and /saveSingleFile to items
|
||||||
|
let ids1, ids2;
|
||||||
|
let promise = waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids1 = ids;
|
||||||
|
return waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids2 = ids;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let title = Zotero.Utilities.randomString();
|
||||||
|
let payload = {
|
||||||
|
sessionID,
|
||||||
|
url: "http://example.com/test",
|
||||||
|
title,
|
||||||
|
singleFile: true
|
||||||
|
};
|
||||||
|
|
||||||
|
await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveSnapshot",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let body = new FormData();
|
||||||
|
body.append("payload", JSON.stringify(Object.assign(payload, {
|
||||||
|
pageData: {
|
||||||
|
content: '<html><head><title>Title</title><body>Body',
|
||||||
|
resources: {}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
|
||||||
|
let req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveSingleFile",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check an item exists
|
||||||
|
await promise;
|
||||||
|
assert.equal(req.status, 201);
|
||||||
|
let item1 = Zotero.Items.get(ids1[0]);
|
||||||
|
assert.equal(item1.numAttachments(), 1);
|
||||||
|
|
||||||
|
// Check attachment item
|
||||||
|
let item2 = Zotero.Items.get(ids2[0]);
|
||||||
|
assert.equal(item2.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
|
assert.equal(item2.parentItemID, item1.id);
|
||||||
|
|
||||||
|
// Move item to group without file attachment
|
||||||
|
promise = waitForItemEvent('add');
|
||||||
|
req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/updateSession",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionID,
|
||||||
|
target: group.treeViewID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Old items are gone
|
||||||
|
let ids3 = await promise;
|
||||||
|
assert.equal(req.status, 200);
|
||||||
|
assert.isFalse(Zotero.Items.exists(item2.id));
|
||||||
|
assert.isFalse(Zotero.Items.exists(item1.id));
|
||||||
|
|
||||||
|
// New item exists
|
||||||
|
let item3 = Zotero.Items.get(ids3[0]);
|
||||||
|
assert.equal(item3.libraryID, group.libraryID);
|
||||||
|
assert.equal(item3.numAttachments(), 0);
|
||||||
|
|
||||||
|
// Move back to My Library and resave attachment
|
||||||
|
let ids4, ids5;
|
||||||
|
promise = waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids4 = ids;
|
||||||
|
return waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids5 = ids;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/updateSession",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionID,
|
||||||
|
target: Zotero.Libraries.userLibrary.treeViewID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
let item4 = Zotero.Items.get(ids4[0]);
|
||||||
|
let item5 = Zotero.Items.get(ids5[0]);
|
||||||
|
|
||||||
|
// Check item
|
||||||
|
assert.equal(req.status, 200);
|
||||||
|
assert.isFalse(Zotero.Items.exists(item3.id));
|
||||||
|
assert.equal(item4.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
|
assert.equal(item5.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
|
assert.equal(item4.numAttachments(), 1);
|
||||||
|
|
||||||
|
// Check attachment html file
|
||||||
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item5).path;
|
||||||
|
let path = OS.Path.join(attachmentDirectory, 'test.html');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
let contents = await Zotero.File.getContentsAsync(path);
|
||||||
|
assert.equal(contents, '<html><head><title>Title</title><body>Body');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resave item saved via /saveSnapshot and /saveSingleFile when moved to filesEditable library", async function () {
|
||||||
|
let group = await createGroup({ editable: true, filesEditable: false });
|
||||||
|
await selectLibrary(win);
|
||||||
|
await waitForItemsLoad(win);
|
||||||
|
let sessionID = Zotero.Utilities.randomString();
|
||||||
|
|
||||||
|
// Wait for /saveSnapshot to save parent item
|
||||||
|
let promise = waitForItemEvent('add');
|
||||||
|
|
||||||
|
let title = Zotero.Utilities.randomString();
|
||||||
|
let payload = {
|
||||||
|
sessionID,
|
||||||
|
url: "http://example.com/test",
|
||||||
|
title,
|
||||||
|
singleFile: true
|
||||||
|
};
|
||||||
|
|
||||||
|
await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveSnapshot",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check an item exists
|
||||||
|
let ids1 = await promise;
|
||||||
|
let item1 = Zotero.Items.get(ids1[0]);
|
||||||
|
|
||||||
|
// Move item to group without file attachment
|
||||||
|
promise = waitForItemEvent('add');
|
||||||
|
let reqPromise = Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/updateSession",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionID,
|
||||||
|
target: group.treeViewID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let req = await reqPromise;
|
||||||
|
assert.equal(req.status, 200);
|
||||||
|
// Assert original item no longer exists
|
||||||
|
assert.isFalse(Zotero.Items.exists(item1.id));
|
||||||
|
|
||||||
|
// Get new item
|
||||||
|
let ids2 = await promise;
|
||||||
|
let item2 = Zotero.Items.get(ids2[0]);
|
||||||
|
assert.equal(item2.libraryID, group.libraryID);
|
||||||
|
assert.equal(item2.numAttachments(), 0);
|
||||||
|
|
||||||
|
let body = new FormData();
|
||||||
|
body.append("payload", JSON.stringify(Object.assign(payload, {
|
||||||
|
pageData: {
|
||||||
|
content: '<html><head><title>Title</title><body>Body',
|
||||||
|
resources: {}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
|
||||||
|
req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveSingleFile",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check the attachment was not saved
|
||||||
|
assert.equal(req.status, 200);
|
||||||
|
assert.equal(item2.numAttachments(), 0);
|
||||||
|
|
||||||
|
// Move back to My Library and resave attachment
|
||||||
|
let ids3, ids4;
|
||||||
|
promise = waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids3 = ids;
|
||||||
|
return waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids4 = ids;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/updateSession",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionID,
|
||||||
|
target: Zotero.Libraries.userLibrary.treeViewID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for item add and then attachment add
|
||||||
|
await promise;
|
||||||
|
let item3 = Zotero.Items.get(ids3[0]);
|
||||||
|
let item4 = Zotero.Items.get(ids4[0]);
|
||||||
|
|
||||||
|
// Check item
|
||||||
|
assert.equal(req.status, 200);
|
||||||
|
assert.isFalse(Zotero.Items.exists(item2.id));
|
||||||
|
assert.equal(item3.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
|
assert.equal(item3.numAttachments(), 1);
|
||||||
|
|
||||||
|
// Check attachment
|
||||||
|
assert.equal(item4.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
|
assert.equal(item4.parentItemID, item3.id);
|
||||||
|
// Check attachment html file
|
||||||
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item4).path;
|
||||||
|
let path = OS.Path.join(attachmentDirectory, 'test.html');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
let contents = await Zotero.File.getContentsAsync(path);
|
||||||
|
assert.equal(contents, '<html><head><title>Title</title><body>Body');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should save item saved via /saveItems and /saveSingleFile to another library", async function () {
|
||||||
|
let group = await createGroup({ editable: true, filesEditable: false });
|
||||||
|
await selectLibrary(win);
|
||||||
|
await waitForItemsLoad(win);
|
||||||
|
let sessionID = Zotero.Utilities.randomString();
|
||||||
|
|
||||||
|
// Wait for /saveItems and /saveSingleFile to items
|
||||||
|
let ids1, ids2;
|
||||||
|
let promise = waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids1 = ids;
|
||||||
|
return waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids2 = ids;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let title = Zotero.Utilities.randomString();
|
||||||
|
let payload = {
|
||||||
|
sessionID: sessionID,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
itemType: "newspaperArticle",
|
||||||
|
title: title,
|
||||||
|
creators: [
|
||||||
|
{
|
||||||
|
firstName: "First",
|
||||||
|
lastName: "Last",
|
||||||
|
creatorType: "author"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
title: "Snapshot",
|
||||||
|
url: `https://example.com/attachment`,
|
||||||
|
mimeType: "text/html",
|
||||||
|
singleFile: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
uri: "http://example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveItems",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let body = new FormData();
|
||||||
|
body.append("payload", JSON.stringify(Object.assign(payload, {
|
||||||
|
pageData: {
|
||||||
|
content: '<html><head><title>Title</title><body>Body',
|
||||||
|
resources: {}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
|
||||||
|
let req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveSingleFile",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check an item exists
|
||||||
|
await promise;
|
||||||
|
assert.equal(req.status, 201);
|
||||||
|
let item1 = Zotero.Items.get(ids1[0]);
|
||||||
|
assert.equal(item1.numAttachments(), 1);
|
||||||
|
|
||||||
|
// Check attachment item
|
||||||
|
let item2 = Zotero.Items.get(ids2[0]);
|
||||||
|
assert.equal(item2.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
|
assert.equal(item2.parentItemID, item1.id);
|
||||||
|
|
||||||
|
// Move item to group without file attachment
|
||||||
|
promise = waitForItemEvent('add');
|
||||||
|
req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/updateSession",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionID,
|
||||||
|
target: group.treeViewID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Old items are gone
|
||||||
|
let ids3 = await promise;
|
||||||
|
assert.equal(req.status, 200);
|
||||||
|
assert.isFalse(Zotero.Items.exists(item2.id));
|
||||||
|
assert.isFalse(Zotero.Items.exists(item1.id));
|
||||||
|
|
||||||
|
// New item exists
|
||||||
|
let item3 = Zotero.Items.get(ids3[0]);
|
||||||
|
assert.equal(item3.libraryID, group.libraryID);
|
||||||
|
assert.equal(item3.numAttachments(), 0);
|
||||||
|
|
||||||
|
// Move back to My Library and resave attachment
|
||||||
|
let ids4, ids5;
|
||||||
|
promise = waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids4 = ids;
|
||||||
|
return waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids5 = ids;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/updateSession",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionID,
|
||||||
|
target: Zotero.Libraries.userLibrary.treeViewID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
let item4 = Zotero.Items.get(ids4[0]);
|
||||||
|
let item5 = Zotero.Items.get(ids5[0]);
|
||||||
|
|
||||||
|
// Check item
|
||||||
|
assert.equal(req.status, 200);
|
||||||
|
assert.isFalse(Zotero.Items.exists(item3.id));
|
||||||
|
assert.equal(item4.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
|
assert.equal(item5.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
|
assert.equal(item4.numAttachments(), 1);
|
||||||
|
|
||||||
|
// Check attachment html file
|
||||||
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item5).path;
|
||||||
|
let path = OS.Path.join(attachmentDirectory, 'attachment.html');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
let contents = await Zotero.File.getContentsAsync(path);
|
||||||
|
assert.equal(contents, '<html><head><title>Title</title><body>Body');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should save item saved via /saveItems and /saveSingleFile when moved to filesEditable library", async function () {
|
||||||
|
let group = await createGroup({ editable: true, filesEditable: false });
|
||||||
|
await selectLibrary(win);
|
||||||
|
await waitForItemsLoad(win);
|
||||||
|
let sessionID = Zotero.Utilities.randomString();
|
||||||
|
|
||||||
|
// Wait for /saveItems to save parent item
|
||||||
|
let promise = waitForItemEvent('add');
|
||||||
|
|
||||||
|
let title = Zotero.Utilities.randomString();
|
||||||
|
let payload = {
|
||||||
|
sessionID: sessionID,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
itemType: "newspaperArticle",
|
||||||
|
title: title,
|
||||||
|
creators: [
|
||||||
|
{
|
||||||
|
firstName: "First",
|
||||||
|
lastName: "Last",
|
||||||
|
creatorType: "author"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
title: "Snapshot",
|
||||||
|
url: `https://example.com/attachment`,
|
||||||
|
mimeType: "text/html",
|
||||||
|
singleFile: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
uri: "http://example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveItems",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check an item exists
|
||||||
|
let ids1 = await promise;
|
||||||
|
let item1 = Zotero.Items.get(ids1[0]);
|
||||||
|
|
||||||
|
// Move item to group without file attachment
|
||||||
|
promise = waitForItemEvent('add');
|
||||||
|
let reqPromise = Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/updateSession",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionID,
|
||||||
|
target: group.treeViewID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let req = await reqPromise;
|
||||||
|
assert.equal(req.status, 200);
|
||||||
|
// Assert original item no longer exists
|
||||||
|
assert.isFalse(Zotero.Items.exists(item1.id));
|
||||||
|
|
||||||
|
// Get new item
|
||||||
|
let ids2 = await promise;
|
||||||
|
let item2 = Zotero.Items.get(ids2[0]);
|
||||||
|
assert.equal(item2.libraryID, group.libraryID);
|
||||||
|
assert.equal(item2.numAttachments(), 0);
|
||||||
|
|
||||||
|
let body = new FormData();
|
||||||
|
body.append("payload", JSON.stringify(Object.assign(payload, {
|
||||||
|
pageData: {
|
||||||
|
content: '<html><head><title>Title</title><body>Body',
|
||||||
|
resources: {}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
|
||||||
|
req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/saveSingleFile",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
"zotero-allowed-request": "true"
|
||||||
|
},
|
||||||
|
body
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check the attachment was not saved
|
||||||
|
assert.equal(req.status, 200);
|
||||||
|
assert.equal(item2.numAttachments(), 0);
|
||||||
|
|
||||||
|
// Move back to My Library and resave attachment
|
||||||
|
let ids3, ids4;
|
||||||
|
promise = waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids3 = ids;
|
||||||
|
return waitForItemEvent('add').then(function (ids) {
|
||||||
|
ids4 = ids;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req = await Zotero.HTTP.request(
|
||||||
|
'POST',
|
||||||
|
connectorServerPath + "/connector/updateSession",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionID,
|
||||||
|
target: Zotero.Libraries.userLibrary.treeViewID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for item add and then attachment add
|
||||||
|
await promise;
|
||||||
|
let item3 = Zotero.Items.get(ids3[0]);
|
||||||
|
let item4 = Zotero.Items.get(ids4[0]);
|
||||||
|
|
||||||
|
// Check item
|
||||||
|
assert.equal(req.status, 200);
|
||||||
|
assert.isFalse(Zotero.Items.exists(item2.id));
|
||||||
|
assert.equal(item3.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
|
assert.equal(item3.numAttachments(), 1);
|
||||||
|
|
||||||
|
// Check attachment
|
||||||
|
assert.equal(item4.libraryID, Zotero.Libraries.userLibraryID);
|
||||||
|
assert.equal(item4.parentItemID, item3.id);
|
||||||
|
// Check attachment html file
|
||||||
|
let attachmentDirectory = Zotero.Attachments.getStorageDirectory(item4).path;
|
||||||
|
let path = OS.Path.join(attachmentDirectory, 'attachment.html');
|
||||||
|
assert.isTrue(await OS.File.exists(path));
|
||||||
|
let contents = await Zotero.File.getContentsAsync(path);
|
||||||
|
assert.equal(contents, '<html><head><title>Title</title><body>Body');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('/connector/installStyle', function() {
|
describe('/connector/installStyle', function() {
|
||||||
|
|
|
@ -59,6 +59,15 @@ describe("Zotero.Utilities.Internal", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("#decodeUTF8()", function () {
|
||||||
|
it("should properly decode binary string", async function () {
|
||||||
|
let text = String.fromCharCode.apply(null, new Uint8Array([226, 130, 172]));
|
||||||
|
let utf8 = Zotero.Utilities.Internal.decodeUTF8(text);
|
||||||
|
assert.equal(utf8, "€");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("#delayGenerator", function () {
|
describe("#delayGenerator", function () {
|
||||||
var spy;
|
var spy;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue