/* ***** 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 <http://www.gnu.org/licenses/>. 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"; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); // Dummy chrome URL used to obtain a valid chrome channel // This one was chosen at random and should be able to be substituted // for any other well known chrome URL in the browser installation const DUMMY_CHROME_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; var Zotero = Components.classes["@zotero.org/Zotero;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject; var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); function ZoteroProtocolHandler() { this.wrappedJSObject = this; this._principal = null; this._extensions = {}; /** * 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.path; if (!path) { return 'Invalid URL'; } // Strip leading '/' path = path.substr(1); // Proxy CSS files if (path.endsWith('.css')) { var chromeURL = 'chrome://zotero/skin/report/' + path; Zotero.debug(chromeURL); var ios = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); 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), function () { return '<span style="color: red; font-weight: bold">Error generating report</span>'; } ); } }); } }; /** * 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; 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 // 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 = '<body onload="onLoad('; var a = (theIntervals[intervals[0]]) ? theIntervals[intervals[0]] : 'Timeline.DateTime.MONTH'; var b = (theIntervals[intervals[1]]) ? theIntervals[intervals[1]] : 'Timeline.DateTime.YEAR'; var c = (theIntervals[intervals[2]]) ? theIntervals[intervals[2]] : 'Timeline.DateTime.DECADE'; content = content.replace(tempStr, tempStr + a + ',' + b + ',' + c + ',\'' + timelineDate + '\''); tempStr = 'document.write("<title>'; if (params.scopeObject == 'collections') { content = content.replace(tempStr, tempStr + collection.name + ' - '); } else if (params.scopeObject == 'searches') { content = content.replace(tempStr, tempStr + search.name + ' - '); } else { content = content.replace(tempStr, tempStr + Zotero.getString('pane.collections.library') + ' - '); } tempStr = 'Timeline.loadXML("zotero://timeline/data/'; var d = ''; if (params.groupID) { d += 'groups/' + params.groupID + '/'; } else { d += 'library/'; } if (params.scopeObject) { d += params.scopeObject + "/" + params.scopeObjectKey; } if (dateType) { d += '?t=' + dateType; } return content.replace(tempStr, tempStr + d); }); } }; /* zotero://attachment/[id]/ */ var AttachmentExtension = { loadAsChrome: false, newChannel: function (uri) { var self = this; return new AsyncChannel(uri, function* () { try { var errorMsg; var [id, fileName] = uri.path.substr(1).split('/'); if (parseInt(id) != id) { // Proxy annotation icons if (id.match(/^annotation.*\.(png|html|css|gif)$/)) { var chromeURL = 'chrome://zotero/skin/' + id; var ios = Components.classes["@mozilla.org/network/io-service;1"]. getService(Components.interfaces.nsIIOService); let uri = ios.newURI(chromeURL, null, null); var chromeReg = Components.classes["@mozilla.org/chrome/chrome-registry;1"] .getService(Components.interfaces.nsIChromeRegistry); var fileURI = chromeReg.convertChromeURL(uri); } else { return self._errorChannel("Attachment id not an integer"); } } if (!fileURI) { var item = yield Zotero.Items.getAsync(id); if (!item) { return self._errorChannel("Item not found"); } var path = yield item.getFilePathAsync(); if (!path) { return self._errorChannel("File not found"); } if (fileName) { Components.utils.import("resource://gre/modules/osfile.jsm"); path = OS.Path.join(OS.Path.dirname(path), fileName) if (!(yield OS.File.exists(path))) { return self._errorChannel("File 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 is not allowed this.originalURI = uri; return Zotero.File.pathToFile(path); } catch (e) { Zotero.debug(e); throw (e); } }); }, _errorChannel: function (msg) { this.status = Components.results.NS_ERROR_FAILURE; this.contentType = 'text/plain'; return msg; } }; /** * zotero://select/[type]/0_ABCD1234 * zotero://select/[type]/1234 (not consistent across synced machines) */ var SelectExtension = { noContent: true, doAction: Zotero.Promise.coroutine(function* (uri) { var userLibraryID = Zotero.Libraries.userLibraryID; var path = uri.path; if (!path) { return 'Invalid URL'; } // Strip leading '/' path = path.substr(1); var mimeType, content = ''; var params = { objectType: 'item' }; var router = new Zotero.Router(params); // Item within a collection or search router.add('library/:scopeObject/:scopeObjectKey/items/:objectKey', function () { params.libraryID = userLibraryID; }); router.add('groups/:groupID/:scopeObject/:scopeObjectKey/items/:objectKey'); // All items router.add('library/items/:objectKey', function () { params.libraryID = userLibraryID; }); router.add('groups/:groupID/items/:objectKey'); // Old-style URLs router.add('items/:id', function () { var lkh = Zotero.Items.parseLibraryKeyHash(params.id); if (lkh) { params.libraryID = lkh.libraryID || userLibraryID; params.objectKey = lkh.key; } else { params.objectID = params.id; } delete params.id; }); router.run(path); Zotero.API.parseParams(params); var results = yield Zotero.API.getResultsFromParams(params); if (!results.length) { var msg = "Items not found"; Zotero.debug(msg, 2); Components.utils.reportError(msg); return; } var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] .getService(Components.interfaces.nsIWindowMediator); var win = wm.getMostRecentWindow("navigator:browser"); // TODO: Currently only able to select one item return win.ZoteroPane.selectItem(results[0].id); }), newChannel: function (uri) { this.doAction(uri); } }; /* zotero://fullscreen */ var FullscreenExtension = { loadAsChrome: false, newChannel: function (uri) { return new AsyncChannel(uri, function* () { try { var window = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] .getService(Components.interfaces.nsIWindowWatcher) .openWindow(null, 'chrome://zotero/content/standalone/standalone.xul', '', 'chrome,centerscreen,resizable', null); } catch (e) { Zotero.debug(e, 1); throw e; } }); } }; /* 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); var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); this.name = uri; this.URI = ioService.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 ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); 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.path; originalURI = decodeURIComponent(originalURI.substr(originalURI.indexOf("/")+1)); 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; } } }; this._extensions[ZOTERO_SCHEME + "://data"] = DataExtension; this._extensions[ZOTERO_SCHEME + "://report"] = ReportExtension; this._extensions[ZOTERO_SCHEME + "://timeline"] = TimelineExtension; this._extensions[ZOTERO_SCHEME + "://attachment"] = AttachmentExtension; this._extensions[ZOTERO_SCHEME + "://select"] = SelectExtension; this._extensions[ZOTERO_SCHEME + "://fullscreen"] = FullscreenExtension; this._extensions[ZOTERO_SCHEME + "://debug"] = DebugExtension; this._extensions[ZOTERO_SCHEME + "://connector"] = ConnectorExtension; } /* * 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) { var newURL = Components.classes["@mozilla.org/network/standard-url;1"] .createInstance(Components.interfaces.nsIStandardURL); newURL.init(1, -1, spec, charset, baseURI); return newURL.QueryInterface(Components.interfaces.nsIURI); }, newChannel : function(uri) { var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); 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) { if (ext.loadAsChrome) { var chromeURI = chromeService.newURI(DUMMY_CHROME_URL, null, null); var chromeChannel = chromeService.newChannel(chromeURI); // Cache System Principal from chrome request // so proxied pages load with chrome privileges this._principal = chromeChannel.owner; var chromeRequest = chromeChannel.QueryInterface(Components.interfaces.nsIRequest); chromeRequest.cancel(0x804b0002); // BINDING_ABORTED } } 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; // Proxy requests to other zotero:// URIs let uri2 = this.URI.clone(); if (uri2.path.startsWith('/proxy/')) { let re = new RegExp(uri2.scheme + '://' + uri2.host + '/proxy/([^/]+)(.*)'); let matches = uri2.spec.match(re); uri2.spec = uri2.scheme + '://' + matches[1] + '/' + (matches[2] ? matches[2] : ''); var data = Zotero.File.getContentsFromURL(uri2.spec); } 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); 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 = ioService.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(data, 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]);