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/. */
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, minimum size necessary to
16 // properly show the error page in the panel.
17 const PANEL_MIN_HEIGHT = 190;
18 const PANEL_MIN_WIDTH = 330;
20 Cu.import("resource://gre/modules/Services.jsm");
21 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
23 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
24 "resource:///modules/CustomizableUI.jsm");
25 XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
26 "resource://gre/modules/SocialService.jsm");
27 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
28 "resource://gre/modules/PlacesUtils.jsm");
29 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
30 "resource://gre/modules/PrivateBrowsingUtils.jsm");
31 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
32 "resource://gre/modules/Promise.jsm");
34 XPCOMUtils.defineLazyServiceGetter(this, "unescapeService",
35 "@mozilla.org/feed-unescapehtml;1",
36 "nsIScriptableUnescapeHTML");
38 function promiseSetAnnotation(aURI, providerList) {
39 let deferred = Promise.defer();
41 // Delaying to catch issues with asynchronous behavior while waiting
42 // to implement asynchronous annotations in bug 699844.
43 Services.tm.mainThread.dispatch(function() {
45 if (providerList && providerList.length > 0) {
46 PlacesUtils.annotations.setPageAnnotation(
47 aURI, "social/mark", JSON.stringify(providerList), 0,
48 PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
50 PlacesUtils.annotations.removePageAnnotation(aURI, "social/mark");
53 Cu.reportError("SocialAnnotation failed: " + e);
56 }, Ci.nsIThread.DISPATCH_NORMAL);
58 return deferred.promise;
61 function promiseGetAnnotation(aURI) {
62 let deferred = Promise.defer();
64 // Delaying to catch issues with asynchronous behavior while waiting
65 // to implement asynchronous annotations in bug 699844.
66 Services.tm.mainThread.dispatch(function() {
69 val = PlacesUtils.annotations.getPageAnnotation(aURI, "social/mark");
72 deferred.resolve(val);
73 }, Ci.nsIThread.DISPATCH_NORMAL);
75 return deferred.promise;
82 _disabledForSafeMode: false,
84 init: function Social_init() {
85 this._disabledForSafeMode = Services.appinfo.inSafeMode && this.enabled;
86 let deferred = Promise.defer();
88 if (this.initialized) {
89 deferred.resolve(true);
90 return deferred.promise;
92 this.initialized = true;
93 // if SocialService.hasEnabledProviders, retreive the providers so the
94 // front-end can generate UI
95 if (SocialService.hasEnabledProviders) {
96 // Retrieve the current set of providers, and set the current provider.
97 SocialService.getOrderedProviderList(function (providers) {
98 Social._updateProviderCache(providers);
99 Social._updateWorkerState(SocialService.enabled);
100 deferred.resolve(false);
103 deferred.resolve(false);
106 // Register an observer for changes to the provider list
107 SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
108 // An engine change caused by adding/removing a provider should notify.
109 // any providers we receive are enabled in the AddonsManager
110 if (topic == "provider-installed" || topic == "provider-uninstalled") {
111 // installed/uninstalled do not send the providers param
112 Services.obs.notifyObservers(null, "social:" + topic, origin);
115 if (topic == "provider-enabled") {
116 Social._updateProviderCache(providers);
117 Social._updateWorkerState(true);
118 Services.obs.notifyObservers(null, "social:" + topic, origin);
121 if (topic == "provider-disabled") {
122 // a provider was removed from the list of providers, that does not
123 // affect worker state for other providers
124 Social._updateProviderCache(providers);
125 Social._updateWorkerState(providers.length > 0);
126 Services.obs.notifyObservers(null, "social:" + topic, origin);
129 if (topic == "provider-update") {
130 // a provider has self-updated its manifest, we need to update our cache
131 // and reload the provider.
132 Social._updateProviderCache(providers);
133 let provider = Social._getProviderFromOrigin(origin);
137 return deferred.promise;
140 _updateWorkerState: function(enable) {
141 [p.enabled = enable for (p of Social.providers) if (p.enabled != enable)];
144 // Called to update our cache of providers and set the current provider
145 _updateProviderCache: function (providers) {
146 this.providers = providers;
147 Services.obs.notifyObservers(null, "social:providers-changed", null);
151 return !this._disabledForSafeMode && this.providers.length > 0;
154 toggleNotifications: function SocialNotifications_toggle() {
155 let prefValue = Services.prefs.getBoolPref("social.toast-notifications.enabled");
156 Services.prefs.setBoolPref("social.toast-notifications.enabled", !prefValue);
159 _getProviderFromOrigin: function (origin) {
160 for (let p of this.providers) {
161 if (p.origin == origin) {
168 getManifestByOrigin: function(origin) {
169 return SocialService.getManifestByOrigin(origin);
172 installProvider: function(data, installCallback, options={}) {
173 SocialService.installProvider(data, installCallback, options);
176 uninstallProvider: function(origin, aCallback) {
177 SocialService.uninstallProvider(origin, aCallback);
180 // Activation functionality
181 activateFromOrigin: function (origin, callback) {
182 // It's OK if the provider has already been activated - we still get called
184 SocialService.enableProvider(origin, callback);
187 // Page Marking functionality
188 isURIMarked: function(origin, aURI, aCallback) {
189 promiseGetAnnotation(aURI).then(function(val) {
191 let providerList = JSON.parse(val);
192 val = providerList.indexOf(origin) >= 0;
195 }).then(null, Cu.reportError);
198 markURI: function(origin, aURI, aCallback) {
199 // update or set our annotation
200 promiseGetAnnotation(aURI).then(function(val) {
202 let providerList = val ? JSON.parse(val) : [];
203 let marked = providerList.indexOf(origin) >= 0;
206 providerList.push(origin);
207 // we allow marking links in a page that may not have been visited yet.
208 // make sure there is a history entry for the uri, then annotate it.
212 visitDate: Date.now() + 1000,
213 transitionType: Ci.nsINavHistoryService.TRANSITION_LINK
216 PlacesUtils.asyncHistory.updatePlaces(place, {
217 handleError: function () Cu.reportError("couldn't update history for socialmark annotation"),
218 handleResult: function () {},
219 handleCompletion: function () {
220 promiseSetAnnotation(aURI, providerList).then(function() {
222 schedule(function() { aCallback(true); } );
223 }).then(null, Cu.reportError);
226 }).then(null, Cu.reportError);
229 unmarkURI: function(origin, aURI, aCallback) {
230 // this should not be called if this.provider or the port is null
231 // set our annotation
232 promiseGetAnnotation(aURI).then(function(val) {
233 let providerList = val ? JSON.parse(val) : [];
234 let marked = providerList.indexOf(origin) >= 0;
236 // remove the annotation
237 providerList.splice(providerList.indexOf(origin), 1);
238 promiseSetAnnotation(aURI, providerList).then(function() {
240 schedule(function() { aCallback(false); } );
241 }).then(null, Cu.reportError);
243 }).then(null, Cu.reportError);
246 setErrorListener: function(iframe, errorHandler) {
247 if (iframe.socialErrorListener)
248 return iframe.socialErrorListener;
249 return new SocialErrorListener(iframe, errorHandler);
253 function schedule(callback) {
254 Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
257 function CreateSocialStatusWidget(aId, aProvider) {
258 if (!aProvider.statusURL)
260 let widget = CustomizableUI.getWidget(aId);
261 // The widget is only null if we've created then destroyed the widget.
262 // Once we've actually called createWidget the provider will be set to
264 if (widget && widget.provider == CustomizableUI.PROVIDER_API)
267 CustomizableUI.createWidget({
271 defaultArea: CustomizableUI.AREA_NAVBAR,
272 onBuild: function(aDocument) {
273 let node = aDocument.createElement('toolbarbutton');
275 node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional social-status-button badged-button');
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");
290 function CreateSocialMarkWidget(aId, aProvider) {
291 if (!aProvider.markURL)
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
297 if (widget && widget.provider == CustomizableUI.PROVIDER_API)
300 CustomizableUI.createWidget({
304 defaultArea: CustomizableUI.AREA_NAVBAR,
305 onBuild: function(aDocument) {
306 let node = aDocument.createElement('toolbarbutton');
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);
313 let window = aDocument.defaultView;
314 let menuLabel = window.gNavigatorBundle.getFormattedString("social.markpageMenu.label", [aProvider.name]);
315 node.setAttribute("label", menuLabel);
316 node.setAttribute("tooltiptext", menuLabel);
317 node.setAttribute("observes", "Social:PageShareOrMark");
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 // Force a layout flush by calling .clientTop so that the docShell of this
331 // frame is created for the error listener
333 iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
334 .getInterface(Ci.nsIWebProgress)
335 .addProgressListener(this,
336 Ci.nsIWebProgress.NOTIFY_STATE_REQUEST |
337 Ci.nsIWebProgress.NOTIFY_LOCATION);
340 SocialErrorListener.prototype = {
341 QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
342 Ci.nsISupportsWeakReference,
346 this.iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
347 .getInterface(Ci.nsIWebProgress)
348 .removeProgressListener(this);
349 delete this.iframe.socialErrorListener;
352 onStateChange: function SPL_onStateChange(aWebProgress, aRequest, aState, aStatus) {
354 if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
355 if (aRequest instanceof Ci.nsIHttpChannel) {
357 // Change the frame to an error page on 4xx (client errors)
358 // and 5xx (server errors). responseStatus throws if it is not set.
359 failure = aRequest.responseStatus >= 400 &&
360 aRequest.responseStatus < 600;
362 failure = aStatus == Components.results.NS_ERROR_CONNECTION_REFUSED;
367 // Calling cancel() will raise some OnStateChange notifications by itself,
368 // so avoid doing that more than once
369 if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
370 aRequest.cancel(Components.results.NS_BINDING_ABORTED);
371 let origin = this.iframe.getAttribute("origin");
373 let provider = Social._getProviderFromOrigin(origin);
374 provider.errorState = "content-error";
376 this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
377 .chromeEventHandler);
381 onLocationChange: function SPL_onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
382 if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
383 aRequest.cancel(Components.results.NS_BINDING_ABORTED);
384 let origin = this.iframe.getAttribute("origin");
386 let provider = Social._getProviderFromOrigin(origin);
387 if (!provider.errorState)
388 provider.errorState = "content-error";
390 schedule(function() {
391 this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
392 .chromeEventHandler);
397 onProgressChange: function SPL_onProgressChange() {},
398 onStatusChange: function SPL_onStatusChange() {},
399 onSecurityChange: function SPL_onSecurityChange() {},
403 function sizeSocialPanelToContent(panel, iframe, requestedSize) {
404 let doc = iframe.contentDocument;
405 if (!doc || !doc.body) {
408 // We need an element to use for sizing our panel. See if the body defines
409 // an id for that element, otherwise use the body itself.
411 let docEl = doc.documentElement;
412 let bodyId = body.getAttribute("contentid");
414 body = doc.getElementById(bodyId) || doc.body;
416 // offsetHeight/Width don't include margins, so account for that.
417 let cs = doc.defaultView.getComputedStyle(body);
418 let width = Math.max(PANEL_MIN_WIDTH, docEl.offsetWidth);
419 let height = Math.max(PANEL_MIN_HEIGHT, docEl.offsetHeight);
420 // if the panel is preloaded prior to being shown, cs will be null. in that
421 // case use the minimum size for the panel until it is shown.
423 let computedHeight = parseInt(cs.marginTop) + body.offsetHeight + parseInt(cs.marginBottom);
424 height = Math.max(computedHeight, height);
425 let computedWidth = parseInt(cs.marginLeft) + body.offsetWidth + parseInt(cs.marginRight);
426 width = Math.max(computedWidth, width);
429 // if our scrollHeight is still larger than the iframe, the css calculations
430 // above did not work for this site, increase the height. This can happen if
431 // the site increases its height for additional UI.
432 if (docEl.scrollHeight > iframe.boxObject.height)
433 height = docEl.scrollHeight;
435 // if a size was defined in the manifest use it as a minimum
437 if (requestedSize.height)
438 height = Math.max(height, requestedSize.height);
439 if (requestedSize.width)
440 width = Math.max(width, requestedSize.width);
443 // add the extra space used by the panel (toolbar, borders, etc) if the iframe
445 if (iframe.boxObject.width && iframe.boxObject.height) {
446 // add extra space the panel needs if any
447 width += panel.boxObject.width - iframe.boxObject.width;
448 height += panel.boxObject.height - iframe.boxObject.height;
451 // using panel.sizeTo will ignore css transitions, set size via style
452 if (Math.abs(panel.boxObject.width - width) >= 2)
453 panel.style.width = width + "px";
454 if (Math.abs(panel.boxObject.height - height) >= 2)
455 panel.style.height = height + "px";
458 function DynamicResizeWatcher() {
459 this._mutationObserver = null;
462 DynamicResizeWatcher.prototype = {
463 start: function DynamicResizeWatcher_start(panel, iframe, requestedSize) {
464 this.stop(); // just in case...
465 let doc = iframe.contentDocument;
466 this._mutationObserver = new iframe.contentWindow.MutationObserver((mutations) => {
467 sizeSocialPanelToContent(panel, iframe, requestedSize);
469 // Observe anything that causes the size to change.
470 let config = {attributes: true, characterData: true, childList: true, subtree: true};
471 this._mutationObserver.observe(doc, config);
472 // and since this may be setup after the load event has fired we do an
473 // initial resize now.
474 sizeSocialPanelToContent(panel, iframe, requestedSize);
476 stop: function DynamicResizeWatcher_stop() {
477 if (this._mutationObserver) {
479 this._mutationObserver.disconnect();
481 // may get "TypeError: can't access dead object" which seems strange,
482 // but doesn't seem to indicate a real problem, so ignore it...
484 this._mutationObserver = null;
490 this.OpenGraphBuilder = {
491 generateEndpointURL: function(URLTemplate, pageData) {
492 // support for existing oexchange style endpoints by supporting their
493 // querystring arguments. parse the query string template and do
494 // replacements where necessary the query names may be different than ours,
495 // so we could see u=%{url} or url=%{url}
496 let [endpointURL, queryString] = URLTemplate.split("?");
499 queryString.split('&').forEach(function (val) {
500 let [name, value] = val.split('=');
501 let p = /%\{(.+)\}/.exec(value);
503 // preserve non-template query vars
505 } else if (pageData[p[1]]) {
506 if (p[1] == "previews")
507 query[name] = pageData[p[1]][0];
509 query[name] = pageData[p[1]];
510 } else if (p[1] == "body") {
511 // build a body for emailers
514 body += pageData.title + "\n\n";
515 if (pageData.description)
516 body += pageData.description + "\n\n";
518 body += pageData.text + "\n\n";
519 body += pageData.url;
520 query["body"] = body;
526 str.push(p + "=" + encodeURIComponent(query[p]));
528 endpointURL = endpointURL + "?" + str.join("&");
532 getData: function(aDocument, target) {
534 url: this._validateURL(aDocument, aDocument.documentURI),
535 title: aDocument.title,
538 this._getMetaData(aDocument, res);
539 this._getLinkData(aDocument, res);
540 this._getPageData(aDocument, res);
541 res.microdata = this.getMicrodata(aDocument, target);
545 getMicrodata: function (aDocument, target) {
546 return getMicrodata(aDocument, target);
549 _getMetaData: function(aDocument, o) {
550 // query for standardized meta data
551 let els = aDocument.querySelectorAll("head > meta[property], head > meta[name]");
555 for (let el of els) {
556 let value = el.getAttribute("content")
559 value = unescapeService.unescape(value.trim());
560 let key = el.getAttribute("property") || el.getAttribute("name");
563 // There are a wide array of possible meta tags, expressing articles,
564 // products, etc. so all meta tags are passed through but we touch up the
565 // most common attributes.
573 case "og:description":
574 o.description = value;
584 url = this._validateURL(aDocument, value);
589 url = this._validateURL(aDocument, value);
594 url = this._validateURL(aDocument, value);
596 o.previews.push(url);
602 _getLinkData: function(aDocument, o) {
603 let els = aDocument.querySelectorAll("head > link[rel], head > link[id]");
604 for (let el of els) {
605 let url = el.getAttribute("href");
608 url = this._validateURL(aDocument, unescapeService.unescape(url.trim()));
609 switch (el.getAttribute("rel") || el.getAttribute("id")) {
619 o.previews.push(url);
622 // expressly for oembed support but we're liberal here and will let
623 // other alternate links through. oembed defines an href, supplied by
624 // the site, where you can fetch additional meta data about a page.
625 // We'll let the client fetch the oembed data themselves, but they
626 // need the data from this link.
630 "type": el.getAttribute("type"),
631 "href": el.getAttribute("href"),
632 "title": el.getAttribute("title")
638 // scrape through the page for data we want
639 _getPageData: function(aDocument, o) {
640 if (o.previews.length < 1)
641 o.previews = this._getImageUrls(aDocument);
644 _validateURL: function(aDocument, url) {
645 let docURI = Services.io.newURI(aDocument.documentURI, null, null);
646 let uri = Services.io.newURI(docURI.resolve(url), null, null);
647 if (["http", "https", "ftp", "ftps"].indexOf(uri.scheme) < 0)
653 _getImageUrls: function(aDocument) {
655 let els = aDocument.querySelectorAll("img");
656 for (let el of els) {
657 let src = el.getAttribute("src");
659 l.push(this._validateURL(aDocument, unescapeService.unescape(src)));
660 // we don't want a billion images
669 // getMicrodata (and getObject) based on wg algorythm to convert microdata to json
670 // http://www.whatwg.org/specs/web-apps/current-work/multipage/microdata-2.html#json
671 function getMicrodata(document, target) {
673 function _getObject(item) {
675 if (item.itemType.length)
676 result.types = [i for (i of item.itemType)];
678 result.itemId = item.itemid;
679 if (item.properties.length)
680 result.properties = {};
681 for (let elem of item.properties) {
684 value = _getObject(elem);
685 else if (elem.itemValue)
686 value = elem.itemValue;
687 // handle mis-formatted microdata
688 else if (elem.hasAttribute("content"))
689 value = elem.getAttribute("content");
691 for (let prop of elem.itemProp) {
692 if (!result.properties[prop])
693 result.properties[prop] = [];
694 result.properties[prop].push(value);
700 let result = { items: [] };
701 let elms = target ? [target] : document.getItems();
702 for (let el of elms) {
704 result.items.push(_getObject(el));