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