Bug 961529 - Run DOMLinkHandler in a content script (r=felipe)
authorBill McCloskey <wmccloskey@mozilla.com>
Sat, 08 Feb 2014 17:41:34 -0800
changeset 184946 e6ea08c515609fab961b860d9336ab4c8a77811c
parent 184945 639da97e170319492d16f4036d90f87d0d26d310
child 184947 4b87ae646c62347e7d14c90a80fda04d0b0a31bb
push id3503
push userraliiev@mozilla.com
push dateMon, 28 Apr 2014 18:51:11 +0000
treeherdermozilla-beta@c95ac01e332e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe
bugs961529
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 961529 - Run DOMLinkHandler in a content script (r=felipe)
browser/base/content/browser-feeds.js
browser/base/content/browser.js
browser/base/content/content.js
browser/base/content/socialchat.xml
browser/base/content/socialmarks.xml
browser/modules/ContentLinkHandler.jsm
browser/modules/moz.build
--- a/browser/base/content/browser-feeds.js
+++ b/browser/base/content/browser-feeds.js
@@ -147,19 +147,17 @@ var FeedHandler = {
     } else {
       this._feedMenuitem.setAttribute("feed", feeds[0].href);
       this._feedMenuitem.removeAttribute("disabled");
       this._feedMenuitem.removeAttribute("hidden");
       this._feedMenupopup.setAttribute("hidden", "true");
     }
   },
 
-  addFeed: function(link, targetDoc) {
-    // find which tab this is for, and set the attribute on the browser
-    var browserForLink = gBrowser.getBrowserForDocument(targetDoc);
+  addFeed: function(link, browserForLink) {
     if (!browserForLink) {
       // ignore feeds loaded in subframes (see bug 305472)
       return;
     }
 
     if (!browserForLink.feeds)
       browserForLink.feeds = [];
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6,18 +6,16 @@
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/NotificationDB.jsm");
 Cu.import("resource:///modules/RecentWindow.jsm");
 Cu.import("resource://gre/modules/WindowsPrefSync.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
-                                  "resource:///modules/Feeds.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
                                   "resource://gre/modules/CharsetMenu.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
                                   "resource://gre/modules/ShortcutUtils.jsm");
 
 const nsIWebNavigation = Ci.nsIWebNavigation;
@@ -799,17 +797,17 @@ var gBrowserInit = {
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
       new nsBrowserAccess();
 
     // hook up UI through progress listener
     gBrowser.addProgressListener(window.XULBrowserWindow);
     gBrowser.addTabsProgressListener(window.TabsProgressListener);
 
     // setup our common DOMLinkAdded listener
-    gBrowser.addEventListener("DOMLinkAdded", DOMLinkHandler, false);
+    DOMLinkHandler.init();
 
     // setup simple gestures support
     gGestureSupport.init(true);
 
     // setup history swipe animation
     gHistorySwipeAnimation.init();
 
     if (window.opener && !window.opener.closed &&
@@ -2761,157 +2759,78 @@ var newWindowButtonObserver = {
         // allow third-party services to fixup this URL
         openNewWindowWith(data.url, null, data.postData, true);
       }
     });
   }
 }
 
 const DOMLinkHandler = {
-  handleEvent: function (event) {
-    switch (event.type) {
-      case "DOMLinkAdded":
-        this.onLinkAdded(event);
+  init: function() {
+    let mm = window.messageManager;
+    mm.addMessageListener("Link:AddFeed", this);
+    mm.addMessageListener("Link:AddIcon", this);
+    mm.addMessageListener("Link:AddSearch", this);
+  },
+
+  receiveMessage: function (aMsg) {
+    switch (aMsg.name) {
+      case "Link:AddFeed":
+        let link = {type: aMsg.data.type, href: aMsg.data.href, title: aMsg.data.title};
+        FeedHandler.addFeed(link, aMsg.target);
+        break;
+
+      case "Link:AddIcon":
+        return this.addIcon(aMsg.target, aMsg.data.url);
+        break;
+
+      case "Link:AddSearch":
+        this.addSearch(aMsg.target, aMsg.data.engine, aMsg.data.url);
         break;
     }
   },
-  getLinkIconURI: function(aLink) {
-    let targetDoc = aLink.ownerDocument;
-    var uri = makeURI(aLink.href, targetDoc.characterSet);
-
-    // Verify that the load of this icon is legal.
-    // Some error or special pages can load their favicon.
-    // To be on the safe side, only allow chrome:// favicons.
-    var isAllowedPage = [
-      /^about:neterror\?/,
-      /^about:blocked\?/,
-      /^about:certerror\?/,
-      /^about:home$/,
-    ].some(function (re) re.test(targetDoc.documentURI));
-
-    if (!isAllowedPage || !uri.schemeIs("chrome")) {
-      var ssm = Services.scriptSecurityManager;
-      try {
-        ssm.checkLoadURIWithPrincipal(targetDoc.nodePrincipal, uri,
-                                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
-      } catch(e) {
-        return null;
-      }
-    }
-
-    try {
-      var contentPolicy = Cc["@mozilla.org/layout/content-policy;1"].
-                          getService(Ci.nsIContentPolicy);
-    } catch(e) {
-      return null; // Refuse to load if we can't do a security check.
-    }
-
-    // Security says okay, now ask content policy
-    if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_IMAGE,
-                                 uri, targetDoc.documentURIObject,
-                                 aLink, aLink.type, null)
-                                 != Ci.nsIContentPolicy.ACCEPT)
-      return null;
-
-    try {
-      uri.userPass = "";
-    } catch(e) {
-      // some URIs are immutable
-    }
-    return uri;
-  },
-  onLinkAdded: function (event) {
-    var link = event.originalTarget;
-    var rel = link.rel && link.rel.toLowerCase();
-    if (!link || !link.ownerDocument || !rel || !link.href)
-      return;
-
-    var feedAdded = false;
-    var iconAdded = false;
-    var searchAdded = false;
-    var rels = {};
-    for (let relString of rel.split(/\s+/))
-      rels[relString] = true;
-
-    for (let relVal in rels) {
-      switch (relVal) {
-        case "feed":
-        case "alternate":
-          if (!feedAdded) {
-            if (!rels.feed && rels.alternate && rels.stylesheet)
-              break;
-
-            if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) {
-              FeedHandler.addFeed(link, link.ownerDocument);
-              feedAdded = true;
-            }
-          }
-          break;
-        case "icon":
-          if (!iconAdded) {
-            if (!gPrefService.getBoolPref("browser.chrome.site_icons"))
-              break;
-
-            var uri = this.getLinkIconURI(link);
-            if (!uri)
-              break;
-
-            if (gBrowser.isFailedIcon(uri))
-              break;
-
-            var browserIndex = gBrowser.getBrowserIndexForDocument(link.ownerDocument);
-            // no browser? no favicon.
-            if (browserIndex == -1)
-              break;
-
-            let tab = gBrowser.tabs[browserIndex];
-            gBrowser.setIcon(tab, uri.spec);
-            iconAdded = true;
-          }
-          break;
-        case "search":
-          if (!searchAdded) {
-            var type = link.type && link.type.toLowerCase();
-            type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
-
-            if (type == "application/opensearchdescription+xml" && link.title &&
-                /^(?:https?|ftp):/i.test(link.href)) {
-              var engine = { title: link.title, href: link.href };
-              BrowserSearch.addEngine(engine, link.ownerDocument);
-              searchAdded = true;
-            }
-          }
-          break;
-      }
-    }
-  }
+
+  addIcon: function(aBrowser, aURL) {
+    if (gBrowser.isFailedIcon(aURL))
+      return false;
+
+    let tab = gBrowser._getTabForBrowser(aBrowser);
+    if (!tab)
+      return false;
+
+    gBrowser.setIcon(tab, aURL);
+    return true;
+  },
+
+  addSearch: function(aBrowser, aEngine, aURL) {
+    let tab = gBrowser._getTabForBrowser(aBrowser);
+    if (!tab)
+      return false;
+
+    BrowserSearch.addEngine(aBrowser, aEngine, makeURI(aURL));
+  },
 }
 
 const BrowserSearch = {
-  addEngine: function(engine, targetDoc) {
+  addEngine: function(browser, engine, uri) {
     if (!this.searchBar)
       return;
 
-    var browser = gBrowser.getBrowserForDocument(targetDoc);
-    // ignore search engines from subframes (see bug 479408)
-    if (!browser)
-      return;
-
     // Check to see whether we've already added an engine with this title
     if (browser.engines) {
       if (browser.engines.some(function (e) e.title == engine.title))
         return;
     }
 
     // Append the URI and an appropriate title to the browser data.
     // Use documentURIObject in the check for shouldLoadFavIcon so that we
     // do the right thing with about:-style error pages.  Bug 453442
     var iconURL = null;
-    if (gBrowser.shouldLoadFavIcon(targetDoc.documentURIObject))
-      iconURL = targetDoc.documentURIObject.prePath + "/favicon.ico";
+    if (gBrowser.shouldLoadFavIcon(uri))
+      iconURL = uri.prePath + "/favicon.ico";
 
     var hidden = false;
     // If this engine (identified by title) is already in the list, add it
     // to the list of hidden engines rather than to the main list.
     // XXX This will need to be changed when engines are identified by URL;
     // see bug 335102.
     if (Services.search.getEngineByName(engine.title))
       hidden = true;
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -4,20 +4,22 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this,
-  "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm");
-XPCOMUtils.defineLazyModuleGetter(this,
-  "InsecurePasswordUtils", "resource://gre/modules/InsecurePasswordUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler",
+  "resource:///modules/ContentLinkHandler.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
+  "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
+  "resource://gre/modules/InsecurePasswordUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITour",
   "resource:///modules/UITour.jsm");
 
 // Creates a new nsIURI object.
 function makeURI(uri, originCharset, baseURI) {
   return Services.io.newURI(uri, originCharset, baseURI);
@@ -278,11 +280,13 @@ let ClickEventHandler = {
     // In case of XLink, we don't return the node we got href from since
     // callers expect <a>-like elements.
     // Note: makeURI() will throw if aUri is not a valid URI.
     return [href ? makeURI(href, null, baseURI).spec : null, null];
   }
 };
 ClickEventHandler.init();
 
+ContentLinkHandler.init(this);
+
 addEventListener("DOMWebNotificationClicked", function(event) {
   sendAsyncMessage("DOMWebNotificationClicked", {});
 }, false);
--- a/browser/base/content/socialchat.xml
+++ b/browser/base/content/socialchat.xml
@@ -232,17 +232,18 @@
         // this sets the presence icon for a chat user, we simply use favicon style updating
         let link = event.originalTarget;
         let rel = link.rel && link.rel.toLowerCase();
         if (!link || !link.ownerDocument || !rel || !link.href)
           return;
         if (link.rel.indexOf("icon") < 0)
           return;
 
-        let uri = DOMLinkHandler.getLinkIconURI(link);
+        let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {}).ContentLinkHandler;
+        let uri = ContentLinkHandler.getLinkIconURI(link);
         if (!uri)
           return;
 
         // we made it this far, use it
         this.setAttribute('image', uri.spec);
         if (this.chatbar)
           this.chatbar.updateTitlebar(this);
       ]]></handler>
--- a/browser/base/content/socialmarks.xml
+++ b/browser/base/content/socialmarks.xml
@@ -302,17 +302,18 @@
             // updating
             let link = aEvent.originalTarget;
             let rel = link.rel && link.rel.toLowerCase();
             if (!link || !link.ownerDocument || !rel || !link.href)
               return;
             if (link.rel.indexOf("icon") < 0)
               return;
 
-            let uri = DOMLinkHandler.getLinkIconURI(link);
+            let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {}).ContentLinkHandler;
+            let uri = ContentLinkHandler.getLinkIconURI(link);
             if (!uri)
               return;
 
             // we cannot size the image when we apply it via listStyleImage, so
             // use the toolbar image
             this.setAttribute("image", uri.spec);
             }
             break;
new file mode 100644
--- /dev/null
+++ b/browser/modules/ContentLinkHandler.jsm
@@ -0,0 +1,136 @@
+/* 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";
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "ContentLinkHandler" ];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
+  "resource:///modules/Feeds.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+  "resource://gre/modules/BrowserUtils.jsm");
+
+this.ContentLinkHandler = {
+  init: function(chromeGlobal) {
+    chromeGlobal.addEventListener("DOMLinkAdded", (event) => {
+      this.onLinkAdded(event, chromeGlobal);
+    }, false);
+  },
+
+  onLinkAdded: function(event, chromeGlobal) {
+    var link = event.originalTarget;
+    var rel = link.rel && link.rel.toLowerCase();
+    if (!link || !link.ownerDocument || !rel || !link.href)
+      return;
+
+    var feedAdded = false;
+    var iconAdded = false;
+    var searchAdded = false;
+    var rels = {};
+    for (let relString of rel.split(/\s+/))
+      rels[relString] = true;
+
+    for (let relVal in rels) {
+      switch (relVal) {
+        case "feed":
+        case "alternate":
+          if (!feedAdded) {
+            if (!rels.feed && rels.alternate && rels.stylesheet)
+              break;
+
+            if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) {
+              chromeGlobal.sendAsyncMessage("Link:AddFeed",
+                                            {type: link.type,
+                                             href: link.href,
+                                             title: link.title});
+              feedAdded = true;
+            }
+          }
+          break;
+        case "icon":
+          if (!iconAdded) {
+            if (!Services.prefs.getBoolPref("browser.chrome.site_icons"))
+              break;
+
+            var uri = this.getLinkIconURI(link);
+            if (!uri)
+              break;
+
+            [iconAdded] = chromeGlobal.sendSyncMessage("Link:AddIcon", {url: uri.spec});
+          }
+          break;
+        case "search":
+          if (!searchAdded) {
+            var type = link.type && link.type.toLowerCase();
+            type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
+
+            let re = /^(?:https?|ftp):/i;
+            if (type == "application/opensearchdescription+xml" && link.title &&
+                re.test(link.href))
+            {
+              let engine = { title: link.title, href: link.href };
+              chromeGlobal.sendAsyncMessage("Link:AddSearch",
+                                            {engine: engine,
+                                             url: link.ownerDocument.documentURI});
+              searchAdded = true;
+            }
+          }
+          break;
+      }
+    }
+  },
+
+  getLinkIconURI: function(aLink) {
+    let targetDoc = aLink.ownerDocument;
+    var uri = BrowserUtils.makeURI(aLink.href, targetDoc.characterSet);
+
+    // Verify that the load of this icon is legal.
+    // Some error or special pages can load their favicon.
+    // To be on the safe side, only allow chrome:// favicons.
+    var isAllowedPage = [
+      /^about:neterror\?/,
+      /^about:blocked\?/,
+      /^about:certerror\?/,
+      /^about:home$/,
+    ].some(function (re) re.test(targetDoc.documentURI));
+
+    if (!isAllowedPage || !uri.schemeIs("chrome")) {
+      var ssm = Services.scriptSecurityManager;
+      try {
+        ssm.checkLoadURIWithPrincipal(targetDoc.nodePrincipal, uri,
+                                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+      } catch(e) {
+        return null;
+      }
+    }
+
+    try {
+      var contentPolicy = Cc["@mozilla.org/layout/content-policy;1"].
+                          getService(Ci.nsIContentPolicy);
+    } catch(e) {
+      return null; // Refuse to load if we can't do a security check.
+    }
+
+    // Security says okay, now ask content policy
+    if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_IMAGE,
+                                 uri, targetDoc.documentURIObject,
+                                 aLink, aLink.type, null)
+                                 != Ci.nsIContentPolicy.ACCEPT)
+      return null;
+
+    try {
+      uri.userPass = "";
+    } catch(e) {
+      // some URIs are immutable
+    }
+    return uri;
+  },
+};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -5,16 +5,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 TEST_DIRS += ['test']
 
 EXTRA_JS_MODULES += [
     'BrowserNewTabPreloader.jsm',
     'BrowserUITelemetry.jsm',
     'ContentClick.jsm',
+    'ContentLinkHandler.jsm',
     'Feeds.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'SharedFrame.jsm',
     'SignInToWebsite.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
     'TabCrashReporter.jsm',