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