/* ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2006 Center for History and New Media George Mason University, Fairfax, Virginia, USA http://chnm.gmu.edu Licensed under the Educational Community License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.opensource.org/licenses/ecl1.php Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 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"; // 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"; function ChromeExtensionHandler() { this.wrappedJSObject = this; this._systemPrincipal = null; this._extensions = {}; /* * Report generation extension for Zotero protocol * * Example URLs: * * zotero://report/ -- library * zotero://report/collection/0_ABCD1234 * zotero://report/search/0_ABCD1234 * zotero://report/items/0_ABCD1234-0_BCDE2345-0_CDEF3456 * zotero://report/item/0_ABCD1234 * * Optional format can be specified after hashes * * - 'html', 'rtf', 'csv' ['rtf' and 'csv' not yet supported] * - defaults to 'html' if not specified * * e.g. zotero://report/collection/0_ABCD1234/rtf * * * Sorting: * * - 'sort' query string variable * - format is field[/order] [, field[/order], ...] * - order can be 'asc', 'a', 'desc' or 'd'; defaults to ascending order * * zotero://report/collection/0_ABCD1234?sort=itemType/d,title * * * Also supports ids (e.g., zotero://report/collection/1234), but ids are not * guaranteed to be consistent across synced machines */ var ReportExtension = new function(){ this.newChannel = newChannel; this.__defineGetter__('loadAsChrome', function () { return false; }); function newChannel(uri){ var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); var Zotero = Components.classes["@zotero.org/Zotero;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject; generateContent:try { var mimeType, content = ''; var [path, queryString] = uri.path.substr(1).split('?'); var [type, ids, format] = path.split('/'); // Get query string variables if (queryString) { var queryVars = queryString.split('&'); for (var i in queryVars) { var [key, val] = queryVars[i].split('='); switch (key) { case 'sort': var sortBy = val; break; } } } switch (type){ case 'collection': var lkh = Zotero.Collections.parseLibraryKeyHash(ids); if (lkh) { var col = Zotero.Collections.getByLibraryAndKey(lkh.libraryID, lkh.key); } else { var col = Zotero.Collections.get(ids); } if (!col) { mimeType = 'text/html'; content = 'Invalid collection ID or key'; break generateContent; } var results = col.getChildItems(); break; case 'search': var lkh = Zotero.Searches.parseLibraryKeyHash(ids); if (lkh) { var s = Zotero.Searches.getByLibraryAndKey(lkh.libraryID, lkh.key); } else { var s = Zotero.Searches.get(ids); } if (!s) { mimeType = 'text/html'; content = 'Invalid search ID or key'; break generateContent; } // FIXME: Hack to exclude group libraries for now var s2 = new Zotero.Search(); s2.setScope(s); var groups = Zotero.Groups.getAll(); for each(var group in groups) { s2.addCondition('libraryID', 'isNot', group.libraryID); } var ids = s2.search(); var results = Zotero.Items.get(ids); break; case 'items': case 'item': ids = ids.split('-'); // Keys if (Zotero.Items.parseLibraryKeyHash(ids[0])) { var results = []; for each(var lkh in ids) { var lkh = Zotero.Items.parseLibraryKeyHash(lkh); var item = Zotero.Items.getByLibraryAndKey(lkh.libraryID, lkh.key); if (item) { results.push(item); } } } // IDs else { var results = Zotero.Items.get(ids); } if (!results.length) { mimeType = 'text/html'; content = 'Invalid ID'; break generateContent; } break; default: // Proxy CSS files if (type.match(/^detail.*\.css$/)) { var chromeURL = 'chrome://zotero/skin/report/' + type; var ios = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); var 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); var ph = Components.classes["@mozilla.org/network/protocol;1?name=file"] .createInstance(Components.interfaces.nsIFileProtocolHandler); var channel = ioService.newChannelFromURI(fileURI); return channel; } // Display all items var type = 'library'; var s = new Zotero.Search(); s.addCondition('noChildren', 'true'); var ids = s.search(); var results = Zotero.Items.get(ids); } var items = []; var itemsHash = {}; // key = itemID, val = position in |items| var searchItemIDs = {}; // hash of all selected items var searchParentIDs = {}; // hash of parents of selected child items var searchChildIDs = {}; // hash of selected chlid items var includeAllChildItems = Zotero.Prefs.get('report.includeAllChildItems'); var combineChildItems = Zotero.Prefs.get('report.combineChildItems'); var unhandledParents = {}; for (var i=0; i creates HTML for timeline (defaults: type = library | intervals = month, year, decade | timelineDate = today's date | dateType = date) Example URLs: zotero://timeline/library?i=yec zotero://timeline/collection/12345?t=da&d=Jul.24.2008 zotero://timeline/search/54321?d=Dec.1.-500&i=dmy&t=d zotero://timeline/data ----->creates XML file (defaults: type = library | dateType = date) Example URLs: zotero://timeline/data/library?t=da zotero://timeline/data/collection/12345 zotero://timeline/data/search/54321?t=dm */ function newChannel(uri) { var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); var Zotero = Components.classes["@zotero.org/Zotero;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject; generateContent:try { var mimeType, content = ''; var [path, queryString] = uri.path.substr(1).split('?'); var [intervals, timelineDate, dateType] = ['','','']; if (queryString) { var queryVars = queryString.split('&'); for (var i in queryVars) { var [key, val] = queryVars[i].split('='); if(val) { switch (key) { case 'i': intervals = val; break; case 'd': timelineDate = val; break; case 't': dateType = val; break; } } } } var pathParts = path.split('/'); if (pathParts[0] != 'data') { var [type, id] = pathParts; } else { var [, type, id] = pathParts; } // Get the collection or search object var collection, search; switch (type) { case 'collection': var lkh = Zotero.Collections.parseLibraryKeyHash(id); if (lkh) { collection = Zotero.Collections.getByLibraryAndKey(lkh.libraryID, lkh.key); } else { collection = Zotero.Collections.get(id); } if (!collection) { mimeType = 'text/html'; content = 'Invalid collection ID or key'; break generateContent; } break; case 'search': var lkh = Zotero.Searches.parseLibraryKeyHash(id); if (lkh) { var s = Zotero.Searches.getByLibraryAndKey(lkh.libraryID, lkh.key); } else { var s = Zotero.Searches.get(id); } if (!s) { mimeType = 'text/html'; content = 'Invalid search ID or key'; break generateContent; } // FIXME: Hack to exclude group libraries for now var search = new Zotero.Search(); search.setScope(s); var groups = Zotero.Groups.getAll(); for each(var group in groups) { search.addCondition('libraryID', 'isNot', group.libraryID); } break; } if (pathParts[0] != 'data') { //creates HTML file content = Zotero.File.getContentsFromURL('chrome://zotero/skin/timeline/timeline.html'); mimeType = 'text/html'; var [type, id] = pathParts; if(!timelineDate){ timelineDate=Date(); var dateParts=timelineDate.toString().split(' '); timelineDate=dateParts[1]+'.'+dateParts[2]+'.'+dateParts[3]; } if (intervals.length < 3) { intervals += "mye".substr(intervals.length); } var theIntervals = new Object(); theIntervals['d'] = 'Timeline.DateTime.DAY'; theIntervals['m'] = 'Timeline.DateTime.MONTH'; theIntervals['y'] = 'Timeline.DateTime.YEAR'; theIntervals['e'] = 'Timeline.DateTime.DECADE'; theIntervals['c'] = 'Timeline.DateTime.CENTURY'; theIntervals['i'] = 'Timeline.DateTime.MILLENNIUM'; //sets the intervals of the timeline bands var theTemp = ''; if(type == 'collection') { content = content.replace(theTemp, theTemp + collection.name + ' - '); } else if(type == 'search') { content = content.replace(theTemp, theTemp + search.name + ' - '); } else { content = content.replace(theTemp, theTemp + Zotero.getString('pane.collections.library') + ' - '); } theTemp = 'Timeline.loadXML("zotero://timeline/data/'; var d = ''; //passes information (type,ids, dateType) for when the XML is created if(!type || (type != 'collection' && type != 'search')) { d += 'library'; } else { d += type + '/' + id; } if(dateType) { d += '?t=' + dateType; } content = content.replace(theTemp, theTemp + d); var uri_str = 'data:' + (mimeType ? mimeType + ',' : '') + encodeURIComponent(content); var ext_uri = ioService.newURI(uri_str, null, null); var extChannel = ioService.newChannelFromURI(ext_uri); return extChannel; } // Create XML file else { switch (type) { case 'collection': var results = collection.getChildItems(); break; case 'search': var ids = search.search(); var results = Zotero.Items.get(ids); break; default: type = 'library'; var s = new Zotero.Search(); s.addCondition('noChildren', 'true'); var ids = s.search(); var results = Zotero.Items.get(ids); } var items = []; // Only include parent items for (var i = 0; i < results.length; i++) { if (!results[i].getSource()) { items.push(results[i]); } } if (!items) { mimeType = 'text/html'; content = 'Invalid ID'; break generateContent; } // Convert item objects to export arrays for (var i = 0; i < items.length; i++) { items[i] = items[i].toArray(); } mimeType = 'application/xml'; var theDateTypes = new Object(); theDateTypes['d'] = 'date'; theDateTypes['da'] = 'dateAdded'; theDateTypes['dm'] = 'dateModified'; //default dateType = date if (!dateType || !theDateTypes[dateType]) { dateType = 'd'; } content = Zotero.Timeline.generateXMLDetails(items, theDateTypes[dateType]); } var uri_str = 'data:' + (mimeType ? mimeType + ',' : '') + encodeURIComponent(content); var ext_uri = ioService.newURI(uri_str, null, null); var extChannel = ioService.newChannelFromURI(ext_uri); return extChannel; } catch (e){ Zotero.debug(e); throw (e); } } }; /* zotero://attachment/[id]/ */ var AttachmentExtension = new function() { this.newChannel = newChannel; this.__defineGetter__('loadAsChrome', function () { return false; }); function newChannel(uri) { var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); var Zotero = Components.classes["@zotero.org/Zotero;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject; 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); var 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 _errorChannel("Attachment id not an integer"); } } if (!fileURI) { var item = Zotero.Items.get(id); if (!item) { return _errorChannel("Item not found"); } var file = item.getFile(); if (!file) { return _errorChannel("File not found"); } if (fileName) { file = file.parent; file.append(fileName); if (!file.exists()) { return _errorChannel("File not found"); } } } var ph = Components.classes["@mozilla.org/network/protocol;1?name=file"]. createInstance(Components.interfaces.nsIFileProtocolHandler); if (!fileURI) { var fileURI = ph.newFileURI(file); } var channel = ioService.newChannelFromURI(fileURI); return channel; } catch (e) { Zotero.debug(e); throw (e); } } function _errorChannel(msg) { var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); var uriStr = 'data:text/plain,' + encodeURIComponent(msg); var dataURI = ioService.newURI(uriStr, null, null); var channel = ioService.newChannelFromURI(dataURI); return channel; } }; /** * zotero://select/[type]/0_ABCD1234 * zotero://select/[type]/1234 (not consistent across synced machines) */ var SelectExtension = new function(){ this.newChannel = newChannel; function newChannel(uri) { var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); var Zotero = Components.classes["@zotero.org/Zotero;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject; generateContent:try { var mimeType, content = ''; var [path, queryString] = uri.path.substr(1).split('?'); var [type, id] = path.split('/'); //currently only able to select one item var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] .getService(Components.interfaces.nsIWindowMediator); var win = wm.getMostRecentWindow(null); if(!win.ZoteroPane.isShowing()){ win.ZoteroPane.toggleDisplay(); } var lkh = Zotero.Items.parseLibraryKeyHash(id); if (lkh) { var item = Zotero.Items.getByLibraryAndKey(lkh.libraryID, lkh.key); } else { var item = Zotero.Items.get(id); } if (!item) { var msg = "Item " + id + " not found in zotero://select"; Zotero.debug(msg, 2); Components.utils.reportError(msg); return; } win.ZoteroPane.selectItem(item.id); } catch (e){ Zotero.debug(e); throw (e); } } }; /* zotero://fullscreen */ var FullscreenExtension = new function() { this.newChannel = newChannel; this.__defineGetter__('loadAsChrome', function () { return false; }); function newChannel(uri) { var Zotero = Components.classes["@zotero.org/Zotero;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject; generateContent: try { var win = Components.classes["@mozilla.org/appshell/window-mediator;1"] .getService(Components.interfaces.nsIWindowMediator) .getMostRecentWindow("navigator:browser"); var zp = win.ZoteroPane; // When using fullscreen as home page, Zotero pane is reset to // 0 height, so get saved height and set it below var pane = win.document.getElementById('zotero-pane'); var height = pane.getAttribute('savedHeight'); zp.fullScreen(true); if(!zp.isShowing()) { zp.toggleDisplay(); } pane.setAttribute('height', height); // FIXME: The above should run in a callback after about:blank // is loaded so that the window title is set correctly, but I // can't get the event handlers to work. - D.S. win.loadURI("about:blank"); } catch (e) { Zotero.debug(e); throw (e); } } }; /* zotero://debug/ */ var DebugExtension = new function() { this.newChannel = newChannel; this.__defineGetter__('loadAsChrome', function () { return false; }); function newChannel(uri) { var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); var Zotero = Components.classes["@zotero.org/Zotero;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject; try { var output = Zotero.Debug.get(); var uriStr = 'data:text/plain,' + encodeURIComponent(output); var extURI = ioService.newURI(uriStr, null, null); return ioService.newChannelFromURI(extURI); } catch (e) { Zotero.debug(e); throw (e); } } }; var ReportExtensionSpec = ZOTERO_SCHEME + "://report" this._extensions[ReportExtensionSpec] = ReportExtension; var TimelineExtensionSpec = ZOTERO_SCHEME + "://timeline" this._extensions[TimelineExtensionSpec] = TimelineExtension; var AttachmentExtensionSpec = ZOTERO_SCHEME + "://attachment" this._extensions[AttachmentExtensionSpec] = AttachmentExtension; var SelectExtensionSpec = ZOTERO_SCHEME + "://select" this._extensions[SelectExtensionSpec] = SelectExtension; var FullscreenExtensionSpec = ZOTERO_SCHEME + "://fullscreen" this._extensions[FullscreenExtensionSpec] = FullscreenExtension; var DebugExtensionSpec = ZOTERO_SCHEME + "://debug" this._extensions[DebugExtensionSpec] = DebugExtension; } /* * Implements nsIProtocolHandler */ ChromeExtensionHandler.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; }, 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 { var uriString = uri.spec.toLowerCase(); for (var extSpec in this._extensions) { var ext = this._extensions[extSpec]; if (uriString.indexOf(extSpec) == 0) { if (ext.loadAsChrome && this._systemPrincipal == null) { 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._systemPrincipal = 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 system principal to extension channel if (ext.loadAsChrome) { extChannel.owner = this._systemPrincipal; } extChannel.originalURI = uri; return extChannel; } } // pass request through to ChromeProtocolHandler::newChannel if (uriString.indexOf("chrome") != 0) { uriString = uri.spec; uriString = "chrome" + uriString.substring(uriString.indexOf(":")); uri = chromeService.newURI(uriString, null, null); } newChannel = chromeService.newChannel(uri); } catch (e) { throw Components.results.NS_ERROR_FAILURE; } return newChannel; }, QueryInterface : function(iid) { if (!iid.equals(Components.interfaces.nsIProtocolHandler) && !iid.equals(Components.interfaces.nsISupports)) { throw Components.results.NS_ERROR_NO_INTERFACE; } return this; } }; // // XPCOM goop // var ChromeExtensionModule = { cid: ZOTERO_PROTOCOL_CID, contractId: ZOTERO_PROTOCOL_CONTRACTID, registerSelf : function(compMgr, fileSpec, location, type) { compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar); compMgr.registerFactoryLocation( ZOTERO_PROTOCOL_CID, ZOTERO_PROTOCOL_NAME, ZOTERO_PROTOCOL_CONTRACTID, fileSpec, location, type ); }, getClassObject : function(compMgr, cid, iid) { if (!cid.equals(ZOTERO_PROTOCOL_CID)) { throw Components.results.NS_ERROR_NO_INTERFACE; } if (!iid.equals(Components.interfaces.nsIFactory)) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; } return this.myFactory; }, canUnload : function(compMgr) { return true; }, myFactory : { createInstance : function(outer, iid) { if (outer != null) { throw Components.results.NS_ERROR_NO_AGGREGATION; } return new ChromeExtensionHandler().QueryInterface(iid); } } }; function NSGetModule(compMgr, fileSpec) { return ChromeExtensionModule; }