www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

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 }