clipboard.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. module.metadata = {
  6. "stability": "stable",
  7. "engines": {
  8. // TODO Fennec Support 789757
  9. "Firefox": "*"
  10. }
  11. };
  12. const { Cc, Ci } = require("chrome");
  13. const { DataURL } = require("./url");
  14. const errors = require("./deprecated/errors");
  15. const apiUtils = require("./deprecated/api-utils");
  16. /*
  17. While these data flavors resemble Internet media types, they do
  18. no directly map to them.
  19. */
  20. const kAllowableFlavors = [
  21. "text/unicode",
  22. "text/html",
  23. "image/png"
  24. /* CURRENTLY UNSUPPORTED FLAVORS
  25. "text/plain",
  26. "image/jpg",
  27. "image/jpeg",
  28. "image/gif",
  29. "text/x-moz-text-internal",
  30. "AOLMAIL",
  31. "application/x-moz-file",
  32. "text/x-moz-url",
  33. "text/x-moz-url-data",
  34. "text/x-moz-url-desc",
  35. "text/x-moz-url-priv",
  36. "application/x-moz-nativeimage",
  37. "application/x-moz-nativehtml",
  38. "application/x-moz-file-promise-url",
  39. "application/x-moz-file-promise-dest-filename",
  40. "application/x-moz-file-promise",
  41. "application/x-moz-file-promise-dir"
  42. */
  43. ];
  44. /*
  45. Aliases for common flavors. Not all flavors will
  46. get an alias. New aliases must be approved by a
  47. Jetpack API druid.
  48. */
  49. const kFlavorMap = [
  50. { short: "text", long: "text/unicode" },
  51. { short: "html", long: "text/html" },
  52. { short: "image", long: "image/png" }
  53. ];
  54. let clipboardService = Cc["@mozilla.org/widget/clipboard;1"].
  55. getService(Ci.nsIClipboard);
  56. let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].
  57. getService(Ci.nsIClipboardHelper);
  58. let imageTools = Cc["@mozilla.org/image/tools;1"].
  59. getService(Ci.imgITools);
  60. exports.set = function(aData, aDataType) {
  61. let options = {
  62. data: aData,
  63. datatype: aDataType || "text"
  64. };
  65. // If `aDataType` is not given or if it's "image", the data is parsed as
  66. // data URL to detect a better datatype
  67. if (aData && (!aDataType || aDataType === "image")) {
  68. try {
  69. let dataURL = new DataURL(aData);
  70. options.datatype = dataURL.mimeType;
  71. options.data = dataURL.data;
  72. }
  73. catch (e if e.name === "URIError") {
  74. // Not a valid Data URL
  75. }
  76. }
  77. options = apiUtils.validateOptions(options, {
  78. data: {
  79. is: ["string"]
  80. },
  81. datatype: {
  82. is: ["string"]
  83. }
  84. });
  85. let flavor = fromJetpackFlavor(options.datatype);
  86. if (!flavor)
  87. throw new Error("Invalid flavor for " + options.datatype);
  88. // Additional checks for using the simple case
  89. if (flavor == "text/unicode") {
  90. clipboardHelper.copyString(options.data);
  91. return true;
  92. }
  93. // Below are the more complex cases where we actually have to work with a
  94. // nsITransferable object
  95. var xferable = Cc["@mozilla.org/widget/transferable;1"].
  96. createInstance(Ci.nsITransferable);
  97. if (!xferable)
  98. throw new Error("Couldn't set the clipboard due to an internal error " +
  99. "(couldn't create a Transferable object).");
  100. // Bug 769440: Starting with FF16, transferable have to be inited
  101. if ("init" in xferable)
  102. xferable.init(null);
  103. switch (flavor) {
  104. case "text/html":
  105. // add text/html flavor
  106. let (str = Cc["@mozilla.org/supports-string;1"].
  107. createInstance(Ci.nsISupportsString))
  108. {
  109. str.data = options.data;
  110. xferable.addDataFlavor(flavor);
  111. xferable.setTransferData(flavor, str, str.data.length * 2);
  112. }
  113. // add a text/unicode flavor (html converted to plain text)
  114. let (str = Cc["@mozilla.org/supports-string;1"].
  115. createInstance(Ci.nsISupportsString),
  116. converter = Cc["@mozilla.org/feed-textconstruct;1"].
  117. createInstance(Ci.nsIFeedTextConstruct))
  118. {
  119. converter.type = "html";
  120. converter.text = options.data;
  121. str.data = converter.plainText();
  122. xferable.addDataFlavor("text/unicode");
  123. xferable.setTransferData("text/unicode", str, str.data.length * 2);
  124. }
  125. break;
  126. // Set images to the clipboard is not straightforward, to have an idea how
  127. // it works on platform side, see:
  128. // http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsCopySupport.cpp?rev=7857c5bff017#530
  129. case "image/png":
  130. let image = options.data;
  131. let container = {};
  132. try {
  133. let input = Cc["@mozilla.org/io/string-input-stream;1"].
  134. createInstance(Ci.nsIStringInputStream);
  135. input.setData(image, image.length);
  136. imageTools.decodeImageData(input, flavor, container);
  137. }
  138. catch (e) {
  139. throw new Error("Unable to decode data given in a valid image.");
  140. }
  141. // Store directly the input stream makes the cliboard's data available
  142. // for Firefox but not to the others application or to the OS. Therefore,
  143. // a `nsISupportsInterfacePointer` object that reference an `imgIContainer`
  144. // with the image is needed.
  145. var imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"].
  146. createInstance(Ci.nsISupportsInterfacePointer);
  147. imgPtr.data = container.value;
  148. xferable.addDataFlavor(flavor);
  149. xferable.setTransferData(flavor, imgPtr, -1);
  150. break;
  151. default:
  152. throw new Error("Unable to handle the flavor " + flavor + ".");
  153. }
  154. // TODO: Not sure if this will ever actually throw. -zpao
  155. try {
  156. clipboardService.setData(
  157. xferable,
  158. null,
  159. clipboardService.kGlobalClipboard
  160. );
  161. } catch (e) {
  162. throw new Error("Couldn't set clipboard data due to an internal error: " + e);
  163. }
  164. return true;
  165. };
  166. exports.get = function(aDataType) {
  167. let options = {
  168. datatype: aDataType
  169. };
  170. // Figure out the best data type for the clipboard's data, if omitted
  171. if (!aDataType) {
  172. if (~currentFlavors().indexOf("image"))
  173. options.datatype = "image";
  174. else
  175. options.datatype = "text";
  176. }
  177. options = apiUtils.validateOptions(options, {
  178. datatype: {
  179. is: ["string"]
  180. }
  181. });
  182. var xferable = Cc["@mozilla.org/widget/transferable;1"].
  183. createInstance(Ci.nsITransferable);
  184. if (!xferable)
  185. throw new Error("Couldn't set the clipboard due to an internal error " +
  186. "(couldn't create a Transferable object).");
  187. // Bug 769440: Starting with FF16, transferable have to be inited
  188. if ("init" in xferable)
  189. xferable.init(null);
  190. var flavor = fromJetpackFlavor(options.datatype);
  191. // Ensure that the user hasn't requested a flavor that we don't support.
  192. if (!flavor)
  193. throw new Error("Getting the clipboard with the flavor '" + flavor +
  194. "' is not supported.");
  195. // TODO: Check for matching flavor first? Probably not worth it.
  196. xferable.addDataFlavor(flavor);
  197. // Get the data into our transferable.
  198. clipboardService.getData(
  199. xferable,
  200. clipboardService.kGlobalClipboard
  201. );
  202. var data = {};
  203. var dataLen = {};
  204. try {
  205. xferable.getTransferData(flavor, data, dataLen);
  206. } catch (e) {
  207. // Clipboard doesn't contain data in flavor, return null.
  208. return null;
  209. }
  210. // There's no data available, return.
  211. if (data.value === null)
  212. return null;
  213. // TODO: Add flavors here as we support more in kAllowableFlavors.
  214. switch (flavor) {
  215. case "text/unicode":
  216. case "text/html":
  217. data = data.value.QueryInterface(Ci.nsISupportsString).data;
  218. break;
  219. case "image/png":
  220. let dataURL = new DataURL();
  221. dataURL.mimeType = flavor;
  222. dataURL.base64 = true;
  223. let image = data.value;
  224. // Due to the differences in how images could be stored in the clipboard
  225. // the checks below are needed. The clipboard could already provide the
  226. // image as byte streams, but also as pointer, or as image container.
  227. // If it's not possible obtain a byte stream, the function returns `null`.
  228. if (image instanceof Ci.nsISupportsInterfacePointer)
  229. image = image.data;
  230. if (image instanceof Ci.imgIContainer)
  231. image = imageTools.encodeImage(image, flavor);
  232. if (image instanceof Ci.nsIInputStream) {
  233. let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].
  234. createInstance(Ci.nsIBinaryInputStream);
  235. binaryStream.setInputStream(image);
  236. dataURL.data = binaryStream.readBytes(binaryStream.available());
  237. data = dataURL.toString();
  238. }
  239. else
  240. data = null;
  241. break;
  242. default:
  243. data = null;
  244. }
  245. return data;
  246. };
  247. function currentFlavors() {
  248. // Loop over kAllowableFlavors, calling hasDataMatchingFlavors for each.
  249. // This doesn't seem like the most efficient way, but we can't get
  250. // confirmation for specific flavors any other way. This is supposed to be
  251. // an inexpensive call, so performance shouldn't be impacted (much).
  252. var currentFlavors = [];
  253. for each (var flavor in kAllowableFlavors) {
  254. var matches = clipboardService.hasDataMatchingFlavors(
  255. [flavor],
  256. 1,
  257. clipboardService.kGlobalClipboard
  258. );
  259. if (matches)
  260. currentFlavors.push(toJetpackFlavor(flavor));
  261. }
  262. return currentFlavors;
  263. };
  264. Object.defineProperty(exports, "currentFlavors", { get : currentFlavors });
  265. // SUPPORT FUNCTIONS ////////////////////////////////////////////////////////
  266. function toJetpackFlavor(aFlavor) {
  267. for each (let flavorMap in kFlavorMap)
  268. if (flavorMap.long == aFlavor)
  269. return flavorMap.short;
  270. // Return null in the case where we don't match
  271. return null;
  272. }
  273. function fromJetpackFlavor(aJetpackFlavor) {
  274. // TODO: Handle proper flavors better
  275. for each (let flavorMap in kFlavorMap)
  276. if (flavorMap.short == aJetpackFlavor || flavorMap.long == aJetpackFlavor)
  277. return flavorMap.long;
  278. // Return null in the case where we don't match.
  279. return null;
  280. }