1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/modules/Social.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,605 @@ 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 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["Social", "CreateSocialStatusWidget", 1.11 + "CreateSocialMarkWidget", "OpenGraphBuilder", 1.12 + "DynamicResizeWatcher", "sizeSocialPanelToContent"]; 1.13 + 1.14 +const Ci = Components.interfaces; 1.15 +const Cc = Components.classes; 1.16 +const Cu = Components.utils; 1.17 + 1.18 +// The minimum sizes for the auto-resize panel code. 1.19 +const PANEL_MIN_HEIGHT = 100; 1.20 +const PANEL_MIN_WIDTH = 330; 1.21 + 1.22 +Cu.import("resource://gre/modules/Services.jsm"); 1.23 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.24 + 1.25 +XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", 1.26 + "resource:///modules/CustomizableUI.jsm"); 1.27 +XPCOMUtils.defineLazyModuleGetter(this, "SocialService", 1.28 + "resource://gre/modules/SocialService.jsm"); 1.29 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", 1.30 + "resource://gre/modules/PlacesUtils.jsm"); 1.31 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", 1.32 + "resource://gre/modules/PrivateBrowsingUtils.jsm"); 1.33 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.34 + "resource://gre/modules/Promise.jsm"); 1.35 + 1.36 +XPCOMUtils.defineLazyServiceGetter(this, "unescapeService", 1.37 + "@mozilla.org/feed-unescapehtml;1", 1.38 + "nsIScriptableUnescapeHTML"); 1.39 + 1.40 +function promiseSetAnnotation(aURI, providerList) { 1.41 + let deferred = Promise.defer(); 1.42 + 1.43 + // Delaying to catch issues with asynchronous behavior while waiting 1.44 + // to implement asynchronous annotations in bug 699844. 1.45 + Services.tm.mainThread.dispatch(function() { 1.46 + try { 1.47 + if (providerList && providerList.length > 0) { 1.48 + PlacesUtils.annotations.setPageAnnotation( 1.49 + aURI, "social/mark", JSON.stringify(providerList), 0, 1.50 + PlacesUtils.annotations.EXPIRE_WITH_HISTORY); 1.51 + } else { 1.52 + PlacesUtils.annotations.removePageAnnotation(aURI, "social/mark"); 1.53 + } 1.54 + } catch(e) { 1.55 + Cu.reportError("SocialAnnotation failed: " + e); 1.56 + } 1.57 + deferred.resolve(); 1.58 + }, Ci.nsIThread.DISPATCH_NORMAL); 1.59 + 1.60 + return deferred.promise; 1.61 +} 1.62 + 1.63 +function promiseGetAnnotation(aURI) { 1.64 + let deferred = Promise.defer(); 1.65 + 1.66 + // Delaying to catch issues with asynchronous behavior while waiting 1.67 + // to implement asynchronous annotations in bug 699844. 1.68 + Services.tm.mainThread.dispatch(function() { 1.69 + let val = null; 1.70 + try { 1.71 + val = PlacesUtils.annotations.getPageAnnotation(aURI, "social/mark"); 1.72 + } catch (ex) { } 1.73 + 1.74 + deferred.resolve(val); 1.75 + }, Ci.nsIThread.DISPATCH_NORMAL); 1.76 + 1.77 + return deferred.promise; 1.78 +} 1.79 + 1.80 +this.Social = { 1.81 + initialized: false, 1.82 + lastEventReceived: 0, 1.83 + providers: [], 1.84 + _disabledForSafeMode: false, 1.85 + 1.86 + init: function Social_init() { 1.87 + this._disabledForSafeMode = Services.appinfo.inSafeMode && this.enabled; 1.88 + let deferred = Promise.defer(); 1.89 + 1.90 + if (this.initialized) { 1.91 + deferred.resolve(true); 1.92 + return deferred.promise; 1.93 + } 1.94 + this.initialized = true; 1.95 + // if SocialService.hasEnabledProviders, retreive the providers so the 1.96 + // front-end can generate UI 1.97 + if (SocialService.hasEnabledProviders) { 1.98 + // Retrieve the current set of providers, and set the current provider. 1.99 + SocialService.getOrderedProviderList(function (providers) { 1.100 + Social._updateProviderCache(providers); 1.101 + Social._updateWorkerState(SocialService.enabled); 1.102 + deferred.resolve(false); 1.103 + }); 1.104 + } else { 1.105 + deferred.resolve(false); 1.106 + } 1.107 + 1.108 + // Register an observer for changes to the provider list 1.109 + SocialService.registerProviderListener(function providerListener(topic, origin, providers) { 1.110 + // An engine change caused by adding/removing a provider should notify. 1.111 + // any providers we receive are enabled in the AddonsManager 1.112 + if (topic == "provider-installed" || topic == "provider-uninstalled") { 1.113 + // installed/uninstalled do not send the providers param 1.114 + Services.obs.notifyObservers(null, "social:" + topic, origin); 1.115 + return; 1.116 + } 1.117 + if (topic == "provider-enabled") { 1.118 + Social._updateProviderCache(providers); 1.119 + Social._updateWorkerState(true); 1.120 + Services.obs.notifyObservers(null, "social:" + topic, origin); 1.121 + return; 1.122 + } 1.123 + if (topic == "provider-disabled") { 1.124 + // a provider was removed from the list of providers, that does not 1.125 + // affect worker state for other providers 1.126 + Social._updateProviderCache(providers); 1.127 + Social._updateWorkerState(providers.length > 0); 1.128 + Services.obs.notifyObservers(null, "social:" + topic, origin); 1.129 + return; 1.130 + } 1.131 + if (topic == "provider-update") { 1.132 + // a provider has self-updated its manifest, we need to update our cache 1.133 + // and reload the provider. 1.134 + Social._updateProviderCache(providers); 1.135 + let provider = Social._getProviderFromOrigin(origin); 1.136 + provider.reload(); 1.137 + } 1.138 + }); 1.139 + return deferred.promise; 1.140 + }, 1.141 + 1.142 + _updateWorkerState: function(enable) { 1.143 + [p.enabled = enable for (p of Social.providers) if (p.enabled != enable)]; 1.144 + }, 1.145 + 1.146 + // Called to update our cache of providers and set the current provider 1.147 + _updateProviderCache: function (providers) { 1.148 + this.providers = providers; 1.149 + Services.obs.notifyObservers(null, "social:providers-changed", null); 1.150 + }, 1.151 + 1.152 + get enabled() { 1.153 + return !this._disabledForSafeMode && this.providers.length > 0; 1.154 + }, 1.155 + 1.156 + toggleNotifications: function SocialNotifications_toggle() { 1.157 + let prefValue = Services.prefs.getBoolPref("social.toast-notifications.enabled"); 1.158 + Services.prefs.setBoolPref("social.toast-notifications.enabled", !prefValue); 1.159 + }, 1.160 + 1.161 + _getProviderFromOrigin: function (origin) { 1.162 + for (let p of this.providers) { 1.163 + if (p.origin == origin) { 1.164 + return p; 1.165 + } 1.166 + } 1.167 + return null; 1.168 + }, 1.169 + 1.170 + getManifestByOrigin: function(origin) { 1.171 + return SocialService.getManifestByOrigin(origin); 1.172 + }, 1.173 + 1.174 + installProvider: function(doc, data, installCallback) { 1.175 + SocialService.installProvider(doc, data, installCallback); 1.176 + }, 1.177 + 1.178 + uninstallProvider: function(origin, aCallback) { 1.179 + SocialService.uninstallProvider(origin, aCallback); 1.180 + }, 1.181 + 1.182 + // Activation functionality 1.183 + activateFromOrigin: function (origin, callback) { 1.184 + // For now only "builtin" providers can be activated. It's OK if the 1.185 + // provider has already been activated - we still get called back with it. 1.186 + SocialService.addBuiltinProvider(origin, callback); 1.187 + }, 1.188 + 1.189 + // Page Marking functionality 1.190 + isURIMarked: function(origin, aURI, aCallback) { 1.191 + promiseGetAnnotation(aURI).then(function(val) { 1.192 + if (val) { 1.193 + let providerList = JSON.parse(val); 1.194 + val = providerList.indexOf(origin) >= 0; 1.195 + } 1.196 + aCallback(!!val); 1.197 + }).then(null, Cu.reportError); 1.198 + }, 1.199 + 1.200 + markURI: function(origin, aURI, aCallback) { 1.201 + // update or set our annotation 1.202 + promiseGetAnnotation(aURI).then(function(val) { 1.203 + 1.204 + let providerList = val ? JSON.parse(val) : []; 1.205 + let marked = providerList.indexOf(origin) >= 0; 1.206 + if (marked) 1.207 + return; 1.208 + providerList.push(origin); 1.209 + // we allow marking links in a page that may not have been visited yet. 1.210 + // make sure there is a history entry for the uri, then annotate it. 1.211 + let place = { 1.212 + uri: aURI, 1.213 + visits: [{ 1.214 + visitDate: Date.now() + 1000, 1.215 + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK 1.216 + }] 1.217 + }; 1.218 + PlacesUtils.asyncHistory.updatePlaces(place, { 1.219 + handleError: function () Cu.reportError("couldn't update history for socialmark annotation"), 1.220 + handleResult: function () {}, 1.221 + handleCompletion: function () { 1.222 + promiseSetAnnotation(aURI, providerList).then(function() { 1.223 + if (aCallback) 1.224 + schedule(function() { aCallback(true); } ); 1.225 + }).then(null, Cu.reportError); 1.226 + } 1.227 + }); 1.228 + }).then(null, Cu.reportError); 1.229 + }, 1.230 + 1.231 + unmarkURI: function(origin, aURI, aCallback) { 1.232 + // this should not be called if this.provider or the port is null 1.233 + // set our annotation 1.234 + promiseGetAnnotation(aURI).then(function(val) { 1.235 + let providerList = val ? JSON.parse(val) : []; 1.236 + let marked = providerList.indexOf(origin) >= 0; 1.237 + if (marked) { 1.238 + // remove the annotation 1.239 + providerList.splice(providerList.indexOf(origin), 1); 1.240 + promiseSetAnnotation(aURI, providerList).then(function() { 1.241 + if (aCallback) 1.242 + schedule(function() { aCallback(false); } ); 1.243 + }).then(null, Cu.reportError); 1.244 + } 1.245 + }).then(null, Cu.reportError); 1.246 + }, 1.247 + 1.248 + setErrorListener: function(iframe, errorHandler) { 1.249 + if (iframe.socialErrorListener) 1.250 + return iframe.socialErrorListener; 1.251 + return new SocialErrorListener(iframe, errorHandler); 1.252 + } 1.253 +}; 1.254 + 1.255 +function schedule(callback) { 1.256 + Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); 1.257 +} 1.258 + 1.259 +function CreateSocialStatusWidget(aId, aProvider) { 1.260 + if (!aProvider.statusURL) 1.261 + return; 1.262 + let widget = CustomizableUI.getWidget(aId); 1.263 + // The widget is only null if we've created then destroyed the widget. 1.264 + // Once we've actually called createWidget the provider will be set to 1.265 + // PROVIDER_API. 1.266 + if (widget && widget.provider == CustomizableUI.PROVIDER_API) 1.267 + return; 1.268 + 1.269 + CustomizableUI.createWidget({ 1.270 + id: aId, 1.271 + type: 'custom', 1.272 + removable: true, 1.273 + defaultArea: CustomizableUI.AREA_NAVBAR, 1.274 + onBuild: function(aDocument) { 1.275 + let node = aDocument.createElement('toolbarbutton'); 1.276 + node.id = this.id; 1.277 + node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional social-status-button'); 1.278 + node.setAttribute('type', "badged"); 1.279 + node.style.listStyleImage = "url(" + (aProvider.icon32URL || aProvider.iconURL) + ")"; 1.280 + node.setAttribute("origin", aProvider.origin); 1.281 + node.setAttribute("label", aProvider.name); 1.282 + node.setAttribute("tooltiptext", aProvider.name); 1.283 + node.setAttribute("oncommand", "SocialStatus.showPopup(this);"); 1.284 + 1.285 + if (PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView)) 1.286 + node.setAttribute("disabled", "true"); 1.287 + 1.288 + return node; 1.289 + } 1.290 + }); 1.291 +}; 1.292 + 1.293 +function CreateSocialMarkWidget(aId, aProvider) { 1.294 + if (!aProvider.markURL) 1.295 + return; 1.296 + let widget = CustomizableUI.getWidget(aId); 1.297 + // The widget is only null if we've created then destroyed the widget. 1.298 + // Once we've actually called createWidget the provider will be set to 1.299 + // PROVIDER_API. 1.300 + if (widget && widget.provider == CustomizableUI.PROVIDER_API) 1.301 + return; 1.302 + 1.303 + CustomizableUI.createWidget({ 1.304 + id: aId, 1.305 + type: 'custom', 1.306 + removable: true, 1.307 + defaultArea: CustomizableUI.AREA_NAVBAR, 1.308 + onBuild: function(aDocument) { 1.309 + let node = aDocument.createElement('toolbarbutton'); 1.310 + node.id = this.id; 1.311 + node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional social-mark-button'); 1.312 + node.setAttribute('type', "socialmark"); 1.313 + node.style.listStyleImage = "url(" + (aProvider.unmarkedIcon || aProvider.icon32URL || aProvider.iconURL) + ")"; 1.314 + node.setAttribute("origin", aProvider.origin); 1.315 + node.setAttribute("oncommand", "this.markCurrentPage();"); 1.316 + 1.317 + let window = aDocument.defaultView; 1.318 + let menuLabel = window.gNavigatorBundle.getFormattedString("social.markpageMenu.label", [aProvider.name]); 1.319 + node.setAttribute("label", menuLabel); 1.320 + node.setAttribute("tooltiptext", menuLabel); 1.321 + 1.322 + return node; 1.323 + } 1.324 + }); 1.325 +}; 1.326 + 1.327 +// Error handling class used to listen for network errors in the social frames 1.328 +// and replace them with a social-specific error page 1.329 +function SocialErrorListener(iframe, errorHandler) { 1.330 + this.setErrorMessage = errorHandler; 1.331 + this.iframe = iframe; 1.332 + iframe.socialErrorListener = this; 1.333 + iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor) 1.334 + .getInterface(Ci.nsIWebProgress) 1.335 + .addProgressListener(this, 1.336 + Ci.nsIWebProgress.NOTIFY_STATE_REQUEST | 1.337 + Ci.nsIWebProgress.NOTIFY_LOCATION); 1.338 +} 1.339 + 1.340 +SocialErrorListener.prototype = { 1.341 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, 1.342 + Ci.nsISupportsWeakReference, 1.343 + Ci.nsISupports]), 1.344 + 1.345 + remove: function() { 1.346 + this.iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor) 1.347 + .getInterface(Ci.nsIWebProgress) 1.348 + .removeProgressListener(this); 1.349 + delete this.iframe.socialErrorListener; 1.350 + }, 1.351 + 1.352 + onStateChange: function SPL_onStateChange(aWebProgress, aRequest, aState, aStatus) { 1.353 + let failure = false; 1.354 + if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) { 1.355 + if (aRequest instanceof Ci.nsIHttpChannel) { 1.356 + try { 1.357 + // Change the frame to an error page on 4xx (client errors) 1.358 + // and 5xx (server errors) 1.359 + failure = aRequest.responseStatus >= 400 && 1.360 + aRequest.responseStatus < 600; 1.361 + } catch (e) {} 1.362 + } 1.363 + } 1.364 + 1.365 + // Calling cancel() will raise some OnStateChange notifications by itself, 1.366 + // so avoid doing that more than once 1.367 + if (failure && aStatus != Components.results.NS_BINDING_ABORTED) { 1.368 + aRequest.cancel(Components.results.NS_BINDING_ABORTED); 1.369 + let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin")); 1.370 + provider.errorState = "content-error"; 1.371 + this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell) 1.372 + .chromeEventHandler); 1.373 + } 1.374 + }, 1.375 + 1.376 + onLocationChange: function SPL_onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { 1.377 + if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) { 1.378 + aRequest.cancel(Components.results.NS_BINDING_ABORTED); 1.379 + let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin")); 1.380 + if (!provider.errorState) 1.381 + provider.errorState = "content-error"; 1.382 + schedule(function() { 1.383 + this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell) 1.384 + .chromeEventHandler); 1.385 + }.bind(this)); 1.386 + } 1.387 + }, 1.388 + 1.389 + onProgressChange: function SPL_onProgressChange() {}, 1.390 + onStatusChange: function SPL_onStatusChange() {}, 1.391 + onSecurityChange: function SPL_onSecurityChange() {}, 1.392 +}; 1.393 + 1.394 + 1.395 +function sizeSocialPanelToContent(panel, iframe) { 1.396 + let doc = iframe.contentDocument; 1.397 + if (!doc || !doc.body) { 1.398 + return; 1.399 + } 1.400 + // We need an element to use for sizing our panel. See if the body defines 1.401 + // an id for that element, otherwise use the body itself. 1.402 + let body = doc.body; 1.403 + let bodyId = body.getAttribute("contentid"); 1.404 + if (bodyId) { 1.405 + body = doc.getElementById(bodyId) || doc.body; 1.406 + } 1.407 + // offsetHeight/Width don't include margins, so account for that. 1.408 + let cs = doc.defaultView.getComputedStyle(body); 1.409 + let width = PANEL_MIN_WIDTH; 1.410 + let height = PANEL_MIN_HEIGHT; 1.411 + // if the panel is preloaded prior to being shown, cs will be null. in that 1.412 + // case use the minimum size for the panel until it is shown. 1.413 + if (cs) { 1.414 + let computedHeight = parseInt(cs.marginTop) + body.offsetHeight + parseInt(cs.marginBottom); 1.415 + height = Math.max(computedHeight, height); 1.416 + let computedWidth = parseInt(cs.marginLeft) + body.offsetWidth + parseInt(cs.marginRight); 1.417 + width = Math.max(computedWidth, width); 1.418 + } 1.419 + iframe.style.width = width + "px"; 1.420 + iframe.style.height = height + "px"; 1.421 + // since we do not use panel.sizeTo, we need to adjust the arrow ourselves 1.422 + if (panel.state == "open") 1.423 + panel.adjustArrowPosition(); 1.424 +} 1.425 + 1.426 +function DynamicResizeWatcher() { 1.427 + this._mutationObserver = null; 1.428 +} 1.429 + 1.430 +DynamicResizeWatcher.prototype = { 1.431 + start: function DynamicResizeWatcher_start(panel, iframe) { 1.432 + this.stop(); // just in case... 1.433 + let doc = iframe.contentDocument; 1.434 + this._mutationObserver = new iframe.contentWindow.MutationObserver(function(mutations) { 1.435 + sizeSocialPanelToContent(panel, iframe); 1.436 + }); 1.437 + // Observe anything that causes the size to change. 1.438 + let config = {attributes: true, characterData: true, childList: true, subtree: true}; 1.439 + this._mutationObserver.observe(doc, config); 1.440 + // and since this may be setup after the load event has fired we do an 1.441 + // initial resize now. 1.442 + sizeSocialPanelToContent(panel, iframe); 1.443 + }, 1.444 + stop: function DynamicResizeWatcher_stop() { 1.445 + if (this._mutationObserver) { 1.446 + try { 1.447 + this._mutationObserver.disconnect(); 1.448 + } catch (ex) { 1.449 + // may get "TypeError: can't access dead object" which seems strange, 1.450 + // but doesn't seem to indicate a real problem, so ignore it... 1.451 + } 1.452 + this._mutationObserver = null; 1.453 + } 1.454 + } 1.455 +} 1.456 + 1.457 + 1.458 +this.OpenGraphBuilder = { 1.459 + generateEndpointURL: function(URLTemplate, pageData) { 1.460 + // support for existing oexchange style endpoints by supporting their 1.461 + // querystring arguments. parse the query string template and do 1.462 + // replacements where necessary the query names may be different than ours, 1.463 + // so we could see u=%{url} or url=%{url} 1.464 + let [endpointURL, queryString] = URLTemplate.split("?"); 1.465 + let query = {}; 1.466 + if (queryString) { 1.467 + queryString.split('&').forEach(function (val) { 1.468 + let [name, value] = val.split('='); 1.469 + let p = /%\{(.+)\}/.exec(value); 1.470 + if (!p) { 1.471 + // preserve non-template query vars 1.472 + query[name] = value; 1.473 + } else if (pageData[p[1]]) { 1.474 + query[name] = pageData[p[1]]; 1.475 + } else if (p[1] == "body") { 1.476 + // build a body for emailers 1.477 + let body = ""; 1.478 + if (pageData.title) 1.479 + body += pageData.title + "\n\n"; 1.480 + if (pageData.description) 1.481 + body += pageData.description + "\n\n"; 1.482 + if (pageData.text) 1.483 + body += pageData.text + "\n\n"; 1.484 + body += pageData.url; 1.485 + query["body"] = body; 1.486 + } 1.487 + }); 1.488 + } 1.489 + var str = []; 1.490 + for (let p in query) 1.491 + str.push(p + "=" + encodeURIComponent(query[p])); 1.492 + if (str.length) 1.493 + endpointURL = endpointURL + "?" + str.join("&"); 1.494 + return endpointURL; 1.495 + }, 1.496 + 1.497 + getData: function(browser) { 1.498 + let res = { 1.499 + url: this._validateURL(browser, browser.currentURI.spec), 1.500 + title: browser.contentDocument.title, 1.501 + previews: [] 1.502 + }; 1.503 + this._getMetaData(browser, res); 1.504 + this._getLinkData(browser, res); 1.505 + this._getPageData(browser, res); 1.506 + return res; 1.507 + }, 1.508 + 1.509 + _getMetaData: function(browser, o) { 1.510 + // query for standardized meta data 1.511 + let els = browser.contentDocument 1.512 + .querySelectorAll("head > meta[property], head > meta[name]"); 1.513 + if (els.length < 1) 1.514 + return; 1.515 + let url; 1.516 + for (let el of els) { 1.517 + let value = el.getAttribute("content") 1.518 + if (!value) 1.519 + continue; 1.520 + value = unescapeService.unescape(value.trim()); 1.521 + switch (el.getAttribute("property") || el.getAttribute("name")) { 1.522 + case "title": 1.523 + case "og:title": 1.524 + o.title = value; 1.525 + break; 1.526 + case "description": 1.527 + case "og:description": 1.528 + o.description = value; 1.529 + break; 1.530 + case "og:site_name": 1.531 + o.siteName = value; 1.532 + break; 1.533 + case "medium": 1.534 + case "og:type": 1.535 + o.medium = value; 1.536 + break; 1.537 + case "og:video": 1.538 + url = this._validateURL(browser, value); 1.539 + if (url) 1.540 + o.source = url; 1.541 + break; 1.542 + case "og:url": 1.543 + url = this._validateURL(browser, value); 1.544 + if (url) 1.545 + o.url = url; 1.546 + break; 1.547 + case "og:image": 1.548 + url = this._validateURL(browser, value); 1.549 + if (url) 1.550 + o.previews.push(url); 1.551 + break; 1.552 + } 1.553 + } 1.554 + }, 1.555 + 1.556 + _getLinkData: function(browser, o) { 1.557 + let els = browser.contentDocument 1.558 + .querySelectorAll("head > link[rel], head > link[id]"); 1.559 + for (let el of els) { 1.560 + let url = el.getAttribute("href"); 1.561 + if (!url) 1.562 + continue; 1.563 + url = this._validateURL(browser, unescapeService.unescape(url.trim())); 1.564 + switch (el.getAttribute("rel") || el.getAttribute("id")) { 1.565 + case "shorturl": 1.566 + case "shortlink": 1.567 + o.shortUrl = url; 1.568 + break; 1.569 + case "canonicalurl": 1.570 + case "canonical": 1.571 + o.url = url; 1.572 + break; 1.573 + case "image_src": 1.574 + o.previews.push(url); 1.575 + break; 1.576 + } 1.577 + } 1.578 + }, 1.579 + 1.580 + // scrape through the page for data we want 1.581 + _getPageData: function(browser, o) { 1.582 + if (o.previews.length < 1) 1.583 + o.previews = this._getImageUrls(browser); 1.584 + }, 1.585 + 1.586 + _validateURL: function(browser, url) { 1.587 + let uri = Services.io.newURI(browser.currentURI.resolve(url), null, null); 1.588 + if (["http", "https", "ftp", "ftps"].indexOf(uri.scheme) < 0) 1.589 + return null; 1.590 + uri.userPass = ""; 1.591 + return uri.spec; 1.592 + }, 1.593 + 1.594 + _getImageUrls: function(browser) { 1.595 + let l = []; 1.596 + let els = browser.contentDocument.querySelectorAll("img"); 1.597 + for (let el of els) { 1.598 + let content = el.getAttribute("src"); 1.599 + if (content) { 1.600 + l.push(this._validateURL(browser, unescapeService.unescape(content))); 1.601 + // we don't want a billion images 1.602 + if (l.length > 5) 1.603 + break; 1.604 + } 1.605 + } 1.606 + return l; 1.607 + } 1.608 +};