www

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

fileInterface.js (26210B)


      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 Components.utils.import("resource://gre/modules/osfile.jsm")
     27 
     28 /****Zotero_File_Exporter****
     29  **
     30  * A class to handle exporting of items, collections, or the entire library
     31  **/
     32 
     33 /**
     34  * Constructs a new Zotero_File_Exporter with defaults
     35  **/
     36 var Zotero_File_Exporter = function() {
     37 	this.name = Zotero.getString("fileInterface.exportedItems");
     38 	this.collection = false;
     39 	this.items = false;
     40 }
     41 
     42 /**
     43  * Performs the actual export operation
     44  *
     45  * @return {Promise}
     46  **/
     47 Zotero_File_Exporter.prototype.save = Zotero.Promise.coroutine(function* () {
     48 	var translation = new Zotero.Translate.Export();
     49 	var translators = yield translation.getTranslators();
     50 	
     51 	// present options dialog
     52 	var io = {translators:translators}
     53 	window.openDialog("chrome://zotero/content/exportOptions.xul",
     54 		"_blank", "chrome,modal,centerscreen,resizable=no", io);
     55 	if(!io.selectedTranslator) {
     56 		return false;
     57 	}
     58 	
     59 	const nsIFilePicker = Components.interfaces.nsIFilePicker;
     60 	var fp = Components.classes["@mozilla.org/filepicker;1"]
     61 			.createInstance(nsIFilePicker);
     62 	fp.init(window, Zotero.getString("fileInterface.export"), nsIFilePicker.modeSave);
     63 	
     64 	// set file name and extension
     65 	if(io.displayOptions.exportFileData) {
     66 		// if the result will be a folder, don't append any extension or use
     67 		// filters
     68 		fp.defaultString = this.name;
     69 		fp.appendFilters(Components.interfaces.nsIFilePicker.filterAll);
     70 	} else {
     71 		// if the result will be a file, append an extension and use filters
     72 		fp.defaultString = this.name+(io.selectedTranslator.target ? "."+io.selectedTranslator.target : "");
     73 		fp.defaultExtension = io.selectedTranslator.target;
     74 		fp.appendFilter(io.selectedTranslator.label, "*."+(io.selectedTranslator.target ? io.selectedTranslator.target : "*"));
     75 	}
     76 	
     77 	var rv = fp.show();
     78 	if (rv != nsIFilePicker.returnOK && rv != nsIFilePicker.returnReplace) {
     79 		return;
     80 	}
     81 	
     82 	if(this.collection) {
     83 		translation.setCollection(this.collection);
     84 	} else if(this.items) {
     85 		translation.setItems(this.items);
     86 	} else if(this.libraryID === undefined) {
     87 		throw new Error('No export configured');
     88 	} else {
     89 		translation.setLibraryID(this.libraryID);
     90 	}
     91 	
     92 	translation.setLocation(fp.file);
     93 	translation.setTranslator(io.selectedTranslator);
     94 	translation.setDisplayOptions(io.displayOptions);
     95 	translation.setHandler("itemDone", function () {
     96 		Zotero.updateZoteroPaneProgressMeter(translation.getProgress());
     97 	});
     98 	translation.setHandler("done", this._exportDone);
     99 	Zotero_File_Interface.Progress.show(
    100 		Zotero.getString("fileInterface.itemsExported")
    101 	);
    102 	translation.translate()
    103 });
    104 	
    105 /*
    106  * Closes the items exported indicator
    107  */
    108 Zotero_File_Exporter.prototype._exportDone = function(obj, worked) {
    109 	Zotero_File_Interface.Progress.close();
    110 	
    111 	if(!worked) {
    112 		Zotero.alert(
    113 			null,
    114 			Zotero.getString('general.error'),
    115 			Zotero.getString("fileInterface.exportError")
    116 		);
    117 	}
    118 }
    119 
    120 /****Zotero_File_Interface****
    121  **
    122  * A singleton to interface with ZoteroPane to provide export/bibliography
    123  * capabilities
    124  **/
    125 var Zotero_File_Interface = new function() {
    126 	var _unlock;
    127 	
    128 	this.exportCollection = exportCollection;
    129 	this.exportItemsToClipboard = exportItemsToClipboard;
    130 	this.exportItems = exportItems;
    131 	this.bibliographyFromItems = bibliographyFromItems;
    132 	
    133 	/**
    134 	 * Creates Zotero.Translate instance and shows file picker for file export
    135 	 *
    136 	 * @return {Promise}
    137 	 */
    138 	this.exportFile = Zotero.Promise.method(function () {
    139 		var exporter = new Zotero_File_Exporter();
    140 		exporter.libraryID = ZoteroPane_Local.getSelectedLibraryID();
    141 		if (exporter.libraryID === false) {
    142 			throw new Error('No library selected');
    143 		}
    144 		exporter.name = Zotero.Libraries.getName(exporter.libraryID);
    145 		return exporter.save();
    146 	});
    147 	
    148 	/*
    149 	 * exports a collection or saved search
    150 	 */
    151 	function exportCollection() {
    152 		var exporter = new Zotero_File_Exporter();
    153 	
    154 		var collection = ZoteroPane_Local.getSelectedCollection();
    155 		if(collection) {
    156 			exporter.name = collection.getName();
    157 			exporter.collection = collection;
    158 		} else {
    159 			// find sorted items
    160 			exporter.items = ZoteroPane_Local.getSortedItems();
    161 			if(!exporter.items) throw ("No items to save");
    162 			
    163 			// find name
    164 			var search = ZoteroPane_Local.getSelectedSavedSearch();
    165 			if(search) {
    166 				exporter.name = search.name;
    167 			}
    168 		}
    169 		exporter.save();
    170 	}
    171 	
    172 	
    173 	/*
    174 	 * exports items
    175 	 */
    176 	function exportItems() {
    177 		var exporter = new Zotero_File_Exporter();
    178 		
    179 		exporter.items = ZoteroPane_Local.getSelectedItems();
    180 		if(!exporter.items || !exporter.items.length) throw("no items currently selected");
    181 		
    182 		exporter.save();
    183 	}
    184 	
    185 	/*
    186 	 * exports items to clipboard
    187 	 */
    188 	function exportItemsToClipboard(items, translatorID) {
    189 		var translation = new Zotero.Translate.Export();
    190 		translation.setItems(items);
    191 		translation.setTranslator(translatorID);
    192 		translation.setHandler("done", _copyToClipboard);
    193 		translation.translate();
    194 	}
    195 	
    196 	/*
    197 	 * handler when done exporting items to clipboard
    198 	 */
    199 	function _copyToClipboard(obj, worked) {
    200 		if(!worked) {
    201 			Zotero.alert(
    202 				null, Zotero.getString('general.error'), Zotero.getString("fileInterface.exportError")
    203 			);
    204 		} else {
    205 			Components.classes["@mozilla.org/widget/clipboardhelper;1"]
    206                       .getService(Components.interfaces.nsIClipboardHelper)
    207                       .copyString(obj.string.replace(/\r\n/g, "\n"));
    208 		}
    209 	}
    210 	
    211 	
    212 	this.getMendeleyDirectory = function () {
    213 		Components.classes["@mozilla.org/net/osfileconstantsservice;1"]
    214 			.getService(Components.interfaces.nsIOSFileConstantsService)
    215 			.init();
    216 		var path = OS.Constants.Path.homeDir;
    217 		if (Zotero.isMac) {
    218 			path = OS.Path.join(path, 'Library', 'Application Support', 'Mendeley Desktop');
    219 		}
    220 		else if (Zotero.isWin) {
    221 			path = OS.Path.join(path, 'AppData', 'Local', 'Mendeley Ltd', 'Mendeley Desktop');
    222 		}
    223 		else if (Zotero.isLinux) {
    224 			path = OS.Path.join(path, '.local', 'share', 'data', 'Mendeley Ltd.', 'Mendeley Desktop');
    225 		}
    226 		else {
    227 			throw new Error("Invalid platform");
    228 		}
    229 		return path;
    230 	};
    231 	
    232 	
    233 	this.findMendeleyDatabases = async function () {
    234 		var dbs = [];
    235 		try {
    236 			var dir = this.getMendeleyDirectory();
    237 			if (!await OS.File.exists(dir)) {
    238 				Zotero.debug(`${dir} does not exist`);
    239 				return dbs;
    240 			}
    241 			await Zotero.File.iterateDirectory(dir, function* (iterator) {
    242 				while (true) {
    243 					let entry = yield iterator.next();
    244 					if (entry.isDir) continue;
    245 					// online.sqlite, counterintuitively, is the default database before you sign in
    246 					if (entry.name == 'online.sqlite' || entry.name.endsWith('@www.mendeley.com.sqlite')) {
    247 						dbs.push({
    248 							name: entry.name,
    249 							path: entry.path,
    250 							lastModified: null,
    251 							size: null
    252 						});
    253 					}
    254 				}
    255 			});
    256 			for (let i = 0; i < dbs.length; i++) {
    257 				let dbPath = OS.Path.join(dir, dbs[i].name);
    258 				let info = await OS.File.stat(dbPath);
    259 				dbs[i].size = info.size;
    260 				dbs[i].lastModified = info.lastModificationDate;
    261 			}
    262 			dbs.sort((a, b) => {
    263 				return b.lastModified - a.lastModified;
    264 			});
    265 		}
    266 		catch (e) {
    267 			Zotero.logError(e);
    268 		}
    269 		return dbs;
    270 	};
    271 	
    272 	
    273 	this.showImportWizard = function () {
    274 		var libraryID = Zotero.Libraries.userLibraryID;
    275 		try {
    276 			let zp = Zotero.getActiveZoteroPane();
    277 			libraryID = zp.getSelectedLibraryID();
    278 		}
    279 		catch (e) {
    280 			Zotero.logError(e);
    281 		}
    282 		var args = {
    283 			libraryID
    284 		};
    285 		args.wrappedJSObject = args;
    286 		
    287 		Services.ww.openWindow(null, "chrome://zotero/content/import/importWizard.xul",
    288 			"importFile", "chrome,dialog=yes,centerscreen,width=600,height=400", args);
    289 	};
    290 	
    291 	
    292 	/**
    293 	 * Creates Zotero.Translate instance and shows file picker for file import
    294 	 *
    295 	 * @param {Object} options
    296 	 * @param {nsIFile|string|null} [options.file=null] - File to import, or none to show a filepicker
    297 	 * @param {Boolean} [options.addToLibraryRoot=false]
    298 	 * @param {Boolean} [options.createNewCollection=true] - Put items in a new collection
    299 	 * @param {Function} [options.onBeforeImport] - Callback to receive translation object, useful
    300 	 *     for displaying progress in a different way. This also causes an error to be throw
    301 	 *     instead of shown in the main window.
    302 	 */
    303 	this.importFile = Zotero.Promise.coroutine(function* (options = {}) {
    304 		if (!options) {
    305 			options = {};
    306 		}
    307 		if (typeof options == 'string' || options instanceof Components.interfaces.nsIFile) {
    308 			Zotero.debug("WARNING: importFile() now takes a single options object -- update your code");
    309 			options = {
    310 				file: options,
    311 				createNewCollection: arguments[1]
    312 			};
    313 		}
    314 		
    315 		var file = options.file ? Zotero.File.pathToFile(options.file) : null;
    316 		var createNewCollection = options.createNewCollection;
    317 		var addToLibraryRoot = options.addToLibraryRoot;
    318 		var onBeforeImport = options.onBeforeImport;
    319 		
    320 		if (createNewCollection === undefined && !addToLibraryRoot) {
    321 			createNewCollection = true;
    322 		}
    323 		else if (!createNewCollection) {
    324 			try {
    325 				if (!ZoteroPane.collectionsView.editable) {
    326 					ZoteroPane.collectionsView.selectLibrary(null);
    327 				}
    328 			} catch(e) {}
    329 		}
    330 		
    331 		var defaultNewCollectionPrefix = Zotero.getString("fileInterface.imported");
    332 		
    333 		var translation;
    334 		// Check if the file is an SQLite database
    335 		var sample = yield Zotero.File.getSample(file.path);
    336 		if (file.path == Zotero.DataDirectory.getDatabase()) {
    337 			// Blacklist the current Zotero database, which would cause a hang
    338 		}
    339 		else if (Zotero.MIME.sniffForMIMEType(sample) == 'application/x-sqlite3') {
    340 			// Mendeley import doesn't use the real translation architecture, but we create a
    341 			// translation object with the same interface
    342 			translation = yield _getMendeleyTranslation();
    343 			translation.createNewCollection = createNewCollection;
    344 			defaultNewCollectionPrefix = Zotero.getString(
    345 				'fileInterface.appImportCollection', 'Mendeley'
    346 			);
    347 		}
    348 		else if (file.path.endsWith('@www.mendeley.com.sqlite')
    349 				|| file.path.endsWith('online.sqlite')) {
    350 			// Keep in sync with importWizard.js
    351 			throw new Error('Encrypted Mendeley database');
    352 		}
    353 		
    354 		if (!translation) {
    355 			translation = new Zotero.Translate.Import();
    356 		}
    357 		translation.setLocation(file);
    358 		return _finishImport({
    359 			translation,
    360 			createNewCollection,
    361 			addToLibraryRoot,
    362 			defaultNewCollectionPrefix,
    363 			onBeforeImport
    364 		});
    365 	});
    366 	
    367 	
    368 	/**
    369 	 * Imports from clipboard
    370 	 */
    371 	this.importFromClipboard = Zotero.Promise.coroutine(function* () {
    372 		var str = Zotero.Utilities.Internal.getClipboard("text/unicode");
    373 		if(!str) {
    374 			var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    375 									.getService(Components.interfaces.nsIPromptService);
    376 			ps.alert(
    377 				null,
    378 				Zotero.getString('general.error'),
    379 				Zotero.getString('fileInterface.importClipboardNoDataError')
    380 			);
    381 		}
    382 		
    383 		var translation = new Zotero.Translate.Import();
    384 		translation.setString(str);
    385 	
    386 		try {
    387 			if (!ZoteroPane.collectionsView.editable) {
    388 				yield ZoteroPane.collectionsView.selectLibrary();
    389 			}
    390 		} catch(e) {}
    391 		
    392 		yield _finishImport({
    393 			translation,
    394 			createNewCollection: false
    395 		});
    396 		
    397 		// Select imported items
    398 		try {
    399 			if (translation.newItems) {
    400 				ZoteroPane.itemsView.selectItems(translation.newItems.map(item => item.id));
    401 			}
    402 		}
    403 		catch (e) {
    404 			Zotero.logError(e, 2);
    405 		}
    406 	});
    407 	
    408 	
    409 	var _finishImport = Zotero.Promise.coroutine(function* (options) {
    410 		var t = performance.now();
    411 		
    412 		var translation = options.translation;
    413 		var addToLibraryRoot = options.addToLibraryRoot;
    414 		var createNewCollection = options.createNewCollection;
    415 		var defaultNewCollectionPrefix = options.defaultNewCollectionPrefix;
    416 		var onBeforeImport = options.onBeforeImport;
    417 		
    418 		if (addToLibraryRoot && createNewCollection) {
    419 			throw new Error("Can't add to library root and create new collection");
    420 		}
    421 		
    422 		var showProgressWindow = !onBeforeImport;
    423 		
    424 		let translators = yield translation.getTranslators();
    425 		
    426 		// Unrecognized file
    427 		if (!translators.length) {
    428 			if (onBeforeImport) {
    429 				yield onBeforeImport(false);
    430 			}
    431 			
    432 			let ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
    433 				.getService(Components.interfaces.nsIPromptService);
    434 			let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK
    435 				+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING;
    436 			let index = ps.confirmEx(
    437 				null,
    438 				Zotero.getString('general.error'),
    439 				Zotero.getString("fileInterface.unsupportedFormat"),
    440 				buttonFlags,
    441 				null,
    442 				Zotero.getString("fileInterface.viewSupportedFormats"),
    443 				null, null, {}
    444 			);
    445 			if (index == 1) {
    446 				Zotero.launchURL("https://www.zotero.org/support/kb/importing");
    447 			}
    448 			return false;
    449 		}
    450 		
    451 		var libraryID = Zotero.Libraries.userLibraryID;
    452 		var importCollection = null;
    453 		try {
    454 			let zp = Zotero.getActiveZoteroPane();
    455 			libraryID = zp.getSelectedLibraryID();
    456 			if (addToLibraryRoot) {
    457 				yield zp.collectionsView.selectLibrary(libraryID);
    458 			}
    459 			else if (!createNewCollection) {
    460 				importCollection = zp.getSelectedCollection();
    461 			}
    462 		}
    463 		catch (e) {
    464 			Zotero.logError(e);
    465 		}
    466 		
    467 		if(createNewCollection) {
    468 			// Create a new collection to take imported items
    469 			let collectionName;
    470 			if(translation.location instanceof Components.interfaces.nsIFile) {
    471 				let leafName = translation.location.leafName;
    472 				collectionName = (translation.location.isDirectory() || leafName.indexOf(".") === -1 ? leafName
    473 					: leafName.substr(0, leafName.lastIndexOf(".")));
    474 				let allCollections = Zotero.Collections.getByLibrary(libraryID);
    475 				for(var i=0; i<allCollections.length; i++) {
    476 					if(allCollections[i].name == collectionName) {
    477 						collectionName += " "+(new Date()).toLocaleString();
    478 						break;
    479 					}
    480 				}
    481 			}
    482 			else {
    483 				collectionName = defaultNewCollectionPrefix + " " + (new Date()).toLocaleString();
    484 			}
    485 			importCollection = new Zotero.Collection;
    486 			importCollection.libraryID = libraryID;
    487 			importCollection.name = collectionName;
    488 			yield importCollection.saveTx();
    489 		}
    490 		
    491 		translation.setTranslator(translators[0]);
    492 		
    493 		// Show progress popup
    494 		var progressWin;
    495 		var progress;
    496 		if (showProgressWindow) {
    497 			progressWin = new Zotero.ProgressWindow({
    498 				closeOnClick: false
    499 			});
    500 			progressWin.changeHeadline(Zotero.getString('fileInterface.importing'));
    501 			let icon = 'chrome://zotero/skin/treesource-unfiled' + (Zotero.hiDPI ? "@2x" : "") + '.png';
    502 			progress = new progressWin.ItemProgress(
    503 				icon, translation.path ? OS.Path.basename(translation.path) : translators[0].label
    504 			);
    505 			progressWin.show();
    506 			
    507 			translation.setHandler("itemDone",  function () {
    508 				progress.setProgress(translation.getProgress());
    509 			});
    510 			
    511 			yield Zotero.Promise.delay(0);
    512 		}
    513 		else {
    514 			yield onBeforeImport(translation);
    515 		}
    516 		
    517 		let failed = false;
    518 		try {
    519 			yield translation.translate({
    520 				libraryID,
    521 				collections: importCollection ? [importCollection.id] : null
    522 			});
    523 		} catch(e) {
    524 			if (!showProgressWindow) {
    525 				throw e;
    526 			}
    527 			
    528 			progressWin.close();
    529 			Zotero.logError(e);
    530 			Zotero.alert(
    531 				null,
    532 				Zotero.getString('general.error'),
    533 				Zotero.getString("fileInterface.importError")
    534 			);
    535 			return false;
    536 		}
    537 		
    538 		var numItems = translation.newItems.length;
    539 		
    540 		// Show popup on completion
    541 		if (showProgressWindow) {
    542 			progressWin.changeHeadline(Zotero.getString('fileInterface.importComplete'));
    543 			let icon;
    544 			if (numItems == 1) {
    545 				icon = translation.newItems[0].getImageSrc();
    546 			}
    547 			else {
    548 				icon = 'chrome://zotero/skin/treesource-unfiled' + (Zotero.hiDPI ? "@2x" : "") + '.png';
    549 			}
    550 			let text = Zotero.getString(`fileInterface.itemsWereImported`, numItems, numItems);
    551 			progress.setIcon(icon);
    552 			progress.setText(text);
    553 			// For synchronous translators, which don't update progress
    554 			progress.setProgress(100);
    555 			progressWin.startCloseTimer(5000);
    556 		}
    557 		
    558 		Zotero.debug(`Imported ${numItems} item(s) in ${performance.now() - t} ms`);
    559 		
    560 		return true;
    561 	});
    562 	
    563 	
    564 	var _getMendeleyTranslation = async function () {
    565 		if (true) {
    566 			Components.utils.import("chrome://zotero/content/import/mendeley/mendeleyImport.js");
    567 		}
    568 		// TEMP: Load uncached from ~/zotero-client for development
    569 		else {
    570 			Components.utils.import("resource://gre/modules/FileUtils.jsm");
    571 			let file = FileUtils.getDir("Home", []);
    572 			file = OS.Path.join(
    573 				file.path,
    574 				'zotero-client', 'chrome', 'content', 'zotero', 'import', 'mendeley', 'mendeleyImport.js'
    575 			);
    576 			let fileURI = OS.Path.toFileURI(file);
    577 			let xmlhttp = await Zotero.HTTP.request(
    578 				'GET',
    579 				fileURI,
    580 				{
    581 					dontCache: true,
    582 					responseType: 'text'
    583 				}
    584 			);
    585 			eval(xmlhttp.response);
    586 		}
    587 		return new Zotero_Import_Mendeley();
    588 	}
    589 	
    590 	
    591 	/**
    592 	 * Creates a bibliography from a collection or saved search
    593 	 */
    594 	this.bibliographyFromCollection = function () {
    595 		var items = ZoteroPane.getSortedItems();
    596 		
    597 		// Find collection name
    598 		var name = false;
    599 		var collection = ZoteroPane.getSelectedCollection();
    600 		if (collection) {
    601 			name = collection.name;
    602 		}
    603 		else {
    604 			let search = ZoteroPane.getSelectedSavedSearch();
    605 			if (search) {
    606 				name = search.name;
    607 			}
    608 		}
    609 		
    610 		_doBibliographyOptions(name, items);
    611 	}
    612 	
    613 	/*
    614 	 * Creates a bibliography from a items
    615 	 */
    616 	function bibliographyFromItems() {
    617 		var items = ZoteroPane_Local.getSelectedItems();
    618 		if(!items || !items.length) throw("no items currently selected");
    619 		
    620 		_doBibliographyOptions(Zotero.getString("fileInterface.untitledBibliography"), items);
    621 	}
    622 	
    623 	
    624 	/**
    625 	 * Copies HTML and text citations or bibliography entries for passed items in given style
    626 	 *
    627 	 * Does not check that items are actual references (and not notes or attachments)
    628 	 *
    629 	 * @param {Zotero.Item[]} items
    630 	 * @param {String} style - Style id string (e.g., 'http://www.zotero.org/styles/apa')
    631 	 * @param {String} locale - Locale (e.g., 'en-US')
    632 	 * @param {Boolean} [asHTML=false] - Use HTML source for plain-text data
    633 	 * @param {Boolean} [asCitations=false] - Copy citation cluster instead of bibliography
    634 	 */
    635 	this.copyItemsToClipboard = function (items, style, locale, asHTML, asCitations) {
    636 		// copy to clipboard
    637 		var transferable = Components.classes["@mozilla.org/widget/transferable;1"].
    638 						   createInstance(Components.interfaces.nsITransferable);
    639 		var clipboardService = Components.classes["@mozilla.org/widget/clipboard;1"].
    640 							   getService(Components.interfaces.nsIClipboard);
    641 		style = Zotero.Styles.get(style);
    642 		var cslEngine = style.getCiteProc(locale);
    643 		
    644 		if (asCitations) {
    645 			cslEngine.updateItems(items.map(item => item.id));
    646 			var citation = {
    647 				citationItems: items.map(item => ({ id: item.id })),
    648 				properties: {}
    649 			};
    650 			var output = cslEngine.previewCitationCluster(citation, [], [], "html");
    651 		}
    652 		else {
    653 			var output = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "html");
    654 		}
    655 		
    656 		// add HTML
    657 		var str = Components.classes["@mozilla.org/supports-string;1"].
    658 				  createInstance(Components.interfaces.nsISupportsString);
    659 		str.data = output;
    660 		transferable.addDataFlavor("text/html");
    661 		transferable.setTransferData("text/html", str, output.length * 2);
    662 		
    663 		// If not "Copy as HTML", add plaintext; otherwise use HTML from above and just mark as text
    664 		if(!asHTML) {
    665 			if (asCitations) {
    666 				output = cslEngine.previewCitationCluster(citation, [], [], "text");
    667 			}
    668 			else {
    669 				// Generate engine again to work around citeproc-js problem:
    670 				// https://github.com/zotero/zotero/commit/4a475ff3
    671 				cslEngine = style.getCiteProc(locale);
    672 				output = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "text");
    673 			}
    674 		}
    675 		
    676 		var str = Components.classes["@mozilla.org/supports-string;1"].
    677 				  createInstance(Components.interfaces.nsISupportsString);
    678 		str.data = output;
    679 		transferable.addDataFlavor("text/unicode");
    680 		transferable.setTransferData("text/unicode", str, output.length * 2);
    681 		
    682 		clipboardService.setData(transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard);
    683 	}
    684 	
    685 	
    686 	/*
    687 	 * Shows bibliography options and creates a bibliography
    688 	 */
    689 	function _doBibliographyOptions(name, items) {
    690 		// make sure at least one item is not a standalone note or attachment
    691 		var haveRegularItem = false;
    692 		for (let item of items) {
    693 			if (item.isRegularItem()) {
    694 				haveRegularItem = true;
    695 				break;
    696 			}
    697 		}
    698 		if (!haveRegularItem) {
    699 			Zotero.alert(
    700 				null,
    701 				Zotero.getString('general.error'),
    702 				Zotero.getString("fileInterface.noReferencesError")
    703 			);
    704 			return;
    705 		}
    706 		
    707 		var io = new Object();
    708 		var newDialog = window.openDialog("chrome://zotero/content/bibliography.xul",
    709 			"_blank","chrome,modal,centerscreen", io);
    710 		
    711 		if(!io.method) return;
    712 		
    713 		// determine output format
    714 		var format = "html";
    715 		if(io.method == "save-as-rtf") {
    716 			format = "rtf";
    717 		}
    718 		
    719 		// determine locale preference
    720 		var locale = io.locale;
    721 		
    722 		// generate bibliography
    723 		try {
    724 			if(io.method == 'copy-to-clipboard') {
    725 				Zotero_File_Interface.copyItemsToClipboard(items, io.style, locale, false, io.mode === "citations");
    726 			}
    727 			else {
    728 				var style = Zotero.Styles.get(io.style);
    729 				var cslEngine = style.getCiteProc(locale);
    730 				var bibliography = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine,
    731 					items, format, io.mode === "citations");
    732 			}
    733 		} catch(e) {
    734 			Zotero.alert(
    735 				null,
    736 				Zotero.getString('general.error'),
    737 				Zotero.getString("fileInterface.bibliographyGenerationError")
    738 			);
    739 			throw(e);
    740 		}
    741 		
    742 		if(io.method == "print") {
    743 			// printable bibliography, using a hidden browser
    744 			var browser = Zotero.Browser.createHiddenBrowser(window);
    745 			
    746 			var listener = function() {
    747 				if(browser.contentDocument.location.href == "about:blank") return;
    748 				browser.removeEventListener("pageshow", listener, false);
    749 				
    750 				// this is kinda nasty, but we have to temporarily modify the user's
    751 				// settings to eliminate the header and footer. the other way to do
    752 				// this would be to attempt to print with an embedded browser, but
    753 				// it's not even clear how to attempt to create one
    754 				var prefService = Components.classes["@mozilla.org/preferences-service;1"].
    755 								  getService(Components.interfaces.nsIPrefBranch);
    756 				var prefsToClear = ["print.print_headerleft", "print.print_headercenter", 
    757 									"print.print_headerright", "print.print_footerleft", 
    758 									"print.print_footercenter", "print.print_footerright"];
    759 				var oldPrefs = [];
    760 				for(var i in prefsToClear) {
    761 					oldPrefs[i] = prefService.getCharPref(prefsToClear[i]);
    762 					prefService.setCharPref(prefsToClear[i], "");
    763 				}
    764 				
    765 				// print
    766 				browser.contentWindow.print();
    767 				
    768 				// set the prefs back
    769 				for(var i in prefsToClear) {
    770 					prefService.setCharPref(prefsToClear[i], oldPrefs[i]);
    771 				}
    772 				
    773 				// TODO can't delete hidden browser object here or else print will fail...
    774 			}
    775 			
    776 			browser.addEventListener("pageshow", listener, false);
    777 			browser.loadURIWithFlags("data:text/html;charset=utf-8,"+encodeURI(bibliography),
    778 				Components.interfaces.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, "utf-8", null);
    779 		} else if(io.method == "save-as-html") {
    780 			var fStream = _saveBibliography(name, "HTML");
    781 			
    782 			if(fStream !== false) {			
    783 				var html = "";
    784 				html +='<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n';
    785 				html +='<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n';
    786 				html +='<head>\n';
    787 				html +='<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n';
    788 				html +='<title>'+Zotero.getString("fileInterface.bibliographyHTMLTitle")+'</title>\n';
    789 				html +='</head>\n';
    790 				html +='<body>\n';
    791 				html += bibliography;
    792 				html +='</body>\n';
    793 				html +='</html>\n';
    794 				
    795 				// create UTF-8 output stream
    796 				var os = Components.classes["@mozilla.org/intl/converter-output-stream;1"].
    797 						 createInstance(Components.interfaces.nsIConverterOutputStream);
    798 				os.init(fStream, "UTF-8", 0, "?".charCodeAt(0));
    799 
    800 				os.writeString(html);
    801 				
    802 				os.close();
    803 				fStream.close();
    804 			}
    805 		} else if(io.method == "save-as-rtf") {
    806 			var fStream = _saveBibliography(name, "RTF");
    807 			if(fStream !== false) {
    808 				fStream.write(bibliography, bibliography.length);
    809 				fStream.close();
    810 			}
    811 		}
    812 	}
    813 	
    814 	
    815 	function _saveBibliography(name, format) {	
    816 		// savable bibliography, using a file stream
    817 		const nsIFilePicker = Components.interfaces.nsIFilePicker;
    818 		var fp = Components.classes["@mozilla.org/filepicker;1"]
    819 				.createInstance(nsIFilePicker);
    820 		fp.init(window, "Save Bibliography", nsIFilePicker.modeSave);
    821 		
    822 		if(format == "RTF") {
    823 			var extension = "rtf";
    824 			fp.appendFilter("RTF", "*.rtf");
    825 		} else {
    826 			var extension = "html";
    827 			fp.appendFilters(nsIFilePicker.filterHTML);
    828 		}
    829 		
    830 		fp.defaultString = name+"."+extension;
    831 		
    832 		var rv = fp.show();
    833 		if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {				
    834 			// open file
    835 			var fStream = Components.classes["@mozilla.org/network/file-output-stream;1"].
    836 						  createInstance(Components.interfaces.nsIFileOutputStream);
    837 			fStream.init(fp.file, 0x02 | 0x08 | 0x20, 0o664, 0); // write, create, truncate
    838 			return fStream;
    839 		} else {
    840 			return false;
    841 		}
    842 	}
    843 }
    844 
    845 // Handles the display of a progress indicator
    846 Zotero_File_Interface.Progress = new function() {
    847 	this.show = show;
    848 	this.close = close;
    849 	
    850 	function show(headline) {
    851 		Zotero.showZoteroPaneProgressMeter(headline);
    852 	}
    853 	
    854 	function close() {
    855 		Zotero.hideZoteroPaneOverlays();
    856 	}
    857 }