Bug 1029942 - Allow activation from about: chrome urls. r=felipc, a=lmandel
authorShane Caraveo <scaraveo@mozilla.com>
Fri, 11 Jul 2014 20:03:00 -0400
changeset 208995 cb0c8492ff29639834d8f72f6b07f9bd33cf7a83
parent 208994 d538f9245a44be74e20c1fb6d6295c29fd2f2272
child 208996 b3f6dbd37993c5247f926da788ebd940f0a1b160
push id494
push userraliiev@mozilla.com
push dateMon, 25 Aug 2014 18:42:16 +0000
treeherdermozilla-release@a3cc3e46b571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipc, lmandel
bugs1029942
milestone32.0a2
Bug 1029942 - Allow activation from about: chrome urls. r=felipc, a=lmandel
browser/base/content/browser-social.js
browser/base/content/test/social/browser.ini
browser/base/content/test/social/browser_aboutHome_activation.js
browser/modules/Social.jsm
toolkit/components/social/SocialService.jsm
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -175,33 +175,35 @@ SocialUI = {
     SocialSidebar.update();
     SocialShare.populateProviderMenu();
     SocialStatus.populateToolbarPalette();
     SocialMarks.populateToolbarPalette();
     SocialShare.update();
   },
 
   // This handles "ActivateSocialFeature" events fired against content documents
-  // in this window.
-  _activationEventHandler: function SocialUI_activationHandler(e) {
+  // in this window.  If this activation happens from within Firefox, such as
+  // about:home or the share panel, we bypass the enable prompt. Any website
+  // activation, such as from the activations directory or a providers website
+  // will still get the prompt.
+  _activationEventHandler: function SocialUI_activationHandler(e, aBypassUserEnable=false) {
     let targetDoc;
     let node;
     if (e.target instanceof HTMLDocument) {
       // version 0 support
       targetDoc = e.target;
       node = targetDoc.documentElement
     } else {
       targetDoc = e.target.ownerDocument;
       node = e.target;
     }
     if (!(targetDoc instanceof HTMLDocument))
       return;
 
-    // Ignore events fired in background tabs or iframes
-    if (targetDoc.defaultView != content)
+    if (!aBypassUserEnable && targetDoc.defaultView != content)
       return;
 
     // If we are in PB mode, we silently do nothing (bug 829404 exists to
     // do something sensible here...)
     if (PrivateBrowsingUtils.isWindowPrivate(window))
       return;
 
     // If the last event was received < 1s ago, ignore this one
@@ -231,17 +233,17 @@ SocialUI = {
       Social.activateFromOrigin(manifest.origin, function(provider) {
         if (provider.sidebarURL) {
           SocialSidebar.show(provider.origin);
         }
         if (provider.postActivationURL) {
           openUILinkIn(provider.postActivationURL, "tab");
         }
       });
-    });
+    }, aBypassUserEnable);
   },
 
   showLearnMore: function() {
     let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api";
     openUILinkIn(url, "tab");
   },
 
   closeSocialPanelForLinkTraversal: function (target, linkNode) {
--- a/browser/base/content/test/social/browser.ini
+++ b/browser/base/content/test/social/browser.ini
@@ -19,16 +19,17 @@ support-files =
   social_panel.html
   social_postActivation.html
   social_sidebar.html
   social_sidebar_empty.html
   social_window.html
   social_worker.js
   unchecked.jpg
 
+[browser_aboutHome_activation.js]
 [browser_addons.js]
 [browser_blocklist.js]
 [browser_defaults.js]
 [browser_share.js]
 [browser_social_activation.js]
 [browser_social_chatwindow.js]
 [browser_social_chatwindow_resize.js]
 [browser_social_chatwindowfocus.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_aboutHome_activation.js
@@ -0,0 +1,286 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils",
+  "resource:///modules/AboutHome.jsm");
+
+registerCleanupFunction(function() {
+  // Ensure we don't pollute prefs for next tests.
+  Services.prefs.clearUserPref("browser.aboutHomeSnippets.updateUrl");
+});
+
+let snippet =
+'     <script>' +
+'       var manifest = {' +
+'         "name": "Demo Social Service",' +
+'         "origin": "https://example.com",' +
+'         "iconURL": "chrome://branding/content/icon16.png",' +
+'         "icon32URL": "chrome://branding/content/favicon32.png",' +
+'         "icon64URL": "chrome://branding/content/icon64.png",' +
+'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",' +
+'         "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
+'       };' +
+'       function activateProvider(node) {' +
+'         node.setAttribute("data-service", JSON.stringify(manifest));' +
+'         var event = new CustomEvent("ActivateSocialFeature");' +
+'         node.dispatchEvent(event);' +
+'       }' +
+'     </script>' +
+'     <div id="activationSnippet" onclick="activateProvider(this)">' +
+'     <img src="chrome://branding/content/favicon32.png"></img>' +
+'     </div>';
+
+// enable one-click activation
+let snippet2 =
+'     <script>' +
+'       var manifest = {' +
+'         "name": "Demo Social Service",' +
+'         "origin": "https://example.com",' +
+'         "iconURL": "chrome://branding/content/icon16.png",' +
+'         "icon32URL": "chrome://branding/content/favicon32.png",' +
+'         "icon64URL": "chrome://branding/content/icon64.png",' +
+'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",' +
+'         "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
+'         "oneclick": true' +
+'       };' +
+'       function activateProvider(node) {' +
+'         node.setAttribute("data-service", JSON.stringify(manifest));' +
+'         var event = new CustomEvent("ActivateSocialFeature");' +
+'         node.dispatchEvent(event);' +
+'       }' +
+'     </script>' +
+'     <div id="activationSnippet" onclick="activateProvider(this)">' +
+'     <img src="chrome://branding/content/favicon32.png"></img>' +
+'     </div>';
+
+let gTests = [
+
+{
+  desc: "Test activation with enable panel",
+  setup: function (aSnippetsMap)
+  {
+    // This must be some incorrect xhtml code.
+    aSnippetsMap.set("snippets", snippet);
+  },
+  run: function (aSnippetsMap)
+  {
+    let deferred = Promise.defer();
+    let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
+
+    let snippetsElt = doc.getElementById("snippets");
+    ok(snippetsElt, "Found snippets element");
+    ok(!!doc.getElementById("activationSnippet"),
+       "The snippet is present.");
+
+    activateProvider(gBrowser.selectedTab, true, function() {
+      ok(SocialSidebar.provider, "provider activated");
+      checkSocialUI();
+      is(gBrowser.contentDocument.location.href, SocialSidebar.provider.manifest.postActivationURL);
+      gBrowser.removeTab(gBrowser.selectedTab);
+      SocialService.uninstallProvider(SocialSidebar.provider.origin, function () {
+        info("provider uninstalled");
+        aSnippetsMap.delete("snippets");
+        deferred.resolve(true);
+      });
+    });
+    return deferred.promise;
+  }
+},
+
+{
+  desc: "Test activation bypassing enable panel",
+  setup: function (aSnippetsMap)
+  {
+    // This must be some incorrect xhtml code.
+    aSnippetsMap.set("snippets", snippet2);
+  },
+  run: function (aSnippetsMap)
+  {
+    let deferred = Promise.defer();
+    let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
+
+    let snippetsElt = doc.getElementById("snippets");
+    ok(snippetsElt, "Found snippets element");
+    ok(!!doc.getElementById("activationSnippet"),
+       "The snippet is present.");
+
+    activateProvider(gBrowser.selectedTab, false, function() {
+      ok(SocialSidebar.provider, "provider activated");
+      checkSocialUI();
+      is(gBrowser.contentDocument.location.href, SocialSidebar.provider.manifest.postActivationURL);
+      gBrowser.removeTab(gBrowser.selectedTab);
+      SocialService.uninstallProvider(SocialSidebar.provider.origin, function () {
+        info("provider uninstalled");
+        aSnippetsMap.delete("snippets");
+        deferred.resolve(true);
+      });
+    });
+    return deferred.promise;
+  }
+}
+];
+
+function test()
+{
+  waitForExplicitFinish();
+  requestLongerTimeout(2);
+  ignoreAllUncaughtExceptions();
+
+  Task.spawn(function () {
+    for (let test of gTests) {
+      info(test.desc);
+
+      // Make sure we don't try to load snippets from the network.
+      Services.prefs.setCharPref("browser.aboutHomeSnippets.updateUrl", "nonexistent://test");
+
+      // Create a tab to run the test.
+      let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank");
+
+      // Add an event handler to modify the snippets map once it's ready.
+      let snippetsPromise = promiseSetupSnippetsMap(tab, test.setup);
+
+      // Start loading about:home and wait for it to complete.
+      yield promiseTabLoadEvent(tab, "about:home", "AboutHomeLoadSnippetsSucceeded");
+
+      // This promise should already be resolved since the page is done,
+      // but we still want to get the snippets map out of it.
+      let snippetsMap = yield snippetsPromise;
+
+      info("Running test");
+      let testPromise = test.run(snippetsMap);
+      yield testPromise;
+      info("Cleanup");
+      gBrowser.removeCurrentTab();
+    }
+  }).then(finish, ex => {
+    ok(false, "Unexpected Exception: " + ex);
+    finish();
+  });
+}
+
+/**
+ * Starts a load in an existing tab and waits for it to finish (via some event).
+ *
+ * @param aTab
+ *        The tab to load into.
+ * @param aUrl
+ *        The url to load.
+ * @param aEvent
+ *        The load event type to wait for.  Defaults to "load".
+ * @return {Promise} resolved when the event is handled.
+ */
+function promiseTabLoadEvent(aTab, aURL, aEventType="load")
+{
+  let deferred = Promise.defer();
+  info("Wait tab event: " + aEventType);
+  aTab.linkedBrowser.addEventListener(aEventType, function load(event) {
+    if (event.originalTarget != aTab.linkedBrowser.contentDocument ||
+        event.target.location.href == "about:blank") {
+      info("skipping spurious load event");
+      return;
+    }
+    aTab.linkedBrowser.removeEventListener(aEventType, load, true);
+    info("Tab event received: " + aEventType);
+    deferred.resolve();
+  }, true, true);
+  aTab.linkedBrowser.loadURI(aURL);
+  return deferred.promise;
+}
+
+/**
+ * Cleans up snippets and ensures that by default we don't try to check for
+ * remote snippets since that may cause network bustage or slowness.
+ *
+ * @param aTab
+ *        The tab containing about:home.
+ * @param aSetupFn
+ *        The setup function to be run.
+ * @return {Promise} resolved when the snippets are ready.  Gets the snippets map.
+ */
+function promiseSetupSnippetsMap(aTab, aSetupFn)
+{
+  let deferred = Promise.defer();
+  info("Waiting for snippets map");
+  aTab.linkedBrowser.addEventListener("AboutHomeLoadSnippets", function load(event) {
+    aTab.linkedBrowser.removeEventListener("AboutHomeLoadSnippets", load, true);
+
+    let cw = aTab.linkedBrowser.contentWindow.wrappedJSObject;
+    // The snippets should already be ready by this point. Here we're
+    // just obtaining a reference to the snippets map.
+    cw.ensureSnippetsMapThen(function (aSnippetsMap) {
+      aSnippetsMap = Cu.waiveXrays(aSnippetsMap);
+      info("Got snippets map: " +
+           "{ last-update: " + aSnippetsMap.get("snippets-last-update") +
+           ", cached-version: " + aSnippetsMap.get("snippets-cached-version") +
+           " }");
+      // Don't try to update.
+      aSnippetsMap.set("snippets-last-update", Date.now());
+      aSnippetsMap.set("snippets-cached-version", AboutHomeUtils.snippetsVersion);
+      // Clear snippets.
+      aSnippetsMap.delete("snippets");
+      aSetupFn(aSnippetsMap);
+      deferred.resolve(aSnippetsMap);
+    });
+  }, true, true);
+  return deferred.promise;
+}
+
+
+function sendActivationEvent(tab, callback) {
+  // hack Social.lastEventReceived so we don't hit the "too many events" check.
+  Social.lastEventReceived = 0;
+  let doc = tab.linkedBrowser.contentDocument;
+  // if our test has a frame, use it
+  if (doc.defaultView.frames[0])
+    doc = doc.defaultView.frames[0].document;
+  let button = doc.getElementById("activationSnippet");
+  EventUtils.synthesizeMouseAtCenter(button, {}, doc.defaultView);
+  executeSoon(callback);
+}
+
+function activateProvider(tab, expectPanel, aCallback) {
+  if (expectPanel) {
+    let panel = document.getElementById("servicesInstall-notification");
+    PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
+      PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
+      panel.button.click();
+    });
+  }
+  sendActivationEvent(tab, function() {
+    waitForProviderLoad(function() {
+      ok(SocialSidebar.provider, "new provider is active");
+      ok(SocialSidebar.opened, "sidebar is open");
+      checkSocialUI();
+      executeSoon(aCallback);
+    });
+  });
+}
+
+function waitForProviderLoad(cb) {
+  Services.obs.addObserver(function providerSet(subject, topic, data) {
+    Services.obs.removeObserver(providerSet, "social:provider-enabled");
+    info("social:provider-enabled observer was notified");
+    waitForCondition(function() {
+      let sbrowser = document.getElementById("social-sidebar-browser");
+      let provider = SocialSidebar.provider;
+      let postActivation = provider && gBrowser.contentDocument.location.href == provider.origin + "/browser/browser/base/content/test/social/social_postActivation.html";
+
+      return provider &&
+             postActivation &&
+             sbrowser.docShellIsActive;
+    }, function() {
+      // executeSoon to let the browser UI observers run first
+      executeSoon(cb);
+    },
+    "waitForProviderLoad: provider profile was not set");
+  }, "social:provider-enabled", false);
+}
+
+
--- a/browser/modules/Social.jsm
+++ b/browser/modules/Social.jsm
@@ -163,18 +163,18 @@ this.Social = {
     }
     return null;
   },
 
   getManifestByOrigin: function(origin) {
     return SocialService.getManifestByOrigin(origin);
   },
 
-  installProvider: function(doc, data, installCallback) {
-    SocialService.installProvider(doc, data, installCallback);
+  installProvider: function(doc, data, installCallback, aBypassUserEnable=false) {
+    SocialService.installProvider(doc, data, installCallback, aBypassUserEnable);
   },
 
   uninstallProvider: function(origin, aCallback) {
     SocialService.uninstallProvider(origin, aCallback);
   },
 
   // Activation functionality
   activateFromOrigin: function (origin, callback) {
--- a/toolkit/components/social/SocialService.jsm
+++ b/toolkit/components/social/SocialService.jsm
@@ -146,17 +146,27 @@ XPCOMUtils.defineLazyGetter(SocialServic
       Cu.reportError("SocialService: failed to load provider: " + manifest.origin +
                      ", exception: " + err);
     }
   }
   return providers;
 });
 
 function getOriginActivationType(origin) {
-  let prefname = SocialServiceInternal.getManifestPrefname(origin);
+  // access from moz-safe-about scheme will throw exception in getManifestPrefname
+  try {
+    var prefname = SocialServiceInternal.getManifestPrefname(origin);
+  } catch(e) {
+    // if this is an about uri, treat it as a directory
+    let originUri = Services.io.newURI(origin, null, null);
+    if (originUri.scheme == "moz-safe-about") {
+      return "internal";
+    }
+    throw e;
+  }
   if (Services.prefs.getDefaultBranch("social.manifest.").getPrefType(prefname) == Services.prefs.PREF_STRING)
     return 'builtin';
 
   let whitelist = Services.prefs.getCharPref("social.whitelist").split(',');
   if (whitelist.indexOf(origin) >= 0)
     return 'whitelist';
 
   let directories = Services.prefs.getCharPref("social.directories").split(',');
@@ -499,17 +509,17 @@ this.SocialService = {
       }
     }
   },
 
   _manifestFromData: function(type, data, principal) {
     let featureURLs = ['workerURL', 'sidebarURL', 'shareURL', 'statusURL', 'markURL'];
     let resolveURLs = featureURLs.concat(['postActivationURL']);
 
-    if (type == 'directory') {
+    if (type == 'directory' || type == 'internal') {
       // directory provided manifests must have origin in manifest, use that
       if (!data['origin']) {
         Cu.reportError("SocialService.manifestFromData directory service provided manifest without origin.");
         return null;
       }
       let URI = Services.io.newURI(data.origin, null, null);
       principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(URI);
     }
@@ -564,17 +574,17 @@ this.SocialService = {
     let requestingWindow = aDOMDocument.defaultView.top;
     let chromeWin = this._getChromeWindow(requestingWindow).wrappedJSObject;
     let browser = chromeWin.gBrowser.getBrowserForDocument(aDOMDocument);
     let requestingURI =  Services.io.newURI(aDOMDocument.location.href, null, null);
 
     let productName = brandBundle.GetStringFromName("brandShortName");
 
     let message = browserBundle.formatStringFromName("service.install.description",
-                                                     [requestingURI.host, productName], 2);
+                                                     [aAddonInstaller.addon.manifest.name, productName], 2);
 
     let action = {
       label: browserBundle.GetStringFromName("service.install.ok.label"),
       accessKey: browserBundle.GetStringFromName("service.install.ok.accesskey"),
       callback: function() {
         aAddonInstaller.install();
       },
     };
@@ -583,49 +593,53 @@ this.SocialService = {
                     learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api",
                   };
     let anchor = "servicesInstall-notification-icon";
     let notificationid = "servicesInstall";
     chromeWin.PopupNotifications.show(browser, notificationid, message, anchor,
                                       action, [], options);
   },
 
-  installProvider: function(aDOMDocument, data, installCallback) {
+  installProvider: function(aDOMDocument, data, installCallback, aBypassUserEnable=false) {
     let manifest;
     let installOrigin = aDOMDocument.nodePrincipal.origin;
 
     if (data) {
       let installType = getOriginActivationType(installOrigin);
       // if we get data, we MUST have a valid manifest generated from the data
       manifest = this._manifestFromData(installType, data, aDOMDocument.nodePrincipal);
       if (!manifest)
         throw new Error("SocialService.installProvider: service configuration is invalid from " + aDOMDocument.location.href);
 
       let addon = new AddonWrapper(manifest);
       if (addon && addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
         throw new Error("installProvider: provider with origin [" +
                         installOrigin + "] is blocklisted");
+      // manifestFromData call above will enforce correct origin. To support
+      // activation from about: uris, we need to be sure to use the updated
+      // origin on the manifest.
+      installOrigin = manifest.origin;
     }
 
     let id = getAddonIDFromOrigin(installOrigin);
     AddonManager.getAddonByID(id, function(aAddon) {
       if (aAddon && aAddon.userDisabled) {
         aAddon.cancelUninstall();
         aAddon.userDisabled = false;
       }
       schedule(function () {
-        this._installProvider(aDOMDocument, manifest, aManifest => {
+        this._installProvider(aDOMDocument, manifest, aBypassUserEnable, aManifest => {
           this._notifyProviderListeners("provider-installed", aManifest.origin);
           installCallback(aManifest);
         });
       }.bind(this));
     }.bind(this));
   },
 
-  _installProvider: function(aDOMDocument, manifest, installCallback) {
+  _installProvider: function(aDOMDocument, manifest, aBypassUserEnable, installCallback) {
     let sourceURI = aDOMDocument.location.href;
     let installOrigin = aDOMDocument.nodePrincipal.origin;
 
     let installType = getOriginActivationType(installOrigin);
     let installer;
     switch(installType) {
       case "foreign":
         if (!Services.prefs.getBoolPref("social.remote-install.enabled"))
@@ -648,18 +662,28 @@ this.SocialService = {
           let prefname = getPrefnameFromOrigin(installOrigin);
           manifest = Services.prefs.getDefaultBranch(null)
                           .getComplexValue(prefname, Ci.nsISupportsString).data;
           manifest = JSON.parse(manifest);
           // ensure we override a builtin manifest by having a different value in it
           if (manifest.builtin)
             delete manifest.builtin;
         }
+      case "internal":
+        // double check here since "builtin" falls through this as well.
+        aBypassUserEnable = installType == "internal" && manifest.oneclick;
       case "directory":
-        // a manifest is requried, and will have been vetted by reviewers
+        // a manifest is requried, and will have been vetted by reviewers. We
+        // also handle in-product installations without the verification step.
+        if (aBypassUserEnable) {
+          installer = new AddonInstaller(sourceURI, manifest, installCallback);
+          installer.install();
+          return;
+        }
+        // otherwise fall through to the install below which presents the panel
       case "whitelist":
         // a manifest is required, we'll catch a missing manifest below.
         if (!manifest)
           throw new Error("Cannot install provider without manifest data");
         installer = new AddonInstaller(sourceURI, manifest, installCallback);
         this._showInstallNotification(aDOMDocument, installer);
         break;
       default: