diff --git a/chrome/content/zotero/xpcom/connector/server_connector.js b/chrome/content/zotero/xpcom/connector/server_connector.js index 1f1eae2a96..6e22f478b0 100644 --- a/chrome/content/zotero/xpcom/connector/server_connector.js +++ b/chrome/content/zotero/xpcom/connector/server_connector.js @@ -163,7 +163,7 @@ Zotero.Server.Connector.SaveSession = function (id, action, requestData) { Zotero.Server.Connector.SaveSession.prototype.onProgress = function (item, progress, error) { - if (!item.id) { + if (item.id === null || item.id === undefined) { throw new Error("ID not provided"); } @@ -230,6 +230,10 @@ Zotero.Server.Connector.SaveSession.prototype.addItems = async function (items) await this._updateItems(items); }; +Zotero.Server.Connector.SaveSession.prototype.remove = function () { + delete Zotero.Server.Connector.SessionManager._sessions[this.id]; +} + /** * Change the target data for this session and update any items that have already been saved */ @@ -399,7 +403,7 @@ Zotero.Server.Connector.GetTranslators.prototype = { // Translator data var me = this; if(data.url) { - Zotero.Translators.getWebTranslatorsForLocation(data.url, data.rootUrl).then(function(data) { + Zotero.Translators.getWebTranslatorsForLocation(data.url, data.url).then(function(data) { sendResponseCallback(200, "application/json", JSON.stringify(me._serializeTranslators(data[0]))); }); @@ -444,63 +448,45 @@ Zotero.Server.Connector.Detect.prototype = { /** * Loads HTML into a hidden browser and initiates translator detection - * @param {Object} data POST data or GET query string - * @param {Function} sendResponseCallback function to send HTTP response */ - init: function(url, data, sendResponseCallback) { - this.sendResponse = sendResponseCallback; - this._parsedPostData = data; + init: async function(requestData) { + try { + var translators = await this.getTranslators(requestData); + } catch (e) { + Zotero.logError(e); + return 500; + } - this._translate = new Zotero.Translate("web"); - this._translate.setHandler("translators", function(obj, item) { me._translatorsAvailable(obj, item) }); - - Zotero.Server.Connector.Data[this._parsedPostData["uri"]] = ""+this._parsedPostData["html"]+""; - this._browser = Zotero.Browser.createHiddenBrowser(); - - var ioService = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); - var uri = ioService.newURI(this._parsedPostData["uri"], "UTF-8", null); - - var pageShowCalled = false; - var me = this; - this._translate.setCookieSandbox(new Zotero.CookieSandbox(this._browser, - this._parsedPostData["uri"], this._parsedPostData["cookie"], url.userAgent)); - this._browser.addEventListener("DOMContentLoaded", function() { - try { - if(me._browser.contentDocument.location.href == "about:blank") return; - if(pageShowCalled) return; - pageShowCalled = true; - delete Zotero.Server.Connector.Data[me._parsedPostData["uri"]]; - - // get translators - me._translate.setDocument(me._browser.contentDocument); - me._translate.setLocation(me._parsedPostData["uri"], me._parsedPostData["uri"]); - me._translate.getTranslators(); - } catch(e) { - sendResponseCallback(500); - throw e; - } - }, false); - - me._browser.loadURI("zotero://connector/"+encodeURIComponent(this._parsedPostData["uri"])); - }, - - /** - * Callback to be executed when list of translators becomes available. Sends standard - * translator passing properties with proxies where available for translators. - * @param {Zotero.Translate} translate - * @param {Zotero.Translator[]} translators - */ - _translatorsAvailable: function(translate, translators) { translators = translators.map(function(translator) { translator = translator.serialize(TRANSLATOR_PASSING_PROPERTIES.concat('proxy')); translator.proxy = translator.proxy ? translator.proxy.toJSON() : null; return translator; }); - this.sendResponse(200, "application/json", JSON.stringify(translators)); + return [200, "application/json", JSON.stringify(translators)]; + }, + + async getTranslators(requestData) { + var data = requestData.data; + var cookieSandbox = data.uri + ? new Zotero.CookieSandbox( + null, + data.uri, + data.cookie || "", + requestData.headers["User-Agent"] + ) + : null; - Zotero.Browser.deleteHiddenBrowser(this._browser); - } + var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Components.interfaces.nsIDOMParser); + var doc = parser.parseFromString(`${data.html}`, 'text/html'); + doc = Zotero.HTTP.wrapDocument(doc, data.uri); + + let translate = this._translate = new Zotero.Translate.Web(); + translate.setDocument(doc); + cookieSandbox && translate.setCookieSandbox(cookieSandbox); + + return await translate.getTranslators(); + }, } /** @@ -529,20 +515,118 @@ Zotero.Server.Connector.SavePage.prototype = { /** * Either loads HTML into a hidden browser and initiates translation, or saves items directly * to the database - * @param {Object} data POST data or GET query string - * @param {Function} sendResponseCallback function to send HTTP response */ - init: function(url, data, sendResponseCallback) { - var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget(); - - // Shouldn't happen as long as My Library exists - if (!library.editable) { - Zotero.logError("Can't add item to read-only library " + library.name); - return sendResponseCallback(500, "application/json", JSON.stringify({ libraryEditable: false })); - } - - this.sendResponse = sendResponseCallback; - Zotero.Server.Connector.Detect.prototype.init.apply(this, [url, data, sendResponseCallback]) + init: function(requestData) { + return new Zotero.Promise(async function(resolve) { + function sendResponseCallback() { + if (arguments.length > 1) { + return resolve(arguments); + } + return resolve(arguments[0]); + } + var data = requestData.data; + var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget(); + var libraryID = library.libraryID; + var targetID = collection ? collection.treeViewID : library.treeViewID; + + if (Zotero.Server.Connector.SessionManager.get(data.sessionID)) { + return sendResponseCallback(409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })); + } + + // Shouldn't happen as long as My Library exists + if (!library.editable) { + Zotero.logError("Can't add item to read-only library " + library.name); + return sendResponseCallback(500, "application/json", JSON.stringify({ libraryEditable: false })); + } + + var session = Zotero.Server.Connector.SessionManager.create(data.sessionID); + await session.update(targetID); + + this.sendResponse = sendResponseCallback; + this._parsedPostData = data; + + try { + var translators = await Zotero.Server.Connector.Detect.prototype.getTranslators.call(this, requestData); + } catch(e) { + Zotero.logError(e); + session.remove(); + return sendResponseCallback(500); + } + + if(!translators.length) { + Zotero.debug(`No translators available for /connector/savePage ${data.uri}`); + session.remove(); + return this.sendResponse(500); + } + + // set handlers for translation + var me = this; + var translate = this._translate; + translate.setHandler("select", function(obj, item, callback) { return me._selectItems(obj, item, callback) }); + let attachmentTitleData = {}; + translate.setHandler("itemsDone", function(obj, items) { + if(items.length || me.selectedItems === false) { + items = items.map((item) => { + let o = { + id: item.id, + title: item.title, + itemType: item.itemType, + contentType: item.mimeType, + mimeType: item.mimeType, // TODO: Remove + }; + if (item.attachments) { + let id = 0; + for (let attachment of item.attachments) { + attachment.parent = item.id; + attachment.id = id++; + } + o.attachments = item.attachments.map((attachment) => { + // Retaining id and parent info for session progress management + attachmentTitleData[attachment.title] = {id: attachment.id, parent: item.id}; + return { + id: session.id + '_' + attachment.id, // TODO: Remove prefix + title: attachment.title, + contentType: attachment.contentType, + mimeType: attachment.mimeType, // TODO: Remove + }; + }); + }; + session.onProgress(item, 100); + return o; + }); + me.sendResponse(201, "application/json", JSON.stringify({items})); + } else { + me.sendResponse(500); + session.remove(); + } + }); + + translate.setHandler("attachmentProgress", function(obj, attachment, progress, error) { + if (attachmentTitleData[attachment.title]) { + session.onProgress(Object.assign( + {}, + attachment, + attachmentTitleData[attachment.title], + ), progress, error); + } + }); + + translate.setHandler("error", function(obj, err) { + Zotero.logError(err); + sendResponseCallback(500); + session.remove(); + }); + + if (this._parsedPostData.translatorID) { + translate.setTranslator(this._parsedPostData.translatorID); + } else { + translate.setTranslator(translators[0]); + } + let items = await translate.translate({libraryID, collections: collection ? [collection.id] : false}); + session.addItems(items); + // Return 'done: true' so the connector stops checking for updates + session.savingDone = true; + }.bind(this)); }, /** @@ -566,51 +650,40 @@ Zotero.Server.Connector.SavePage.prototype = { // Send "Multiple Choices" HTTP response this.sendResponse(300, "application/json", JSON.stringify({selectItems: itemList, instanceID: instanceID, uri: this._parsedPostData.uri})); this.selectedItemsCallback = callback; - }, + } +} +/** + * Handle item selection + * + * Accepts: + * selectedItems - a list of items to translate in ID => text format as returned by a selectItems handler + * instanceID - as returned by savePage call + * Returns: + * 201 response code with empty body + */ +Zotero.Server.Connector.SelectItems = function() {}; +Zotero.Server.Endpoints["/connector/selectItems"] = Zotero.Server.Connector.SelectItems; +Zotero.Server.Connector.SelectItems.prototype = { + supportedMethods: ["POST"], + supportedDataTypes: ["application/json"], + permitBookmarklet: true, + /** - * Callback to be executed when list of translators becomes available. Opens progress window, - * selects specified translator, and initiates translation. - * @param {Zotero.Translate} translate - * @param {Zotero.Translator[]} translators + * Finishes up translation when item selection is complete + * @param {String} data POST data or GET query string + * @param {Function} sendResponseCallback function to send HTTP response */ - _translatorsAvailable: function(translate, translators) { - // make sure translatorsAvailable succeded - if(!translators.length) { - Zotero.Browser.deleteHiddenBrowser(this._browser); - this.sendResponse(500); - return; + init: function(data, sendResponseCallback) { + var saveInstance = Zotero.Server.Connector._waitingForSelection[data.instanceID]; + saveInstance.sendResponse = sendResponseCallback; + + var selectedItems = false; + for(var i in data.selectedItems) { + selectedItems = data.selectedItems; + break; } - - var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget(); - var libraryID = library.libraryID; - - // set handlers for translation - var me = this; - var jsonItems = []; - translate.setHandler("select", function(obj, item, callback) { return me._selectItems(obj, item, callback) }); - translate.setHandler("itemDone", function(obj, item, jsonItem) { - //Zotero.Server.Connector.AttachmentProgressManager.add(jsonItem.attachments); - jsonItems.push(jsonItem); - }); - translate.setHandler("attachmentProgress", function(obj, attachment, progress, error) { - //Zotero.Server.Connector.AttachmentProgressManager.onProgress(attachment, progress, error); - }); - translate.setHandler("done", function(obj, item) { - Zotero.Browser.deleteHiddenBrowser(me._browser); - if(jsonItems.length || me.selectedItems === false) { - me.sendResponse(201, "application/json", JSON.stringify({items: jsonItems})); - } else { - me.sendResponse(500); - } - }); - - if (this._parsedPostData.translatorID) { - translate.setTranslator(this._parsedPostData.translatorID); - } else { - translate.setTranslator(translators[0]); - } - translate.translate({libraryID, collections: collection ? [collection.id] : false}); + saveInstance.selectedItemsCallback(selectedItems); } } @@ -698,6 +771,7 @@ Zotero.Server.Connector.SaveItems.prototype = { } catch (e) { Zotero.logError(e); + session.remove(); resolve(500); } }); @@ -881,40 +955,6 @@ Zotero.Server.Connector.SaveSnapshot.prototype = { } }; -/** - * Handle item selection - * - * Accepts: - * selectedItems - a list of items to translate in ID => text format as returned by a selectItems handler - * instanceID - as returned by savePage call - * Returns: - * 201 response code with empty body - */ -Zotero.Server.Connector.SelectItems = function() {}; -Zotero.Server.Endpoints["/connector/selectItems"] = Zotero.Server.Connector.SelectItems; -Zotero.Server.Connector.SelectItems.prototype = { - supportedMethods: ["POST"], - supportedDataTypes: ["application/json"], - permitBookmarklet: true, - - /** - * Finishes up translation when item selection is complete - * @param {String} data POST data or GET query string - * @param {Function} sendResponseCallback function to send HTTP response - */ - init: function(data, sendResponseCallback) { - var saveInstance = Zotero.Server.Connector._waitingForSelection[data.instanceID]; - saveInstance.sendResponse = sendResponseCallback; - - var selectedItems = false; - for(var i in data.selectedItems) { - selectedItems = data.selectedItems; - break; - } - saveInstance.selectedItemsCallback(selectedItems); - } -} - /** * * @@ -1508,30 +1548,3 @@ Zotero.Server.Connector.IEHack.prototype = { ''); } } - -// XXX For compatibility with older connectors; to be removed -Zotero.Server.Connector.IncompatibleVersion = function() {}; -Zotero.Server.Connector.IncompatibleVersion._errorShown = false -Zotero.Server.Endpoints["/translate/list"] = Zotero.Server.Connector.IncompatibleVersion; -Zotero.Server.Endpoints["/translate/detect"] = Zotero.Server.Connector.IncompatibleVersion; -Zotero.Server.Endpoints["/translate/save"] = Zotero.Server.Connector.IncompatibleVersion; -Zotero.Server.Endpoints["/translate/select"] = Zotero.Server.Connector.IncompatibleVersion; -Zotero.Server.Connector.IncompatibleVersion.prototype = { - supportedMethods: ["POST"], - supportedDataTypes: ["application/json"], - permitBookmarklet: true, - - init: function(postData, sendResponseCallback) { - sendResponseCallback(404); - if(Zotero.Server.Connector.IncompatibleVersion._errorShown) return; - - Zotero.Utilities.Internal.activate(); - var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]. - createInstance(Components.interfaces.nsIPromptService); - ps.alert(null, - Zotero.getString("connector.error.title"), - Zotero.getString("integration.error.incompatibleVersion2", - ["Standalone "+Zotero.version, "Connector", "2.999.1"])); - Zotero.Server.Connector.IncompatibleVersion._errorShown = true; - } -}; \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/server.js b/chrome/content/zotero/xpcom/server.js index c67b1b6d61..bc8fbc547d 100755 --- a/chrome/content/zotero/xpcom/server.js +++ b/chrome/content/zotero/xpcom/server.js @@ -356,8 +356,7 @@ Zotero.Server.DataListener.prototype._generateResponse = function (status, conte response += "X-Zotero-Version: "+Zotero.version+"\r\n"; response += "X-Zotero-Connector-API-Version: "+CONNECTOR_API_VERSION+"\r\n"; - if (this.origin === ZOTERO_CONFIG.BOOKMARKLET_ORIGIN || - this.origin === ZOTERO_CONFIG.HTTP_BOOKMARKLET_ORIGIN) { + if (this.origin === ZOTERO_CONFIG.BOOKMARKLET_ORIGIN) { response += "Access-Control-Allow-Origin: " + this.origin + "\r\n"; response += "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n"; response += "Access-Control-Allow-Headers: Content-Type,X-Zotero-Connector-API-Version,X-Zotero-Version\r\n"; diff --git a/chrome/content/zotero/xpcom/translation/translate.js b/chrome/content/zotero/xpcom/translation/translate.js index d1d50c38b0..578b874ea4 100644 --- a/chrome/content/zotero/xpcom/translation/translate.js +++ b/chrome/content/zotero/xpcom/translation/translate.js @@ -1213,12 +1213,14 @@ Zotero.Translate.Base.prototype = { if(this._waitingForRPC) { // Try detect in Zotero Standalone. If this fails, it fails; we shouldn't // get hung up about it. + let html = this.document.documentElement.innerHTML; + html = html.replace(new RegExp(Zotero.Utilities.quotemeta(ZOTERO_CONFIG.BOOKMARKLET_URL), 'g'), "about:blank"); Zotero.Connector.callMethod( "detect", { uri: this.location.toString(), cookie: this.document.cookie, - html: this.document.documentElement.innerHTML + html }).catch(() => false).then(function (rpcTranslators) { this._waitingForRPC = false; @@ -1652,25 +1654,26 @@ Zotero.Translate.Base.prototype = { } } - return this._itemSaver.saveItems(items.slice(), attachmentCallback.bind(this)) - .then(function(newItems) { - // Remove attachments not being saved from item.attachments - for(var i=0; i newItems); - }.bind(this)) + + // Trigger itemDone events, waiting for them if they return promises + var maybePromises = []; + for(var i=0, nItems = items.length; i newItems); + }.bind(this)) .then(function (newItems) { // Specify that itemDone event was dispatched, so that we don't defer // attachmentProgress notifications anymore @@ -1761,7 +1764,13 @@ Zotero.Translate.Base.prototype = { */ "_detectTranslatorsCollected":function() { Zotero.debug("Translate: All translator detect calls and RPC calls complete:"); - this._foundTranslators.sort(function(a, b) { return a.priority-b.priority }); + this._foundTranslators.sort(function(a, b) { + // If priority is equal, prioritize translators that run in browser over the client + if (a.priority == b.priority) { + return a.runMode - b.runMode; + } + return a.priority-b.priority; + }); if (this._foundTranslators.length) { this._foundTranslators.forEach(function(t) { Zotero.debug("\t" + t.label + ": " + t.priority); @@ -2127,26 +2136,31 @@ Zotero.Translate.Web.prototype.translate = function (options = {}, ...args) { /** * Overload _translateTranslatorLoaded to send an RPC call if necessary */ -Zotero.Translate.Web.prototype._translateTranslatorLoaded = function() { +Zotero.Translate.Web.prototype._translateTranslatorLoaded = async function() { var runMode = this.translator[0].runMode; if(runMode === Zotero.Translator.RUN_MODE_IN_BROWSER || this._parentTranslator) { Zotero.Translate.Base.prototype._translateTranslatorLoaded.apply(this); } else if(runMode === Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE || - (runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER && Zotero.Connector.isOnline)) { + (runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER && await Zotero.Connector.checkIsOnline())) { var me = this; - Zotero.Connector.callMethod("savePage", { + let html = this.document.documentElement.innerHTML; + html = html.replace(new RegExp(Zotero.Utilities.quotemeta(ZOTERO_CONFIG.BOOKMARKLET_URL), 'g'), "about:blank") + // Higher timeout since translation might take a while if additional HTTP requests are made + Zotero.Connector.callMethod({method: "savePage", timeout: 60*1000}, { + sessionID: this._sessionID, uri: this.location.toString(), translatorID: (typeof this.translator[0] === "object" ? this.translator[0].translatorID : this.translator[0]), cookie: this.document.cookie, proxy: this._proxy ? this._proxy.toJSON() : null, - html: this.document.documentElement.innerHTML - }).then(obj => me._translateRPCComplete(obj)); + html + }).then(me._translateRPCComplete.bind(me), me._translateRPCComplete.bind(me, null)); } else if(runMode === Zotero.Translator.RUN_MODE_ZOTERO_SERVER) { var me = this; - Zotero.API.createItem({"url":this.document.location.href.toString()}, - function(statusCode, response) { - me._translateServerComplete(statusCode, response); + Zotero.API.createItem({"url":this.document.location.href.toString()}).then(function(response) { + me._translateServerComplete(201, response); + }, function(error) { + me._translateServerComplete(error.status, error.responseText); }); } } @@ -2154,8 +2168,8 @@ Zotero.Translate.Web.prototype._translateTranslatorLoaded = function() { /** * Called when an call to Zotero Standalone for translation completes */ -Zotero.Translate.Web.prototype._translateRPCComplete = function(obj, failureCode) { - if(!obj) this.complete(false, failureCode); +Zotero.Translate.Web.prototype._translateRPCComplete = async function(obj, failureCode) { + if(!obj) return this.complete(false, failureCode); if(obj.selectItems) { // if we have to select items, call the selectItems handler and do it @@ -2173,6 +2187,17 @@ Zotero.Translate.Web.prototype._translateRPCComplete = function(obj, failureCode this._runHandler("itemDone", null, obj.items[i]); } this.newItems = obj.items; + let itemSaver = new Zotero.Translate.ItemSaver({ + libraryID: this._libraryID, + collections: this._collections, + attachmentMode: Zotero.Translate.ItemSaver[(this._saveAttachments ? "ATTACHMENT_MODE_DOWNLOAD" : "ATTACHMENT_MODE_IGNORE")], + forceTagType: 1, + sessionID: this._sessionID, + cookieSandbox: this._cookieSandbox, + proxy: this._proxy, + baseURI: this.location + }); + await itemSaver._pollForProgress(obj.items, this._runHandler.bind(this, 'attachmentProgress')); this.complete(true); } } @@ -2191,56 +2216,39 @@ Zotero.Translate.Web.prototype._translateServerComplete = function(statusCode, r return; } var me = this; - this._runHandler("select", response, + this._runHandler("select", response.items, function(selectedItems) { Zotero.API.createItem({ - "url":me.document.location.href.toString(), - "items":selectedItems - }, - function(statusCode, response) { - me._translateServerComplete(statusCode, response); - }); + url: me.document.location.href.toString(), + items: selectedItems, + token: response.token + }).then(function(response) { + me._translateServerComplete(201, response); + }, function(error) { + me._translateServerComplete(error.status, error.responseText); + }); } ); } else if(statusCode === 201) { // Created try { - response = (new DOMParser()).parseFromString(response, "application/xml"); + response = JSON.parse(response); } catch(e) { Zotero.logError(e); - this.complete(false, "Invalid XML response received from server"); + this.complete(false, "Invalid JSON response received from server"); return; } - // Extract items from ATOM/JSON response - var items = [], contents; - if("getElementsByTagNameNS" in response) { - contents = response.getElementsByTagNameNS("http://www.w3.org/2005/Atom", "content"); - } else { // IE... - contents = response.getElementsByTagName("content"); - } - for(var i=0, n=contents.length; i jsonByItem.get(item))); + } // Save standalone attachments for (let jsonItem of standaloneAttachments) { @@ -186,10 +190,6 @@ Zotero.Translate.ItemSaver.prototype = { } } - if (itemsDoneCallback) { - itemsDoneCallback(items.map(item => jsonByItem.get(item))); - } - // For items with DOIs and without PDFs from the translator, look for possible // open-access PDFs. There's no guarantee that either translated PDFs or OA PDFs will // successfully download, but this lets us update the progress window sooner with diff --git a/chrome/content/zotero/xpcom/utilities_translate.js b/chrome/content/zotero/xpcom/utilities_translate.js index af489bf59d..bc49373b3c 100644 --- a/chrome/content/zotero/xpcom/utilities_translate.js +++ b/chrome/content/zotero/xpcom/utilities_translate.js @@ -326,7 +326,7 @@ Zotero.Utilities.Translate.prototype.doGet = function(urls, processor, done, res translate.incrementAsyncProcesses("Zotero.Utilities.Translate#doGet"); var xmlhttp = Zotero.HTTP.doGet(url, function(xmlhttp) { - if (xmlhttp.status >= 400) { + if (xmlhttp.status >= 400 || !xmlhttp.status) { translate.complete(false, `HTTP GET ${url} failed with status code ${xmlhttp.status}`); return; } @@ -360,7 +360,7 @@ Zotero.Utilities.Translate.prototype.doPost = function(url, body, onDone, header translate.incrementAsyncProcesses("Zotero.Utilities.Translate#doPost"); var xmlhttp = Zotero.HTTP.doPost(url, body, function(xmlhttp) { - if (xmlhttp.status >= 400) { + if (xmlhttp.status >= 400 || !xmlhttp.status) { translate.complete(false, `HTTP POST ${url} failed with status code ${xmlhttp.status}`); return; } diff --git a/resource/config.js b/resource/config.js index 0ed1925825..6af850634b 100644 --- a/resource/config.js +++ b/resource/config.js @@ -17,7 +17,6 @@ var ZOTERO_CONFIG = { CONNECTOR_MIN_VERSION: '5.0.39', // show upgrade prompt for requests from below this version PREF_BRANCH: 'extensions.zotero.', BOOKMARKLET_ORIGIN: 'https://www.zotero.org', - HTTP_BOOKMARKLET_ORIGIN: 'http://www.zotero.org', BOOKMARKLET_URL: 'https://www.zotero.org/bookmarklet/', START_URL: "https://www.zotero.org/start", QUICK_START_URL: "https://www.zotero.org/support/quick_start_guide",