michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "stable", michael@0: "engines": { michael@0: // TODO Fennec Support 789757 michael@0: "Firefox": "*" michael@0: } michael@0: }; michael@0: michael@0: const { Cc, Ci } = require("chrome"); michael@0: const { DataURL } = require("./url"); michael@0: const errors = require("./deprecated/errors"); michael@0: const apiUtils = require("./deprecated/api-utils"); michael@0: /* michael@0: While these data flavors resemble Internet media types, they do michael@0: no directly map to them. michael@0: */ michael@0: const kAllowableFlavors = [ michael@0: "text/unicode", michael@0: "text/html", michael@0: "image/png" michael@0: /* CURRENTLY UNSUPPORTED FLAVORS michael@0: "text/plain", michael@0: "image/jpg", michael@0: "image/jpeg", michael@0: "image/gif", michael@0: "text/x-moz-text-internal", michael@0: "AOLMAIL", michael@0: "application/x-moz-file", michael@0: "text/x-moz-url", michael@0: "text/x-moz-url-data", michael@0: "text/x-moz-url-desc", michael@0: "text/x-moz-url-priv", michael@0: "application/x-moz-nativeimage", michael@0: "application/x-moz-nativehtml", michael@0: "application/x-moz-file-promise-url", michael@0: "application/x-moz-file-promise-dest-filename", michael@0: "application/x-moz-file-promise", michael@0: "application/x-moz-file-promise-dir" michael@0: */ michael@0: ]; michael@0: michael@0: /* michael@0: Aliases for common flavors. Not all flavors will michael@0: get an alias. New aliases must be approved by a michael@0: Jetpack API druid. michael@0: */ michael@0: const kFlavorMap = [ michael@0: { short: "text", long: "text/unicode" }, michael@0: { short: "html", long: "text/html" }, michael@0: { short: "image", long: "image/png" } michael@0: ]; michael@0: michael@0: let clipboardService = Cc["@mozilla.org/widget/clipboard;1"]. michael@0: getService(Ci.nsIClipboard); michael@0: michael@0: let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]. michael@0: getService(Ci.nsIClipboardHelper); michael@0: michael@0: let imageTools = Cc["@mozilla.org/image/tools;1"]. michael@0: getService(Ci.imgITools); michael@0: michael@0: exports.set = function(aData, aDataType) { michael@0: michael@0: let options = { michael@0: data: aData, michael@0: datatype: aDataType || "text" michael@0: }; michael@0: michael@0: // If `aDataType` is not given or if it's "image", the data is parsed as michael@0: // data URL to detect a better datatype michael@0: if (aData && (!aDataType || aDataType === "image")) { michael@0: try { michael@0: let dataURL = new DataURL(aData); michael@0: michael@0: options.datatype = dataURL.mimeType; michael@0: options.data = dataURL.data; michael@0: } michael@0: catch (e) { michael@0: // Ignore invalid URIs michael@0: if (e.name !== "URIError") { michael@0: throw e; michael@0: } michael@0: } michael@0: } michael@0: michael@0: options = apiUtils.validateOptions(options, { michael@0: data: { michael@0: is: ["string"] michael@0: }, michael@0: datatype: { michael@0: is: ["string"] michael@0: } michael@0: }); michael@0: michael@0: let flavor = fromJetpackFlavor(options.datatype); michael@0: michael@0: if (!flavor) michael@0: throw new Error("Invalid flavor for " + options.datatype); michael@0: michael@0: // Additional checks for using the simple case michael@0: if (flavor == "text/unicode") { michael@0: clipboardHelper.copyString(options.data); michael@0: return true; michael@0: } michael@0: michael@0: // Below are the more complex cases where we actually have to work with a michael@0: // nsITransferable object michael@0: var xferable = Cc["@mozilla.org/widget/transferable;1"]. michael@0: createInstance(Ci.nsITransferable); michael@0: if (!xferable) michael@0: throw new Error("Couldn't set the clipboard due to an internal error " + michael@0: "(couldn't create a Transferable object)."); michael@0: // Bug 769440: Starting with FF16, transferable have to be inited michael@0: if ("init" in xferable) michael@0: xferable.init(null); michael@0: michael@0: switch (flavor) { michael@0: case "text/html": michael@0: // add text/html flavor michael@0: let (str = Cc["@mozilla.org/supports-string;1"]. michael@0: createInstance(Ci.nsISupportsString)) michael@0: { michael@0: str.data = options.data; michael@0: xferable.addDataFlavor(flavor); michael@0: xferable.setTransferData(flavor, str, str.data.length * 2); michael@0: } michael@0: michael@0: // add a text/unicode flavor (html converted to plain text) michael@0: let (str = Cc["@mozilla.org/supports-string;1"]. michael@0: createInstance(Ci.nsISupportsString), michael@0: converter = Cc["@mozilla.org/feed-textconstruct;1"]. michael@0: createInstance(Ci.nsIFeedTextConstruct)) michael@0: { michael@0: converter.type = "html"; michael@0: converter.text = options.data; michael@0: str.data = converter.plainText(); michael@0: xferable.addDataFlavor("text/unicode"); michael@0: xferable.setTransferData("text/unicode", str, str.data.length * 2); michael@0: } michael@0: break; michael@0: michael@0: // Set images to the clipboard is not straightforward, to have an idea how michael@0: // it works on platform side, see: michael@0: // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530 michael@0: case "image/png": michael@0: let image = options.data; michael@0: michael@0: let container = {}; michael@0: michael@0: try { michael@0: let input = Cc["@mozilla.org/io/string-input-stream;1"]. michael@0: createInstance(Ci.nsIStringInputStream); michael@0: michael@0: input.setData(image, image.length); michael@0: michael@0: imageTools.decodeImageData(input, flavor, container); michael@0: } michael@0: catch (e) { michael@0: throw new Error("Unable to decode data given in a valid image."); michael@0: } michael@0: michael@0: // Store directly the input stream makes the cliboard's data available michael@0: // for Firefox but not to the others application or to the OS. Therefore, michael@0: // a `nsISupportsInterfacePointer` object that reference an `imgIContainer` michael@0: // with the image is needed. michael@0: var imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"]. michael@0: createInstance(Ci.nsISupportsInterfacePointer); michael@0: michael@0: imgPtr.data = container.value; michael@0: michael@0: xferable.addDataFlavor(flavor); michael@0: xferable.setTransferData(flavor, imgPtr, -1); michael@0: michael@0: break; michael@0: default: michael@0: throw new Error("Unable to handle the flavor " + flavor + "."); michael@0: } michael@0: michael@0: // TODO: Not sure if this will ever actually throw. -zpao michael@0: try { michael@0: clipboardService.setData( michael@0: xferable, michael@0: null, michael@0: clipboardService.kGlobalClipboard michael@0: ); michael@0: } catch (e) { michael@0: throw new Error("Couldn't set clipboard data due to an internal error: " + e); michael@0: } michael@0: return true; michael@0: }; michael@0: michael@0: michael@0: exports.get = function(aDataType) { michael@0: let options = { michael@0: datatype: aDataType michael@0: }; michael@0: michael@0: // Figure out the best data type for the clipboard's data, if omitted michael@0: if (!aDataType) { michael@0: if (~currentFlavors().indexOf("image")) michael@0: options.datatype = "image"; michael@0: else michael@0: options.datatype = "text"; michael@0: } michael@0: michael@0: options = apiUtils.validateOptions(options, { michael@0: datatype: { michael@0: is: ["string"] michael@0: } michael@0: }); michael@0: michael@0: var xferable = Cc["@mozilla.org/widget/transferable;1"]. michael@0: createInstance(Ci.nsITransferable); michael@0: if (!xferable) michael@0: throw new Error("Couldn't set the clipboard due to an internal error " + michael@0: "(couldn't create a Transferable object)."); michael@0: // Bug 769440: Starting with FF16, transferable have to be inited michael@0: if ("init" in xferable) michael@0: xferable.init(null); michael@0: michael@0: var flavor = fromJetpackFlavor(options.datatype); michael@0: michael@0: // Ensure that the user hasn't requested a flavor that we don't support. michael@0: if (!flavor) michael@0: throw new Error("Getting the clipboard with the flavor '" + flavor + michael@0: "' is not supported."); michael@0: michael@0: // TODO: Check for matching flavor first? Probably not worth it. michael@0: michael@0: xferable.addDataFlavor(flavor); michael@0: // Get the data into our transferable. michael@0: clipboardService.getData( michael@0: xferable, michael@0: clipboardService.kGlobalClipboard michael@0: ); michael@0: michael@0: var data = {}; michael@0: var dataLen = {}; michael@0: try { michael@0: xferable.getTransferData(flavor, data, dataLen); michael@0: } catch (e) { michael@0: // Clipboard doesn't contain data in flavor, return null. michael@0: return null; michael@0: } michael@0: michael@0: // There's no data available, return. michael@0: if (data.value === null) michael@0: return null; michael@0: michael@0: // TODO: Add flavors here as we support more in kAllowableFlavors. michael@0: switch (flavor) { michael@0: case "text/unicode": michael@0: case "text/html": michael@0: data = data.value.QueryInterface(Ci.nsISupportsString).data; michael@0: break; michael@0: case "image/png": michael@0: let dataURL = new DataURL(); michael@0: michael@0: dataURL.mimeType = flavor; michael@0: dataURL.base64 = true; michael@0: michael@0: let image = data.value; michael@0: michael@0: // Due to the differences in how images could be stored in the clipboard michael@0: // the checks below are needed. The clipboard could already provide the michael@0: // image as byte streams, but also as pointer, or as image container. michael@0: // If it's not possible obtain a byte stream, the function returns `null`. michael@0: if (image instanceof Ci.nsISupportsInterfacePointer) michael@0: image = image.data; michael@0: michael@0: if (image instanceof Ci.imgIContainer) michael@0: image = imageTools.encodeImage(image, flavor); michael@0: michael@0: if (image instanceof Ci.nsIInputStream) { michael@0: let binaryStream = Cc["@mozilla.org/binaryinputstream;1"]. michael@0: createInstance(Ci.nsIBinaryInputStream); michael@0: michael@0: binaryStream.setInputStream(image); michael@0: michael@0: dataURL.data = binaryStream.readBytes(binaryStream.available()); michael@0: michael@0: data = dataURL.toString(); michael@0: } michael@0: else michael@0: data = null; michael@0: michael@0: break; michael@0: default: michael@0: data = null; michael@0: } michael@0: michael@0: return data; michael@0: }; michael@0: michael@0: function currentFlavors() { michael@0: // Loop over kAllowableFlavors, calling hasDataMatchingFlavors for each. michael@0: // This doesn't seem like the most efficient way, but we can't get michael@0: // confirmation for specific flavors any other way. This is supposed to be michael@0: // an inexpensive call, so performance shouldn't be impacted (much). michael@0: var currentFlavors = []; michael@0: for each (var flavor in kAllowableFlavors) { michael@0: var matches = clipboardService.hasDataMatchingFlavors( michael@0: [flavor], michael@0: 1, michael@0: clipboardService.kGlobalClipboard michael@0: ); michael@0: if (matches) michael@0: currentFlavors.push(toJetpackFlavor(flavor)); michael@0: } michael@0: return currentFlavors; michael@0: }; michael@0: michael@0: Object.defineProperty(exports, "currentFlavors", { get : currentFlavors }); michael@0: michael@0: // SUPPORT FUNCTIONS //////////////////////////////////////////////////////// michael@0: michael@0: function toJetpackFlavor(aFlavor) { michael@0: for each (let flavorMap in kFlavorMap) michael@0: if (flavorMap.long == aFlavor) michael@0: return flavorMap.short; michael@0: // Return null in the case where we don't match michael@0: return null; michael@0: } michael@0: michael@0: function fromJetpackFlavor(aJetpackFlavor) { michael@0: // TODO: Handle proper flavors better michael@0: for each (let flavorMap in kFlavorMap) michael@0: if (flavorMap.short == aJetpackFlavor || flavorMap.long == aJetpackFlavor) michael@0: return flavorMap.long; michael@0: // Return null in the case where we don't match. michael@0: return null; michael@0: }