Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
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/. */
5 "use strict";
7 module.metadata = {
8 "stability": "stable",
9 "engines": {
10 "Firefox": "*"
11 }
12 };
14 const { Ci, Cc } = require("chrome"),
15 { setTimeout } = require("./timers"),
16 { emit, off } = require("./event/core"),
17 { Class, obscure } = require("./core/heritage"),
18 { EventTarget } = require("./event/target"),
19 { ns } = require("./core/namespace"),
20 { when: unload } = require("./system/unload"),
21 { ignoreWindow } = require('./private-browsing/utils'),
22 { getTabs, getTabContentWindow, getTabForContentWindow,
23 getAllTabContentWindows } = require('./tabs/utils'),
24 winUtils = require("./window/utils"),
25 events = require("./system/events"),
26 { iteratorSymbol, forInIterator } = require("./util/iteration");
28 // The selection types
29 const HTML = 0x01,
30 TEXT = 0x02,
31 DOM = 0x03; // internal use only
33 // A more developer-friendly message than the caught exception when is not
34 // possible change a selection.
35 const ERR_CANNOT_CHANGE_SELECTION =
36 "It isn't possible to change the selection, as there isn't currently a selection";
38 const selections = ns();
40 const Selection = Class({
41 /**
42 * Creates an object from which a selection can be set, get, etc. Each
43 * object has an associated with a range number. Range numbers are the
44 * 0-indexed counter of selection ranges as explained at
45 * https://developer.mozilla.org/en/DOM/Selection.
46 *
47 * @param rangeNumber
48 * The zero-based range index into the selection
49 */
50 initialize: function initialize(rangeNumber) {
51 // In order to hide the private `rangeNumber` argument from API consumers
52 // while still enabling Selection getters/setters to access it, we define
53 // it as non enumerable, non configurable property. While consumers still
54 // may discover it they won't be able to do any harm which is good enough
55 // in this case.
56 Object.defineProperties(this, {
57 rangeNumber: {
58 enumerable: false,
59 configurable: false,
60 value: rangeNumber
61 }
62 });
63 },
64 get text() { return getSelection(TEXT, this.rangeNumber); },
65 set text(value) { setSelection(TEXT, value, this.rangeNumber); },
66 get html() { return getSelection(HTML, this.rangeNumber); },
67 set html(value) { setSelection(HTML, value, this.rangeNumber); },
68 get isContiguous() {
70 // If there are multiple non empty ranges, the selection is definitely
71 // discontiguous. It returns `false` also if there are no valid selection.
72 let count = 0;
73 for (let sel in selectionIterator)
74 if (++count > 1)
75 break;
77 return count === 1;
78 }
79 });
81 const selectionListener = {
82 notifySelectionChanged: function (document, selection, reason) {
83 if (!["SELECTALL", "KEYPRESS", "MOUSEUP"].some(function(type) reason &
84 Ci.nsISelectionListener[type + "_REASON"]) || selection.toString() == "")
85 return;
87 this.onSelect();
88 },
90 onSelect: function() {
91 emit(module.exports, "select");
92 }
93 }
95 /**
96 * Defines iterators so that discontiguous selections can be iterated.
97 * Empty selections are skipped - see `safeGetRange` for further details.
98 *
99 * If discontiguous selections are in a text field, only the first one
100 * is returned because the text field selection APIs doesn't support
101 * multiple selections.
102 */
103 function* forOfIterator() {
104 let selection = getSelection(DOM);
105 let count = 0;
107 if (selection)
108 count = selection.rangeCount || (getElementWithSelection() ? 1 : 0);
110 for (let i = 0; i < count; i++) {
111 let sel = Selection(i);
113 if (sel.text)
114 yield Selection(i);
115 }
116 }
118 const selectionIteratorOptions = {
119 __iterator__: forInIterator
120 }
121 selectionIteratorOptions[iteratorSymbol] = forOfIterator;
122 const selectionIterator = obscure(selectionIteratorOptions);
124 /**
125 * Returns the most recent focused window.
126 * if private browsing window is most recent and not supported,
127 * then ignore it and return `null`, because the focused window
128 * can't be targeted.
129 */
130 function getFocusedWindow() {
131 let window = winUtils.getFocusedWindow();
133 return ignoreWindow(window) ? null : window;
134 }
136 /**
137 * Returns the focused element in the most recent focused window
138 * if private browsing window is most recent and not supported,
139 * then ignore it and return `null`, because the focused element
140 * can't be targeted.
141 */
142 function getFocusedElement() {
143 let element = winUtils.getFocusedElement();
145 if (!element || ignoreWindow(element.ownerDocument.defaultView))
146 return null;
148 return element;
149 }
151 /**
152 * Returns the current selection from most recent content window. Depending on
153 * the specified |type|, the value returned can be a string of text, stringified
154 * HTML, or a DOM selection object as described at
155 * https://developer.mozilla.org/en/DOM/Selection.
156 *
157 * @param type
158 * Specifies the return type of the selection. Valid values are the one
159 * of the constants HTML, TEXT, or DOM.
160 *
161 * @param rangeNumber
162 * Specifies the zero-based range index of the returned selection.
163 */
164 function getSelection(type, rangeNumber) {
165 let window, selection;
166 try {
167 window = getFocusedWindow();
168 selection = window.getSelection();
169 }
170 catch (e) {
171 return null;
172 }
174 // Get the selected content as the specified type
175 if (type == DOM) {
176 return selection;
177 }
178 else if (type == TEXT) {
179 let range = safeGetRange(selection, rangeNumber);
181 if (range)
182 return range.toString();
184 let node = getElementWithSelection();
186 if (!node)
187 return null;
189 return node.value.substring(node.selectionStart, node.selectionEnd);
190 }
191 else if (type == HTML) {
192 let range = safeGetRange(selection, rangeNumber);
193 // Another way, but this includes the xmlns attribute for all elements in
194 // Gecko 1.9.2+ :
195 // return Cc["@mozilla.org/xmlextras/xmlserializer;1"].
196 // createInstance(Ci.nsIDOMSerializer).serializeToSTring(range.
197 // cloneContents());
198 if (!range)
199 return null;
201 let node = window.document.createElement("span");
202 node.appendChild(range.cloneContents());
203 return node.innerHTML;
204 }
206 throw new Error("Type " + type + " is unrecognized.");
207 }
209 /**
210 * Sets the current selection of the most recent content document by changing
211 * the existing selected text/HTML range to the specified value.
212 *
213 * @param val
214 * The value for the new selection
215 *
216 * @param rangeNumber
217 * The zero-based range index of the selection to be set
218 *
219 */
220 function setSelection(type, val, rangeNumber) {
221 // Make sure we have a window context & that there is a current selection.
222 // Selection cannot be set unless there is an existing selection.
223 let window, selection;
225 try {
226 window = getFocusedWindow();
227 selection = window.getSelection();
228 }
229 catch (e) {
230 throw new Error(ERR_CANNOT_CHANGE_SELECTION);
231 }
233 let range = safeGetRange(selection, rangeNumber);
235 if (range) {
236 let fragment;
238 if (type === HTML)
239 fragment = range.createContextualFragment(val);
240 else {
241 fragment = range.createContextualFragment("");
242 fragment.textContent = val;
243 }
245 range.deleteContents();
246 range.insertNode(fragment);
247 }
248 else {
249 let node = getElementWithSelection();
251 if (!node)
252 throw new Error(ERR_CANNOT_CHANGE_SELECTION);
254 let { value, selectionStart, selectionEnd } = node;
256 let newSelectionEnd = selectionStart + val.length;
258 node.value = value.substring(0, selectionStart) +
259 val +
260 value.substring(selectionEnd, value.length);
262 node.setSelectionRange(selectionStart, newSelectionEnd);
263 }
264 }
266 /**
267 * Returns the specified range in a selection without throwing an exception.
268 *
269 * @param selection
270 * A selection object as described at
271 * https://developer.mozilla.org/en/DOM/Selection
272 *
273 * @param [rangeNumber]
274 * Specifies the zero-based range index of the returned selection.
275 * If it's not provided the function will return the first non empty
276 * range, if any.
277 */
278 function safeGetRange(selection, rangeNumber) {
279 try {
280 let { rangeCount } = selection;
281 let range = null;
283 if (typeof rangeNumber === "undefined")
284 rangeNumber = 0;
285 else
286 rangeCount = rangeNumber + 1;
288 for (; rangeNumber < rangeCount; rangeNumber++ ) {
289 range = selection.getRangeAt(rangeNumber);
291 if (range && range.toString())
292 break;
294 range = null;
295 }
297 return range;
298 }
299 catch (e) {
300 return null;
301 }
302 }
304 /**
305 * Returns a reference of the DOM's active element for the window given, if it
306 * supports the text field selection API and has a text selected.
307 *
308 * Note:
309 * we need this method because window.getSelection doesn't return a selection
310 * for text selected in a form field (see bug 85686)
311 */
312 function getElementWithSelection() {
313 let element = getFocusedElement();
315 if (!element)
316 return null;
318 try {
319 // Accessing selectionStart and selectionEnd on e.g. a button
320 // results in an exception thrown as per the HTML5 spec. See
321 // http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#textFieldSelection
323 let { value, selectionStart, selectionEnd } = element;
325 let hasSelection = typeof value === "string" &&
326 !isNaN(selectionStart) &&
327 !isNaN(selectionEnd) &&
328 selectionStart !== selectionEnd;
330 return hasSelection ? element : null;
331 }
332 catch (err) {
333 return null;
334 }
336 }
338 /**
339 * Adds the Selection Listener to the content's window given
340 */
341 function addSelectionListener(window) {
342 let selection = window.getSelection();
344 // Don't add the selection's listener more than once to the same window,
345 // if the selection object is the same
346 if ("selection" in selections(window) && selections(window).selection === selection)
347 return;
349 // We ensure that the current selection is an instance of
350 // `nsISelectionPrivate` before working on it, in case is `null`.
351 //
352 // If it's `null` it's likely too early to add the listener, and we demand
353 // that operation to `document-shown` - it can easily happens for frames
354 if (selection instanceof Ci.nsISelectionPrivate)
355 selection.addSelectionListener(selectionListener);
357 // nsISelectionListener implementation seems not fire a notification if
358 // a selection is in a text field, therefore we need to add a listener to
359 // window.onselect, that is fired only for text fields.
360 // For consistency, we add it only when the nsISelectionListener is added.
361 //
362 // https://developer.mozilla.org/en/DOM/window.onselect
363 window.addEventListener("select", selectionListener.onSelect, true);
365 selections(window).selection = selection;
366 };
368 /**
369 * Removes the Selection Listener to the content's window given
370 */
371 function removeSelectionListener(window) {
372 // Don't remove the selection's listener to a window that wasn't handled.
373 if (!("selection" in selections(window)))
374 return;
376 let selection = window.getSelection();
377 let isSameSelection = selection === selections(window).selection;
379 // Before remove the listener, we ensure that the current selection is an
380 // instance of `nsISelectionPrivate` (it could be `null`), and that is still
381 // the selection we managed for this window (it could be detached).
382 if (selection instanceof Ci.nsISelectionPrivate && isSameSelection)
383 selection.removeSelectionListener(selectionListener);
385 window.removeEventListener("select", selectionListener.onSelect, true);
387 delete selections(window).selection;
388 };
390 function onContent(event) {
391 let window = event.subject.defaultView;
393 // We are not interested in documents without valid defaultView (e.g. XML)
394 // that aren't in a tab (e.g. Panel); or in private windows
395 if (window && getTabForContentWindow(window) && !ignoreWindow(window)) {
396 addSelectionListener(window);
397 }
398 }
400 // Adds Selection listener to new documents
401 // Note that strong reference is needed for documents that are loading slowly or
402 // where the server didn't close the connection (e.g. "comet").
403 events.on("document-element-inserted", onContent, true);
405 // Adds Selection listeners to existing documents
406 getAllTabContentWindows().forEach(addSelectionListener);
408 // When a document is not visible anymore the selection object is detached, and
409 // a new selection object is created when it becomes visible again.
410 // That makes the previous selection's listeners added previously totally
411 // useless – the listeners are not notified anymore.
412 // To fix that we're listening for `document-shown` event in order to add
413 // the listeners to the new selection object created.
414 //
415 // See bug 665386 for further details.
417 function onShown(event) {
418 let window = event.subject.defaultView;
420 // We are not interested in documents without valid defaultView.
421 // For example XML documents don't have windows and we don't yet support them.
422 if (!window)
423 return;
425 // We want to handle only the windows where we added selection's listeners
426 if ("selection" in selections(window)) {
427 let currentSelection = window.getSelection();
428 let { selection } = selections(window);
430 // If the current selection for the window given is different from the one
431 // stored in the namespace, we need to add the listeners again, and replace
432 // the previous selection in our list with the new one.
433 //
434 // Notice that we don't have to remove the listeners from the old selection,
435 // because is detached. An attempt to remove the listener, will raise an
436 // error (see http://mxr.mozilla.org/mozilla-central/source/layout/generic/nsSelection.cpp#5343 )
437 //
438 // We ensure that the current selection is an instance of
439 // `nsISelectionPrivate` before working on it, in case is `null`.
440 if (currentSelection instanceof Ci.nsISelectionPrivate &&
441 currentSelection !== selection) {
443 window.addEventListener("select", selectionListener.onSelect, true);
444 currentSelection.addSelectionListener(selectionListener);
445 selections(window).selection = currentSelection;
446 }
447 }
448 }
450 events.on("document-shown", onShown, true);
452 // Removes Selection listeners when the add-on is unloaded
453 unload(function(){
454 getAllTabContentWindows().forEach(removeSelectionListener);
456 events.off("document-element-inserted", onContent);
457 events.off("document-shown", onShown);
459 off(exports);
460 });
462 const selection = Class({
463 extends: EventTarget,
464 implements: [ Selection, selectionIterator ]
465 })();
467 module.exports = selection;