Server-side translation and attachment upload, part 1

This commit is contained in:
Simon Kornblith 2012-04-09 00:39:55 -04:00
parent 7452e14090
commit 6fa4f8d02b
10 changed files with 710 additions and 70 deletions

@ -1 +1 @@
Subproject commit 843dcc3b3f1f16ab317a9551be7446e098a9a7ee
Subproject commit 73f3050d7c66caeb338a54c53d79c1b4be0ea8aa

View file

@ -78,11 +78,10 @@ Zotero.Connector_Types = new function() {
this.getImageSrc = function(idOrName) {
var itemType = Zotero.Connector_Types["itemTypes"][idOrName];
if(!itemType) return false;
var icon = itemType[6]/* icon */;
var icon = itemType ? itemType[6]/* icon */ : "treeitem-"+idOrName+".png";
if(Zotero.isBookmarklet) {
return ZOTERO_CONFIG.BOOKMARKLET_URL+"icons/"+icon;
return ZOTERO_CONFIG.BOOKMARKLET_URL+"images/"+icon;
} else if(Zotero.isFx) {
return "chrome://zotero/skin/"+icon;
} else if(Zotero.isChrome) {

View file

@ -27,8 +27,7 @@ Zotero.Connector = new function() {
const CONNECTOR_URI = "http://127.0.0.1:23119/";
const CONNECTOR_API_VERSION = 2;
var _ieStandaloneIframeTarget;
var _ieConnectorCallbacks;
var _ieStandaloneIframeTarget, _ieConnectorCallbacks;
this.isOnline = null;
/**
@ -67,16 +66,26 @@ Zotero.Connector = new function() {
Zotero.debug("Connector: Standalone found; trying IE hack");
_ieConnectorCallbacks = [];
Zotero.Messaging.addMessageListener("standaloneLoaded", function(data, event) {
var listener = function(event) {
if(event.origin !== "http://127.0.0.1:23119") return;
event.stopPropagation();
Zotero.debug("Connector: Standalone loaded");
_ieStandaloneIframeTarget = iframe.contentWindow;
callback(true);
});
Zotero.Messaging.addMessageListener("connectorResponse", function(data, event) {
if(event.origin !== "http://127.0.0.1:23119") return;
// If this is the first time the target was loaded, then this is a loaded
// event
if(!_ieStandaloneIframeTarget) {
Zotero.debug("Connector: Standalone loaded");
_ieStandaloneIframeTarget = iframe.contentWindow;
callback(true);
return;
}
// Otherwise, this is a response event
try {
var data = JSON.parse(event.data);
} catch(e) {
Zotero.debug("Invalid JSON received: "+event.data);
return;
}
var xhrSurrogate = {
"status":data[1],
"responseText":data[2],
@ -84,7 +93,13 @@ Zotero.Connector = new function() {
};
_ieConnectorCallbacks[data[0]](xhrSurrogate);
delete _ieConnectorCallbacks[data[0]];
});
};
if(window.addEventListener) {
window.addEventListener("message", listener, false);
} else {
window.attachEvent("onmessage", function() { listener(event); });
}
var iframe = document.createElement("iframe");
iframe.src = "http://127.0.0.1:23119/connector/ieHack";
@ -169,10 +184,10 @@ Zotero.Connector = new function() {
};
if(Zotero.isIE) { // IE requires XDR for CORS
if(_ieStandaloneIframeTarget !== undefined) {
if(_ieStandaloneIframeTarget) {
var requestID = Zotero.Utilities.randomString();
_ieConnectorCallbacks[requestID] = newCallback;
_ieStandaloneIframeTarget.postMessage("ZOTERO_MSG "+JSON.stringify([null, "connectorRequest",
_ieStandaloneIframeTarget.postMessage(JSON.stringify([null, "connectorRequest",
[requestID, method, JSON.stringify(data)]]), "http://127.0.0.1:23119/connector/ieHack");
} else {
Zotero.debug("Connector: No iframe target; not sending to Standalone");

View file

@ -32,7 +32,26 @@ Zotero.Translate.ItemSaver = function(libraryID, attachmentMode, forceTagType, d
this._uri = document.location.toString();
this._cookie = document.cookie;
}
// Add listener for callbacks
if(!Zotero.Translate.ItemSaver._attachmentCallbackListenerAdded) {
Zotero.Messaging.addMessageListener("attachmentCallback", function(data) {
var id = data[0],
status = data[1];
var callback = Zotero.Translate.ItemSaver._attachmentCallbacks[id];
if(callback) {
if(status === false || status === 100) {
delete Zotero.Translate.ItemSaver._attachmentCallbacks[id];
}
data[1] = 50+data[1]/2;
callback(data[1], data[2]);
}
});
Zotero.Translate.ItemSaver._attachmentCallbackListenerAdded = true;
}
}
Zotero.Translate.ItemSaver._attachmentCallbackListenerAdded = false;
Zotero.Translate.ItemSaver._attachmentCallbacks = {};
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE = 0;
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD = 1;
@ -41,8 +60,16 @@ Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2;
Zotero.Translate.ItemSaver.prototype = {
/**
* Saves items to Standalone or the server
* @param items Items in Zotero.Item.toArray() format
* @param {Function} callback A callback to be executed when saving is complete. If saving
* succeeded, this callback will be passed true as the first argument and a list of items
* saved as the second. If saving failed, the callback will be passed false as the first
* argument and an error object as the second
* @param {Function} [attachmentCallback] A callback that receives information about attachment
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
*/
"saveItems":function(items, callback) {
"saveItems":function(items, callback, attachmentCallback) {
var me = this;
// first try to save items via connector
var payload = {"items":items};
@ -58,30 +85,453 @@ Zotero.Translate.ItemSaver.prototype = {
} else if(Zotero.isFx) {
callback(false, new Error("Save via Standalone failed with "+status));
} else {
me._saveToServer(items, callback);
me._saveToServer(items, callback, attachmentCallback);
}
});
},
/**
* Saves items to server
* @param items Items in Zotero.Item.toArray() format
* @param {Function} callback A callback to be executed when saving is complete. If saving
* succeeded, this callback will be passed true as the first argument and a list of items
* saved as the second. If saving failed, the callback will be passed false as the first
* argument and an error object as the second
* @param {Function} attachmentCallback A callback that receives information about attachment
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
*/
"_saveToServer":function(items, callback) {
var newItems = [];
"_saveToServer":function(items, callback, attachmentCallback) {
var newItems = [], typedArraysSupported = false;
try {
typedArraysSupported = new Uint8Array(1);
} catch(e) {}
for(var i=0, n=items.length; i<n; i++) {
newItems.push(Zotero.Utilities.itemToServerJSON(items[i]));
var item = items[i];
newItems.push(Zotero.Utilities.itemToServerJSON(item));
if(typedArraysSupported) {
// Get rid of attachments that we won't be able to save properly and add ids
for(var j=0; j<item.attachments.length; j++) {
if(!item.attachments[j].url || item.attachments[j].mimeType === "text/html") {
item.attachments.splice(j, 1);
} else {
item.attachments[j].id = Zotero.Utilities.randomString();
}
}
} else {
item.attachments = [];
}
}
var url = 'users/%%USERID%%/items';
var payload = JSON.stringify({"items":newItems}, null, "\t")
Zotero.OAuth.doAuthenticatedPost(url, payload, function(status) {
if(!status) {
var me = this;
Zotero.OAuth.createItem({"items":newItems}, null, function(statusCode, response) {
if(statusCode !== 201) {
callback(false, new Error("Save to server failed"));
} else {
Zotero.debug("Translate: Save to server complete");
callback(true, newItems);
callback(true, items);
if(typedArraysSupported) {
try {
var newKeys = me._getItemKeysFromServerResponse(response);
} catch(e) {
callback(false, e);
return;
}
for(var i=0; i<items.length; i++) {
var item = items[i], key = newKeys[i];
if(item.attachments && item.attachments.length) {
me._saveAttachmentsToServer(key, me._getFileBaseNameFromItem(item),
item.attachments, attachmentCallback);
}
}
}
}
}, true);
}
});
},
/**
* Saves an attachment to server
* @param {String} itemKey The key of the parent item
* @param {String} baseName A string to use as the base name for attachments
* @param {Object[]} attachments An array of attachment objects
* @param {Function} attachmentCallback A callback that receives information about attachment
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
*/
"_saveAttachmentsToServer":function(itemKey, baseName, attachments, attachmentCallback) {
var me = this,
uploadAttachments = [],
retrieveHeadersForAttachments = attachments.length;
/**
* Creates attachments on the z.org server. This is executed after we have received
* headers for all attachments to be downloaded, but before they are uploaded to
* z.org.
* @inner
*/
var createAttachments = function() {
var attachmentPayload = [];
for(var i=0; i<uploadAttachments.length; i++) {
var attachment = uploadAttachments[i];
attachmentPayload.push({
"itemType":"attachment",
"linkMode":attachment.linkMode,
"title":(attachment.title ? attachment.title.toString() : "Untitled Attachment"),
"accessDate":"CURRENT_TIMESTAMP",
"url":attachment.url,
"note":(attachment.note ? attachment.note.toString() : ""),
"tags":(attachment.tags && attachment.tags instanceof Array ? attachment.tags : [])
});
}
Zotero.OAuth.createItem({"items":attachmentPayload}, itemKey, function(statusCode, response) {
var err;
if(statusCode === 201) {
try {
var newKeys = me._getItemKeysFromServerResponse(response);
} catch(e) {
err = new Error("Unexpected response received from server");
}
} else {
err = new Error("Unexpected status "+statusCode+" received from server");
}
for(var i=0; i<uploadAttachments.length; i++) {
var attachment = uploadAttachments[i];
if(err) {
attachmentProgress(attachment, false, err);
} else {
attachment.key = newKeys[i];
Zotero.debug("Finished creating item");
if(attachment.linkMode === "linked_url") {
attachmentCallback(attachment, 100);
} else if("data" in attachment) {
me._uploadAttachmentToServer(attachment, attachmentCallback);
}
}
}
if(err) throw err;
});
};
for(var i=0; i<attachments.length; i++) {
// Also begin to download attachments
(function(attachment) {
var headersValidated = null;
// Ensure these are undefined before continuing, since we'll use them to determine
// whether an attachment has been created on the Zotero server and downloaded from
// the host
delete attachment.key;
delete attachment.data;
/**
* Checks headers to ensure that they reflect our expectations. When headers have
* been checked for all attachments, creates new items on the z.org server and
* begins uploading them.
* @inner
*/
var checkHeaders = function() {
if(headersValidated !== null) return headersValidated;
retrieveHeadersForAttachments--;
headersValidated = false;
var err = null,
status = xhr.status;
// Validate status
if(status === 0) {
// Probably failed due to SOP
attachmentCallback(attachment, 50);
attachment.linkMode = "linked_url";
} else if(status !== 200) {
err = new Error("Server returned unexpected status code "+status);
} else {
// Validate content type
var contentType = "application/octet-stream",
charset = null,
contentTypeHeader = xhr.getResponseHeader("Content-Type");
if(contentTypeHeader) {
// See RFC 2616 sec 3.7
var m = /^[^\x00-\x1F\x7F()<>@,;:\\"\/\[\]?={} ]+\/[^\x00-\x1F\x7F()<>@,;:\\"\/\[\]?={} ]+/.exec(contentTypeHeader);
if(m) contentType = m[0].toLowerCase();
m = /;\s*charset\s*=\s*("[^"]+"|[^\x00-\x1F\x7F()<>@,;:\\"\/\[\]?={} ]+)/.exec(contentTypeHeader);
if(m) {
charset = m[1];
if(charset[0] === '"') charset = charset.substring(1, charset.length-1);
}
if(attachment.mimeType
&& attachment.mimeType.toLowerCase() !== contentType.toLowerCase()) {
err = new Error("Attachment MIME type "+contentType+
" does not match specified type "+attachment.mimeType);
}
}
attachment.mimeType = contentType;
attachment.linkMode = "imported_url";
switch(contentType.toLowerCase()) {
case "application/pdf":
attachment.filename = baseName+".pdf";
break;
case "text/html":
case "application/xhtml+xml":
attachment.filename = baseName+".html";
break;
default:
attachment.filename = baseName;
}
if(charset) attachment.charset = charset;
headersValidated = true;
}
// If we didn't validate the headers, cancel the request
if(headersValidated === false && "abort" in xhr) xhr.abort();
// Add attachments to attachment payload if there was no error
if(!err) {
uploadAttachments.push(attachment);
}
// If we have retrieved the headers for all attachments, create items on z.org
// server
if(retrieveHeadersForAttachments === 0) createAttachments();
// If there was an error, throw it now
if(err) {
attachmentCallback(attachment, false, err);
throw err;
}
};
var xhr = new XMLHttpRequest();
xhr.open("GET", attachment.url, true);
xhr.responseType = "arraybuffer";
xhr.onloadend = function() {
if(!checkHeaders()) return;
attachmentCallback(attachment, 50);
attachment.data = xhr.response;
// If item already created, head to upload
if("key" in attachment) {
me._uploadAttachmentToServer(attachment, attachmentCallback);
}
};
xhr.onprogress = function(event) {
if(this.readyState < 2 || !checkHeaders()) return;
if(event.total && attachmentCallback) {
attachmentCallback(attachment, event.loaded/event.total*50);
}
};
xhr.send();
if(attachmentCallback) {
attachmentCallback(attachment, 0);
}
})(attachments[i]);
}
},
/**
* Uploads an attachment to the Zotero server
* @param {Object} attachment Attachment object, including
* @param {Function} attachmentCallback A callback that receives information about attachment
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
*/
"_uploadAttachmentToServer":function(attachment, attachmentCallback) {
var binaryHash = this._md5(new Uint8Array(attachment.data), 0, attachment.data.byteLength),
hash = "";
for(var i=0; i<binaryHash.length; i++) {
if(binaryHash[i] < 16) hash += "0";
hash += binaryHash[i].toString(16);
}
attachment.md5 = hash;
Zotero.Translate.ItemSaver._attachmentCallbacks[attachment.id] = function(status, error) {
attachmentCallback(attachment, status, error);
};
Zotero.OAuth.uploadAttachment(attachment);
},
/**
* Gets item keys from a server response
* @param {String} response ATOM response
*/
"_getItemKeysFromServerResponse":function(response) {
try {
response = (new DOMParser()).parseFromString(response, "text/xml");
} catch(e) {
throw new Error("Save to server returned invalid output");
}
var keyNodes = response.getElementsByTagNameNS("http://zotero.org/ns/api", "key");
var newKeys = [];
for(var i=0, n=keyNodes.length; i<n; i++) {
newKeys.push("textContent" in keyNodes[i] ? keyNodes[i].textContent
: keyNodes[i].innerText);
}
return newKeys;
},
/**
* Gets the base name for an attachment from an item object. This mimics the default behavior
* of Zotero.Attachments.getFileBaseNameFromItem
* @param {Object} item
*/
"_getFileBaseNameFromItem":function(item) {
var parts = [];
if(item.creators && item.creators.length) {
if(item.creators.length === 1) {
parts.push(item.creators[0].lastName);
} else if(item.creators.length === 2) {
parts.push(item.creators[0].lastName+" and "+item.creators[1].lastName);
} else {
parts.push(item.creators[0].lastName+" et al.");
}
}
if(item.date) {
var date = Zotero.Date.strToDate(item.date);
if(date.year) parts.push(date.year);
}
if(item.title) {
parts.push(item.title.substr(0, 50));
}
if(parts.length) return parts.join(" - ");
return "Attachment";
},
/*
pdf.js MD5 implementation
Copyright (c) 2011 Mozilla Foundation
Contributors: Andreas Gal <gal@mozilla.com>
Chris G Jones <cjones@mozilla.com>
Shaon Barman <shaon.barman@gmail.com>
Vivien Nicolas <21@vingtetun.org>
Justin D'Arcangelo <justindarc@gmail.com>
Yury Delendik
Kalervo Kujala
Adil Allawi <@ironymark>
Jakob Miland <saebekassebil@gmail.com>
Artur Adib <aadib@mozilla.com>
Brendan Dahl <bdahl@mozilla.com>
David Quintana <gigaherz@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
"_md5":(function calculateMD5Closure() {
// Don't throw if typed arrays are not supported
try {
var r = new Uint8Array([
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]);
var k = new Int32Array([
-680876936, -389564586, 606105819, -1044525330, -176418897, 1200080426,
-1473231341, -45705983, 1770035416, -1958414417, -42063, -1990404162,
1804603682, -40341101, -1502002290, 1236535329, -165796510, -1069501632,
643717713, -373897302, -701558691, 38016083, -660478335, -405537848,
568446438, -1019803690, -187363961, 1163531501, -1444681467, -51403784,
1735328473, -1926607734, -378558, -2022574463, 1839030562, -35309556,
-1530992060, 1272893353, -155497632, -1094730640, 681279174, -358537222,
-722521979, 76029189, -640364487, -421815835, 530742520, -995338651,
-198630844, 1126891415, -1416354905, -57434055, 1700485571, -1894986606,
-1051523, -2054922799, 1873313359, -30611744, -1560198380, 1309151649,
-145523070, -1120210379, 718787259, -343485551]);
} catch(e) {};
function hash(data, offset, length) {
var h0 = 1732584193, h1 = -271733879, h2 = -1732584194, h3 = 271733878;
// pre-processing
var paddedLength = (length + 72) & ~63; // data + 9 extra bytes
var padded = new Uint8Array(paddedLength);
var i, j, n;
if (offset || length != data.byteLength) {
padded.set(new Uint8Array(data.buffer, offset, length));
} else {
padded.set(data);
}
i = length;
padded[i++] = 0x80;
n = paddedLength - 8;
while (i < n)
padded[i++] = 0;
padded[i++] = (length << 3) & 0xFF;
padded[i++] = (length >> 5) & 0xFF;
padded[i++] = (length >> 13) & 0xFF;
padded[i++] = (length >> 21) & 0xFF;
padded[i++] = (length >>> 29) & 0xFF;
padded[i++] = 0;
padded[i++] = 0;
padded[i++] = 0;
// chunking
// TODO ArrayBuffer ?
var w = new Int32Array(16);
for (i = 0; i < paddedLength;) {
for (j = 0; j < 16; ++j, i += 4) {
w[j] = (padded[i] | (padded[i + 1] << 8) |
(padded[i + 2] << 16) | (padded[i + 3] << 24));
}
var a = h0, b = h1, c = h2, d = h3, f, g;
for (j = 0; j < 64; ++j) {
if (j < 16) {
f = (b & c) | ((~b) & d);
g = j;
} else if (j < 32) {
f = (d & b) | ((~d) & c);
g = (5 * j + 1) & 15;
} else if (j < 48) {
f = b ^ c ^ d;
g = (3 * j + 5) & 15;
} else {
f = c ^ (b | (~d));
g = (7 * j) & 15;
}
var tmp = d, rotateArg = (a + f + k[j] + w[g]) | 0, rotate = r[j];
d = c;
c = b;
b = (b + ((rotateArg << rotate) | (rotateArg >>> (32 - rotate)))) | 0;
a = tmp;
}
h0 = (h0 + a) | 0;
h1 = (h1 + b) | 0;
h2 = (h2 + c) | 0;
h3 = (h3 + d) | 0;
}
return new Uint8Array([
h0 & 0xFF, (h0 >> 8) & 0xFF, (h0 >> 16) & 0xFF, (h0 >>> 24) & 0xFF,
h1 & 0xFF, (h1 >> 8) & 0xFF, (h1 >> 16) & 0xFF, (h1 >>> 24) & 0xFF,
h2 & 0xFF, (h2 >> 8) & 0xFF, (h2 >> 16) & 0xFF, (h2 >>> 24) & 0xFF,
h3 & 0xFF, (h3 >> 8) & 0xFF, (h3 >> 16) & 0xFF, (h3 >>> 24) & 0xFF
]);
}
return hash;
})()
};

View file

@ -90,6 +90,7 @@ Zotero.Translate.Sandbox = {
const allowedObjects = ["complete", "attachments", "seeAlso", "creators", "tags", "notes"];
delete item.complete;
for(var i in item) {
var val = item[i];
var type = typeof val;
@ -99,7 +100,7 @@ Zotero.Translate.Sandbox = {
} else if(type === "string") {
// trim strings
item[i] = val.trim();
} else if((type === "object" || type === "xml") && allowedObjects.indexOf(i) === -1) {
} else if((type === "object" || type === "xml" || type === "function") && allowedObjects.indexOf(i) === -1) {
// convert things that shouldn't be objecst to objects
translate._debug("Translate: WARNING: typeof "+i+" is "+type+"; converting to string");
item[i] = val.toString();
@ -144,6 +145,8 @@ Zotero.Translate.Sandbox = {
translate.complete(false, data);
throw data;
}
}, function(arg1, arg2, arg3) {
translate._attachmentProgress(arg1, arg2, arg3);
});
translate._runHandler("itemSaving", item);
@ -985,6 +988,7 @@ Zotero.Translate.Base.prototype = {
this._libraryID = libraryID;
this._saveAttachments = saveAttachments === undefined || saveAttachments;
this._attachmentsSaving = [];
var me = this;
if(typeof this.translator[0] === "object") {
@ -1035,30 +1039,6 @@ Zotero.Translate.Base.prototype = {
this.decrementAsyncProcesses("Zotero.Translate#translate()");
},
/**
* Executed when items have been saved (which may happen asynchronously, if in connector)
*
* @param {Boolean} returnValue Whether saving was successful
* @param {Zotero.Item[]|Error} data If returnValue is true, this will be an array of
* Zotero.Item objects. If returnValue is false, this will
* be a string error message.
*/
"itemsSaved":function(returnValue, data) {
if(returnValue) {
// trigger deferred itemDone events
var nItems = data.length;
for(var i=0; i<nItems; i++) {
this._runHandler("itemDone", data[i], this.saveQueue[i]);
}
this.saveQueue = [];
} else {
Zotero.logError(data);
}
this._runHandler("done", returnValue);
},
/**
* Return the progress of the import operation, or null if progress cannot be determined
*/
@ -1078,9 +1058,14 @@ Zotero.Translate.Base.prototype = {
// Make sure this isn't called twice
if(this._currentState === null) {
var e = new Error();
Zotero.debug("Translate: WARNING: Zotero.done() called after translation completion. This should never happen. Please examine the stack below.");
Zotero.debug(e.stack);
if(!returnValue) {
Zotero.debug("Translate: WARNING: Zotero.done() called after translator completion with error");
Zotero.debug(error);
} else {
var e = new Error();
Zotero.debug("Translate: WARNING: Zotero.done() called after translation completion. This should never happen. Please examine the stack below.");
Zotero.debug(e.stack);
}
return;
}
var oldState = this._currentState;
@ -1102,8 +1087,19 @@ Zotero.Translate.Base.prototype = {
var lastProperToProxyFunction = this._properToProxyFunctions ? this._properToProxyFunctions.shift() : null;
if(returnValue) {
var dupeTranslator = {"itemType":returnValue, "properToProxy":lastProperToProxyFunction};
var dupeTranslator = {"properToProxy":lastProperToProxyFunction};
for(var i in lastTranslator) dupeTranslator[i] = lastTranslator[i];
if(Zotero.isBookmarklet && returnValue === "server") {
// In the bookmarklet, the return value from detectWeb can be "server" to
// indicate the translator should be run on the Zotero server
dupeTranslator.runMode = Zotero.Translator.RUN_MODE_ZOTERO_SERVER;
} else {
// Usually the return value from detectWeb will be either an item type or
// the string "multiple"
dupeTranslator.itemType = returnValue;
}
this._foundTranslators.push(dupeTranslator);
} else if(error) {
this._debug("Detect using "+lastTranslator.label+" failed: \n"+errorString, 2);
@ -1127,7 +1123,8 @@ Zotero.Translate.Base.prototype = {
if(this.saveQueue.length) {
var me = this;
this._itemSaver.saveItems(this.saveQueue.slice(),
function(returnValue, data) { me.itemsSaved(returnValue, data) });
function(returnValue, data) { me._itemsSaved(returnValue, data); },
function(arg1, arg2, arg3) { me._attachmentProgress(arg1, arg2, arg3); });
return;
} else {
this._debug("Translation successful");
@ -1145,12 +1142,77 @@ Zotero.Translate.Base.prototype = {
}
// call handlers
this._runHandler("done", returnValue);
this._runHandler("itemsDone", returnValue);
if(returnValue) {
this._checkIfDone();
} else {
this._runHandler("done", returnValue);
}
}
return errorString;
},
/**
* Callback executed when items have been saved (which may happen asynchronously, if in
* connector)
*
* @param {Boolean} returnValue Whether saving was successful
* @param {Zotero.Item[]|Error} data If returnValue is true, this will be an array of
* Zotero.Item objects. If returnValue is false, this will
* be a string error message.
*/
"_itemsSaved":function(returnValue, data) {
if(returnValue) {
// trigger deferred itemDone events
var nItems = data.length;
for(var i=0; i<nItems; i++) {
this._runHandler("itemDone", data[i], this.saveQueue[i]);
}
this.saveQueue = [];
} else {
Zotero.logError(data);
}
if(returnValue) {
this._checkIfDone();
} else {
this._runHandler("done", returnValue);
}
},
/**
* Callback for attachment progress, passed as third argument to Zotero.ItemSaver#saveItems
*
* @param {Object} attachment Attachment object to be saved. Should remain the same between
* repeated calls to callback.
* @param {Boolean|Number} progress Percent complete, or false if an error occurred.
* @param {Error} [error] Error, if an error occurred during saving.
*/
"_attachmentProgress":function(attachment, progress, error) {
Zotero.debug("Attachment progress (progress = "+progress+")");
Zotero.debug(attachment);
var attachmentIndex = this._attachmentsSaving.indexOf(attachment);
if((progress === false || progress === 100) && attachmentIndex !== -1) {
this._attachmentsSaving.splice(attachmentIndex, 1);
} else if(attachmentIndex === -1) {
this._attachmentsSaving.push(attachment);
}
this._runHandler("attachmentProgress", attachment, progress, error);
this._checkIfDone();
},
/**
* Checks if saving done, and if so, fires done event
*/
"_checkIfDone":function() {
if(!this._attachmentsSaving.length) {
this._runHandler("done", true);
}
},
/**
* Begins running detect code for a translator, first loading it
*/
@ -1466,12 +1528,11 @@ Zotero.Translate.Web.prototype.translate = function(libraryID, saveAttachments,
* Overload _translateTranslatorLoaded to send an RPC call if necessary
*/
Zotero.Translate.Web.prototype._translateTranslatorLoaded = function() {
if(this.translator[0].runMode === Zotero.Translator.RUN_MODE_IN_BROWSER
|| this._parentTranslator) {
// begin process to run translator in browser
var runMode = this.translator[0].runMode;
if(runMode === Zotero.Translator.RUN_MODE_IN_BROWSER || this._parentTranslator) {
Zotero.Translate.Base.prototype._translateTranslatorLoaded.apply(this);
} else {
// otherwise, ferry translator load to RPC
} else if(runMode === Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE ||
(runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER && Zotero.Connector.isOnline)) {
var me = this;
Zotero.Connector.callMethod("savePage", {
"uri":this.location.toString(),
@ -1480,11 +1541,17 @@ Zotero.Translate.Web.prototype._translateTranslatorLoaded = function() {
"cookie":this.document.cookie,
"html":this.document.documentElement.innerHTML
}, function(obj) { me._translateRPCComplete(obj) });
} else if(runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER) {
var me = this;
Zotero.OAuth.createItem({"url":this.document.location.href.toString()}, null,
function(statusCode, response) {
me._translateServerComplete(statusCode, response);
});
}
}
/**
* Called when an RPC call for remote translation completes
* Called when an call to Zotero Standalone for translation completes
*/
Zotero.Translate.Web.prototype._translateRPCComplete = function(obj, failureCode) {
if(!obj) this.complete(false, failureCode);
@ -1492,7 +1559,7 @@ Zotero.Translate.Web.prototype._translateRPCComplete = function(obj, failureCode
if(obj.selectItems) {
// if we have to select items, call the selectItems handler and do it
var me = this;
var items = this._runHandler("select", obj.selectItems,
this._runHandler("select", obj.selectItems,
function(selectedItems) {
Zotero.Connector.callMethod("selectItems",
{"instanceID":obj.instanceID, "selectedItems":selectedItems},
@ -1508,6 +1575,66 @@ Zotero.Translate.Web.prototype._translateRPCComplete = function(obj, failureCode
this.complete(true);
}
}
/**
* Called when an call to the Zotero Translator Server for translation completes
*/
Zotero.Translate.Web.prototype._translateServerComplete = function(statusCode, response) {
if(statusCode === 300) {
// Multiple Choices
try {
response = JSON.parse(response);
} catch(e) {
Zotero.logError(e);
this.complete(false, "Invalid JSON response received from server");
return;
}
var me = this;
this._runHandler("select", response,
function(selectedItems) {
Zotero.OAuth.createItem({
"url":me.document.location.href.toString(),
"items":selectedItems
}, null,
function(statusCode, response) {
me._translateServerComplete(statusCode, response);
});
}
);
} else if(statusCode === 201) {
// Created
try {
response = (new DOMParser()).parseFromString(response, "application/xml");
} catch(e) {
Zotero.logError(e);
this.complete(false, "Invalid XML response received from server");
return;
}
// Extract items from ATOM/JSON response
var items = [];
var contents = response.getElementsByTagNameNS("http://www.w3.org/2005/Atom", "content");
for(var i=0, n=contents.length; i<n; i++) {
var content = contents[i];
if(content.getAttributeNS("http://zotero.org/ns/api", "type") != "json") continue;
try {
item = JSON.parse("textContent" in content ?
content.textContent : content.innerText);
} catch(e) {
Zotero.logError(e);
this.complete(false, "Invalid JSON response received from server");
return;
}
this._runHandler("itemDone", null, item);
items.push(item);
}
this.newItems = items;
this.complete(true);
} else {
this.complete(false, response);
}
}
/**
* Overload complete to report translation failure

View file

@ -1476,7 +1476,6 @@ Zotero.Utilities = {
}
},
/**
* Get the real target URL from an intermediate URL
*/
@ -1501,5 +1500,55 @@ Zotero.Utilities = {
}
return url;
},
/**
* Adds a string to a given array at a given offset, converted to UTF-8
* @param {String} string The string to convert to UTF-8
* @param {Array|Uint8Array} array The array to which to add the string
* @param {Integer} [offset] Offset at which to add the string
*/
"stringToUTF8Array":function(string, array, offset) {
if(!offset) offset = 0;
var n = string.length;
for(var i=0; i<n; i++) {
var val = string.charCodeAt(i);
if(val >= 128) {
if(val >= 2048) {
array[offset] = ((val >>> 6) | 192);
array[offset+1] = (val & 63) | 128;
offset += 2;
} else {
array[offset] = (val >>> 12) | 224;
array[offset+1] = ((val >>> 6) & 63) | 128;
array[offset+2] = (val & 63) | 128;
offset += 3;
}
} else {
array[offset++] = val;
}
}
},
/**
* Gets the byte length of the UTF-8 representation of a given string
* @param {String} string
* @return {Integer}
*/
"getStringByteLength":function(string) {
var length = 0, n = string.length;
for(var i=0; i<n; i++) {
var val = string.charCodeAt(i);
if(val >= 128) {
if(val >= 2048) {
length += 3;
} else {
length += 2;
}
} else {
length += 1;
}
}
return length;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 531 B

After

Width:  |  Height:  |  Size: 692 B

2
styles

@ -1 +1 @@
Subproject commit cb42bb0ac96581e1a18737df1a105604e1b5144b
Subproject commit a322796a088e261718bc7d88e16a338be4143545

@ -1 +1 @@
Subproject commit 1ba73cb10ad0c77c171a389dd60bad78ea6a007f
Subproject commit c4f989eb17cedb10a1f0d62c601c15a22309518d