summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/BrowserUtils.jsm
blob: 862f9619c4eb21a023f6c366ff20ab69c96e8020 (plain)
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
/* -*- 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";

this.EXPORTED_SYMBOLS = [ "BrowserUtils" ];

const {interfaces: Ci, utils: Cu, classes: Cc} = Components;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
  "resource://gre/modules/PlacesUtils.jsm");

Cu.importGlobalProperties(['URL']);

this.BrowserUtils = {

  /**
   * Prints arguments separated by a space and appends a new line.
   */
  dumpLn: function (...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: function() {
    let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
                       .getService(Ci.nsIAppStartup);
    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) {
      appStartup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
      return undefined;
    }
    appStartup.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: function(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 "Load of " + aURL + principalStr + " denied.";
    }
  },

  /**
   * Return or create a principal with the codebase 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 codebase/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 codebase/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.isCodebasePrincipal) {
      return secMan.createCodebasePrincipal(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.
   */
  makeURI: function(aURL, aOriginCharset, aBaseURI) {
    return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
  },

  makeFileURI: function(aFile) {
    return Services.io.newFileURI(aFile);
  },

  makeURIFromCPOW: function(aCPOWURI) {
    return Services.io.newURI(aCPOWURI.spec, aCPOWURI.originCharset, null);
  },

  /**
   * 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: function(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: function(aElement, aInScreenCoords) {
    let rect = aElement.getBoundingClientRect();
    let win = aElement.ownerDocument.defaultView;

    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.ownerDocument.defaultView;
      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.getInterface(Ci.nsIDOMWindowUtils).fullZoom;
    rect = {
      left: x * fullZoom,
      top: y * fullZoom,
      width: rect.width * fullZoom,
      height: rect.height * fullZoom
    };

    return rect;
  },

  onBeforeLinkTraversal: function(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: function (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;
  },

  /**
   * Return true if linkNode has a rel="noreferrer" attribute.
   *
   * @param linkNode The <a> element, or null.
   * @return a boolean indicating if linkNode has a rel="noreferrer" attribute.
   */
  linkHasNoReferrer: function (linkNode) {
    // A null linkNode typically means that we're checking a link that wasn't
    // provided via an <a> link, like a text-selected URL.  Don't leak
    // referrer information in this case.
    if (!linkNode)
      return true;

    let rel = linkNode.getAttribute("rel");
    if (!rel)
      return false;

    // The HTML spec says that rel should be split on spaces before looking
    // for particular rel values.
    let values = rel.split(/[ \t\r\n\f]/);
    return values.indexOf('noreferrer') != -1;
  },

  /**
   * Returns true if |mimeType| is text-based, or false otherwise.
   *
   * @param mimeType
   *        The MIME type to check.
   */
  mimeTypeIsTextBased: function(mimeType) {
    return mimeType.startsWith("text/") ||
           mimeType.endsWith("+xml") ||
           mimeType == "application/x-javascript" ||
           mimeType == "application/javascript" ||
           mimeType == "application/json" ||
           mimeType == "application/xml" ||
           mimeType == "mozilla.application/cached-xul";
  },

  /**
   * Return true if we should FAYT for this node + window (could be CPOW):
   *
   * @param elt
   *        The element that is focused
   * @param win
   *        The window that is focused
   *
   */
  shouldFastFind: function(elt, win) {
    if (elt) {
      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;
    }

    return true;
  },

  /**
   * Return true if we can FAYT for this window (could be CPOW):
   *
   * @param win
   *        The top level window that is focused
   *
   */
  canFastFind: function(win) {
    if (!win)
      return false;

    if (!this.mimeTypeIsTextBased(win.document.contentType))
      return false;

    // disable FAYT in about:blank to prevent FAYT opening unexpectedly.
    let loc = win.location;
    if (loc.href == "about:blank")
      return false;

    // disable FAYT in documents that ask for it to be disabled.
    if ((loc.protocol == "about:" || loc.protocol == "chrome:") &&
        (win.document.documentElement &&
         win.document.documentElement.getAttribute("disablefastfind") == "true"))
      return false;

    return true;
  },

  _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);
  },

  /**
   * 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.QueryInterface(Ci.nsIDocShellTreeItem)
      .sameTypeRootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIDOMWindow);
  },

  getSelectionDetails: function(topWindow, aCharLen) {
    // selections of more than 150 characters aren't useful
    const kMaxSelectionLen = 150;
    const charLen = Math.min(aCharLen || kMaxSelectionLen, kMaxSelectionLen);

    let focusedWindow = {};
    let focusedElement = Services.focus.getFocusedElementForWindow(topWindow, true, focusedWindow);
    focusedWindow = focusedWindow.value;

    let selection = focusedWindow.getSelection();
    let selectionStr = selection.toString();

    let collapsed = selection.isCollapsed;

    let url;
    let linkText;
    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) {}
      }
      // Check if this could be a valid url, just missing the protocol.
      else if (/^(?:[a-z\d-]+\.)+[a-z]+$/i.test(linkText)) {
        // 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) {
          let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"]
                           .getService(Ci.nsIURIFixup);
          try {
            url = uriFixup.createFixupURI(linkText, uriFixup.FIXUP_FLAG_NONE);
          } catch (ex) {}
        }
      }
    }

    // try getting a selected text in text input.
    if (!selectionStr && focusedElement instanceof Ci.nsIDOMNSEditableElement) {
      // Don't get the selection for password fields. See bug 565717.
      if (focusedElement instanceof Ci.nsIDOMHTMLTextAreaElement ||
          (focusedElement instanceof Ci.nsIDOMHTMLInputElement &&
           focusedElement.mozIsTextField(true))) {
        selectionStr = focusedElement.editor.selection.toString();
      }
    }

    if (selectionStr) {
      if (selectionStr.length > charLen) {
        // only use the first charLen important chars. see bug 221361
        var pattern = new RegExp("^(?:\\s*.){0," + charLen + "}");
        pattern.test(selectionStr);
        selectionStr = RegExp.lastMatch;
      }

      selectionStr = selectionStr.trim().replace(/\s+/g, " ");

      if (selectionStr.length > charLen) {
        selectionStr = selectionStr.substr(0, charLen);
      }
    }

    if (url && !url.host) {
      url = null;
    }

    return { text: selectionStr, docSelectionIsCollapsed: collapsed,
             linkURL: url ? url.spec : null, linkText: url ? linkText : "" };
  },

  // Iterates through every docshell in the window and calls PermitUnload.
  canCloseWindow(window) {
    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIWebNavigation);
    let node = docShell.QueryInterface(Ci.nsIDocShellTreeItem);
    for (let i = 0; i < node.childCount; ++i) {
      let docShell = node.getChildAt(i).QueryInterface(Ci.nsIDocShell);
      let contentViewer = docShell.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.
   */
  parseUrlAndPostData: Task.async(function* (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.
        charset = yield PlacesUtils.getCharsetForURI(this.makeURI(url));
      } 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];
  }),
};