browser/modules/Social.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 this.EXPORTED_SYMBOLS = ["Social", "CreateSocialStatusWidget",
     8                          "CreateSocialMarkWidget", "OpenGraphBuilder",
     9                          "DynamicResizeWatcher", "sizeSocialPanelToContent"];
    11 const Ci = Components.interfaces;
    12 const Cc = Components.classes;
    13 const Cu = Components.utils;
    15 // The minimum sizes for the auto-resize panel code.
    16 const PANEL_MIN_HEIGHT = 100;
    17 const PANEL_MIN_WIDTH = 330;
    19 Cu.import("resource://gre/modules/Services.jsm");
    20 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    22 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
    23   "resource:///modules/CustomizableUI.jsm");
    24 XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
    25   "resource://gre/modules/SocialService.jsm");
    26 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    27   "resource://gre/modules/PlacesUtils.jsm");
    28 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    29   "resource://gre/modules/PrivateBrowsingUtils.jsm");
    30 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    31   "resource://gre/modules/Promise.jsm");
    33 XPCOMUtils.defineLazyServiceGetter(this, "unescapeService",
    34                                    "@mozilla.org/feed-unescapehtml;1",
    35                                    "nsIScriptableUnescapeHTML");
    37 function promiseSetAnnotation(aURI, providerList) {
    38   let deferred = Promise.defer();
    40   // Delaying to catch issues with asynchronous behavior while waiting
    41   // to implement asynchronous annotations in bug 699844.
    42   Services.tm.mainThread.dispatch(function() {
    43     try {
    44       if (providerList && providerList.length > 0) {
    45         PlacesUtils.annotations.setPageAnnotation(
    46           aURI, "social/mark", JSON.stringify(providerList), 0,
    47           PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
    48       } else {
    49         PlacesUtils.annotations.removePageAnnotation(aURI, "social/mark");
    50       }
    51     } catch(e) {
    52       Cu.reportError("SocialAnnotation failed: " + e);
    53     }
    54     deferred.resolve();
    55   }, Ci.nsIThread.DISPATCH_NORMAL);
    57   return deferred.promise;
    58 }
    60 function promiseGetAnnotation(aURI) {
    61   let deferred = Promise.defer();
    63   // Delaying to catch issues with asynchronous behavior while waiting
    64   // to implement asynchronous annotations in bug 699844.
    65   Services.tm.mainThread.dispatch(function() {
    66     let val = null;
    67     try {
    68       val = PlacesUtils.annotations.getPageAnnotation(aURI, "social/mark");
    69     } catch (ex) { }
    71     deferred.resolve(val);
    72   }, Ci.nsIThread.DISPATCH_NORMAL);
    74   return deferred.promise;
    75 }
    77 this.Social = {
    78   initialized: false,
    79   lastEventReceived: 0,
    80   providers: [],
    81   _disabledForSafeMode: false,
    83   init: function Social_init() {
    84     this._disabledForSafeMode = Services.appinfo.inSafeMode && this.enabled;
    85     let deferred = Promise.defer();
    87     if (this.initialized) {
    88       deferred.resolve(true);
    89       return deferred.promise;
    90     }
    91     this.initialized = true;
    92     // if SocialService.hasEnabledProviders, retreive the providers so the
    93     // front-end can generate UI
    94     if (SocialService.hasEnabledProviders) {
    95       // Retrieve the current set of providers, and set the current provider.
    96       SocialService.getOrderedProviderList(function (providers) {
    97         Social._updateProviderCache(providers);
    98         Social._updateWorkerState(SocialService.enabled);
    99         deferred.resolve(false);
   100       });
   101     } else {
   102       deferred.resolve(false);
   103     }
   105     // Register an observer for changes to the provider list
   106     SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
   107       // An engine change caused by adding/removing a provider should notify.
   108       // any providers we receive are enabled in the AddonsManager
   109       if (topic == "provider-installed" || topic == "provider-uninstalled") {
   110         // installed/uninstalled do not send the providers param
   111         Services.obs.notifyObservers(null, "social:" + topic, origin);
   112         return;
   113       }
   114       if (topic == "provider-enabled") {
   115         Social._updateProviderCache(providers);
   116         Social._updateWorkerState(true);
   117         Services.obs.notifyObservers(null, "social:" + topic, origin);
   118         return;
   119       }
   120       if (topic == "provider-disabled") {
   121         // a provider was removed from the list of providers, that does not
   122         // affect worker state for other providers
   123         Social._updateProviderCache(providers);
   124         Social._updateWorkerState(providers.length > 0);
   125         Services.obs.notifyObservers(null, "social:" + topic, origin);
   126         return;
   127       }
   128       if (topic == "provider-update") {
   129         // a provider has self-updated its manifest, we need to update our cache
   130         // and reload the provider.
   131         Social._updateProviderCache(providers);
   132         let provider = Social._getProviderFromOrigin(origin);
   133         provider.reload();
   134       }
   135     });
   136     return deferred.promise;
   137   },
   139   _updateWorkerState: function(enable) {
   140     [p.enabled = enable for (p of Social.providers) if (p.enabled != enable)];
   141   },
   143   // Called to update our cache of providers and set the current provider
   144   _updateProviderCache: function (providers) {
   145     this.providers = providers;
   146     Services.obs.notifyObservers(null, "social:providers-changed", null);
   147   },
   149   get enabled() {
   150     return !this._disabledForSafeMode && this.providers.length > 0;
   151   },
   153   toggleNotifications: function SocialNotifications_toggle() {
   154     let prefValue = Services.prefs.getBoolPref("social.toast-notifications.enabled");
   155     Services.prefs.setBoolPref("social.toast-notifications.enabled", !prefValue);
   156   },
   158   _getProviderFromOrigin: function (origin) {
   159     for (let p of this.providers) {
   160       if (p.origin == origin) {
   161         return p;
   162       }
   163     }
   164     return null;
   165   },
   167   getManifestByOrigin: function(origin) {
   168     return SocialService.getManifestByOrigin(origin);
   169   },
   171   installProvider: function(doc, data, installCallback) {
   172     SocialService.installProvider(doc, data, installCallback);
   173   },
   175   uninstallProvider: function(origin, aCallback) {
   176     SocialService.uninstallProvider(origin, aCallback);
   177   },
   179   // Activation functionality
   180   activateFromOrigin: function (origin, callback) {
   181     // For now only "builtin" providers can be activated.  It's OK if the
   182     // provider has already been activated - we still get called back with it.
   183     SocialService.addBuiltinProvider(origin, callback);
   184   },
   186   // Page Marking functionality
   187   isURIMarked: function(origin, aURI, aCallback) {
   188     promiseGetAnnotation(aURI).then(function(val) {
   189       if (val) {
   190         let providerList = JSON.parse(val);
   191         val = providerList.indexOf(origin) >= 0;
   192       }
   193       aCallback(!!val);
   194     }).then(null, Cu.reportError);
   195   },
   197   markURI: function(origin, aURI, aCallback) {
   198     // update or set our annotation
   199     promiseGetAnnotation(aURI).then(function(val) {
   201       let providerList = val ? JSON.parse(val) : [];
   202       let marked = providerList.indexOf(origin) >= 0;
   203       if (marked)
   204         return;
   205       providerList.push(origin);
   206       // we allow marking links in a page that may not have been visited yet.
   207       // make sure there is a history entry for the uri, then annotate it.
   208       let place = {
   209         uri: aURI,
   210         visits: [{
   211           visitDate: Date.now() + 1000,
   212           transitionType: Ci.nsINavHistoryService.TRANSITION_LINK
   213         }]
   214       };
   215       PlacesUtils.asyncHistory.updatePlaces(place, {
   216         handleError: function () Cu.reportError("couldn't update history for socialmark annotation"),
   217         handleResult: function () {},
   218         handleCompletion: function () {
   219           promiseSetAnnotation(aURI, providerList).then(function() {
   220             if (aCallback)
   221               schedule(function() { aCallback(true); } );
   222           }).then(null, Cu.reportError);
   223         }
   224       });
   225     }).then(null, Cu.reportError);
   226   },
   228   unmarkURI: function(origin, aURI, aCallback) {
   229     // this should not be called if this.provider or the port is null
   230     // set our annotation
   231     promiseGetAnnotation(aURI).then(function(val) {
   232       let providerList = val ? JSON.parse(val) : [];
   233       let marked = providerList.indexOf(origin) >= 0;
   234       if (marked) {
   235         // remove the annotation
   236         providerList.splice(providerList.indexOf(origin), 1);
   237         promiseSetAnnotation(aURI, providerList).then(function() {
   238           if (aCallback)
   239             schedule(function() { aCallback(false); } );
   240         }).then(null, Cu.reportError);
   241       }
   242     }).then(null, Cu.reportError);
   243   },
   245   setErrorListener: function(iframe, errorHandler) {
   246     if (iframe.socialErrorListener)
   247       return iframe.socialErrorListener;
   248     return new SocialErrorListener(iframe, errorHandler);
   249   }
   250 };
   252 function schedule(callback) {
   253   Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
   254 }
   256 function CreateSocialStatusWidget(aId, aProvider) {
   257   if (!aProvider.statusURL)
   258     return;
   259   let widget = CustomizableUI.getWidget(aId);
   260   // The widget is only null if we've created then destroyed the widget.
   261   // Once we've actually called createWidget the provider will be set to
   262   // PROVIDER_API.
   263   if (widget && widget.provider == CustomizableUI.PROVIDER_API)
   264     return;
   266   CustomizableUI.createWidget({
   267     id: aId,
   268     type: 'custom',
   269     removable: true,
   270     defaultArea: CustomizableUI.AREA_NAVBAR,
   271     onBuild: function(aDocument) {
   272       let node = aDocument.createElement('toolbarbutton');
   273       node.id = this.id;
   274       node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional social-status-button');
   275       node.setAttribute('type', "badged");
   276       node.style.listStyleImage = "url(" + (aProvider.icon32URL || aProvider.iconURL) + ")";
   277       node.setAttribute("origin", aProvider.origin);
   278       node.setAttribute("label", aProvider.name);
   279       node.setAttribute("tooltiptext", aProvider.name);
   280       node.setAttribute("oncommand", "SocialStatus.showPopup(this);");
   282       if (PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView))
   283         node.setAttribute("disabled", "true");
   285       return node;
   286     }
   287   });
   288 };
   290 function CreateSocialMarkWidget(aId, aProvider) {
   291   if (!aProvider.markURL)
   292     return;
   293   let widget = CustomizableUI.getWidget(aId);
   294   // The widget is only null if we've created then destroyed the widget.
   295   // Once we've actually called createWidget the provider will be set to
   296   // PROVIDER_API.
   297   if (widget && widget.provider == CustomizableUI.PROVIDER_API)
   298     return;
   300   CustomizableUI.createWidget({
   301     id: aId,
   302     type: 'custom',
   303     removable: true,
   304     defaultArea: CustomizableUI.AREA_NAVBAR,
   305     onBuild: function(aDocument) {
   306       let node = aDocument.createElement('toolbarbutton');
   307       node.id = this.id;
   308       node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional social-mark-button');
   309       node.setAttribute('type', "socialmark");
   310       node.style.listStyleImage = "url(" + (aProvider.unmarkedIcon || aProvider.icon32URL || aProvider.iconURL) + ")";
   311       node.setAttribute("origin", aProvider.origin);
   312       node.setAttribute("oncommand", "this.markCurrentPage();");
   314       let window = aDocument.defaultView;
   315       let menuLabel = window.gNavigatorBundle.getFormattedString("social.markpageMenu.label", [aProvider.name]);
   316       node.setAttribute("label", menuLabel);
   317       node.setAttribute("tooltiptext", menuLabel);
   319       return node;
   320     }
   321   });
   322 };
   324 // Error handling class used to listen for network errors in the social frames
   325 // and replace them with a social-specific error page
   326 function SocialErrorListener(iframe, errorHandler) {
   327   this.setErrorMessage = errorHandler;
   328   this.iframe = iframe;
   329   iframe.socialErrorListener = this;
   330   iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
   331                                    .getInterface(Ci.nsIWebProgress)
   332                                    .addProgressListener(this,
   333                                                         Ci.nsIWebProgress.NOTIFY_STATE_REQUEST |
   334                                                         Ci.nsIWebProgress.NOTIFY_LOCATION);
   335 }
   337 SocialErrorListener.prototype = {
   338   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
   339                                          Ci.nsISupportsWeakReference,
   340                                          Ci.nsISupports]),
   342   remove: function() {
   343     this.iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
   344                                      .getInterface(Ci.nsIWebProgress)
   345                                      .removeProgressListener(this);
   346     delete this.iframe.socialErrorListener;
   347   },
   349   onStateChange: function SPL_onStateChange(aWebProgress, aRequest, aState, aStatus) {
   350     let failure = false;
   351     if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
   352       if (aRequest instanceof Ci.nsIHttpChannel) {
   353         try {
   354           // Change the frame to an error page on 4xx (client errors)
   355           // and 5xx (server errors)
   356           failure = aRequest.responseStatus >= 400 &&
   357                     aRequest.responseStatus < 600;
   358         } catch (e) {}
   359       }
   360     }
   362     // Calling cancel() will raise some OnStateChange notifications by itself,
   363     // so avoid doing that more than once
   364     if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
   365       aRequest.cancel(Components.results.NS_BINDING_ABORTED);
   366       let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin"));
   367       provider.errorState = "content-error";
   368       this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
   369                               .chromeEventHandler);
   370     }
   371   },
   373   onLocationChange: function SPL_onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
   374     if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
   375       aRequest.cancel(Components.results.NS_BINDING_ABORTED);
   376       let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin"));
   377       if (!provider.errorState)
   378         provider.errorState = "content-error";
   379       schedule(function() {
   380         this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
   381                               .chromeEventHandler);
   382       }.bind(this));
   383     }
   384   },
   386   onProgressChange: function SPL_onProgressChange() {},
   387   onStatusChange: function SPL_onStatusChange() {},
   388   onSecurityChange: function SPL_onSecurityChange() {},
   389 };
   392 function sizeSocialPanelToContent(panel, iframe) {
   393   let doc = iframe.contentDocument;
   394   if (!doc || !doc.body) {
   395     return;
   396   }
   397   // We need an element to use for sizing our panel.  See if the body defines
   398   // an id for that element, otherwise use the body itself.
   399   let body = doc.body;
   400   let bodyId = body.getAttribute("contentid");
   401   if (bodyId) {
   402     body = doc.getElementById(bodyId) || doc.body;
   403   }
   404   // offsetHeight/Width don't include margins, so account for that.
   405   let cs = doc.defaultView.getComputedStyle(body);
   406   let width = PANEL_MIN_WIDTH;
   407   let height = PANEL_MIN_HEIGHT;
   408   // if the panel is preloaded prior to being shown, cs will be null.  in that
   409   // case use the minimum size for the panel until it is shown.
   410   if (cs) {
   411     let computedHeight = parseInt(cs.marginTop) + body.offsetHeight + parseInt(cs.marginBottom);
   412     height = Math.max(computedHeight, height);
   413     let computedWidth = parseInt(cs.marginLeft) + body.offsetWidth + parseInt(cs.marginRight);
   414     width = Math.max(computedWidth, width);
   415   }
   416   iframe.style.width = width + "px";
   417   iframe.style.height = height + "px";
   418   // since we do not use panel.sizeTo, we need to adjust the arrow ourselves
   419   if (panel.state == "open")
   420     panel.adjustArrowPosition();
   421 }
   423 function DynamicResizeWatcher() {
   424   this._mutationObserver = null;
   425 }
   427 DynamicResizeWatcher.prototype = {
   428   start: function DynamicResizeWatcher_start(panel, iframe) {
   429     this.stop(); // just in case...
   430     let doc = iframe.contentDocument;
   431     this._mutationObserver = new iframe.contentWindow.MutationObserver(function(mutations) {
   432       sizeSocialPanelToContent(panel, iframe);
   433     });
   434     // Observe anything that causes the size to change.
   435     let config = {attributes: true, characterData: true, childList: true, subtree: true};
   436     this._mutationObserver.observe(doc, config);
   437     // and since this may be setup after the load event has fired we do an
   438     // initial resize now.
   439     sizeSocialPanelToContent(panel, iframe);
   440   },
   441   stop: function DynamicResizeWatcher_stop() {
   442     if (this._mutationObserver) {
   443       try {
   444         this._mutationObserver.disconnect();
   445       } catch (ex) {
   446         // may get "TypeError: can't access dead object" which seems strange,
   447         // but doesn't seem to indicate a real problem, so ignore it...
   448       }
   449       this._mutationObserver = null;
   450     }
   451   }
   452 }
   455 this.OpenGraphBuilder = {
   456   generateEndpointURL: function(URLTemplate, pageData) {
   457     // support for existing oexchange style endpoints by supporting their
   458     // querystring arguments. parse the query string template and do
   459     // replacements where necessary the query names may be different than ours,
   460     // so we could see u=%{url} or url=%{url}
   461     let [endpointURL, queryString] = URLTemplate.split("?");
   462     let query = {};
   463     if (queryString) {
   464       queryString.split('&').forEach(function (val) {
   465         let [name, value] = val.split('=');
   466         let p = /%\{(.+)\}/.exec(value);
   467         if (!p) {
   468           // preserve non-template query vars
   469           query[name] = value;
   470         } else if (pageData[p[1]]) {
   471           query[name] = pageData[p[1]];
   472         } else if (p[1] == "body") {
   473           // build a body for emailers
   474           let body = "";
   475           if (pageData.title)
   476             body += pageData.title + "\n\n";
   477           if (pageData.description)
   478             body += pageData.description + "\n\n";
   479           if (pageData.text)
   480             body += pageData.text + "\n\n";
   481           body += pageData.url;
   482           query["body"] = body;
   483         }
   484       });
   485     }
   486     var str = [];
   487     for (let p in query)
   488        str.push(p + "=" + encodeURIComponent(query[p]));
   489     if (str.length)
   490       endpointURL = endpointURL + "?" + str.join("&");
   491     return endpointURL;
   492   },
   494   getData: function(browser) {
   495     let res = {
   496       url: this._validateURL(browser, browser.currentURI.spec),
   497       title: browser.contentDocument.title,
   498       previews: []
   499     };
   500     this._getMetaData(browser, res);
   501     this._getLinkData(browser, res);
   502     this._getPageData(browser, res);
   503     return res;
   504   },
   506   _getMetaData: function(browser, o) {
   507     // query for standardized meta data
   508     let els = browser.contentDocument
   509                   .querySelectorAll("head > meta[property], head > meta[name]");
   510     if (els.length < 1)
   511       return;
   512     let url;
   513     for (let el of els) {
   514       let value = el.getAttribute("content")
   515       if (!value)
   516         continue;
   517       value = unescapeService.unescape(value.trim());
   518       switch (el.getAttribute("property") || el.getAttribute("name")) {
   519         case "title":
   520         case "og:title":
   521           o.title = value;
   522           break;
   523         case "description":
   524         case "og:description":
   525           o.description = value;
   526           break;
   527         case "og:site_name":
   528           o.siteName = value;
   529           break;
   530         case "medium":
   531         case "og:type":
   532           o.medium = value;
   533           break;
   534         case "og:video":
   535           url = this._validateURL(browser, value);
   536           if (url)
   537             o.source = url;
   538           break;
   539         case "og:url":
   540           url = this._validateURL(browser, value);
   541           if (url)
   542             o.url = url;
   543           break;
   544         case "og:image":
   545           url = this._validateURL(browser, value);
   546           if (url)
   547             o.previews.push(url);
   548           break;
   549       }
   550     }
   551   },
   553   _getLinkData: function(browser, o) {
   554     let els = browser.contentDocument
   555                   .querySelectorAll("head > link[rel], head > link[id]");
   556     for (let el of els) {
   557       let url = el.getAttribute("href");
   558       if (!url)
   559         continue;
   560       url = this._validateURL(browser, unescapeService.unescape(url.trim()));
   561       switch (el.getAttribute("rel") || el.getAttribute("id")) {
   562         case "shorturl":
   563         case "shortlink":
   564           o.shortUrl = url;
   565           break;
   566         case "canonicalurl":
   567         case "canonical":
   568           o.url = url;
   569           break;
   570         case "image_src":
   571           o.previews.push(url);
   572           break;
   573       }
   574     }
   575   },
   577   // scrape through the page for data we want
   578   _getPageData: function(browser, o) {
   579     if (o.previews.length < 1)
   580       o.previews = this._getImageUrls(browser);
   581   },
   583   _validateURL: function(browser, url) {
   584     let uri = Services.io.newURI(browser.currentURI.resolve(url), null, null);
   585     if (["http", "https", "ftp", "ftps"].indexOf(uri.scheme) < 0)
   586       return null;
   587     uri.userPass = "";
   588     return uri.spec;
   589   },
   591   _getImageUrls: function(browser) {
   592     let l = [];
   593     let els = browser.contentDocument.querySelectorAll("img");
   594     for (let el of els) {
   595       let content = el.getAttribute("src");
   596       if (content) {
   597         l.push(this._validateURL(browser, unescapeService.unescape(content)));
   598         // we don't want a billion images
   599         if (l.length > 5)
   600           break;
   601       }
   602     }
   603     return l;
   604   }
   605 };

mercurial