/* ***** 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 ***** */ const ZOTERO_SCHEME = "zotero"; const ZOTERO_PROTOCOL_CID = Components.ID("{9BC3D762-9038-486A-9D70-C997AF848A7C}"); 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/ComponentUtils.jsm"); Components.utils.import("resource://gre/modules/osfile.jsm") const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.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/items/[itemKey] * zotero://attachment/groups/[groupID]/items/[itemKey] * * And for snapshot attachments only: * zotero://attachment/library/items/[itemKey]/[resourcePath] * zotero://attachment/groups/[groupID]/items/[itemKey]/[resourcePath] */ var AttachmentExtension = { loadAsChrome: false, newChannel: function (uri, loadInfo) { return new AsyncChannel(uri, loadInfo, 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`); } var resourcePathParts = uriPath.split('/') .slice(params.groupID !== undefined ? 4 : 3) .filter(Boolean); if (resourcePathParts.length) { if (!item.isSnapshotAttachment()) { return this._errorChannel(`Item for ${uriPath} is not a snapshot attachment -- cannot access resources`); } try { path = PathUtils.join(PathUtils.parent(path), ...resourcePathParts); } catch (e) { Zotero.logError(e); return this._errorChannel(`Resource ${resourcePathParts.join('/')} not found`); } if (!(yield IOUtils.exists(path))) { return this._errorChannel(`Resource ${resourcePathParts.join('/')} 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, loadInfo) { return new AsyncChannel(uri, loadInfo, function* () { this.contentType = 'text/plain'; var 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: true, newChannel: function (uri, loadInfo) { return new AsyncChannel(uri, loadInfo, 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; iError 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, loadInfo) { return new AsyncChannel(uri, loadInfo, 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 x.id)); } }), newChannel: function (uri) { this.doAction(uri); } }; /* zotero://debug/ */ var DebugExtension = { loadAsChrome: false, newChannel: function (uri, loadInfo) { return new AsyncChannel(uri, loadInfo, 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) { if(this.loadGroup) this.loadGroup.addRequest(this, null); streamListener.onStartRequest(this); streamListener.onDataAvailable(this, this._stream, 0, this.contentLength); streamListener.onStopRequest(this, 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); } }; 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; } /* * Implements nsIProtocolHandler */ ZoteroProtocolHandler.prototype = { get scheme() { return ZOTERO_SCHEME; }, get 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,*/ return Ci.nsIProtocolHandler.URI_NORELATIVE | Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE // URI_IS_UI_RESOURCE: more secure than URI_LOADABLE_BY_ANYONE, less secure than URI_DANGEROUS_TO_LOAD // This is the security level used by the chrome:// protocol | Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE; }, get defaultPort() { return -1; }, allowPort: function allowPort() { 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, loadInfo) { try { let ext = this.getExtension(uri); // Return cancelled channel for unknown paths if (!ext) { return this._getCancelledChannel(); } var extChannel = ext.newChannel(uri, loadInfo); // Extension returned null, so cancel request if (!extChannel) { return this._getCancelledChannel(); } // Apply cached principal to extension channel if (ext.loadAsChrome) { if (!this._principal) { this._principal = Services.scriptSecurityManager.getSystemPrincipal(); } 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 null; }, _getCancelledChannel: function () { var channel = NetUtil.newChannel({ uri: DUMMY_CHROME_URL, loadUsingSystemPrincipal: true, }) var req = channel.QueryInterface(Components.interfaces.nsIRequest); req.cancel(0x804b0002); // BINDING_ABORTED return channel; }, contractID: ZOTERO_PROTOCOL_CONTRACTID, classDescription: ZOTERO_PROTOCOL_NAME, classID: ZOTERO_PROTOCOL_CID, //QueryInterface: ChromeUtils.generateQI([Components.interfaces.nsIProtocolHandler]) QueryInterface: ChromeUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIProtocolHandler]), }; /** * nsIChannel implementation that takes a promise-yielding generator that returns a * string, nsIAsyncInputStream, or file */ function AsyncChannel(uri, loadInfo, gen) { this.URI = this.originalURI = uri; this.loadInfo = loadInfo; 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) { 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) { //Zotero.debug("Starting request"); streamListener.onStartRequest(channel); }, onDataAvailable: function (request, inputStream, offset, count) { //Zotero.debug("onDataAvailable"); try { streamListener.onDataAvailable(channel, inputStream, offset, count); } catch (e) { channel.cancel(e.result); } }, onStopRequest: function (request, status) { //Zotero.debug("Stopping request"); streamListener.onStopRequest(channel, status); channel._isPending = false; if (status === Cr.NS_OK) { 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); let converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; let inputStream = converter.convertToInputStream(data); listenerWrapper.onDataAvailable(this, inputStream, 0, inputStream.available()); listenerWrapper.onStopRequest(this, 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, null); } 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(uri.spec); 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); try { listenerWrapper.onDataAvailable(channel, inputStream, 0, inputStream.available()); } catch (e) { reject(e); } listenerWrapper.onStopRequest(channel, 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, Components.results.NS_ERROR_FAILURE); channel._isPending = false; } throw e; } finally { try { if (channel.loadGroup) channel.loadGroup.removeRequest(channel, null, 0); } catch (e) {} } }), // 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: ChromeUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest]), /*pdf.js wants this || iid.equals(Components.interfaces.nsIWritablePropertyBag)) {*/ }; var NSGetFactory = ComponentUtils.generateNSGetFactory([ZoteroProtocolHandler]);