mimeTypeHandler.js (13337B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2009 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 ***** END LICENSE BLOCK ***** 24 */ 25 26 Zotero.MIMETypeHandler = new function () { 27 var _typeHandlers, _ignoreContentDispositionTypes, _observers; 28 29 /** 30 * Registers URIContentListener to handle MIME types 31 */ 32 this.init = function() { 33 Zotero.debug("Registering URIContentListener"); 34 // register our nsIURIContentListener and nsIObserver 35 Components.classes["@mozilla.org/uriloader;1"]. 36 getService(Components.interfaces.nsIURILoader). 37 registerContentListener(_URIContentListener); 38 Components.classes["@mozilla.org/observer-service;1"]. 39 getService(Components.interfaces.nsIObserverService). 40 addObserver(_Observer, "http-on-examine-response", false); 41 this.initializeHandlers(); 42 Zotero.addShutdownListener(function() { 43 Components.classes["@mozilla.org/observer-service;1"]. 44 getService(Components.interfaces.nsIObserverService). 45 removeObserver(_Observer, "http-on-examine-response", false); 46 }); 47 } 48 49 /** 50 * Initializes handlers for MIME types 51 */ 52 this.initializeHandlers = function() { 53 _typeHandlers = {}; 54 _ignoreContentDispositionTypes = []; 55 _observers = []; 56 57 if(Zotero.Prefs.get("parseEndNoteMIMETypes")) { 58 this.registerMetadataHandlers(); 59 } 60 Zotero.Prefs.registerObserver("parseEndNoteMIMETypes", function(val) { 61 if (val) this.registerMetadataHandlers(); 62 else this.unregisterMetadataHandlers(); 63 }.bind(this)); 64 65 this.addHandler("application/vnd.citationstyles.style+xml", Zotero.Promise.coroutine(function* (a1, a2) { 66 let win = Services.wm.getMostRecentWindow("zotero:basicViewer"); 67 try { 68 yield Zotero.Styles.install(a1, a2, true); 69 } 70 catch (e) { 71 Zotero.logError(e); 72 (new Zotero.Exception.Alert("styles.install.unexpectedError", 73 a2, "styles.install.title", e)).present(); 74 } 75 // Close styles page in basic viewer after installing a style 76 if (win) { 77 win.close(); 78 } 79 })); 80 this.addHandler("text/x-csl", function(a1, a2) { Zotero.Styles.install(a1, a2) }); // deprecated 81 this.addHandler("application/x-zotero-schema", Zotero.Schema.importSchema); 82 this.addHandler("application/x-zotero-settings", Zotero.Prefs.importSettings); 83 } 84 85 // MIME types that Zotero should handle when parseEndNoteMIMETypes preference 86 // is enabled 87 var metadataMIMETypes = [ 88 "application/x-endnote-refer", "application/x-research-info-systems", 89 "application/x-inst-for-scientific-info", 90 "text/x-bibtex", "application/x-bibtex", 91 // Non-standard 92 "text/x-research-info-systems", 93 "text/application/x-research-info-systems", // Nature serves this 94 "text/ris", // Cell serves this 95 "ris" // Not even trying 96 ]; 97 98 /** 99 * Registers MIME types for parseEndNoteMIMETypes preference 100 */ 101 this.registerMetadataHandlers = function() { 102 for (var i=0; i<metadataMIMETypes.length; i++) { 103 this.addHandler(metadataMIMETypes[i], _importHandler, true); 104 } 105 } 106 107 /** 108 * Unregisters MIME types for parseEndNoteMIMETypes preference 109 */ 110 this.unregisterMetadataHandlers = function() { 111 for (var i=0; i<metadataMIMETypes.length; i++) { 112 this.removeHandler(metadataMIMETypes[i]); 113 } 114 } 115 116 /** 117 * Adds a handler to handle a specific MIME type 118 * @param {String} type MIME type to handle 119 * @param {Function} fn Function to call to handle type - fn(string, uri) 120 * @param {Boolean} ignoreContentDisposition If true, ignores the Content-Disposition header, 121 * which is often used to force a file to download rather than let it be handled by the web 122 * browser 123 */ 124 this.addHandler = function(type, fn, ignoreContentDisposition) { 125 _typeHandlers[type] = fn; 126 _ignoreContentDispositionTypes.push(type); 127 } 128 129 /** 130 * Removes a handler for a specific MIME type 131 * @param {String} type MIME type to handle 132 */ 133 this.removeHandler = function(type) { 134 delete _typeHandlers[type]; 135 var i = _ignoreContentDispositionTypes.indexOf(type); 136 if (i != -1) { 137 _ignoreContentDispositionTypes.splice(i, 1); 138 } 139 } 140 141 /** 142 * Adds an observer to inspect and possibly modify page headers 143 */ 144 this.addObserver = function(fn) { 145 _observers.push(fn); 146 } 147 148 149 /** 150 * Handles Refer/RIS/ISI MIME types 151 * @param {String} string The Refer/RIS/ISI formatted records 152 * @param {String} uri The URI from which the Refer/RIS/ISI formatted records were downloaded 153 */ 154 function _importHandler(string, uri, contentType, channel) { 155 var win = channel.notificationCallbacks.getInterface(Components.interfaces.nsIDOMWindow).top; 156 if(!win) { 157 Zotero.debug("Attempt to import from a channel without an attached document refused"); 158 return false; 159 } 160 161 var hostPort = channel.URI.hostPort.replace(";", "_", "g"); 162 163 var allowedSitesString = Zotero.Prefs.get("ingester.allowedSites"); 164 allowedSites = allowedSitesString.split(";"); 165 166 if(allowedSites.indexOf(hostPort) === -1) { 167 var title = Zotero.getString("ingester.importReferRISDialog.title"); 168 var text = Zotero.getString("ingester.importReferRISDialog.text", channel.URI.hostPort)+"\n\n"; 169 var checkMsg = Zotero.getString("ingester.importReferRISDialog.checkMsg"); 170 var checkValue = {"value":false}; 171 172 // make tab-modal dialog (https://developer.mozilla.org/en/Using_tab-modal_prompts) 173 var prompt = Components.classes["@mozilla.org/prompter;1"] 174 .getService(Components.interfaces.nsIPromptFactory) 175 .getPrompt(win, Components.interfaces.nsIPrompt); 176 177 var bag = prompt.QueryInterface(Components.interfaces.nsIWritablePropertyBag2); 178 bag.setPropertyAsBool("allowTabModal", true); 179 180 var continueDownload = prompt.confirmCheck(title, text, checkMsg, checkValue); 181 if(!continueDownload) return false; 182 if(checkValue.value) { 183 // add to allowed sites if desired 184 Zotero.Prefs.set("ingester.allowedSites", allowedSitesString+";"+hostPort); 185 } 186 } 187 188 var frontWindow = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]. 189 getService(Components.interfaces.nsIWindowWatcher).activeWindow; 190 191 // attempt to import through Zotero.Translate 192 var translation = new Zotero.Translate("import"); 193 translation.setLocation(uri); 194 translation.setString(string); 195 196 // attempt to retrieve translators 197 return translation.getTranslators().then(function(translators) { 198 if(!translators.length) { 199 // we lied. we can't really translate this file. 200 Zotero.debug("No translator found to handle this file"); 201 return false; 202 } 203 204 // translate using first available 205 translation.setTranslator(translators[0]); 206 return frontWindow.Zotero_Browser.performTranslation(translation); 207 }); 208 } 209 210 /** 211 * Called to observe a page load 212 */ 213 var _Observer = new function() { 214 this.observe = function(channel) { 215 if(Zotero.isConnector) return; 216 217 channel.QueryInterface(Components.interfaces.nsIRequest); 218 if(channel.loadFlags & Components.interfaces.nsIHttpChannel.LOAD_DOCUMENT_URI) { 219 channel.QueryInterface(Components.interfaces.nsIHttpChannel); 220 try { 221 // remove content-disposition headers for EndNote, etc. 222 var contentType = channel.getResponseHeader("Content-Type").toLowerCase(); 223 for (let handledType of _ignoreContentDispositionTypes) { 224 if(contentType.length < handledType.length) { 225 break; 226 } else { 227 if(contentType.substr(0, handledType.length) == handledType) { 228 channel.setResponseHeader("Content-Disposition", "", false); 229 break; 230 } 231 } 232 } 233 } catch(e) {} 234 235 for (let observer of _observers) { 236 observer(channel); 237 } 238 } 239 } 240 } 241 242 var _URIContentListener = new function() { 243 /** 244 * Standard QI definiton 245 */ 246 this.QueryInterface = function(iid) { 247 if (iid.equals(Components.interfaces.nsISupports) 248 || iid.equals(Components.interfaces.nsISupportsWeakReference) 249 || iid.equals(Components.interfaces.nsIURIContentListener)) { 250 return this; 251 } 252 throw Components.results.NS_ERROR_NO_INTERFACE; 253 } 254 255 /** 256 * Called to see if we can handle a content type 257 */ 258 this.canHandleContent = this.isPreferred = function(contentType, isContentPreferred, desiredContentType) { 259 if(Zotero.isConnector) return false; 260 return !!_typeHandlers[contentType.toLowerCase()]; 261 } 262 263 /** 264 * Called to begin handling a content type 265 */ 266 this.doContent = function(contentType, isContentPreferred, request, contentHandler) { 267 Zotero.debug("MIMETypeHandler: handling "+contentType+" from " + request.name); 268 contentHandler.value = new _StreamListener(request, contentType.toLowerCase()); 269 return false; 270 } 271 272 /** 273 * Called so that we could stop a load before it happened if we wanted to 274 */ 275 this.onStartURIOpen = function(URI) { 276 return true; 277 } 278 } 279 280 /** 281 * @class Implements nsIStreamListener and nsIRequestObserver interfaces to download MIME types 282 * we've registered ourself as the handler for 283 * @param {nsIRequest} request The request to handle 284 * @param {String} contenType The content type being handled 285 */ 286 var _StreamListener = function(request, contentType) { 287 this._request = request; 288 this._contentType = contentType 289 this._storageStream = null; 290 this._outputStream = null; 291 this._binaryInputStream = null; 292 } 293 294 /** 295 * Standard QI definiton 296 */ 297 _StreamListener.prototype.QueryInterface = function(iid) { 298 if (iid.equals(Components.interfaces.nsISupports) 299 || iid.equals(Components.interfaces.nsIRequestObserver) 300 || iid.equals(Components.interfaces.nsIStreamListener)) { 301 return this; 302 } 303 throw Components.results.NS_ERROR_NO_INTERFACE; 304 } 305 306 /** 307 * Called when the request is started; we ignore this 308 */ 309 _StreamListener.prototype.onStartRequest = function(channel, context) {} 310 311 312 /** 313 * Called when there's data available; we collect this data and keep it until the request is 314 * done 315 */ 316 _StreamListener.prototype.onDataAvailable = function(request, context, inputStream, offset, count) { 317 Zotero.debug(count + " bytes available"); 318 319 if (!this._storageStream) { 320 this._storageStream = Components.classes["@mozilla.org/storagestream;1"]. 321 createInstance(Components.interfaces.nsIStorageStream); 322 this._storageStream.init(16384, 4294967295, null); // PR_UINT32_MAX 323 this._outputStream = this._storageStream.getOutputStream(0); 324 325 this._binaryInputStream = Components.classes["@mozilla.org/binaryinputstream;1"]. 326 createInstance(Components.interfaces.nsIBinaryInputStream); 327 this._binaryInputStream.setInputStream(inputStream); 328 } 329 330 var bytes = this._binaryInputStream.readBytes(count); 331 this._outputStream.write(bytes, count); 332 } 333 334 /** 335 * Called when the request is done 336 */ 337 _StreamListener.prototype.onStopRequest = Zotero.Promise.coroutine(function* (channel, context, status) { 338 Zotero.debug("charset is " + channel.contentCharset); 339 340 var inputStream = this._storageStream.newInputStream(0); 341 var charset = channel.contentCharset ? channel.contentCharset : "UTF-8"; 342 const replacementChar = Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER; 343 var convStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"] 344 .createInstance(Components.interfaces.nsIConverterInputStream); 345 convStream.init(inputStream, charset, 16384, replacementChar); 346 var readString = ""; 347 var str = {}; 348 while (convStream.readString(16384, str) != 0) { 349 readString += str.value; 350 } 351 convStream.close(); 352 inputStream.close(); 353 354 var handled = false; 355 try { 356 handled = _typeHandlers[this._contentType]( 357 readString, 358 this._request.name ? this._request.name : null, 359 this._contentType, 360 channel 361 ); 362 } 363 catch (e) { 364 Zotero.logError(e); 365 } 366 367 if (handled === false) { 368 // Handle using nsIExternalHelperAppService 369 let externalHelperAppService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"] 370 .getService(Components.interfaces.nsIExternalHelperAppService); 371 let frontWindow = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] 372 .getService(Components.interfaces.nsIWindowWatcher).activeWindow; 373 374 let inputStream = this._storageStream.newInputStream(0); 375 let streamListener = externalHelperAppService.doContent( 376 this._contentType, this._request, frontWindow, null 377 ); 378 if (streamListener) { 379 streamListener.onStartRequest(channel, context); 380 streamListener.onDataAvailable( 381 this._request, context, inputStream, 0, this._storageStream.length 382 ); 383 streamListener.onStopRequest(channel, context, status); 384 } 385 } 386 387 this._storageStream.close(); 388 }); 389 }