-
Notifications
You must be signed in to change notification settings - Fork 54
/
BrowserUtils.jsm
896 lines (808 loc) · 28.2 KB
/
BrowserUtils.jsm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["BrowserUtils"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(
this,
"PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm"
);
var BrowserUtils = {
/**
* Prints arguments separated by a space and appends a new line.
*/
dumpLn(...args) {
for (let a of args) {
dump(a + " ");
}
dump("\n");
},
/**
* restartApplication: Restarts the application, keeping it in
* safe mode if it is already in safe mode.
*/
restartApplication() {
let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
Ci.nsISupportsPRBool
);
Services.obs.notifyObservers(
cancelQuit,
"quit-application-requested",
"restart"
);
if (cancelQuit.data) {
// The quit request has been canceled.
return false;
}
// if already in safe mode restart in safe mode
if (Services.appinfo.inSafeMode) {
Services.startup.restartInSafeMode(
Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
);
return undefined;
}
Services.startup.quit(
Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
);
return undefined;
},
/**
* urlSecurityCheck: JavaScript wrapper for checkLoadURIWithPrincipal
* and checkLoadURIStrWithPrincipal.
* If |aPrincipal| is not allowed to link to |aURL|, this function throws with
* an error message.
*
* @param aURL
* The URL a page has linked to. This could be passed either as a string
* or as a nsIURI object.
* @param aPrincipal
* The principal of the document from which aURL came.
* @param aFlags
* Flags to be passed to checkLoadURIStr. If undefined,
* nsIScriptSecurityManager.STANDARD will be passed.
*/
urlSecurityCheck(aURL, aPrincipal, aFlags) {
var secMan = Services.scriptSecurityManager;
if (aFlags === undefined) {
aFlags = secMan.STANDARD;
}
try {
if (aURL instanceof Ci.nsIURI) {
secMan.checkLoadURIWithPrincipal(aPrincipal, aURL, aFlags);
} else {
secMan.checkLoadURIStrWithPrincipal(aPrincipal, aURL, aFlags);
}
} catch (e) {
let principalStr = "";
try {
principalStr = " from " + aPrincipal.URI.spec;
} catch (e2) {}
throw new Error(`Load of ${aURL + principalStr} denied.`);
}
},
/**
* Return or create a principal with the content of one, and the originAttributes
* of an existing principal (e.g. on a docshell, where the originAttributes ought
* not to change, that is, we should keep the userContextId, privateBrowsingId,
* etc. the same when changing the principal).
*
* @param principal
* The principal whose content/null/system-ness we want.
* @param existingPrincipal
* The principal whose originAttributes we want, usually the current
* principal of a docshell.
* @return an nsIPrincipal that matches the content/null/system-ness of the first
* param, and the originAttributes of the second.
*/
principalWithMatchingOA(principal, existingPrincipal) {
// Don't care about system principals:
if (principal.isSystemPrincipal) {
return principal;
}
// If the originAttributes already match, just return the principal as-is.
if (existingPrincipal.originSuffix == principal.originSuffix) {
return principal;
}
let secMan = Services.scriptSecurityManager;
if (principal.isContentPrincipal) {
return secMan.createContentPrincipal(
principal.URI,
existingPrincipal.originAttributes
);
}
if (principal.isNullPrincipal) {
return secMan.createNullPrincipal(existingPrincipal.originAttributes);
}
throw new Error(
"Can't change the originAttributes of an expanded principal!"
);
},
/**
* Constructs a new URI, using nsIIOService.
* @param aURL The URI spec.
* @param aOriginCharset The charset of the URI.
* @param aBaseURI Base URI to resolve aURL, or null.
* @return an nsIURI object based on aURL.
*
* @deprecated Use Services.io.newURI directly instead.
*/
makeURI(aURL, aOriginCharset, aBaseURI) {
return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
},
/**
* @deprecated Use Services.io.newFileURI directly instead.
*/
makeFileURI(aFile) {
return Services.io.newFileURI(aFile);
},
makeURIFromCPOW(aCPOWURI) {
return Services.io.newURI(aCPOWURI.spec);
},
/**
* For a given DOM element, returns its position in "screen"
* coordinates. In a content process, the coordinates returned will
* be relative to the left/top of the tab. In the chrome process,
* the coordinates are relative to the user's screen.
*/
getElementBoundingScreenRect(aElement) {
return this.getElementBoundingRect(aElement, true);
},
/**
* For a given DOM element, returns its position as an offset from the topmost
* window. In a content process, the coordinates returned will be relative to
* the left/top of the topmost content area. If aInScreenCoords is true,
* screen coordinates will be returned instead.
*/
getElementBoundingRect(aElement, aInScreenCoords) {
let rect = aElement.getBoundingClientRect();
let win = aElement.ownerGlobal;
let x = rect.left,
y = rect.top;
// We need to compensate for any iframes that might shift things
// over. We also need to compensate for zooming.
let parentFrame = win.frameElement;
while (parentFrame) {
win = parentFrame.ownerGlobal;
let cstyle = win.getComputedStyle(parentFrame);
let framerect = parentFrame.getBoundingClientRect();
x +=
framerect.left +
parseFloat(cstyle.borderLeftWidth) +
parseFloat(cstyle.paddingLeft);
y +=
framerect.top +
parseFloat(cstyle.borderTopWidth) +
parseFloat(cstyle.paddingTop);
parentFrame = win.frameElement;
}
if (aInScreenCoords) {
x += win.mozInnerScreenX;
y += win.mozInnerScreenY;
}
let fullZoom = win.windowUtils.fullZoom;
rect = {
left: x * fullZoom,
top: y * fullZoom,
width: rect.width * fullZoom,
height: rect.height * fullZoom,
};
return rect;
},
onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) {
// Don't modify non-default targets or targets that aren't in top-level app
// tab docshells (isAppTab will be false for app tab subframes).
if (originalTarget != "" || !isAppTab) {
return originalTarget;
}
// External links from within app tabs should always open in new tabs
// instead of replacing the app tab's page (Bug 575561)
let linkHost;
let docHost;
try {
linkHost = linkURI.host;
docHost = linkNode.ownerDocument.documentURIObject.host;
} catch (e) {
// nsIURI.host can throw for non-nsStandardURL nsIURIs.
// If we fail to get either host, just return originalTarget.
return originalTarget;
}
if (docHost == linkHost) {
return originalTarget;
}
// Special case: ignore "www" prefix if it is part of host string
let [longHost, shortHost] =
linkHost.length > docHost.length
? [linkHost, docHost]
: [docHost, linkHost];
if (longHost == "www." + shortHost) {
return originalTarget;
}
return "_blank";
},
/**
* Map the plugin's name to a filtered version more suitable for UI.
*
* @param aName The full-length name string of the plugin.
* @return the simplified name string.
*/
makeNicePluginName(aName) {
if (aName == "Shockwave Flash") {
return "Adobe Flash";
}
// Regex checks if aName begins with "Java" + non-letter char
if (/^Java\W/.exec(aName)) {
return "Java";
}
// Clean up the plugin name by stripping off parenthetical clauses,
// trailing version numbers or "plugin".
// EG, "Foo Bar (Linux) Plugin 1.23_02" --> "Foo Bar"
// Do this by first stripping the numbers, etc. off the end, and then
// removing "Plugin" (and then trimming to get rid of any whitespace).
// (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled)
let newName = aName
.replace(/\(.*?\)/g, "")
.replace(/[\s\d\.\-\_\(\)]+$/, "")
.replace(/\bplug-?in\b/i, "")
.trim();
return newName;
},
/**
* Returns true if |mimeType| is text-based, or false otherwise.
*
* @param mimeType
* The MIME type to check.
*/
mimeTypeIsTextBased(mimeType) {
return (
mimeType.startsWith("text/") ||
mimeType.endsWith("+xml") ||
mimeType == "application/x-javascript" ||
mimeType == "application/javascript" ||
mimeType == "application/json" ||
mimeType == "application/xml"
);
},
/**
* Return true if we should FAYT for this node + window (could be CPOW):
*
* @param elt
* The element that is focused
*/
shouldFastFind(elt) {
if (elt) {
let win = elt.ownerGlobal;
if (elt instanceof win.HTMLInputElement && elt.mozIsTextField(false)) {
return false;
}
if (elt.isContentEditable || win.document.designMode == "on") {
return false;
}
if (
elt instanceof win.HTMLTextAreaElement ||
elt instanceof win.HTMLSelectElement ||
elt instanceof win.HTMLObjectElement ||
elt instanceof win.HTMLEmbedElement
) {
return false;
}
if (elt instanceof win.HTMLIFrameElement && elt.mozbrowser) {
// If we're targeting a mozbrowser iframe, it should be allowed to
// handle FastFind itself.
return false;
}
}
return true;
},
/**
* Returns true if we can show a find bar, including FAYT, for the specified
* document location. The location must not be in a blacklist of specific
* "about:" pages for which find is disabled.
*
* This can be called from the parent process or from content processes.
*/
canFindInPage(location) {
return (
!location.startsWith("about:addons") &&
!location.startsWith(
"chrome://mozapps/content/extensions/aboutaddons.html"
) &&
!location.startsWith("about:preferences")
);
},
_visibleToolbarsMap: new WeakMap(),
/**
* Return true if any or a specific toolbar that interacts with the content
* document is visible.
*
* @param {nsIDocShell} docShell The docShell instance that a toolbar should
* be interacting with
* @param {String} which Identifier of a specific toolbar
* @return {Boolean}
*/
isToolbarVisible(docShell, which) {
let window = this.getRootWindow(docShell);
if (!this._visibleToolbarsMap.has(window)) {
return false;
}
let toolbars = this._visibleToolbarsMap.get(window);
return !!toolbars && toolbars.has(which);
},
/**
* Sets the --toolbarbutton-button-height CSS property on the closest
* toolbar to the provided element. Useful if you need to vertically
* center a position:absolute element within a toolbar that uses
* -moz-pack-align:stretch, and thus a height which is dependant on
* the font-size.
*
* @param element An element within the toolbar whose height is desired.
*/
async setToolbarButtonHeightProperty(element) {
let window = element.ownerGlobal;
let dwu = window.windowUtils;
let toolbarItem = element;
let urlBarContainer = element.closest("#urlbar-container");
if (urlBarContainer) {
// The stop-reload-button, which is contained in #urlbar-container,
// needs to use #urlbar-container to calculate the bounds.
toolbarItem = urlBarContainer;
}
if (!toolbarItem) {
return;
}
let bounds = dwu.getBoundsWithoutFlushing(toolbarItem);
if (!bounds.height) {
await window.promiseDocumentFlushed(() => {
bounds = dwu.getBoundsWithoutFlushing(toolbarItem);
});
}
if (bounds.height) {
toolbarItem.style.setProperty(
"--toolbarbutton-height",
bounds.height + "px"
);
}
},
/**
* Track whether a toolbar is visible for a given a docShell.
*
* @param {nsIDocShell} docShell The docShell instance that a toolbar should
* be interacting with
* @param {String} which Identifier of a specific toolbar
* @param {Boolean} [visible] Whether the toolbar is visible. Optional,
* defaults to `true`.
*/
trackToolbarVisibility(docShell, which, visible = true) {
// We have to get the root window object, because XPConnect WrappedNatives
// can't be used as WeakMap keys.
let window = this.getRootWindow(docShell);
let toolbars = this._visibleToolbarsMap.get(window);
if (!toolbars) {
toolbars = new Set();
this._visibleToolbarsMap.set(window, toolbars);
}
if (!visible) {
toolbars.delete(which);
} else {
toolbars.add(which);
}
},
/**
* Retrieve the root window object (i.e. the top-most content global) for a
* specific docShell object.
*
* @param {nsIDocShell} docShell
* @return {nsIDOMWindow}
*/
getRootWindow(docShell) {
return docShell.sameTypeRootTreeItem.domWindow;
},
/**
* Trim the selection text to a reasonable size and sanitize it to make it
* safe for search query input.
*
* @param aSelection
* The selection text to trim.
* @param aMaxLen
* The maximum string length, defaults to a reasonable size if undefined.
* @return The trimmed selection text.
*/
trimSelection(aSelection, aMaxLen) {
// Selections of more than 150 characters aren't useful.
const maxLen = Math.min(aMaxLen || 150, aSelection.length);
if (aSelection.length > maxLen) {
// only use the first maxLen important chars. see bug 221361
let pattern = new RegExp("^(?:\\s*.){0," + maxLen + "}");
pattern.test(aSelection);
aSelection = RegExp.lastMatch;
}
aSelection = aSelection.trim().replace(/\s+/g, " ");
if (aSelection.length > maxLen) {
aSelection = aSelection.substr(0, maxLen);
}
return aSelection;
},
/**
* Retrieve the text selection details for the given window.
*
* @param aTopWindow
* The top window of the element containing the selection.
* @param aCharLen
* The maximum string length for the selection text.
* @return The selection details containing the full and trimmed selection text
* and link details for link selections.
*/
getSelectionDetails(aTopWindow, aCharLen) {
let focusedWindow = {};
let focusedElement = Services.focus.getFocusedElementForWindow(
aTopWindow,
true,
focusedWindow
);
focusedWindow = focusedWindow.value;
let selection = focusedWindow.getSelection();
let selectionStr = selection.toString();
let fullText;
let url;
let linkText;
// try getting a selected text in text input.
if (!selectionStr && focusedElement) {
// Don't get the selection for password fields. See bug 565717.
if (
ChromeUtils.getClassName(focusedElement) === "HTMLTextAreaElement" ||
(ChromeUtils.getClassName(focusedElement) === "HTMLInputElement" &&
focusedElement.mozIsTextField(true))
) {
selection = focusedElement.editor.selection;
selectionStr = selection.toString();
}
}
let collapsed = selection.isCollapsed;
if (selectionStr) {
// Have some text, let's figure out if it looks like a URL that isn't
// actually a link.
linkText = selectionStr.trim();
if (/^(?:https?|ftp):/i.test(linkText)) {
try {
url = this.makeURI(linkText);
} catch (ex) {}
} else if (/^(?:[a-z\d-]+\.)+[a-z]+$/i.test(linkText)) {
// Check if this could be a valid url, just missing the protocol.
// Now let's see if this is an intentional link selection. Our guess is
// based on whether the selection begins/ends with whitespace or is
// preceded/followed by a non-word character.
// selection.toString() trims trailing whitespace, so we look for
// that explicitly in the first and last ranges.
let beginRange = selection.getRangeAt(0);
let delimitedAtStart = /^\s/.test(beginRange);
if (!delimitedAtStart) {
let container = beginRange.startContainer;
let offset = beginRange.startOffset;
if (container.nodeType == container.TEXT_NODE && offset > 0) {
delimitedAtStart = /\W/.test(container.textContent[offset - 1]);
} else {
delimitedAtStart = true;
}
}
let delimitedAtEnd = false;
if (delimitedAtStart) {
let endRange = selection.getRangeAt(selection.rangeCount - 1);
delimitedAtEnd = /\s$/.test(endRange);
if (!delimitedAtEnd) {
let container = endRange.endContainer;
let offset = endRange.endOffset;
if (
container.nodeType == container.TEXT_NODE &&
offset < container.textContent.length
) {
delimitedAtEnd = /\W/.test(container.textContent[offset]);
} else {
delimitedAtEnd = true;
}
}
}
if (delimitedAtStart && delimitedAtEnd) {
try {
url = Services.uriFixup.createFixupURI(
linkText,
Services.uriFixup.FIXUP_FLAG_NONE
);
} catch (ex) {}
}
}
}
if (selectionStr) {
// Pass up to 16K through unmolested. If an add-on needs more, they will
// have to use a content script.
fullText = selectionStr.substr(0, 16384);
selectionStr = this.trimSelection(selectionStr, aCharLen);
}
if (url && !url.host) {
url = null;
}
return {
text: selectionStr,
docSelectionIsCollapsed: collapsed,
fullText,
linkURL: url ? url.spec : null,
linkText: url ? linkText : "",
};
},
// Iterates through every docshell in the window and calls PermitUnload.
canCloseWindow(window) {
let docShell = window.docShell;
for (let i = 0; i < docShell.childCount; ++i) {
let childShell = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
let contentViewer = childShell.contentViewer;
if (contentViewer && !contentViewer.permitUnload()) {
return false;
}
}
return true;
},
/**
* Replaces %s or %S in the provided url or postData with the given parameter,
* acccording to the best charset for the given url.
*
* @return [url, postData]
* @throws if nor url nor postData accept a param, but a param was provided.
*/
async parseUrlAndPostData(url, postData, param) {
let hasGETParam = /%s/i.test(url);
let decodedPostData = postData ? unescape(postData) : "";
let hasPOSTParam = /%s/i.test(decodedPostData);
if (!hasGETParam && !hasPOSTParam) {
if (param) {
// If nor the url, nor postData contain parameters, but a parameter was
// provided, return the original input.
throw new Error(
"A param was provided but there's nothing to bind it to"
);
}
return [url, postData];
}
let charset = "";
const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/;
let matches = url.match(re);
if (matches) {
[, url, charset] = matches;
} else {
// Try to fetch a charset from History.
try {
// Will return an empty string if character-set is not found.
let pageInfo = await PlacesUtils.history.fetch(url, {
includeAnnotations: true,
});
if (pageInfo && pageInfo.annotations.has(PlacesUtils.CHARSET_ANNO)) {
charset = pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO);
}
} catch (ex) {
// makeURI() throws if url is invalid.
Cu.reportError(ex);
}
}
// encodeURIComponent produces UTF-8, and cannot be used for other charsets.
// escape() works in those cases, but it doesn't uri-encode +, @, and /.
// Therefore we need to manually replace these ASCII characters by their
// encodeURIComponent result, to match the behavior of nsEscape() with
// url_XPAlphas.
let encodedParam = "";
if (charset && charset != "UTF-8") {
try {
let converter = Cc[
"@mozilla.org/intl/scriptableunicodeconverter"
].createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = charset;
encodedParam = converter.ConvertFromUnicode(param) + converter.Finish();
} catch (ex) {
encodedParam = param;
}
encodedParam = escape(encodedParam).replace(
/[+@\/]+/g,
encodeURIComponent
);
} else {
// Default charset is UTF-8
encodedParam = encodeURIComponent(param);
}
url = url.replace(/%s/g, encodedParam).replace(/%S/g, param);
if (hasPOSTParam) {
postData = decodedPostData
.replace(/%s/g, encodedParam)
.replace(/%S/g, param);
}
return [url, postData];
},
/**
* Generate a document fragment for a localized string that has DOM
* node replacements. This avoids using getFormattedString followed
* by assigning to innerHTML. Fluent can probably replace this when
* it is in use everywhere.
*
* @param {Document} doc
* @param {String} msg
* The string to put replacements in. Fetch from
* a stringbundle using getString or GetStringFromName,
* or even an inserted dtd string.
* @param {Node|String} nodesOrStrings
* The replacement items. Can be a mix of Nodes
* and Strings. However, for correct behaviour, the
* number of items provided needs to exactly match
* the number of replacement strings in the l10n string.
* @returns {DocumentFragment}
* A document fragment. In the trivial case (no
* replacements), this will simply be a fragment with 1
* child, a text node containing the localized string.
*/
getLocalizedFragment(doc, msg, ...nodesOrStrings) {
// Ensure replacement points are indexed:
for (let i = 1; i <= nodesOrStrings.length; i++) {
if (!msg.includes("%" + i + "$S")) {
msg = msg.replace(/%S/, "%" + i + "$S");
}
}
let numberOfInsertionPoints = msg.match(/%\d+\$S/g).length;
if (numberOfInsertionPoints != nodesOrStrings.length) {
Cu.reportError(
`Message has ${numberOfInsertionPoints} insertion points, ` +
`but got ${nodesOrStrings.length} replacement parameters!`
);
}
let fragment = doc.createDocumentFragment();
let parts = [msg];
let insertionPoint = 1;
for (let replacement of nodesOrStrings) {
let insertionString = "%" + insertionPoint++ + "$S";
let partIndex = parts.findIndex(
part => typeof part == "string" && part.includes(insertionString)
);
if (partIndex == -1) {
fragment.appendChild(doc.createTextNode(msg));
return fragment;
}
if (typeof replacement == "string") {
parts[partIndex] = parts[partIndex].replace(
insertionString,
replacement
);
} else {
let [firstBit, lastBit] = parts[partIndex].split(insertionString);
parts.splice(partIndex, 1, firstBit, replacement, lastBit);
}
}
// Put everything in a document fragment:
for (let part of parts) {
if (typeof part == "string") {
if (part) {
fragment.appendChild(doc.createTextNode(part));
}
} else {
fragment.appendChild(part);
}
}
return fragment;
},
/**
* Returns a Promise which resolves when the given observer topic has been
* observed.
*
* @param {string} topic
* The topic to observe.
* @param {function(nsISupports, string)} [test]
* An optional test function which, when called with the
* observer's subject and data, should return true if this is the
* expected notification, false otherwise.
* @returns {Promise<object>}
*/
promiseObserved(topic, test = () => true) {
return new Promise(resolve => {
let observer = (subject, topic, data) => {
if (test(subject, data)) {
Services.obs.removeObserver(observer, topic);
resolve({ subject, data });
}
};
Services.obs.addObserver(observer, topic);
});
},
removeSingleTrailingSlashFromURL(aURL) {
// remove single trailing slash for http/https/ftp URLs
return aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1");
},
/**
* Returns a URL which has been trimmed by removing 'http://' and any
* trailing slash (in http/https/ftp urls).
*
* @param {string} aURL The URL to trim.
* @returns {string} The trimmed string.
*/
trimURL(aURL) {
// This function must not modify the given URL such that calling
// nsIURIFixup::createFixupURI with the result will produce a different URI.
let url = this.removeSingleTrailingSlashFromURL(aURL);
// remove http://
if (!url.startsWith("http://")) {
return url;
}
let urlWithoutProtocol = url.substring(7);
// It doesn't really matter which search engine is used here, thus it's ok
// to ignore whether we are in a private context. The keyword lookup is only
// used to differentiate between whitelisted and not whitelisted hosts.
// For example, if "someword" is not a whitelisted host, setting the urlbar
// value to "http://someword" should not trim it, because otherwise
// confirming the urlbar value would end up searching for "someword".
let flags =
Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP |
Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
let fixedUpURL, expectedURLSpec;
try {
fixedUpURL = Services.uriFixup.createFixupURI(urlWithoutProtocol, flags);
expectedURLSpec = Services.io.newURI(aURL).displaySpec;
} catch (ex) {
return url;
}
if (fixedUpURL.displaySpec == expectedURLSpec) {
return urlWithoutProtocol;
}
return url;
},
recordSiteOriginTelemetry(aWindows, aIsGeckoView) {
Services.tm.idleDispatchToMainThread(() => {
this._recordSiteOriginTelemetry(aWindows, aIsGeckoView);
});
},
_recordSiteOriginTelemetry(aWindows, aIsGeckoView) {
let currentTime = Date.now();
// default is 5 minutes
if (!this.min_interval) {
this.min_interval = Services.prefs.getIntPref(
"telemetry.number_of_site_origin.min_interval",
300000
);
}
// Discard the first load because most of the time the first load only has 1
// tab and 1 window open, so it is useless to report it.
if (
!this._lastRecordSiteOrigin ||
currentTime < this._lastRecordSiteOrigin + this.min_interval
) {
if (!this._lastRecordSiteOrigin) {
this._lastRecordSiteOrigin = currentTime;
}
return;
}
this._lastRecordSiteOrigin = currentTime;
// Geckoview and Desktop work differently. On desktop, aBrowser objects
// holds an array of tabs which we can use to get the <browser> objects.
// In Geckoview, it is apps' responsibility to keep track of the tabs, so
// there isn't an easy way for us to get the tabs.
let tabs = [];
if (aIsGeckoView) {
// To get all active windows; Each tab has its own window
tabs = aWindows;
} else {
for (const win of aWindows) {
tabs = tabs.concat(win.gBrowser.tabs);
}
}
let topLevelBC = [];
for (const tab of tabs) {
let browser;
if (aIsGeckoView) {
browser = tab.browser;
} else {
browser = tab.linkedBrowser;
}
if (browser.browsingContext) {
// This is the top level browsingContext
topLevelBC.push(browser.browsingContext);
}
}
const count = CanonicalBrowsingContext.countSiteOrigins(topLevelBC);
Services.telemetry
.getHistogramById("FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_ALL_TABS")
.add(count);
},
};