Server-side translation and attachment upload, part 1
This commit is contained in:
parent
7452e14090
commit
6fa4f8d02b
10 changed files with 710 additions and 70 deletions
|
@ -1 +1 @@
|
|||
Subproject commit 843dcc3b3f1f16ab317a9551be7446e098a9a7ee
|
||||
Subproject commit 73f3050d7c66caeb338a54c53d79c1b4be0ea8aa
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
// 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);
|
||||
});
|
||||
Zotero.Messaging.addMessageListener("connectorResponse", function(data, event) {
|
||||
if(event.origin !== "http://127.0.0.1:23119") return;
|
||||
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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}, true);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
})()
|
||||
};
|
|
@ -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) {
|
||||
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("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},
|
||||
|
@ -1509,6 +1576,66 @@ Zotero.Translate.Web.prototype._translateRPCComplete = function(obj, failureCode
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
BIN
chrome/skin/default/zotero/progress_arcs.png
Normal file
BIN
chrome/skin/default/zotero/progress_arcs.png
Normal file
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
2
styles
|
@ -1 +1 @@
|
|||
Subproject commit cb42bb0ac96581e1a18737df1a105604e1b5144b
|
||||
Subproject commit a322796a088e261718bc7d88e16a338be4143545
|
|
@ -1 +1 @@
|
|||
Subproject commit 1ba73cb10ad0c77c171a389dd60bad78ea6a007f
|
||||
Subproject commit c4f989eb17cedb10a1f0d62c601c15a22309518d
|
Loading…
Reference in a new issue