www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 1585ece566094600faa38844ba6efaac5507da9f
parent 7598370af447c4fe91bbe9db82cb12adc900c599
Author: Simon Kornblith <simon@simonster.com>
Date:   Mon,  9 Apr 2012 11:27:01 -0400

Merge branches '3.0' and 'master'

Diffstat:
Mchrome/content/zotero/recognizePDF.js | 16++++++++++++----
Mchrome/content/zotero/xpcom/connector/cachedTypes.js | 5++---
Mchrome/content/zotero/xpcom/connector/connector.js | 39+++++++++++++++++++++++++++------------
Mchrome/content/zotero/xpcom/connector/translate_item.js | 477++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mchrome/content/zotero/xpcom/translation/translate.js | 235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mchrome/content/zotero/xpcom/utilities.js | 51++++++++++++++++++++++++++++++++++++++++++++++++++-
Achrome/skin/default/zotero/progress_arcs.png | 0
Mchrome/skin/default/zotero/treeitem-attachment-pdf.png | 0
8 files changed, 749 insertions(+), 74 deletions(-)

diff --git a/chrome/content/zotero/recognizePDF.js b/chrome/content/zotero/recognizePDF.js @@ -363,7 +363,7 @@ Zotero_RecognizePDF.Recognizer.prototype._queryGoogle = function() { var me = this; if(this._DOI) { // use CrossRef to look for DOI - var translate = new Zotero.Translate("search"); + var translate = new Zotero.Translate.Search(); translate.setTranslator("11645bd1-0420-45c1-badb-53fb41eeb753"); var item = {"itemType":"journalArticle", "DOI":this._DOI}; translate.setSearch(item); @@ -411,7 +411,7 @@ Zotero_RecognizePDF.Recognizer.prototype._queryGoogle = function() { this._hiddenBrowser.docShell.allowImages = false; } - var translate = new Zotero.Translate("web"); + var translate = new Zotero.Translate.Web(); var savedItem = false; translate.setTranslator("57a00950-f0d1-4b41-b6ba-44ff0fc30289"); translate.setHandler("itemDone", function(translate, item) { @@ -425,6 +425,13 @@ Zotero_RecognizePDF.Recognizer.prototype._queryGoogle = function() { translate.setHandler("done", function(translate, success) { if(!success || !savedItem) me._queryGoogle(); }); + translate.setHandler("translators", function(translate, detected) { + if(detected.length) { + translate.translate(me._libraryID, false); + } else { + me._queryGoogle(); + } + }); this._hiddenBrowser.addEventListener("pageshow", function() { me._scrape(translate) }, true); @@ -459,10 +466,11 @@ Zotero_RecognizePDF.Recognizer.prototype._scrape = function(/**Zotero.Translate* this._callback(false, "recognizePDF.limit"); return; } - + this._hiddenBrowser.removeEventListener("pageshow", this._scrape.caller, true); translate.setDocument(this._hiddenBrowser.contentDocument); - translate.translate(this._libraryID, false); + + translate.getTranslators(false, true); } /** diff --git a/chrome/content/zotero/xpcom/connector/cachedTypes.js b/chrome/content/zotero/xpcom/connector/cachedTypes.js @@ -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) { diff --git a/chrome/content/zotero/xpcom/connector/connector.js b/chrome/content/zotero/xpcom/connector/connector.js @@ -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"); diff --git a/chrome/content/zotero/xpcom/connector/translate_item.js b/chrome/content/zotero/xpcom/connector/translate_item.js @@ -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,454 @@ 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); + + 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); + } + } + } + + callback(true, items); } - }, 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; + })() }; \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/translation/translate.js b/chrome/content/zotero/xpcom/translation/translate.js @@ -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); @@ -889,15 +892,41 @@ Zotero.Translate.Base.prototype = { * * @param {Boolean} [getAllTranslators] Whether all applicable translators should be returned, * rather than just the first available. + * @param {Boolean} [checkSetTranslator] If true, the appropriate detect function is run on the + * set document/text/etc. using the translator set by setTranslator. + * getAllTranslators parameter is meaningless in this context. * @return {Zotero.Translator[]} An array of {@link Zotero.Translator} objects */ - "getTranslators":function(getAllTranslators) { + "getTranslators":function(getAllTranslators, checkSetTranslator) { // do not allow simultaneous instances of getTranslators if(this._currentState === "detect") throw new Error("getTranslators: detection is already running"); this._currentState = "detect"; this._getAllTranslators = getAllTranslators; - this._getTranslatorsGetPotentialTranslators(); - + + if(checkSetTranslator) { + // setTranslator must be called beforehand if checkSetTranslator is set + if( !this.translator || !this.translator[0] ) { + throw new Error("getTranslators: translator must be set via setTranslator before calling" + + " getTranslators with the checkSetTranslator flag"); + } + var translators = new Array(); + var t; + for(var i=0, n=this.translator.length; i<n; i++) { + if(typeof(this.translator[i]) == 'string') { + t = Zotero.Translators.get(this.translator[i]); + if(!t) Zotero.debug("getTranslators: could not retrieve translator '" + this.translator[i] + "'"); + } else { + t = this.translator[i]; + } + /**TODO: check that the translator is of appropriate type?*/ + if(t) translators.push(t); + } + if(!translators.length) throw new Error("getTranslators: no valid translators were set."); + this._getTranslatorsTranslatorsReceived(translators); + } else { + this._getTranslatorsGetPotentialTranslators(); + } + // if detection returns immediately, return found translators if(!this._currentState) return this._foundTranslators; }, @@ -985,6 +1014,7 @@ Zotero.Translate.Base.prototype = { this._libraryID = libraryID; this._saveAttachments = saveAttachments === undefined || saveAttachments; + this._attachmentsSaving = []; var me = this; if(typeof this.translator[0] === "object") { @@ -1036,30 +1066,6 @@ Zotero.Translate.Base.prototype = { }, /** - * 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 */ "getProgress":function() { return null }, @@ -1078,9 +1084,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 +1113,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 +1149,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,13 +1168,78 @@ 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 */ "_detect":function() { @@ -1462,12 +1550,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(), @@ -1476,11 +1563,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); @@ -1488,7 +1581,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}, @@ -1504,6 +1597,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 diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js @@ -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; } } diff --git a/chrome/skin/default/zotero/progress_arcs.png b/chrome/skin/default/zotero/progress_arcs.png Binary files differ. diff --git a/chrome/skin/default/zotero/treeitem-attachment-pdf.png b/chrome/skin/default/zotero/treeitem-attachment-pdf.png Binary files differ.