From eb15f873b5a7984ca9333ae6ce54badb8a0de36a Mon Sep 17 00:00:00 2001 From: xiangyu <3170102889@zju.edu.cn> Date: Thu, 29 Sep 2022 18:00:38 +0800 Subject: [PATCH] change: proxy handler --- addon/chrome.manifest | 3 - addon/components/zotero-protocol-handler.js | 1658 ------------------- src/events.ts | 22 + 3 files changed, 22 insertions(+), 1661 deletions(-) delete mode 100644 addon/components/zotero-protocol-handler.js diff --git a/addon/chrome.manifest b/addon/chrome.manifest index 58afe66..3533a05 100644 --- a/addon/chrome.manifest +++ b/addon/chrome.manifest @@ -3,7 +3,4 @@ skin __addonRef__ default chrome/skin/default/__addonRef__/ locale __addonRef__ en-US chrome/locale/en-US/ locale __addonRef__ zh-CN chrome/locale/zh-CN/ -component {9BC3D762-9038-486A-9D70-C997AF848A7D} components/zotero-protocol-handler.js -contract @mozilla.org/network/protocol;1?name=zotero {9BC3D762-9038-486A-9D70-C997AF848A7D} - overlay chrome://zotero/content/zoteroPane.xul chrome://__addonRef__/content/overlay.xul diff --git a/addon/components/zotero-protocol-handler.js b/addon/components/zotero-protocol-handler.js deleted file mode 100644 index 13e74fa..0000000 --- a/addon/components/zotero-protocol-handler.js +++ /dev/null @@ -1,1658 +0,0 @@ -/* - ***** BEGIN LICENSE BLOCK ***** - - Copyright © 2009 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 . - - - Based on nsChromeExtensionHandler example code by Ed Anuff at - http://kb.mozillazine.org/Dev_:_Extending_the_Chrome_Protocol - - ***** END LICENSE BLOCK ***** -*/ - -/* - This file is modified based on the Zotero zotero-protocol-handler.js. - Keep in sync with official file. -*/ - -const ZOTERO_SCHEME = "zotero"; -const ZOTERO_PROTOCOL_CID = Components.ID( - "{9BC3D762-9038-486A-9D70-C997AF848A7D}" -); -const ZOTERO_PROTOCOL_CONTRACTID = - "@mozilla.org/network/protocol;1?name=" + ZOTERO_SCHEME; -const ZOTERO_PROTOCOL_NAME = "Zotero Chrome Extension Protocol"; - -Components.utils.import("resource://gre/modules/Services.jsm"); -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -Components.utils.import("resource://gre/modules/NetUtil.jsm"); -Components.utils.import("resource://gre/modules/osfile.jsm"); - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const ios = Services.io; - -// Dummy chrome URL used to obtain a valid chrome channel -const DUMMY_CHROME_URL = "chrome://zotero/content/zoteroPane.xul"; - -var Zotero = Components.classes["@zotero.org/Zotero;1"].getService( - Components.interfaces.nsISupports -).wrappedJSObject; - -function ZoteroProtocolHandler() { - this.wrappedJSObject = this; - this._principal = null; - this._extensions = {}; - - /** - * zotero://attachment/library/[itemKey] - * zotero://attachment/groups/[groupID]/[itemKey] - */ - var AttachmentExtension = { - loadAsChrome: false, - - newChannel: function (uri) { - return new AsyncChannel( - uri, - function* () { - try { - var uriPath = uri.pathQueryRef; - if (!uriPath) { - return this._errorChannel("Invalid URL"); - } - uriPath = uriPath.substr("//attachment/".length); - - var params = {}; - var router = new Zotero.Router(params); - router.add("library/items/:itemKey", function () { - params.libraryID = Zotero.Libraries.userLibraryID; - }); - router.add("groups/:groupID/items/:itemKey"); - router.run(uriPath); - - if (params.groupID) { - params.libraryID = Zotero.Groups.getLibraryIDFromGroupID( - params.groupID - ); - } - if (!params.itemKey) { - return this._errorChannel("Item key not provided"); - } - var item = yield Zotero.Items.getByLibraryAndKeyAsync( - params.libraryID, - params.itemKey - ); - - if (!item) { - return this._errorChannel(`No item found for ${uriPath}`); - } - if (!item.isFileAttachment()) { - return this._errorChannel( - `Item for ${uriPath} is not a file attachment` - ); - } - - var path = yield item.getFilePathAsync(); - if (!path) { - return this._errorChannel(`${path} not found`); - } - - // Set originalURI so that it seems like we're serving from zotero:// protocol. - // This is necessary to allow url() links to work from within CSS files. - // Otherwise they try to link to files on the file:// protocol, which isn't allowed. - this.originalURI = uri; - - return Zotero.File.pathToFile(path); - } catch (e) { - return this._errorChannel(e.message); - } - }.bind(this) - ); - }, - - _errorChannel: function (msg) { - Zotero.logError(msg); - this.status = Components.results.NS_ERROR_FAILURE; - this.contentType = "text/plain"; - return msg; - }, - }; - - /** - * zotero://data/library/collection/ABCD1234/items?sort=itemType&direction=desc - * zotero://data/groups/12345/collection/ABCD1234/items?sort=title&direction=asc - */ - var DataExtension = { - loadAsChrome: false, - - newChannel: function (uri) { - return new AsyncChannel(uri, function* () { - this.contentType = "text/plain"; - - path = uri.spec.match(/zotero:\/\/[^/]+(.*)/)[1]; - - try { - return Zotero.Utilities.Internal.getAsyncInputStream( - Zotero.API.Data.getGenerator(path) - ); - } catch (e) { - if (e instanceof Zotero.Router.InvalidPathException) { - return "URL could not be parsed"; - } - } - }); - }, - }; - - /* - * Report generation extension for Zotero protocol - */ - var ReportExtension = { - loadAsChrome: false, - - newChannel: function (uri) { - return new AsyncChannel(uri, function* () { - var userLibraryID = Zotero.Libraries.userLibraryID; - - var path = uri.pathQueryRef; - if (!path) { - return "Invalid URL"; - } - path = path.substr("//report/".length); - - // Proxy CSS files - if (path.endsWith(".css")) { - var chromeURL = "chrome://zotero/skin/report/" + path; - Zotero.debug(chromeURL); - let uri = ios.newURI(chromeURL, null, null); - var chromeReg = Components.classes[ - "@mozilla.org/chrome/chrome-registry;1" - ].getService(Components.interfaces.nsIChromeRegistry); - return chromeReg.convertChromeURL(uri); - } - - var params = { - objectType: "item", - format: "html", - sort: "title", - }; - var router = new Zotero.Router(params); - - // Items within a collection or search - router.add("library/:scopeObject/:scopeObjectKey/items", function () { - params.libraryID = userLibraryID; - }); - router.add("groups/:groupID/:scopeObject/:scopeObjectKey/items"); - - // All items - router.add("library/items/:objectKey", function () { - params.libraryID = userLibraryID; - }); - router.add("groups/:groupID/items"); - - // Old-style URLs - router.add("collection/:id/html/report.html", function () { - params.scopeObject = "collections"; - var lkh = Zotero.Collections.parseLibraryKeyHash(params.id); - if (lkh) { - params.libraryID = lkh.libraryID || userLibraryID; - params.scopeObjectKey = lkh.key; - } else { - params.scopeObjectID = params.id; - } - delete params.id; - }); - router.add("search/:id/html/report.html", function () { - params.scopeObject = "searches"; - var lkh = Zotero.Searches.parseLibraryKeyHash(this.id); - if (lkh) { - params.libraryID = lkh.libraryID || userLibraryID; - params.scopeObjectKey = lkh.key; - } else { - params.scopeObjectID = this.id; - } - delete params.id; - }); - router.add("items/:ids/html/report.html", function () { - var ids = this.ids.split("-"); - params.libraryID = ids[0].split("_")[0] || userLibraryID; - params.itemKey = ids.map((x) => x.split("_")[1]); - delete params.ids; - }); - - var parsed = router.run(path); - if (!parsed) { - return "URL could not be parsed"; - } - - // TODO: support old URLs - // collection - // search - // items - // item - if (params.sort.indexOf("/") != -1) { - let parts = params.sort.split("/"); - params.sort = parts[0]; - params.direction = parts[1] == "d" ? "desc" : "asc"; - } - - try { - Zotero.API.parseParams(params); - var results = yield Zotero.API.getResultsFromParams(params); - } catch (e) { - Zotero.debug(e, 1); - return e.toString(); - } - - var mimeType, - content = ""; - var items = []; - var itemsHash = {}; // key = itemID, val = position in |items| - var searchItemIDs = new Set(); // All selected items - var searchParentIDs = new Set(); // Parents of selected child items - var searchChildIDs = new Set(); // Selected chlid items - - var includeAllChildItems = Zotero.Prefs.get( - "report.includeAllChildItems" - ); - var combineChildItems = Zotero.Prefs.get("report.combineChildItems"); - - var unhandledParents = {}; - for (var i = 0; i < results.length; i++) { - // Don't add child items directly - // (instead mark their parents for inclusion below) - var parentItemID = results[i].parentItemID; - if (parentItemID) { - searchParentIDs.add(parentItemID); - searchChildIDs.add(results[i].id); - - // Don't include all child items if any child - // items were selected - includeAllChildItems = false; - } - // If combining children or standalone note/attachment, add matching parents - else if ( - combineChildItems || - !results[i].isRegularItem() || - results[i].numChildren() == 0 - ) { - itemsHash[results[i].id] = [items.length]; - items.push(results[i].toJSON({ mode: "full" })); - // Flag item as a search match - items[items.length - 1].reportSearchMatch = true; - } else { - unhandledParents[i] = true; - } - searchItemIDs.add(results[i].id); - } - - // If including all child items, add children of all matched - // parents to the child array - if (includeAllChildItems) { - for (let id of searchItemIDs) { - if (!searchChildIDs.has(id)) { - var children = []; - var item = yield Zotero.Items.getAsync(id); - if (!item.isRegularItem()) { - continue; - } - var func = function (ids) { - if (ids) { - for (var i = 0; i < ids.length; i++) { - searchChildIDs.add(ids[i]); - } - } - }; - func(item.getNotes()); - func(item.getAttachments()); - } - } - } - // If not including all children, add matching parents, - // in case they don't have any matching children below - else { - for (var i in unhandledParents) { - itemsHash[results[i].id] = [items.length]; - items.push(results[i].toJSON({ mode: "full" })); - // Flag item as a search match - items[items.length - 1].reportSearchMatch = true; - } - } - - if (combineChildItems) { - // Add parents of matches if parents aren't matches themselves - for (let id of searchParentIDs) { - if (!searchItemIDs.has(id) && !itemsHash[id]) { - var item = yield Zotero.Items.getAsync(id); - itemsHash[id] = items.length; - items.push(item.toJSON({ mode: "full" })); - } - } - - // Add children to reportChildren property of parents - for (let id of searchChildIDs) { - let item = yield Zotero.Items.getAsync(id); - var parentID = item.parentID; - if (!items[itemsHash[parentID]].reportChildren) { - items[itemsHash[parentID]].reportChildren = { - notes: [], - attachments: [], - }; - } - if (item.isNote()) { - items[itemsHash[parentID]].reportChildren.notes.push( - item.toJSON({ mode: "full" }) - ); - } - if (item.isAttachment()) { - items[itemsHash[parentID]].reportChildren.attachments.push( - item.toJSON({ mode: "full" }) - ); - } - } - } - // If not combining children, add a parent/child pair - // for each matching child - else { - for (let id of searchChildIDs) { - var item = yield Zotero.Items.getAsync(id); - var parentID = item.parentID; - var parentItem = Zotero.Items.get(parentID); - - if (!itemsHash[parentID]) { - // If parent is a search match and not yet added, - // add on its own - if (searchItemIDs.has(parentID)) { - itemsHash[parentID] = [items.length]; - items.push(parentItem.toJSON({ mode: "full" })); - items[items.length - 1].reportSearchMatch = true; - } else { - itemsHash[parentID] = []; - } - } - - // Now add parent and child - itemsHash[parentID].push(items.length); - items.push(parentItem.toJSON({ mode: "full" })); - if (item.isNote()) { - items[items.length - 1].reportChildren = { - notes: [item.toJSON({ mode: "full" })], - attachments: [], - }; - } else if (item.isAttachment()) { - items[items.length - 1].reportChildren = { - notes: [], - attachments: [item.toJSON({ mode: "full" })], - }; - } - } - } - - // Sort items - // TODO: restore multiple sort fields - var sorts = [ - { - field: params.sort, - order: params.direction != "desc" ? 1 : -1, - }, - ]; - - var collation = Zotero.getLocaleCollation(); - var compareFunction = function (a, b) { - var index = 0; - - // Multidimensional sort - do { - // In combineChildItems, use note or attachment as item - if (!combineChildItems) { - if (a.reportChildren) { - if (a.reportChildren.notes.length) { - a = a.reportChildren.notes[0]; - } else { - a = a.reportChildren.attachments[0]; - } - } - - if (b.reportChildren) { - if (b.reportChildren.notes.length) { - b = b.reportChildren.notes[0]; - } else { - b = b.reportChildren.attachments[0]; - } - } - } - - var valA, valB; - - if (sorts[index].field == "title") { - // For notes, use content for 'title' - if (a.itemType == "note") { - valA = a.note; - } else { - valA = a.title; - } - - if (b.itemType == "note") { - valB = b.note; - } else { - valB = b.title; - } - - valA = Zotero.Items.getSortTitle(valA); - valB = Zotero.Items.getSortTitle(valB); - } else if (sorts[index].field == "date") { - var itemA = Zotero.Items.getByLibraryAndKey( - params.libraryID, - a.key - ); - var itemB = Zotero.Items.getByLibraryAndKey( - params.libraryID, - b.key - ); - valA = itemA.getField("date", true, true); - valB = itemB.getField("date", true, true); - } - // TEMP: This is an ugly hack to make creator sorting - // slightly less broken. To do this right, real creator - // sorting needs to be abstracted from itemTreeView.js. - else if (sorts[index].field == "firstCreator") { - var itemA = Zotero.Items.getByLibraryAndKey( - params.libraryID, - a.key - ); - var itemB = Zotero.Items.getByLibraryAndKey( - params.libraryID, - b.key - ); - valA = itemA.getField("firstCreator"); - valB = itemB.getField("firstCreator"); - } else { - valA = a[sorts[index].field]; - valB = b[sorts[index].field]; - } - - // Put empty values last - if (!valA && valB) { - var cmp = 1; - } else if (valA && !valB) { - var cmp = -1; - } else { - var cmp = collation.compareString(0, valA, valB); - } - - var result = 0; - if (cmp != 0) { - result = cmp * sorts[index].order; - } - index++; - } while (result == 0 && sorts[index]); - - return result; - }; - - items.sort(compareFunction); - for (var i in items) { - if (items[i].reportChildren) { - items[i].reportChildren.notes.sort(compareFunction); - items[i].reportChildren.attachments.sort(compareFunction); - } - } - - // Pass off to the appropriate handler - switch (params.format) { - case "rtf": - this.contentType = "text/rtf"; - return ""; - - case "csv": - this.contentType = "text/plain"; - return ""; - - default: - this.contentType = "text/html"; - return Zotero.Utilities.Internal.getAsyncInputStream( - Zotero.Report.HTML.listGenerator( - items, - combineChildItems, - params.libraryID - ), - function () { - Zotero.logError(e); - return 'Error generating report'; - } - ); - } - }); - }, - }; - - /** - * Generate MIT SIMILE Timeline - * - * Query string key abbreviations: intervals = i - * dateType = t - * timelineDate = d - * - * interval abbreviations: day = d | month = m | year = y | decade = e | century = c | millennium = i - * dateType abbreviations: date = d | dateAdded = da | dateModified = dm - * timelineDate format: shortMonthName.day.year (year is positive for A.D. and negative for B.C.) - * - * Defaults: intervals = month, year, decade - * dateType = date - * timelineDate = today's date - */ - var TimelineExtension = { - loadAsChrome: true, - - newChannel: function (uri) { - return new AsyncChannel(uri, function* () { - var userLibraryID = Zotero.Libraries.userLibraryID; - - var path = uri.spec.match(/zotero:\/\/[^/]+(.*)/)[1]; - if (!path) { - this.contentType = "text/html"; - return "Invalid URL"; - } - - var params = {}; - var router = new Zotero.Router(params); - - // HTML - router.add("library/:scopeObject/:scopeObjectKey", function () { - params.libraryID = userLibraryID; - params.controller = "html"; - }); - router.add("groups/:groupID/:scopeObject/:scopeObjectKey", function () { - params.controller = "html"; - }); - router.add("library", function () { - params.libraryID = userLibraryID; - params.controller = "html"; - }); - router.add("groups/:groupID", function () { - params.controller = "html"; - }); - - // Data - router.add("data/library/:scopeObject/:scopeObjectKey", function () { - params.libraryID = userLibraryID; - params.controller = "data"; - }); - router.add( - "data/groups/:groupID/:scopeObject/:scopeObjectKey", - function () { - params.controller = "data"; - } - ); - router.add("data/library", function () { - params.libraryID = userLibraryID; - params.controller = "data"; - }); - router.add("data/groups/:groupID", function () { - params.controller = "data"; - }); - - // Old-style HTML URLs - router.add("collection/:id", function () { - params.controller = "html"; - params.scopeObject = "collections"; - var lkh = Zotero.Collections.parseLibraryKeyHash(params.id); - if (lkh) { - params.libraryID = lkh.libraryID || userLibraryID; - params.scopeObjectKey = lkh.key; - } else { - params.scopeObjectID = params.id; - } - delete params.id; - }); - router.add("search/:id", function () { - params.controller = "html"; - params.scopeObject = "searches"; - var lkh = Zotero.Searches.parseLibraryKeyHash(params.id); - if (lkh) { - params.libraryID = lkh.libraryID || userLibraryID; - params.scopeObjectKey = lkh.key; - } else { - params.scopeObjectID = params.id; - } - delete params.id; - }); - router.add("/", function () { - params.controller = "html"; - params.libraryID = userLibraryID; - }); - - var parsed = router.run(path); - if (!parsed) { - this.contentType = "text/html"; - return "URL could not be parsed"; - } - if (params.groupID) { - params.libraryID = Zotero.Groups.getLibraryIDFromGroupID( - params.groupID - ); - } - - var intervals = params.i ? params.i : ""; - var timelineDate = params.d ? params.d : ""; - var dateType = params.t ? params.t : ""; - - // Get the collection or search object - var collection, search; - switch (params.scopeObject) { - case "collections": - if (params.scopeObjectKey) { - collection = yield Zotero.Collections.getByLibraryAndKeyAsync( - params.libraryID, - params.scopeObjectKey - ); - } else { - collection = yield Zotero.Collections.getAsync( - params.scopeObjectID - ); - } - if (!collection) { - this.contentType = "text/html"; - return "Invalid collection ID or key"; - } - break; - - case "searches": - if (params.scopeObjectKey) { - var s = yield Zotero.Searches.getByLibraryAndKeyAsync( - params.libraryID, - params.scopeObjectKey - ); - } else { - var s = yield Zotero.Searches.getAsync(params.scopeObjectID); - } - if (!s) { - return "Invalid search ID or key"; - } - - // FIXME: Hack to exclude group libraries for now - var search = new Zotero.Search(); - search.setScope(s); - var groups = Zotero.Groups.getAll(); - for (let group of groups) { - search.addCondition("libraryID", "isNot", group.libraryID); - } - break; - } - - // - // Create XML file - // - if (params.controller == "data") { - switch (params.scopeObject) { - case "collections": - var results = collection.getChildItems(); - break; - - case "searches": - var ids = yield search.search(); - var results = yield Zotero.Items.getAsync(ids); - break; - - default: - if (params.scopeObject) { - return "Invalid scope object '" + params.scopeObject + "'"; - } - - let s = new Zotero.Search(); - s.addCondition("libraryID", "is", params.libraryID); - s.addCondition("noChildren", "true"); - var ids = yield s.search(); - var results = yield Zotero.Items.getAsync(ids); - } - - var items = []; - // Only include parent items - for (let i = 0; i < results.length; i++) { - if (!results[i].parentItemID) { - items.push(results[i]); - } - } - - var dateTypes = { - d: "date", - da: "dateAdded", - dm: "dateModified", - }; - - //default dateType = date - if (!dateType || !dateTypes[dateType]) { - dateType = "d"; - } - - this.contentType = "application/xml"; - return Zotero.Utilities.Internal.getAsyncInputStream( - Zotero.Timeline.generateXMLDetails(items, dateTypes[dateType]) - ); - } - - // - // Generate main HTML page - // - var content = Zotero.File.getContentsFromURL( - "chrome://zotero/skin/timeline/timeline.html" - ); - this.contentType = "text/html"; - - if (!timelineDate) { - timelineDate = Date(); - var dateParts = timelineDate.toString().split(" "); - timelineDate = dateParts[1] + "." + dateParts[2] + "." + dateParts[3]; - } - if (!intervals || intervals.length < 3) { - intervals += "mye".substr(intervals.length); - } - - var theIntervals = { - d: "Timeline.DateTime.DAY", - m: "Timeline.DateTime.MONTH", - y: "Timeline.DateTime.YEAR", - e: "Timeline.DateTime.DECADE", - c: "Timeline.DateTime.CENTURY", - i: "Timeline.DateTime.MILLENNIUM", - }; - - //sets the intervals of the timeline bands - var tempStr = ' x.id)); - } - }), - - newChannel: function (uri) { - this.doAction(uri); - }, - }; - - /* - zotero://debug/ - */ - var DebugExtension = { - loadAsChrome: false, - - newChannel: function (uri) { - return new AsyncChannel(uri, function* () { - this.contentType = "text/plain"; - - try { - return Zotero.Debug.get(); - } catch (e) { - Zotero.debug(e, 1); - throw e; - } - }); - }, - }; - - var ConnectorChannel = function (uri, data) { - var secMan = Components.classes[ - "@mozilla.org/scriptsecuritymanager;1" - ].getService(Components.interfaces.nsIScriptSecurityManager); - - this.name = uri; - this.URI = ios.newURI(uri, "UTF-8", null); - this.owner = ( - secMan.getCodebasePrincipal || secMan.getSimpleCodebasePrincipal - )(this.URI); - this._isPending = true; - - var converter = Components.classes[ - "@mozilla.org/intl/scriptableunicodeconverter" - ].createInstance(Components.interfaces.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - this._stream = converter.convertToInputStream(data); - this.contentLength = this._stream.available(); - }; - - ConnectorChannel.prototype.contentCharset = "UTF-8"; - ConnectorChannel.prototype.contentType = "text/html"; - ConnectorChannel.prototype.notificationCallbacks = null; - ConnectorChannel.prototype.securityInfo = null; - ConnectorChannel.prototype.status = 0; - ConnectorChannel.prototype.loadGroup = null; - ConnectorChannel.prototype.loadFlags = 393216; - - ConnectorChannel.prototype.__defineGetter__("originalURI", function () { - return this.URI; - }); - ConnectorChannel.prototype.__defineSetter__("originalURI", function () {}); - - ConnectorChannel.prototype.asyncOpen = function (streamListener, context) { - if (this.loadGroup) this.loadGroup.addRequest(this, null); - streamListener.onStartRequest(this, context); - streamListener.onDataAvailable( - this, - context, - this._stream, - 0, - this.contentLength - ); - streamListener.onStopRequest(this, context, this.status); - this._isPending = false; - if (this.loadGroup) this.loadGroup.removeRequest(this, null, 0); - }; - - ConnectorChannel.prototype.isPending = function () { - return this._isPending; - }; - - ConnectorChannel.prototype.cancel = function (status) { - this.status = status; - this._isPending = false; - if (this._stream) this._stream.close(); - }; - - ConnectorChannel.prototype.suspend = function () {}; - - ConnectorChannel.prototype.resume = function () {}; - - ConnectorChannel.prototype.open = function () { - return this._stream; - }; - - ConnectorChannel.prototype.QueryInterface = function (iid) { - if ( - !iid.equals(Components.interfaces.nsIChannel) && - !iid.equals(Components.interfaces.nsIRequest) && - !iid.equals(Components.interfaces.nsISupports) - ) { - throw Components.results.NS_ERROR_NO_INTERFACE; - } - return this; - }; - - /** - * zotero://connector/ - * - * URI spoofing for transferring page data across boundaries - */ - var ConnectorExtension = new (function () { - this.loadAsChrome = false; - - this.newChannel = function (uri) { - var secMan = Components.classes[ - "@mozilla.org/scriptsecuritymanager;1" - ].getService(Components.interfaces.nsIScriptSecurityManager); - var Zotero = Components.classes["@zotero.org/Zotero;1"].getService( - Components.interfaces.nsISupports - ).wrappedJSObject; - - try { - var originalURI = uri.pathQueryRef.substr("zotero://connector/".length); - originalURI = decodeURIComponent(originalURI); - if (!Zotero.Server.Connector.Data[originalURI]) { - return null; - } else { - return new ConnectorChannel( - originalURI, - Zotero.Server.Connector.Data[originalURI] - ); - } - } catch (e) { - Zotero.debug(e); - throw e; - } - }; - })(); - - /* - zotero://pdf.js/viewer.html - zotero://pdf.js/pdf/1/ABCD5678 - */ - var PDFJSExtension = { - loadAsChrome: true, - - newChannel: function (uri) { - return new AsyncChannel( - uri, - function* () { - try { - uri = uri.spec; - // Proxy PDF.js files - if ( - uri.startsWith("zotero://pdf.js/") && - !uri.startsWith("zotero://pdf.js/pdf/") - ) { - uri = uri.replace( - /zotero:\/\/pdf.js\//, - "resource://zotero/pdf.js/" - ); - let newURI = Services.io.newURI(uri, null, null); - return this.getURIInputStream(newURI); - } - - // Proxy attachment PDFs - var pdfPrefix = "zotero://pdf.js/pdf/"; - if (!uri.startsWith(pdfPrefix)) { - return this._errorChannel("File not found"); - } - var [libraryID, key] = uri.substr(pdfPrefix.length).split("/"); - libraryID = parseInt(libraryID); - - var item = yield Zotero.Items.getByLibraryAndKeyAsync( - libraryID, - key - ); - if (!item) { - return this._errorChannel("Item not found"); - } - var path = yield item.getFilePathAsync(); - if (!path) { - return this._errorChannel("File not found"); - } - return this.getURIInputStream(OS.Path.toFileURI(path)); - } catch (e) { - Zotero.debug(e, 1); - throw e; - } - }.bind(this) - ); - }, - - getURIInputStream: function (uri) { - return new Zotero.Promise((resolve, reject) => { - NetUtil.asyncFetch(uri, function (inputStream, result) { - if (!Components.isSuccessCode(result)) { - // TODO: Handle error - return; - } - resolve(inputStream); - }); - }); - }, - - _errorChannel: function (msg) { - this.status = Components.results.NS_ERROR_FAILURE; - this.contentType = "text/plain"; - return msg; - }, - }; - - /** - * Open a PDF at a given page (or try to) - * - * zotero://open-pdf/library/items/[itemKey]?page=[page] - * zotero://open-pdf/groups/[groupID]/items/[itemKey]?page=[page] - * - * Also supports ZotFile format: - * zotero://open-pdf/[libraryID]_[key]/[page] - */ - var OpenPDFExtension = { - noContent: true, - - doAction: async function (uri) { - var userLibraryID = Zotero.Libraries.userLibraryID; - - var uriPath = uri.pathQueryRef; - if (!uriPath) { - return "Invalid URL"; - } - uriPath = uriPath.substr("//open-pdf/".length); - var mimeType, - content = ""; - - var params = { - objectType: "item", - }; - var router = new Zotero.Router(params); - - // All items - router.add("library/items/:objectKey", function () { - params.libraryID = userLibraryID; - }); - router.add("groups/:groupID/items/:objectKey"); - - // ZotFile URLs - router.add(":id/:page", function () { - var lkh = Zotero.Items.parseLibraryKeyHash(params.id); - if (!lkh) { - Zotero.warn(`Invalid URL ${url}`); - return; - } - params.libraryID = lkh.libraryID || userLibraryID; - params.objectKey = lkh.key; - delete params.id; - }); - router.run(uriPath); - - Zotero.API.parseParams(params); - var results = await Zotero.API.getResultsFromParams(params); - var page = params.page; - if (parseInt(page) != page) { - page = null; - } - var annotation = params.annotation; - - if (!results.length) { - Zotero.warn(`No item found for ${uriPath}`); - return; - } - - var item = results[0]; - - if (!item.isFileAttachment()) { - Zotero.warn(`Item for ${uriPath} is not a file attachment`); - return; - } - - var path = await item.getFilePathAsync(); - if (!path) { - Zotero.warn(`${path} not found`); - return; - } - - if ( - !path.toLowerCase().endsWith(".pdf") && - Zotero.MIME.sniffForMIMEType(await Zotero.File.getSample(path)) != - "application/pdf" - ) { - Zotero.warn(`${path} is not a PDF`); - return; - } - - var opened = false; - if (page || annotation) { - try { - opened = await Zotero.OpenPDF.openToPage(item, page, annotation); - } catch (e) { - Zotero.logError(e); - } - } - - // If something went wrong, just open PDF without page - if (!opened) { - Zotero.debug("Launching PDF without page number"); - let zp = Zotero.getActiveZoteroPane(); - // TODO: Open pane if closed (macOS) - if (zp) { - zp.viewAttachment([item.id]); - } - return; - } - Zotero.Notifier.trigger("open", "file", item.id); - }, - - newChannel: function (uri) { - this.doAction(uri); - }, - }; - - var openNoteExtension = { - noContent: true, - - doAction: async function (uri) { - let message = { - type: "onNoteLink", - content: { - params: await Zotero.Knowledge4Zotero.knowledge.getNoteFromLink( - uri.spec - ), - }, - }; - - Zotero.Knowledge4Zotero.events.onEditorEvent.bind( - Zotero.Knowledge4Zotero.events - )(message); - }, - - newChannel: function (uri) { - this.doAction(uri); - }, - }; - - this._extensions[ZOTERO_SCHEME + "://attachment"] = AttachmentExtension; - this._extensions[ZOTERO_SCHEME + "://data"] = DataExtension; - this._extensions[ZOTERO_SCHEME + "://report"] = ReportExtension; - this._extensions[ZOTERO_SCHEME + "://timeline"] = TimelineExtension; - this._extensions[ZOTERO_SCHEME + "://select"] = SelectExtension; - this._extensions[ZOTERO_SCHEME + "://debug"] = DebugExtension; - this._extensions[ZOTERO_SCHEME + "://connector"] = ConnectorExtension; - this._extensions[ZOTERO_SCHEME + "://pdf.js"] = PDFJSExtension; - this._extensions[ZOTERO_SCHEME + "://open-pdf"] = OpenPDFExtension; - - this._extensions[ZOTERO_SCHEME + "://note"] = openNoteExtension; -} - -/* - * Implements nsIProtocolHandler - */ -ZoteroProtocolHandler.prototype = { - scheme: ZOTERO_SCHEME, - - defaultPort: -1, - - protocolFlags: - Components.interfaces.nsIProtocolHandler.URI_NORELATIVE | - Components.interfaces.nsIProtocolHandler.URI_NOAUTH | - // DEBUG: This should be URI_IS_LOCAL_FILE, and MUST be if any - // extensions that modify data are added - // - https://www.zotero.org/trac/ticket/1156 - // - Components.interfaces.nsIProtocolHandler.URI_IS_LOCAL_FILE, - //Components.interfaces.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE, - - allowPort: function (port, scheme) { - return false; - }, - - getExtension: function (uri) { - let uriString = uri; - if (uri instanceof Components.interfaces.nsIURI) { - uriString = uri.spec; - } - uriString = uriString.toLowerCase(); - - for (let extSpec in this._extensions) { - if (uriString.startsWith(extSpec)) { - return this._extensions[extSpec]; - } - } - - return false; - }, - - newURI: function (spec, charset, baseURI) { - // A temporary workaround because baseURI.resolve(spec) just returns spec - if (baseURI) { - if (!spec.includes("://") && baseURI.spec.includes("/pdf.js/")) { - let parts = baseURI.spec.split("/"); - parts.pop(); - parts.push(spec); - spec = parts.join("/"); - } - } - - return Components.classes["@mozilla.org/network/simple-uri-mutator;1"] - .createInstance(Components.interfaces.nsIURIMutator) - .setSpec(spec) - .finalize(); - }, - - newChannel: function (uri) { - var chromeService = Components.classes[ - "@mozilla.org/network/protocol;1?name=chrome" - ].getService(Components.interfaces.nsIProtocolHandler); - - var newChannel = null; - - try { - let ext = this.getExtension(uri); - - if (!ext) { - // Return cancelled channel for unknown paths - // - // These can be in the form zotero://example.com/... -- maybe for "//example.com" URLs? - var chromeURI = chromeService.newURI(DUMMY_CHROME_URL, null, null); - var extChannel = chromeService.newChannel(chromeURI); - var chromeRequest = extChannel.QueryInterface( - Components.interfaces.nsIRequest - ); - chromeRequest.cancel(0x804b0002); // BINDING_ABORTED - return extChannel; - } - - if (!this._principal && ext.loadAsChrome) { - this._principal = Services.scriptSecurityManager.getSystemPrincipal(); - } - - var extChannel = ext.newChannel(uri); - // Extension returned null, so cancel request - if (!extChannel) { - var chromeURI = chromeService.newURI(DUMMY_CHROME_URL, null, null); - var extChannel = chromeService.newChannel(chromeURI); - var chromeRequest = extChannel.QueryInterface( - Components.interfaces.nsIRequest - ); - chromeRequest.cancel(0x804b0002); // BINDING_ABORTED - } - - // Apply cached principal to extension channel - if (this._principal) { - extChannel.owner = this._principal; - } - - if (!extChannel.originalURI) extChannel.originalURI = uri; - - return extChannel; - } catch (e) { - Components.utils.reportError(e); - Zotero.debug(e, 1); - throw Components.results.NS_ERROR_FAILURE; - } - - return newChannel; - }, - - contractID: ZOTERO_PROTOCOL_CONTRACTID, - classDescription: ZOTERO_PROTOCOL_NAME, - classID: ZOTERO_PROTOCOL_CID, - QueryInterface: XPCOMUtils.generateQI([ - Components.interfaces.nsISupports, - Components.interfaces.nsIProtocolHandler, - ]), -}; - -/** - * nsIChannel implementation that takes a promise-yielding generator that returns a - * string, nsIAsyncInputStream, or file - */ -function AsyncChannel(uri, gen) { - this._generator = gen; - this._isPending = true; - - // nsIRequest - this.name = uri; - this.loadFlags = 0; - this.loadGroup = null; - this.status = 0; - - // nsIChannel - this.contentLength = -1; - this.contentType = "text/html"; - this.contentCharset = "utf-8"; - this.URI = uri; - this.originalURI = uri; - this.owner = null; - this.notificationCallbacks = null; - this.securityInfo = null; -} - -AsyncChannel.prototype = { - asyncOpen: Zotero.Promise.coroutine(function* (streamListener, context) { - if (this.loadGroup) this.loadGroup.addRequest(this, null); - - var channel = this; - - var resolve; - var reject; - var promise = new Zotero.Promise(function () { - resolve = arguments[0]; - reject = arguments[1]; - }); - - var listenerWrapper = { - onStartRequest: function (request, context) { - //Zotero.debug("Starting request"); - streamListener.onStartRequest(channel, context); - }, - onDataAvailable: function (request, context, inputStream, offset, count) { - //Zotero.debug("onDataAvailable"); - streamListener.onDataAvailable( - channel, - context, - inputStream, - offset, - count - ); - }, - onStopRequest: function (request, context, status) { - //Zotero.debug("Stopping request"); - streamListener.onStopRequest(channel, context, status); - channel._isPending = false; - if (status == 0) { - resolve(); - } else { - reject( - new Error("AsyncChannel request failed with status " + status) - ); - } - }, - }; - - //Zotero.debug("AsyncChannel's asyncOpen called"); - var t = new Date(); - - var data; - try { - if (!data) { - data = yield Zotero.spawn(channel._generator, channel); - } - if (typeof data == "string") { - //Zotero.debug("AsyncChannel: Got string from generator"); - - listenerWrapper.onStartRequest(this, context); - - let converter = Components.classes[ - "@mozilla.org/intl/scriptableunicodeconverter" - ].createInstance(Components.interfaces.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - let inputStream = converter.convertToInputStream(data); - listenerWrapper.onDataAvailable( - this, - context, - inputStream, - 0, - inputStream.available() - ); - - listenerWrapper.onStopRequest(this, context, this.status); - } - // If an async input stream is given, pass the data asynchronously to the stream listener - else if (data instanceof Ci.nsIAsyncInputStream) { - //Zotero.debug("AsyncChannel: Got input stream from generator"); - - var pump = Cc[ - "@mozilla.org/network/input-stream-pump;1" - ].createInstance(Ci.nsIInputStreamPump); - try { - pump.init(data, 0, 0, true); - } catch (e) { - pump.init(data, -1, -1, 0, 0, true); - } - pump.asyncRead(listenerWrapper, context); - } else if (data instanceof Ci.nsIFile || data instanceof Ci.nsIURI) { - if (data instanceof Ci.nsIFile) { - //Zotero.debug("AsyncChannel: Got file from generator"); - data = ios.newFileURI(data); - } else { - //Zotero.debug("AsyncChannel: Got URI from generator"); - } - - let uri = data; - uri.QueryInterface(Ci.nsIURL); - this.contentType = Zotero.MIME.getMIMETypeFromExtension( - uri.fileExtension - ); - if (!this.contentType) { - let sample = yield Zotero.File.getSample(data); - this.contentType = Zotero.MIME.getMIMETypeFromData(sample); - } - - Components.utils.import("resource://gre/modules/NetUtil.jsm"); - NetUtil.asyncFetch( - { uri: data, loadUsingSystemPrincipal: true }, - function (inputStream, status) { - if (!Components.isSuccessCode(status)) { - reject(); - return; - } - - listenerWrapper.onStartRequest(channel, context); - try { - listenerWrapper.onDataAvailable( - channel, - context, - inputStream, - 0, - inputStream.available() - ); - } catch (e) { - reject(e); - } - listenerWrapper.onStopRequest(channel, context, status); - } - ); - } else if (data === undefined) { - this.cancel(0x804b0002); // BINDING_ABORTED - } else { - throw new Error( - "Invalid return type (" + - typeof data + - ") from generator passed to AsyncChannel" - ); - } - - if (this._isPending) { - //Zotero.debug("AsyncChannel request succeeded in " + (new Date - t) + " ms"); - channel._isPending = false; - } - - return promise; - } catch (e) { - Zotero.debug(e, 1); - if (channel._isPending) { - streamListener.onStopRequest( - channel, - context, - Components.results.NS_ERROR_FAILURE - ); - channel._isPending = false; - } - throw e; - } finally { - if (channel.loadGroup) channel.loadGroup.removeRequest(channel, null, 0); - } - }), - - // nsIRequest - isPending: function () { - return this._isPending; - }, - - cancel: function (status) { - Zotero.debug("Cancelling"); - this.status = status; - this._isPending = false; - }, - - resume: function () { - Zotero.debug("Resuming"); - }, - - suspend: function () { - Zotero.debug("Suspending"); - }, - - // nsIWritablePropertyBag - setProperty: function (prop, val) { - this[prop] = val; - }, - - deleteProperty: function (prop) { - delete this[prop]; - }, - - QueryInterface: function (iid) { - if ( - iid.equals(Components.interfaces.nsISupports) || - iid.equals(Components.interfaces.nsIRequest) || - iid.equals(Components.interfaces.nsIChannel) || - // pdf.js wants this - iid.equals(Components.interfaces.nsIWritablePropertyBag) - ) { - return this; - } - throw Components.results.NS_ERROR_NO_INTERFACE; - }, -}; - -var NSGetFactory = XPCOMUtils.generateNSGetFactory([ZoteroProtocolHandler]); diff --git a/src/events.ts b/src/events.ts index fdb5ebe..fcfbc77 100644 --- a/src/events.ts +++ b/src/events.ts @@ -182,6 +182,7 @@ class AddonEvents extends AddonBase { public async onInit() { Zotero.debug("Knowledge4Zotero: init called"); + this.initProxyHandler(); this.addEditorInstanceListener(); // Register the callback in Zotero as an item observer @@ -221,6 +222,27 @@ class AddonEvents extends AddonBase { this._Addon.sync.setSync(); } + private initProxyHandler() { + const openNoteExtension = { + noContent: true, + doAction: async (uri: any) => { + let message = { + type: "onNoteLink", + content: { + params: await this._Addon.knowledge.getNoteFromLink(uri.spec), + }, + }; + await this._Addon.events.onEditorEvent(message); + }, + newChannel: function (uri: any) { + this.doAction(uri); + }, + }; + Services.io.getProtocolHandler("zotero").wrappedJSObject._extensions[ + "zotero://note" + ] = openNoteExtension; + } + private async initWorkspaceTab() { let state = Zotero.Session.state.windows.find((x) => x.type === "pane"); Zotero.debug("initWorkspaceTab");