From f6eae83167748d85009adf8d077f67477294b14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Fri, 6 Oct 2017 17:04:18 +0300 Subject: [PATCH] Make EM the default translator. Rework snapshot saving pref. Also misc browserExt code refactoring --- gulpfile.js | 10 +- package.json | 4 + src/browserExt/background.js | 446 ++--------------------- src/browserExt/manifest.json | 5 + src/browserExt/messaging_inject.js | 38 +- src/browserExt/native-ui.js | 281 ++++++++++++++ src/browserExt/script-injection.js | 149 ++++++++ src/browserExt/test/testSetup.js | 4 +- src/common/connector.js | 1 - src/common/inject/inject.jsx | 254 +++++++------ src/common/preferences/preferences.css | 7 +- src/common/preferences/preferences.html | 29 +- src/common/preferences/preferences.jsx | 4 +- src/common/test/tests/backgroundTest.js | 24 +- src/common/test/tests/translationTest.js | 23 +- src/common/translate_item.js | 36 +- src/common/zotero.js | 2 +- 17 files changed, 713 insertions(+), 604 deletions(-) create mode 100644 src/browserExt/native-ui.js create mode 100644 src/browserExt/script-injection.js diff --git a/gulpfile.js b/gulpfile.js index 4137de0511..0d70bcb958 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -137,10 +137,6 @@ if (!argv.p) { 'test/testSetup.js', 'lib/sinon.js'); } -var backgroundIncludeBrowserExt = ['browser-polyfill.js'].concat(backgroundInclude, [ - 'webRequestIntercept.js', - 'contentTypeHandler.js', -]); function reloadChromeExtensionsTab(cb) { console.log("Reloading Chrome extensions tab"); @@ -229,10 +225,10 @@ function processFile() { case 'manifest.json': file.contents = Buffer.from(file.contents.toString() .replace("/*BACKGROUND SCRIPTS*/", - backgroundIncludeBrowserExt.map((s) => `"${s}"`).join(',\n\t\t\t')) + backgroundInclude.map((s) => `"${s}"`).join(',\n\t\t\t')) .replace(/"version": "[^"]*"/, '"version": "'+argv.version+'"')); break; - case 'background.js': + case 'script-injection.js': file.contents = Buffer.from(file.contents.toString() .replace("/*INJECT SCRIPTS*/", injectIncludeBrowserExt.map((s) => `"${s}"`).join(',\n\t\t'))); @@ -288,7 +284,7 @@ gulp.task('watch-chrome', function () { gulp.task('process-custom-scripts', function() { let sources = [ - './src/browserExt/background.js', + './src/browserExt/script-injection.js', './src/browserExt/manifest.json', './src/safari/global.html', './src/safari/Info.plist', diff --git a/package.json b/package.json index 9e45dde512..ff0066db56 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,9 @@ { "name": "zotero-connectors", + "scripts": { + "start": "./build.sh -d && gulp watch", + "build": "./build.sh -d" + }, "devDependencies": { "babel-cli": "^6.16.0", "babel-core": "^6.16.0", diff --git a/src/browserExt/background.js b/src/browserExt/background.js index 54251f24a1..b8c157a719 100644 --- a/src/browserExt/background.js +++ b/src/browserExt/background.js @@ -26,17 +26,17 @@ Zotero.Connector_Browser = new function() { var _tabInfo = {}; var _incompatibleVersionMessageShown; - var _injectTranslationScripts = [ - /*INJECT SCRIPTS*/ - ]; // Exposed for tests this._tabInfo = _tabInfo; + + // Exposed for injected script access + this.injectScripts = Zotero.Extension.ScriptInjection.injectScripts; /** * Called when translators are available for a given page */ this.onTranslators = function(translators, instanceID, contentType, tab, frameId) { - _enableForTab(tab.id); + browser.browserAction.enable(tab.id); let existingTranslators = _tabInfo[tab.id] && _tabInfo[tab.id].translators; // If translators already exist for tab we need to figure out if the new translators @@ -57,7 +57,7 @@ Zotero.Connector_Browser = new function() { var isPDF = contentType == 'application/pdf'; _tabInfo[tab.id] = Object.assign(_tabInfo[tab.id] || {}, {translators, instanceID, isPDF}); - _updateExtensionUI(tab); + Zotero.Connector_Browser.updateExtensionUI(tab); } /** @@ -71,8 +71,8 @@ Zotero.Connector_Browser = new function() { } browser.tabs.get(tabId).then(function(tab) { _tabInfo[tab.id] = {translators: [], isPDF: true, frameId}; - Zotero.Connector_Browser.injectTranslationScripts(tab, frameId); - _updateExtensionUI(tab); + Zotero.Extension.ScriptInjection.injectTranslationScripts(tab, frameId); + Zotero.Connector_Browser.updateExtensionUI(tab); }); } @@ -140,7 +140,7 @@ Zotero.Connector_Browser = new function() { } this.onTabActivated = function(tab) { - _updateExtensionUI(tab); + Zotero.Connector_Browser.updateExtensionUI(tab); }; /** @@ -166,12 +166,12 @@ Zotero.Connector_Browser = new function() { * @param url - url of the frame */ this.onFrameLoaded = Zotero.Promise.method(function(tab, frameId, url) { - if (_isDisabledForURL(tab.url) && frameId == 0 || _isDisabledForURL(url)) { + if (this.isDisabledForURL(tab.url) && frameId == 0 || this.isDisabledForURL(url)) { return; } if (frameId == 0) { // Always inject in the top-frame - return Zotero.Connector_Browser.injectTranslationScripts(tab, frameId); + return Zotero.Extension.ScriptInjection.injectTranslationScripts(tab, frameId); } else { if (!(tab.id in _tabInfo)) { _tabInfo[tab.id] = {}; @@ -180,7 +180,7 @@ Zotero.Connector_Browser = new function() { // Also in the first frame detected // See https://github.com/zotero/zotero-connectors/issues/156 _tabInfo[tab.id].frameChecked = true; - return Zotero.Connector_Browser.injectTranslationScripts(tab, frameId); + return Zotero.Extension.ScriptInjection.injectTranslationScripts(tab, frameId); } } return Zotero.Translators.getWebTranslatorsForLocation(url, tab.url).then(function(translators) { @@ -189,127 +189,10 @@ Zotero.Connector_Browser = new function() { return; } Zotero.debug(translators[0].length+ " translators found. Injecting into [tab.url, url]: " + tab.url + " , " + url); - return Zotero.Connector_Browser.injectTranslationScripts(tab, frameId); + return Zotero.Extension.ScriptInjection.injectTranslationScripts(tab, frameId); }); }); - /** - * Checks whether translation scripts are already injected into a frame and if not - injects - * @param tab {Object} - * @param [frameId=0] {Number] Defaults to top frame - * @returns {Promise} A promise that resolves when all scripts have been injected - */ - this.injectTranslationScripts = function(tab, frameId=0) { - // Prevent triggering multiple times - let key = tab.id+'-'+frameId; - let deferred = this.injectTranslationScripts[key]; - if (deferred) { - Zotero.debug(`Connector_Browser.injectTranslationScripts: Script injection already in progress for ${key} : ${tab.url}`); - return deferred.promise; - } - deferred = Zotero.Promise.defer(); - this.injectTranslationScripts[key] = deferred; - deferred.promise.catch(function(e) { - Zotero.debug(`Connector_Browser.injectTranslationScripts: Script injection rejected ${key}`); - Zotero.logError(e); - }).then(function() { - delete Zotero.Connector_Browser.injectTranslationScripts[key]; - }); - - Zotero.Messaging.sendMessage('ping', null, tab, frameId).then(function(response) { - if (response && frameId == 0) return deferred.resolve(); - Zotero.debug(`Injecting translation scripts into ${frameId} ${tab.url}`); - return Zotero.Connector_Browser.injectScripts(_injectTranslationScripts, tab, frameId) - .then(deferred.resolve).catch(deferred.reject); - }); - return deferred.promise; - }; - - /** - * Injects custom scripts - * - * @param scripts {Object[]} array of scripts to inject - * @param tab {Object} - * @param [frameId=0] {Number] Defaults to top frame - * @returns {Promise} A promise that resolves when all scripts have been injected - */ - this.injectScripts = async function(scripts, tab, frameId=0) { - function* injectScripts() { - if (! Array.isArray(scripts)) scripts = [scripts]; - // Make sure we're not changing the original list - scripts = Array.from(scripts); - Zotero.debug(`Injecting scripts into ${tab.url} : ${scripts.join(', ')}`); - let timedOut = false; - - for (let script of scripts) { - // Firefox returns an error for unstructured data being returned from scripts - // We are forced to catch these, even though when sometimes they may be legit errors - yield browser.tabs.executeScript(tab.id, {file: script, frameId, runAt: 'document_end'}) - .catch(() => undefined); - } - - // Send a ready message to confirm successful injection - let readyMsg = `ready${Date.now()}`; - yield browser.tabs.executeScript(tab.id, { - code: `browser.runtime.onMessage.addListener(function awaitReady(request) { - if (request == '${readyMsg}') { - browser.runtime.onMessage.removeListener(awaitReady); - return Promise.resolve(true); - } - })`, - frameId, - runAt: 'document_end' - }); - - while (true) { - try { - var response = yield browser.tabs.sendMessage(tab.id, readyMsg, {frameId: frameId}); - } catch (e) {} - if (!response) { - yield Zotero.Promise.delay(100); - } else { - Zotero.debug(`Injection complete ${frameId} : ${tab.url}`); - return true; - } - } - } - var timedOut = Zotero.Promise.defer(); - let timeout = setTimeout(function() { - timedOut.reject(new Error (`Script injection timed out ${tab.id}-${frameId}`)) - }, 5000); - - var urlChanged = Zotero.Promise.defer(); - function urlChangeListener(tabID, changeInfo, changeTab) { - if (tabID != tab.id || (changeInfo && changeTab.url == tab.url)) return; - urlChanged.reject(new Error(`Url changed mid-injection into ${tab.id}-${frameId}`)) - } - browser.tabs.onRemoved.addListener(urlChangeListener); - browser.tabs.onUpdated.addListener(urlChangeListener); - - // This is a bit complex, but we need to cut off script injection as soon as we notice an - // interruption condition, such as a timeout or url change, otherwise we get partial injections - try { - var iter = injectScripts(); - var val = iter.next(); - while (true) { - if (val.done) { - return val.value; - } - if (val.value.then) { - // Will either throw from the first two, or return from the third one - let nextVal = await Promise.race([timedOut.promise, urlChanged.promise, val.value]); - val = iter.next(nextVal); - } else { - val = iter.next(val.value); - } - } - } finally { - browser.tabs.onRemoved.removeListener(urlChangeListener); - browser.tabs.onUpdated.removeListener(urlChangeListener); - clearTimeout(timeout); - } - }; - this.openTab = function(url, tab) { if (tab) { let tabProps = { index: tab.index + 1 }; @@ -379,74 +262,10 @@ Zotero.Connector_Browser = new function() { /** * Update status and tooltip of Zotero button */ - function _updateExtensionUI(tab) { - if (Zotero.Prefs.get('firstUse') && Zotero.isFirefox) return _showFirstUseUI(tab); - browser.contextMenus.removeAll(); + this.updateExtensionUI = function(tab) { + Zotero.Extension.Button.update(tab, _tabInfo[tab.id]); + Zotero.Extension.ContextMenu.update(tab, _tabInfo[tab.id]); - if (_isDisabledForURL(tab.url, true)) { - _showZoteroStatus(); - return; - } else { - _enableForTab(tab.id); - } - - var isPDF = _tabInfo[tab.id] && _tabInfo[tab.id].isPDF; - var translators = _tabInfo[tab.id] && _tabInfo[tab.id].translators; - - // Show the save menu if we have more than one save option to show, which is true in all cases - // other than for PDFs with no translator - var showSaveMenu = (translators && translators.length) || !isPDF; - var showProxyMenu = !isPDF - && _getProxiesForURL(tab.url).length > 0 - // Don't show proxy menu if already proxied - && !Zotero.Proxies.proxyToProper(tab.url, true); - - var saveMenuID; - if (showSaveMenu) { - saveMenuID = "zotero-context-menu-save-menu"; - browser.contextMenus.create({ - id: saveMenuID, - title: "Save to Zotero", - contexts: ['all'] - }); - } - - if (translators && translators.length) { - _showTranslatorIcon(tab, translators[0]); - _showTranslatorContextMenuItem(translators, saveMenuID); - } else if (isPDF) { - Zotero.Connector_Browser._showPDFIcon(tab); - } else { - _showWebpageIcon(tab); - } - - if (isPDF) { - _showPDFContextMenuItem(saveMenuID); - } else { - _showWebpageContextMenuItem(saveMenuID); - } - - // If unproxied, show "Reload via Proxy" options - if (showProxyMenu) { - _showProxyContextMenuItems(tab.url); - } - - if (Zotero.isFirefox) { - _showPreferencesContextMenuItem(); - } - } - - function _showFirstUseUI(tab) { - var icon = `${Zotero.platform}/zotero-z-${window.devicePixelRatio > 1 ? 32 : 16}px-australis.png`; - browser.browserAction.setIcon({ - tabId: tab.id, - path: `images/${icon}` - }); - browser.browserAction.setTitle({ - tabId: tab.id, - title: "Zotero Connector" - }); - browser.browserAction.enable(tab.id); } /** @@ -460,211 +279,10 @@ Zotero.Connector_Browser = new function() { delete _tabInfo[tabID]; } - function _isDisabledForURL(url, excludeTests=false) { + this.isDisabledForURL = function(url, excludeTests=false) { return url.includes('chrome://') || url.includes('about:') || (url.includes('-extension://') && (!excludeTests || !url.includes('/test/data/'))); } - - function _showZoteroStatus(tabID) { - Zotero.Connector.checkIsOnline().then(function(isOnline) { - var icon, title; - if (isOnline) { - icon = "images/zotero-new-z-16px.png"; - title = "Zotero is Online"; - } else { - icon = "images/zotero-z-16px-offline.png"; - title = "Zotero is Offline"; - } - browser.browserAction.setIcon({ - tabId:tabID, - path:icon - }); - - browser.browserAction.setTitle({ - tabId:tabID, - title: title - }); - }); - browser.browserAction.disable(tabID); - browser.contextMenus.removeAll(); - } - - function _enableForTab(tabID) { - browser.browserAction.enable(tabID); - } - - function _showTranslatorIcon(tab, translator) { - var itemType = translator.itemType; - - browser.browserAction.setIcon({ - tabId:tab.id, - path:(itemType === "multiple" - ? "images/treesource-collection.png" - : Zotero.ItemTypes.getImageSrc(itemType)) - }); - - browser.browserAction.setTitle({ - tabId:tab.id, - title: _getTranslatorLabel(translator) - }); - } - - function _showWebpageIcon(tab) { - browser.browserAction.setIcon({ - tabId: tab.id, - path: Zotero.ItemTypes.getImageSrc("webpage-gray") - }); - let withSnapshot = Zotero.Connector.isOnline ? Zotero.Connector.automaticSnapshots : - Zotero.Prefs.get('automaticSnapshots'); - let title = `Save to Zotero (Web Page ${withSnapshot ? 'with' : 'without'} Snapshot)`; - browser.browserAction.setTitle({tabId: tab.id, title}); - } - - this._showPDFIcon = function(tab) { - browser.browserAction.setIcon({ - tabId: tab.id, - path: browser.extension.getURL('images/pdf.png') - }); - browser.browserAction.setTitle({ - tabId: tab.id, - title: "Save to Zotero (PDF)" - }); - } - - function _showTranslatorContextMenuItem(translators, parentID) { - for (var i = 0; i < translators.length; i++) { - browser.contextMenus.create({ - id: "zotero-context-menu-translator-save" + i, - title: _getTranslatorLabel(translators[i]), - onclick: (function (i) { - return function (info, tab) { - Zotero.Connector_Browser.saveWithTranslator(tab, i); - }; - })(i), - parentId: parentID, - contexts: ['page', 'browser_action'] - }); - } - } - - function _showWebpageContextMenuItem(parentID) { - var fns = []; - fns.push(() => browser.contextMenus.create({ - id: "zotero-context-menu-webpage-withSnapshot-save", - title: "Save to Zotero (Web Page with Snapshot)", - onclick: function (info, tab) { - Zotero.Connector_Browser.saveAsWebpage(tab, 0, true); - }, - parentId: parentID, - contexts: ['page', 'browser_action'] - })); - fns.push(() => browser.contextMenus.create({ - id: "zotero-context-menu-webpage-withoutSnapshot-save", - title: "Save to Zotero (Web Page without Snapshot)", - onclick: function (info, tab) { - Zotero.Connector_Browser.saveAsWebpage(tab, 0, false); - }, - parentId: parentID, - contexts: ['page', 'browser_action'] - })); - // Swap order if automatic snapshots disabled - let withSnapshot = Zotero.Connector.isOnline ? Zotero.Connector.automaticSnapshots : - Zotero.Prefs.get('automaticSnapshots'); - if (!withSnapshot) { - fns = [fns[1], fns[0]]; - } - fns.forEach((fn) => fn()); - } - - function _showPDFContextMenuItem(parentID) { - browser.contextMenus.create({ - id: "zotero-context-menu-pdf-save", - title: "Save to Zotero (PDF)", - onclick: function (info, tab) { - Zotero.Connector_Browser.saveAsWebpage(tab); - }, - parentId: parentID, - contexts: ['all'] - }); - } - - function _showProxyContextMenuItems(url) { - var parentID = "zotero-context-menu-proxy-reload-menu"; - browser.contextMenus.create({ - id: parentID, - title: "Reload via Proxy", - contexts: ['page', 'browser_action'] - }); - - var i = 0; - for (let proxy of _getProxiesForURL(url)) { - let name = proxy.toDisplayName({ - includeScheme: true - }); - let proxied = proxy.toProxy(url); - browser.contextMenus.create({ - id: `zotero-context-menu-proxy-reload-${i++}`, - title: `Reload via ${name}`, - onclick: function () { - browser.tabs.update({ url: proxied }); - }, - parentId: parentID, - contexts: ['page', 'browser_action'] - }); - } - } - - /** - * Get the proxies to show for a given URL - * - * This filters the available proxies to skip non-HTTPS proxies for HTTPS URLs - */ - function _getProxiesForURL(url) { - var proxies = Zotero.Proxies.proxies; - // If not an HTTPS site, return all proxies - if (!url.startsWith('https:')) { - return proxies; - } - // Otherwise remove non-HTTPS proxies - return proxies.filter(proxy => proxy.scheme.startsWith('https:')); - } - - function _showPreferencesContextMenuItem() { - browser.contextMenus.create({ - type: "separator", - id: "zotero-context-menu-pref-separator", - contexts: ['all'] - }); - browser.contextMenus.create({ - id: "zotero-context-menu-preferences", - title: "Preferences", - onclick: function () { - browser.tabs.create({url: browser.extension.getURL('preferences/preferences.html')}); - }, - contexts: ['all'] - }); - } - - function _browserAction(tab) { - if (Zotero.Prefs.get('firstUse') && Zotero.isFirefox) { - Zotero.Messaging.sendMessage("firstUse", null, tab) - .then(function () { - Zotero.Prefs.set('firstUse', false); - _updateExtensionUI(tab); - }); - } - else if(_tabInfo[tab.id] && _tabInfo[tab.id].translators && _tabInfo[tab.id].translators.length) { - Zotero.Connector_Browser.saveWithTranslator(tab, 0); - } else { - if (_tabInfo[tab.id] && _tabInfo[tab.id].isPDF) { - Zotero.Connector_Browser.saveAsWebpage(tab, _tabInfo[tab.id].frameId, true); - } else { - let withSnapshot = Zotero.Connector.isOnline ? Zotero.Connector.automaticSnapshots : - Zotero.Prefs.get('automaticSnapshots'); - Zotero.Connector_Browser.saveAsWebpage(tab, 0, withSnapshot); - } - } - } - + this.saveWithTranslator = function(tab, i) { // Set frameId to null - send message to all frames // There is code to figure out which frame should translate with instanceID. @@ -674,28 +292,10 @@ Zotero.Connector_Browser = new function() { ], tab, null); } - this.saveAsWebpage = function(tab, frameId, withSnapshot) { - if (tab.id != -1) { - return Zotero.Messaging.sendMessage("saveAsWebpage", [tab.title, withSnapshot], tab, frameId); - } - // Handle right-click on PDF overlay, which exists in a weird non-tab state - else { - browser.tabs.query( - { - lastFocusedWindow: true, - active: true - }).then(function (tabs) { - Zotero.Messaging.sendMessage("saveAsWebpage", tabs[0].title, tabs[0]); - } - ); - } - } - - function _getTranslatorLabel(translator) { - var translatorName = translator.label; - return "Save to Zotero (" + translatorName + ")"; + this.saveAsWebpage = function(tab, frameId) { + return Zotero.Messaging.sendMessage("saveAsWebpage", tab.title, tab, frameId); } - + Zotero.Messaging.addMessageListener("selectDone", function(data) { _tabInfo[data[0]].selectCallback(data[1]); }); @@ -706,7 +306,7 @@ Zotero.Connector_Browser = new function() { // Ignore item selector if (tab.url.indexOf(browser.extension.getURL("itemSelector/itemSelector.html")) === 0) return; _clearInfoForTab(tabID, changeInfo); - _updateExtensionUI(tab); + Zotero.Connector_Browser.updateExtensionUI(tab); if(!changeInfo.url) return; Zotero.debug("Connector_Browser: URL changed for tab " + tab.url); // Rerun translation @@ -724,7 +324,9 @@ Zotero.Connector_Browser = new function() { }).catch((e) => {Zotero.logError(e); throw(e)}); }); - browser.browserAction.onClicked.addListener(_browserAction); + browser.browserAction.onClicked.addListener(function(tab) { + Zotero.Extension.Button.onClick(tab, _tabInfo[tab.id]); + }); browser.webNavigation.onDOMContentLoaded.addListener(function(details) { return browser.tabs.get(details.tabId).then(function(tab) { diff --git a/src/browserExt/manifest.json b/src/browserExt/manifest.json index 7a975ff139..4bbeb2c9f0 100644 --- a/src/browserExt/manifest.json +++ b/src/browserExt/manifest.json @@ -15,7 +15,12 @@ "webRequest", "webRequestBlocking", "webNavigation", "storage"], "background": { "scripts": [ + "browser-polyfill.js", /*BACKGROUND SCRIPTS*/, + "webRequestIntercept.js", + "contentTypeHandler.js", + "native-ui.js", + "script-injection.js", "background.js" ] }, diff --git a/src/browserExt/messaging_inject.js b/src/browserExt/messaging_inject.js index 28d48abfb8..6e96ffcb6b 100644 --- a/src/browserExt/messaging_inject.js +++ b/src/browserExt/messaging_inject.js @@ -34,7 +34,7 @@ Zotero.Messaging = new function() { * Add a message listener */ this.addMessageListener = function(messageName, callback) { - _messageListeners[messageName] = Zotero.Promise.method(callback); + _messageListeners[messageName] = callback; } /** @@ -106,15 +106,31 @@ Zotero.Messaging = new function() { browser.runtime.onMessage.addListener(function(request, sender) { if (typeof request !== "object" || !request.length || !_messageListeners[request[0]]) return; Zotero.debug(request[0] + " message received in injected page " + window.location.href); - return _messageListeners[request[0]](request[1]).catch(function(err) { - Zotero.logError(err); - err = JSON.stringify(Object.assign({ - name: err.name, - message: err.message, - stack: err.stack - }, err)); - return ['error', err] - }); + try { + var maybePromise = _messageListeners[request[0]](request[1]); + } catch (err) { + return Zotero.Messaging._respondError(err); + } + if (maybePromise && maybePromise.then) { + return maybePromise.catch(Zotero.Messaging._respondError); + } + // Discrepancy between browser-polyfill.js where returning non-undefined responds + // and Firefox where returning non-undefined non-promise means that + // arguments[2] (sendResponse) will be called asynchronously + if (Zotero.isFirefox && maybePromise != undefined) { + return Zotero.Promise.resolve(maybePromise); + } + return maybePromise }); - } + }; + + this._respondError = async function(err) { + Zotero.logError(err); + err = JSON.stringify(Object.assign({ + name: err.name, + message: err.message, + stack: err.stack + }, err)); + return ['error', err] + }; } \ No newline at end of file diff --git a/src/browserExt/native-ui.js b/src/browserExt/native-ui.js new file mode 100644 index 0000000000..acf8bd4c8c --- /dev/null +++ b/src/browserExt/native-ui.js @@ -0,0 +1,281 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2017 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +(function() { + +"use strict"; + +window.Zotero.Extension = window.Zotero.Extension || {}; + +function _getTranslatorLabel(translator) { + var translatorName = translator.label ? ` (${translator.label})` : ''; + return `Save to Zotero${translatorName}`; +} + +Zotero.Extension.Button = { + update: function(tab, tabInfo={}) { + browser.browserAction.enable(tab.id); + if (Zotero.Prefs.get('firstUse') && Zotero.isFirefox) return this.showFirstUseUI(tab); + + if (Zotero.Connector_Browser.isDisabledForURL(tab.url, true)) { + this.showZoteroStatus(); + return; + } + + if (tabInfo.isPDF) { + this.showPDFIcon(tab); + } else if (tabInfo.translators && tabInfo.translators.length) { + this.showTranslatorIcon(tab, tabInfo.translators[0]); + } else { + // Not clear what to show while refreshing. Not changing anything is probably fine? + } + }, + + onClick: async function(tab, tabInfo={}) { + if (Zotero.Prefs.get('firstUse') && Zotero.isFirefox) { + // Wait for response (i.e. click) before setting firstUse to false. + await Zotero.Messaging.sendMessage("firstUse", null, tab); + Zotero.Prefs.set('firstUse', false); + Zotero.Connector_Browser.updateExtensionUI(tab); + } + else if(tabInfo.translators && tabInfo.translators.length) { + Zotero.Connector_Browser.saveWithTranslator(tab, 0); + } + else if (tabInfo.isPDF) { + Zotero.Connector_Browser.saveAsWebpage(tab, tabInfo.frameId, true); + } + // Nothing occurs on click if there's no tabInfo stuff + }, + + showZoteroStatus: function(tabID) { + Zotero.Connector.checkIsOnline().then(function(isOnline) { + var icon, title; + if (isOnline) { + icon = "images/zotero-new-z-16px.png"; + title = "Zotero is Online"; + } else { + icon = "images/zotero-z-16px-offline.png"; + title = "Zotero is Offline"; + } + browser.browserAction.setIcon({ + tabId:tabID, + path:icon + }); + + browser.browserAction.setTitle({ + tabId:tabID, + title: title + }); + }); + browser.browserAction.disable(tabID); + browser.contextMenus.removeAll(); + }, + + showFirstUseUI: function(tab) { + var icon = `${Zotero.platform}/zotero-z-${window.devicePixelRatio > 1 ? 32 : 16}px-australis.png`; + browser.browserAction.setIcon({ + tabId: tab.id, + path: `images/${icon}` + }); + browser.browserAction.setTitle({ + tabId: tab.id, + title: "Zotero Connector" + }); + browser.browserAction.enable(tab.id); + }, + + showTranslatorIcon: function(tab, translator) { + var itemType = translator.itemType; + + browser.browserAction.setIcon({ + tabId:tab.id, + path:(itemType === "multiple" + ? "images/treesource-collection.png" + : Zotero.ItemTypes.getImageSrc(itemType)) + }); + + browser.browserAction.setTitle({ + tabId:tab.id, + title: _getTranslatorLabel(translator) + }); + }, + + showPDFIcon: function(tab) { + browser.browserAction.setIcon({ + tabId: tab.id, + path: browser.extension.getURL('images/pdf.png') + }); + browser.browserAction.setTitle({ + tabId: tab.id, + title: "Save to Zotero (PDF)" + }); + } +}; + +Zotero.Extension.ContextMenu = { + update: function(tab, tabInfo={}) { + browser.contextMenus.removeAll(); + + if (!Zotero.Connector_Browser.isDisabledForURL(tab.url, true)) { + var showSaveMenu = tabInfo.isPDF || tabInfo.translators; + if (showSaveMenu) { + var saveMenuID; + saveMenuID = "zotero-context-menu-save-menu"; + browser.contextMenus.create({ + id: saveMenuID, + title: "Save to Zotero", + contexts: ['all'] + }); + + if (tabInfo.isPDF) { + this.addPDFOption(saveMenuID); + } else if (tabInfo.translators) { + this.addTranslatorOptions(tabInfo.translators, saveMenuID); + this.addAttachmentCheckboxOptions(saveMenuID); + } + } + + var showProxyMenu = !tabInfo.isPDF + && this._getProxiesForURL(tab.url).length > 0 + // Don't show proxy menu if already proxied + && !Zotero.Proxies.proxyToProper(tab.url, true); + + // If unproxied, show "Reload via Proxy" options + if (showProxyMenu) { + this.addProxyOptions(tab.url); + } + } + + if (Zotero.isFirefox) { + this.addPreferencesOption(); + } + }, + + addTranslatorOptions: function(translators, parentID) { + for (let i = 0; i < translators.length; i++) { + browser.contextMenus.create({ + id: "zotero-context-menu-translator-save" + i, + title: _getTranslatorLabel(translators[i]), + onclick: function(info, tab) { + Zotero.Connector_Browser.saveWithTranslator(tab, i); + }, + parentId: parentID, + contexts: ['page', 'browser_action'] + }); + } + }, + + addPDFOption: function(parentID) { + browser.contextMenus.create({ + id: "zotero-context-menu-pdf-save", + title: "Save to Zotero (PDF)", + onclick: function(info, tab) { + Zotero.Connector_Browser.saveAsWebpage(tab); + }, + parentId: parentID, + contexts: ['all'] + }); + }, + + addAttachmentCheckboxOptions: function(parentID) { + let includeSnapshots = Zotero.Prefs.get("automaticSnapshots"); + browser.contextMenus.create({ + type: "separator", + id: "zotero-context-menu-attachment-options-separator", + parentId: parentID, + contexts: ['all'] + }); + browser.contextMenus.create({ + type: 'checkbox', + id: "zotero-context-menu-snapshots-checkbox", + title: "Include Snapshots", + checked: includeSnapshots, + onclick: function(info) { + Zotero.Prefs.set('automaticSnapshots', info.checked); + }, + parentId: parentID, + contexts: ['all'] + }); + }, + + addProxyOptions: function(url) { + var parentID = "zotero-context-menu-proxy-reload-menu"; + browser.contextMenus.create({ + id: parentID, + title: "Reload via Proxy", + contexts: ['page', 'browser_action'] + }); + + var i = 0; + for (let proxy of this._getProxiesForURL(url)) { + let name = proxy.toDisplayName({ + includeScheme: true + }); + let proxied = proxy.toProxy(url); + browser.contextMenus.create({ + id: `zotero-context-menu-proxy-reload-${i++}`, + title: `Reload via ${name}`, + onclick: function() { + browser.tabs.update({ url: proxied }); + }, + parentId: parentID, + contexts: ['page', 'browser_action'] + }); + } + }, + + /** + * Get the proxies to show for a given URL + * + * This filters the available proxies to skip non-HTTPS proxies for HTTPS URLs + */ + _getProxiesForURL: function(url) { + var proxies = Zotero.Proxies.proxies; + // If not an HTTPS site, return all proxies + if (!url.startsWith('https:')) { + return proxies; + } + // Otherwise remove non-HTTPS proxies + return proxies.filter(proxy => proxy.scheme.startsWith('https:')); + }, + + addPreferencesOption: function() { + browser.contextMenus.create({ + type: "separator", + id: "zotero-context-menu-pref-separator", + contexts: ['all'] + }); + browser.contextMenus.create({ + id: "zotero-context-menu-preferences", + title: "Preferences", + onclick: function() { + browser.tabs.create({url: browser.extension.getURL('preferences/preferences.html')}); + }, + contexts: ['all'] + }); + }, +}; + +})(); diff --git a/src/browserExt/script-injection.js b/src/browserExt/script-injection.js new file mode 100644 index 0000000000..4b493fad12 --- /dev/null +++ b/src/browserExt/script-injection.js @@ -0,0 +1,149 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2017 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.Extension = window.Zotero.Extension || {}; + +Zotero.Extension.ScriptInjection = { + + _translationScriptList: [ + /*INJECT SCRIPTS*/ + ], + /** + * Checks whether translation scripts are already injected into a frame and if not - injects + * @param tab {Object} + * @param [frameId=0] {Number] Defaults to top frame + * @returns {Promise} A promise that resolves when all scripts have been injected + */ + injectTranslationScripts: async function(tab, frameId=0) { + // Prevent triggering multiple times + let key = tab.id+'-'+frameId; + let deferred = this.injectTranslationScripts[key]; + if (deferred) { + Zotero.debug(`Extension.ScriptInjection.injectTranslationScripts: Script injection already in progress for ${key} : ${tab.url}`); + return deferred.promise; + } + deferred = Zotero.Promise.defer(); + this.injectTranslationScripts[key] = deferred; + try { + let response = await Zotero.Messaging.sendMessage('ping', null, tab, frameId); + if (!response || frameId != 0) { + Zotero.debug(`Injecting translation scripts into ${frameId} ${tab.url}`); + await this.injectScripts(this._translationScriptList, tab, frameId); + } + } catch(e) { + Zotero.debug(`Extension.ScriptInjection.injectTranslationScripts: Script injection rejected ${key}`); + Zotero.logError(e); + } finally { + delete this.injectTranslationScripts[key]; + } + deferred.resolve(); + return deferred.promise; + }, + + /** + * Injects custom scripts + * + * @param scripts {Object[]} array of scripts to inject + * @param tab {Object} + * @param [frameId=0] {Number] Defaults to top frame + * @returns {Promise} A promise that resolves when all scripts have been injected + */ + injectScripts: async function(scripts, tab, frameId=0) { + function* injectScripts() { + if (! Array.isArray(scripts)) scripts = [scripts]; + // Make sure we're not changing the original list + scripts = Array.from(scripts); + Zotero.debug(`Injecting scripts into ${tab.url} : ${scripts.join(', ')}`); + let timedOut = false; + + for (let script of scripts) { + // Firefox returns an error for unstructured data being returned from scripts + // We are forced to catch these, even though when sometimes they may be legit errors + yield browser.tabs.executeScript(tab.id, {file: script, frameId, runAt: 'document_end'}) + .catch(() => undefined); + } + + // Send a ready message to confirm successful injection + let readyMsg = `ready${Date.now()}`; + yield browser.tabs.executeScript(tab.id, { + code: `browser.runtime.onMessage.addListener(function awaitReady(request) { + if (request == '${readyMsg}') { + browser.runtime.onMessage.removeListener(awaitReady); + return Promise.resolve(true); + } + })`, + frameId, + runAt: 'document_end' + }); + + while (true) { + try { + var response = yield browser.tabs.sendMessage(tab.id, readyMsg, {frameId: frameId}); + } catch (e) {} + if (!response) { + yield Zotero.Promise.delay(100); + } else { + Zotero.debug(`Injection complete ${frameId} : ${tab.url}`); + return true; + } + } + } + var timedOut = Zotero.Promise.defer(); + let timeout = setTimeout(function() { + timedOut.reject(new Error (`Script injection timed out ${tab.id}-${frameId}`)) + }, 5000); + + var urlChanged = Zotero.Promise.defer(); + function urlChangeListener(tabID, changeInfo, changeTab) { + if (tabID != tab.id || (changeInfo && changeTab.url == tab.url)) return; + urlChanged.reject(new Error(`Url changed mid-injection into ${tab.id}-${frameId}`)) + } + browser.tabs.onRemoved.addListener(urlChangeListener); + browser.tabs.onUpdated.addListener(urlChangeListener); + + // This is a bit complex, but we need to cut off script injection as soon as we notice an + // interruption condition, such as a timeout or url change, otherwise we get partial injections + try { + var iter = injectScripts(); + var val = iter.next(); + while (true) { + if (val.done) { + return val.value; + } + if (val.value.then) { + // Will either throw from the first two, or return from the third one + let nextVal = await Promise.race([timedOut.promise, urlChanged.promise, val.value]); + val = iter.next(nextVal); + } else { + val = iter.next(val.value); + } + } + } finally { + browser.tabs.onRemoved.removeListener(urlChangeListener); + browser.tabs.onUpdated.removeListener(urlChangeListener); + clearTimeout(timeout); + } + } +}; diff --git a/src/browserExt/test/testSetup.js b/src/browserExt/test/testSetup.js index 91c8bbe83d..280bd309e7 100644 --- a/src/browserExt/test/testSetup.js +++ b/src/browserExt/test/testSetup.js @@ -106,8 +106,8 @@ Zotero.initDeferred.promise.then(function() { } let scripts = [ 'lib/sinon.js', 'test/testSetup.js' ]; - return Zotero.Connector_Browser.injectTranslationScripts(tab).then(function() { - return Zotero.Connector_Browser.injectScripts(scripts, tab); + return Zotero.Extension.ScriptInjection.injectTranslationScripts(tab).then(function() { + return Zotero.Extension.ScriptInjection.injectScripts(scripts, tab); }).then(function() { deferred.resolved = true; deferred.resolve(tabId); diff --git a/src/common/connector.js b/src/common/connector.js index 4718564625..7e0af9b52b 100644 --- a/src/common/connector.js +++ b/src/common/connector.js @@ -150,7 +150,6 @@ Zotero.Connector = new function() { return Zotero.Connector.callMethod("ping", payload).then(function(response) { if (response && 'prefs' in response) { Zotero.Connector.shouldReportActiveURL = !!response.prefs.reportActiveURL; - Zotero.Connector.automaticSnapshots = !!response.prefs.automaticSnapshots; } return response || {}; }); diff --git a/src/common/inject/inject.jsx b/src/common/inject/inject.jsx index 3487f85a39..7e44c754c7 100644 --- a/src/common/inject/inject.jsx +++ b/src/common/inject/inject.jsx @@ -104,116 +104,6 @@ if(isTopWindow) { Zotero.Inject = new function() { var _translate; this.translators = {}; - - /** - * Initializes the translate machinery and determines whether this page can be translated - */ - this.init = function(force) { - // On OAuth completion, close window and call completion listener - if(document.location.href.substr(0, ZOTERO_CONFIG.OAUTH_CALLBACK_URL.length+1) === ZOTERO_CONFIG.OAUTH_CALLBACK_URL+"?") { - Zotero.API.onAuthorizationComplete(document.location.href.substr(ZOTERO_CONFIG.OAUTH_CALLBACK_URL.length+1)); - return; - } - - // wrap this in try/catch so that errors will reach logError - try { - if(this.translators.length) { - if(force) { - this.translators = []; - } else { - return; - } - } - if(document.location == "about:blank") return; - - if(!_translate) { - _translate = new Zotero.Translate.Web(); - _translate.setHandler("select", function(obj, items, callback) { - // If the handler returns a non-undefined value then it is passed - // back to the callback due to backwards compat code in translate.js - (async function() { - try { - let response = await Zotero.Connector.callMethod("getSelectedCollection", {}); - if (response.libraryEditable === false) { - return callback([]); - } - } catch (e) { - // Zotero is online but an error occured anyway, so let's log it and display - // the dialog just in case - if (e.status != 0) { - Zotero.logError(e); - } - } - Zotero.Connector_Browser.onSelect(items).then(function(returnItems) { - // if no items selected, close save dialog immediately - if(!returnItems || Zotero.Utilities.isEmpty(returnItems)) { - Zotero.Messaging.sendMessage("progressWindow.close", null); - } - callback(returnItems); - }); - })(); - }); - _translate.setHandler("itemSaving", function(obj, item) { - // this relays an item from this tab to the top level of the window - Zotero.Messaging.sendMessage("progressWindow.itemSaving", - [Zotero.ItemTypes.getImageSrc(item.itemType), item.title, item.id]); - }); - _translate.setHandler("itemDone", function(obj, dbItem, item) { - // this relays an item from this tab to the top level of the window - Zotero.Messaging.sendMessage("progressWindow.itemProgress", - [Zotero.ItemTypes.getImageSrc(item.itemType), item.title, item.id, 100]); - for(var i=0; i
-
Save to Zotero.org
+
Saving
-
-

Zotero Connector must be authorized in order to save items to zotero.org when Zotero is not open.

-

-
-
diff --git a/src/common/preferences/preferences.jsx b/src/common/preferences/preferences.jsx index cb2dccf061..6075157aad 100644 --- a/src/common/preferences/preferences.jsx +++ b/src/common/preferences/preferences.jsx @@ -159,8 +159,8 @@ Zotero_Preferences.General = { updateAuthorization: function(userInfo) { document.getElementById('general-authorization-not-authorized').style.display = (userInfo ? 'none' : 'block'); document.getElementById('general-authorization-authorized').style.display = (!userInfo ? 'none' : 'block'); - if(userInfo) { - document.getElementById('general-span-authorization-username').textContent = userInfo.username; + if (userInfo) { + document.getElementById('general-span-authorization-username').textContent = userInfo['auth-username']; } }, diff --git a/src/common/test/tests/backgroundTest.js b/src/common/test/tests/backgroundTest.js index e7384bbf5b..93eee2853c 100644 --- a/src/common/test/tests/backgroundTest.js +++ b/src/common/test/tests/backgroundTest.js @@ -23,7 +23,11 @@ ***** END LICENSE BLOCK ***** */ -describe('Connector_Browser', function() { +/** + * https://github.com/zotero/zotero-connectors/pull/193 effectively reverts + * https://github.com/zotero/zotero-connectors/issues/152 + */ +describe.skip('Connector_Browser', function() { var tab = new Tab(); describe('onPDFFrame', function() { @@ -31,7 +35,11 @@ describe('Connector_Browser', function() { try { let bgPromise = background(function() { Zotero.Prefs.set('firstUse', false); - let stub = sinon.stub(Zotero.Connector_Browser, '_showPDFIcon'); + if (Zotero.isBrowserExt) { + var stub = sinon.stub(Zotero.Extension.Button, 'showPDFIcon'); + } else { + stub = sinon.stub(Zotero.Connector_Browser, '_showPDFIcon'); + } var deferred = Zotero.Promise.defer(); stub.callsFake(deferred.resolve); @@ -50,16 +58,20 @@ describe('Connector_Browser', function() { await bgPromise; let tabId = await background(async function(tabId) { if (Zotero.isBrowserExt) { - return Zotero.Connector_Browser._showPDFIcon.args[0][0].id; + return Zotero.Extension.Button.showPDFIcon.args[0][0].id; } else { return (await Zotero.Background.getTabByID(tabId)).isPDFFrame ? tabId : -1; } }, tab.tabId); assert.equal(tabId, tab.tabId); } finally { - await background(function() { - Zotero.Connector_Browser._showPDFIcon.restore() - }); + await background(function () { + if (Zotero.isBrowserExt) { + Zotero.Extension.Button.showPDFIcon.restore() + } else { + Zotero.Connector_Browser._showPDFIcon.restore(); + } + }); if (tab.tabId) { await tab.close(); } diff --git a/src/common/test/tests/translationTest.js b/src/common/test/tests/translationTest.js index 52bfd96dc6..9e6fe40b5b 100644 --- a/src/common/test/tests/translationTest.js +++ b/src/common/test/tests/translationTest.js @@ -56,7 +56,7 @@ describe("Translation", function() { return Zotero.Inject.translators[key].metadata.label; }); }); - assert.deepEqual(['COinS', 'DOI'], translators); + assert.deepEqual(['COinS', 'DOI', 'Embedded Metadata'], translators); }); }); @@ -119,25 +119,6 @@ describe("Translation", function() { }); assert.include(message, items[0].title); })); - - it('saves as snapshot', async function () { - await background(async function(tabId) { - var stub = sinon.stub(Zotero.Connector, "callMethodWithCookies").resolves([]); - let tab = await Zotero.Background.getTabByID(tabId); - try { - await Zotero.Connector_Browser.saveAsWebpage(tab, false); - } finally { - stub.restore() - } - }, tab.tabId); - await Zotero.Promise.delay(20); - var message = await tab.run(function() { - var message = document.getElementById('zotero-progress-window').textContent; - Zotero.ProgressWindow.close(); - return message; - }); - assert.include(message, "Scarcity or Abundance? Preserving the Past in a Digital Era"); - }); it('displays an error message if Zotero responds with an error', async function () { await background(async function(tabId) { @@ -285,7 +266,7 @@ describe("Translation", function() { }, tab.tabId); assert.notEqual(instanceID, 0); - assert.deepEqual(['COinS', 'DOI'], translators); + assert.deepEqual(['COinS', 'DOI', 'Web Page'], translators); } finally { await tab.close(); } diff --git a/src/common/translate_item.js b/src/common/translate_item.js index 46332ecebb..95dd968bb7 100644 --- a/src/common/translate_item.js +++ b/src/common/translate_item.js @@ -88,13 +88,16 @@ Zotero.Translate.ItemSaver.prototype = { * save progress. The callback will be called as attachmentCallback(attachment, false, error) * on failure or attachmentCallback(attachment, progressPercent) periodically during saving. */ - saveItems: function (items, attachmentCallback) { + saveItems: async function (items, attachmentCallback) { + items = await this._filterAttachments(items); + // first try to save items via connector var payload = { items, uri: this._baseURI }; if (Zotero.isSafari) { // This is the best in terms of cookies we can do in Safari payload.cookie = document.cookie; } + payload.proxy = this._proxy && this._proxy.toJSON(); return Zotero.Connector.callMethodWithCookies("saveItems", payload).then(function(data) { Zotero.debug("Translate: Save via Standalone succeeded"); @@ -123,6 +126,32 @@ Zotero.Translate.ItemSaver.prototype = { throw e; }.bind(this)); }, + + /** + * Filters away attachments from items per user prefs + * + * @param items + * @returns {Promise.} + * @private + */ + _filterAttachments: async function(items) { + let prefs = await Zotero.Prefs.getAsync(["downloadAssociatedFiles", "automaticSnapshots"]); + for (let item of items) { + item.attachments = item.attachments.filter(function(attachment) { + let isSnapshot = false; + if (attachment.mimeType) { + switch (attachment.mimeType.toLowerCase()) { + case "text/html": + case "application/xhtml+xml": + isSnapshot = true; + } + } + + return (isSnapshot && prefs.automaticSnapshots) || (!isSnapshot && prefs.downloadAssociatedFiles); + }); + } + return items; + }, /** * Polls for updates to attachment progress @@ -258,11 +287,6 @@ Zotero.Translate.ItemSaver.prototype = { isSnapshot = true; } } - - if ((isSnapshot && !prefs.automaticSnapshots) || (!isSnapshot && !prefs.downloadAssociatedFiles)) { - // Skip attachment due to prefs - continue; - } let deferredHeadersProcessed = Zotero.Promise.defer(); let itemKeyPromise = deferredHeadersProcessed.promise diff --git a/src/common/zotero.js b/src/common/zotero.js index 7948909593..be386780a1 100644 --- a/src/common/zotero.js +++ b/src/common/zotero.js @@ -244,7 +244,7 @@ Zotero.Prefs = new function() { "debug.time": false, "lastVersion": "", "downloadAssociatedFiles": true, - "automaticSnapshots": true, // only affects saves to zotero.org. saves to client governed by pref in the client + "automaticSnapshots": true, "connector.repo.lastCheck.localTime": 0, "connector.repo.lastCheck.repoTime": 0, "connector.url": ZOTERO_CONFIG.CONNECTOR_SERVER_URL,