toolkit/components/social/MozSocialAPI.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/social/MozSocialAPI.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,361 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
     1.9 +
    1.10 +Cu.import("resource://gre/modules/Services.jsm");
    1.11 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.12 +
    1.13 +XPCOMUtils.defineLazyModuleGetter(this, "SocialService", "resource://gre/modules/SocialService.jsm");
    1.14 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
    1.15 +
    1.16 +this.EXPORTED_SYMBOLS = ["MozSocialAPI", "openChatWindow", "findChromeWindowForChats", "closeAllChatWindows"];
    1.17 +
    1.18 +this.MozSocialAPI = {
    1.19 +  _enabled: false,
    1.20 +  _everEnabled: false,
    1.21 +  set enabled(val) {
    1.22 +    let enable = !!val;
    1.23 +    if (enable == this._enabled) {
    1.24 +      return;
    1.25 +    }
    1.26 +    this._enabled = enable;
    1.27 +
    1.28 +    if (enable) {
    1.29 +      Services.obs.addObserver(injectController, "document-element-inserted", false);
    1.30 +
    1.31 +      if (!this._everEnabled) {
    1.32 +        this._everEnabled = true;
    1.33 +        Services.telemetry.getHistogramById("SOCIAL_ENABLED_ON_SESSION").add(true);
    1.34 +      }
    1.35 +
    1.36 +    } else {
    1.37 +      Services.obs.removeObserver(injectController, "document-element-inserted");
    1.38 +    }
    1.39 +  }
    1.40 +};
    1.41 +
    1.42 +// Called on document-element-inserted, checks that the API should be injected,
    1.43 +// and then calls attachToWindow as appropriate
    1.44 +function injectController(doc, topic, data) {
    1.45 +  try {
    1.46 +    let window = doc.defaultView;
    1.47 +    if (!window || PrivateBrowsingUtils.isWindowPrivate(window))
    1.48 +      return;
    1.49 +
    1.50 +    // Do not attempt to load the API into about: error pages
    1.51 +    if (doc.documentURIObject.scheme == "about") {
    1.52 +      return;
    1.53 +    }
    1.54 +
    1.55 +    let containingBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor)
    1.56 +                                  .getInterface(Ci.nsIWebNavigation)
    1.57 +                                  .QueryInterface(Ci.nsIDocShell)
    1.58 +                                  .chromeEventHandler;
    1.59 +    // limit injecting into social panels or same-origin browser tabs if
    1.60 +    // social.debug.injectIntoTabs is enabled
    1.61 +    let allowTabs = false;
    1.62 +    try {
    1.63 +      allowTabs = containingBrowser.contentWindow == window &&
    1.64 +                  Services.prefs.getBoolPref("social.debug.injectIntoTabs");
    1.65 +    } catch(e) {}
    1.66 +
    1.67 +    let origin = containingBrowser.getAttribute("origin");
    1.68 +    if (!allowTabs && !origin) {
    1.69 +      return;
    1.70 +    }
    1.71 +
    1.72 +    // we always handle window.close on social content, even if they are not
    1.73 +    // "enabled".  "enabled" is about the worker state and a provider may
    1.74 +    // still be in e.g. the share panel without having their worker enabled.
    1.75 +    handleWindowClose(window);
    1.76 +
    1.77 +    SocialService.getProvider(doc.nodePrincipal.origin, function(provider) {
    1.78 +      if (provider && provider.enabled) {
    1.79 +        attachToWindow(provider, window);
    1.80 +      }
    1.81 +    });
    1.82 +  } catch(e) {
    1.83 +    Cu.reportError("MozSocialAPI injectController: unable to attachToWindow for " + doc.location + ": " + e);
    1.84 +  }
    1.85 +}
    1.86 +
    1.87 +// Loads mozSocial support functions associated with provider into targetWindow
    1.88 +function attachToWindow(provider, targetWindow) {
    1.89 +  // If the loaded document isn't from the provider's origin (or a protocol
    1.90 +  // that inherits the principal), don't attach the mozSocial API.
    1.91 +  let targetDocURI = targetWindow.document.documentURIObject;
    1.92 +  if (!provider.isSameOrigin(targetDocURI)) {
    1.93 +    let msg = "MozSocialAPI: not attaching mozSocial API for " + provider.origin +
    1.94 +              " to " + targetDocURI.spec + " since origins differ."
    1.95 +    Services.console.logStringMessage(msg);
    1.96 +    return;
    1.97 +  }
    1.98 +
    1.99 +  let port = provider.workerURL ? provider.getWorkerPort(targetWindow) : null;
   1.100 +
   1.101 +  let mozSocialObj = {
   1.102 +    // Use a method for backwards compat with existing providers, but we
   1.103 +    // should deprecate this in favor of a simple .port getter.
   1.104 +    getWorker: {
   1.105 +      enumerable: true,
   1.106 +      configurable: true,
   1.107 +      writable: true,
   1.108 +      value: function() {
   1.109 +        return {
   1.110 +          port: port,
   1.111 +          __exposedProps__: {
   1.112 +            port: "r"
   1.113 +          }
   1.114 +        };
   1.115 +      }
   1.116 +    },
   1.117 +    hasBeenIdleFor: {
   1.118 +      enumerable: true,
   1.119 +      configurable: true,
   1.120 +      writable: true,
   1.121 +      value: function() {
   1.122 +        return false;
   1.123 +      }
   1.124 +    },
   1.125 +    openChatWindow: {
   1.126 +      enumerable: true,
   1.127 +      configurable: true,
   1.128 +      writable: true,
   1.129 +      value: function(toURL, callback) {
   1.130 +        let url = targetWindow.document.documentURIObject.resolve(toURL);
   1.131 +        openChatWindow(getChromeWindow(targetWindow), provider, url, callback);
   1.132 +      }
   1.133 +    },
   1.134 +    openPanel: {
   1.135 +      enumerable: true,
   1.136 +      configurable: true,
   1.137 +      writable: true,
   1.138 +      value: function(toURL, offset, callback) {
   1.139 +        let chromeWindow = getChromeWindow(targetWindow);
   1.140 +        if (!chromeWindow.SocialFlyout)
   1.141 +          return;
   1.142 +        let url = targetWindow.document.documentURIObject.resolve(toURL);
   1.143 +        if (!provider.isSameOrigin(url))
   1.144 +          return;
   1.145 +        chromeWindow.SocialFlyout.open(url, offset, callback);
   1.146 +      }
   1.147 +    },
   1.148 +    closePanel: {
   1.149 +      enumerable: true,
   1.150 +      configurable: true,
   1.151 +      writable: true,
   1.152 +      value: function(toURL, offset, callback) {
   1.153 +        let chromeWindow = getChromeWindow(targetWindow);
   1.154 +        if (!chromeWindow.SocialFlyout || !chromeWindow.SocialFlyout.panel)
   1.155 +          return;
   1.156 +        chromeWindow.SocialFlyout.panel.hidePopup();
   1.157 +      }
   1.158 +    },
   1.159 +    // allow a provider to share to other providers through the browser
   1.160 +    share: {
   1.161 +      enumerable: true,
   1.162 +      configurable: true,
   1.163 +      writable: true,
   1.164 +      value: function(data) {
   1.165 +        let chromeWindow = getChromeWindow(targetWindow);
   1.166 +        if (!chromeWindow.SocialShare || chromeWindow.SocialShare.shareButton.hidden)
   1.167 +          throw new Error("Share is unavailable");
   1.168 +        // ensure user action initates the share
   1.169 +        let dwu = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.170 +                              .getInterface(Ci.nsIDOMWindowUtils);
   1.171 +        if (!dwu.isHandlingUserInput)
   1.172 +          throw new Error("Attempt to share without user input");
   1.173 +
   1.174 +        // limit to a few params we want to support for now
   1.175 +        let dataOut = {};
   1.176 +        for (let sub of ["url", "title", "description", "source"]) {
   1.177 +          dataOut[sub] = data[sub];
   1.178 +        }
   1.179 +        if (data.image)
   1.180 +          dataOut.previews = [data.image];
   1.181 +
   1.182 +        chromeWindow.SocialShare.sharePage(null, dataOut);
   1.183 +      }
   1.184 +    },
   1.185 +    getAttention: {
   1.186 +      enumerable: true,
   1.187 +      configurable: true,
   1.188 +      writable: true,
   1.189 +      value: function() {
   1.190 +        getChromeWindow(targetWindow).getAttention();
   1.191 +      }
   1.192 +    },
   1.193 +    isVisible: {
   1.194 +      enumerable: true,
   1.195 +      configurable: true,
   1.196 +      get: function() {
   1.197 +        return targetWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.198 +                           .getInterface(Ci.nsIWebNavigation)
   1.199 +                           .QueryInterface(Ci.nsIDocShell).isActive;
   1.200 +      }
   1.201 +    }
   1.202 +  };
   1.203 +
   1.204 +  let contentObj = Cu.createObjectIn(targetWindow);
   1.205 +  Object.defineProperties(contentObj, mozSocialObj);
   1.206 +  Cu.makeObjectPropsNormal(contentObj);
   1.207 +
   1.208 +  targetWindow.navigator.wrappedJSObject.__defineGetter__("mozSocial", function() {
   1.209 +    // We do this in a getter, so that we create these objects
   1.210 +    // only on demand (this is a potential concern, since
   1.211 +    // otherwise we might add one per iframe, and keep them
   1.212 +    // alive for as long as the window is alive).
   1.213 +    delete targetWindow.navigator.wrappedJSObject.mozSocial;
   1.214 +    return targetWindow.navigator.wrappedJSObject.mozSocial = contentObj;
   1.215 +  });
   1.216 +
   1.217 +  if (port) {
   1.218 +    targetWindow.addEventListener("unload", function () {
   1.219 +      // We want to close the port, but also want the target window to be
   1.220 +      // able to use the port during an unload event they setup - so we
   1.221 +      // set a timer which will fire after the unload events have all fired.
   1.222 +      schedule(function () { port.close(); });
   1.223 +    });
   1.224 +  }
   1.225 +}
   1.226 +
   1.227 +function handleWindowClose(targetWindow) {
   1.228 +  // We allow window.close() to close the panel, so add an event handler for
   1.229 +  // this, then cancel the event (so the window itself doesn't die) and
   1.230 +  // close the panel instead.
   1.231 +  // However, this is typically affected by the dom.allow_scripts_to_close_windows
   1.232 +  // preference, but we can avoid that check by setting a flag on the window.
   1.233 +  let dwu = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.234 +                        .getInterface(Ci.nsIDOMWindowUtils);
   1.235 +  dwu.allowScriptsToClose();
   1.236 +
   1.237 +  targetWindow.addEventListener("DOMWindowClose", function _mozSocialDOMWindowClose(evt) {
   1.238 +    let elt = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.239 +                .getInterface(Ci.nsIWebNavigation)
   1.240 +                .QueryInterface(Ci.nsIDocShell)
   1.241 +                .chromeEventHandler;
   1.242 +    while (elt) {
   1.243 +      if (elt.localName == "panel") {
   1.244 +        elt.hidePopup();
   1.245 +        break;
   1.246 +      } else if (elt.localName == "chatbox") {
   1.247 +        elt.close();
   1.248 +        break;
   1.249 +      }
   1.250 +      elt = elt.parentNode;
   1.251 +    }
   1.252 +    // preventDefault stops the default window.close() function being called,
   1.253 +    // which doesn't actually close anything but causes things to get into
   1.254 +    // a bad state (an internal 'closed' flag is set and debug builds start
   1.255 +    // asserting as the window is used.).
   1.256 +    // None of the windows we inject this API into are suitable for this
   1.257 +    // default close behaviour, so even if we took no action above, we avoid
   1.258 +    // the default close from doing anything.
   1.259 +    evt.preventDefault();
   1.260 +  }, true);
   1.261 +}
   1.262 +
   1.263 +function schedule(callback) {
   1.264 +  Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
   1.265 +}
   1.266 +
   1.267 +function getChromeWindow(contentWin) {
   1.268 +  return contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
   1.269 +                   .getInterface(Ci.nsIWebNavigation)
   1.270 +                   .QueryInterface(Ci.nsIDocShellTreeItem)
   1.271 +                   .rootTreeItem
   1.272 +                   .QueryInterface(Ci.nsIInterfaceRequestor)
   1.273 +                   .getInterface(Ci.nsIDOMWindow);
   1.274 +}
   1.275 +
   1.276 +function isWindowGoodForChats(win) {
   1.277 +  return win.SocialChatBar
   1.278 +         && win.SocialChatBar.isAvailable
   1.279 +         && !PrivateBrowsingUtils.isWindowPrivate(win);
   1.280 +}
   1.281 +
   1.282 +function findChromeWindowForChats(preferredWindow) {
   1.283 +  if (preferredWindow && isWindowGoodForChats(preferredWindow))
   1.284 +    return preferredWindow;
   1.285 +  // no good - we just use the "most recent" browser window which can host
   1.286 +  // chats (we used to try and "group" all chats in the same browser window,
   1.287 +  // but that didn't work out so well - see bug 835111
   1.288 +
   1.289 +  // Try first the most recent window as getMostRecentWindow works
   1.290 +  // even on platforms where getZOrderDOMWindowEnumerator is broken
   1.291 +  // (ie. Linux).  This will handle most cases, but won't work if the
   1.292 +  // foreground window is a popup.
   1.293 +
   1.294 +  let mostRecent = Services.wm.getMostRecentWindow("navigator:browser");
   1.295 +  if (isWindowGoodForChats(mostRecent))
   1.296 +    return mostRecent;
   1.297 +
   1.298 +  let topMost, enumerator;
   1.299 +  // *sigh* - getZOrderDOMWindowEnumerator is broken except on Mac and
   1.300 +  // Windows.  We use BROKEN_WM_Z_ORDER as that is what some other code uses
   1.301 +  // and a few bugs recommend searching mxr for this symbol to identify the
   1.302 +  // workarounds - we want this code to be hit in such searches.
   1.303 +  let os = Services.appinfo.OS;
   1.304 +  const BROKEN_WM_Z_ORDER = os != "WINNT" && os != "Darwin";
   1.305 +  if (BROKEN_WM_Z_ORDER) {
   1.306 +    // this is oldest to newest and no way to change the order.
   1.307 +    enumerator = Services.wm.getEnumerator("navigator:browser");
   1.308 +  } else {
   1.309 +    // here we explicitly ask for bottom-to-top so we can use the same logic
   1.310 +    // where BROKEN_WM_Z_ORDER is true.
   1.311 +    enumerator = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", false);
   1.312 +  }
   1.313 +  while (enumerator.hasMoreElements()) {
   1.314 +    let win = enumerator.getNext();
   1.315 +    if (!win.closed && isWindowGoodForChats(win))
   1.316 +      topMost = win;
   1.317 +  }
   1.318 +  return topMost;
   1.319 +}
   1.320 +
   1.321 +this.openChatWindow =
   1.322 + function openChatWindow(chromeWindow, provider, url, callback, mode) {
   1.323 +  chromeWindow = findChromeWindowForChats(chromeWindow);
   1.324 +  if (!chromeWindow) {
   1.325 +    Cu.reportError("Failed to open a social chat window - no host window could be found.");
   1.326 +    return;
   1.327 +  }
   1.328 +  let fullURI = provider.resolveUri(url);
   1.329 +  if (!provider.isSameOrigin(fullURI)) {
   1.330 +    Cu.reportError("Failed to open a social chat window - the requested URL is not the same origin as the provider.");
   1.331 +    return;
   1.332 +  }
   1.333 +  if (!chromeWindow.SocialChatBar.openChat(provider, fullURI.spec, callback, mode)) {
   1.334 +    Cu.reportError("Failed to open a social chat window - the chatbar is not available in the target window.");
   1.335 +    return;
   1.336 +  }
   1.337 +  // getAttention is ignored if the target window is already foreground, so
   1.338 +  // we can call it unconditionally.
   1.339 +  chromeWindow.getAttention();
   1.340 +}
   1.341 +
   1.342 +this.closeAllChatWindows =
   1.343 + function closeAllChatWindows(provider) {
   1.344 +  // close all attached chat windows
   1.345 +  let winEnum = Services.wm.getEnumerator("navigator:browser");
   1.346 +  while (winEnum.hasMoreElements()) {
   1.347 +    let win = winEnum.getNext();
   1.348 +    if (!win.SocialChatBar)
   1.349 +      continue;
   1.350 +    let chats = [c for (c of win.SocialChatBar.chatbar.children) if (c.content.getAttribute("origin") == provider.origin)];
   1.351 +    [c.close() for (c of chats)];
   1.352 +  }
   1.353 +
   1.354 +  // close all standalone chat windows
   1.355 +  winEnum = Services.wm.getEnumerator("Social:Chat");
   1.356 +  while (winEnum.hasMoreElements()) {
   1.357 +    let win = winEnum.getNext();
   1.358 +    if (win.closed)
   1.359 +      continue;
   1.360 +    let origin = win.document.getElementById("chatter").content.getAttribute("origin");
   1.361 +    if (provider.origin == origin)
   1.362 +      win.close();
   1.363 +  }
   1.364 +}

mercurial