michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["Social", "CreateSocialStatusWidget", michael@0: "CreateSocialMarkWidget", "OpenGraphBuilder", michael@0: "DynamicResizeWatcher", "sizeSocialPanelToContent"]; michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cc = Components.classes; michael@0: const Cu = Components.utils; michael@0: michael@0: // The minimum sizes for the auto-resize panel code. michael@0: const PANEL_MIN_HEIGHT = 100; michael@0: const PANEL_MIN_WIDTH = 330; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", michael@0: "resource:///modules/CustomizableUI.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SocialService", michael@0: "resource://gre/modules/SocialService.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", michael@0: "resource://gre/modules/PlacesUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", michael@0: "resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "unescapeService", michael@0: "@mozilla.org/feed-unescapehtml;1", michael@0: "nsIScriptableUnescapeHTML"); michael@0: michael@0: function promiseSetAnnotation(aURI, providerList) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: // Delaying to catch issues with asynchronous behavior while waiting michael@0: // to implement asynchronous annotations in bug 699844. michael@0: Services.tm.mainThread.dispatch(function() { michael@0: try { michael@0: if (providerList && providerList.length > 0) { michael@0: PlacesUtils.annotations.setPageAnnotation( michael@0: aURI, "social/mark", JSON.stringify(providerList), 0, michael@0: PlacesUtils.annotations.EXPIRE_WITH_HISTORY); michael@0: } else { michael@0: PlacesUtils.annotations.removePageAnnotation(aURI, "social/mark"); michael@0: } michael@0: } catch(e) { michael@0: Cu.reportError("SocialAnnotation failed: " + e); michael@0: } michael@0: deferred.resolve(); michael@0: }, Ci.nsIThread.DISPATCH_NORMAL); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function promiseGetAnnotation(aURI) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: // Delaying to catch issues with asynchronous behavior while waiting michael@0: // to implement asynchronous annotations in bug 699844. michael@0: Services.tm.mainThread.dispatch(function() { michael@0: let val = null; michael@0: try { michael@0: val = PlacesUtils.annotations.getPageAnnotation(aURI, "social/mark"); michael@0: } catch (ex) { } michael@0: michael@0: deferred.resolve(val); michael@0: }, Ci.nsIThread.DISPATCH_NORMAL); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: this.Social = { michael@0: initialized: false, michael@0: lastEventReceived: 0, michael@0: providers: [], michael@0: _disabledForSafeMode: false, michael@0: michael@0: init: function Social_init() { michael@0: this._disabledForSafeMode = Services.appinfo.inSafeMode && this.enabled; michael@0: let deferred = Promise.defer(); michael@0: michael@0: if (this.initialized) { michael@0: deferred.resolve(true); michael@0: return deferred.promise; michael@0: } michael@0: this.initialized = true; michael@0: // if SocialService.hasEnabledProviders, retreive the providers so the michael@0: // front-end can generate UI michael@0: if (SocialService.hasEnabledProviders) { michael@0: // Retrieve the current set of providers, and set the current provider. michael@0: SocialService.getOrderedProviderList(function (providers) { michael@0: Social._updateProviderCache(providers); michael@0: Social._updateWorkerState(SocialService.enabled); michael@0: deferred.resolve(false); michael@0: }); michael@0: } else { michael@0: deferred.resolve(false); michael@0: } michael@0: michael@0: // Register an observer for changes to the provider list michael@0: SocialService.registerProviderListener(function providerListener(topic, origin, providers) { michael@0: // An engine change caused by adding/removing a provider should notify. michael@0: // any providers we receive are enabled in the AddonsManager michael@0: if (topic == "provider-installed" || topic == "provider-uninstalled") { michael@0: // installed/uninstalled do not send the providers param michael@0: Services.obs.notifyObservers(null, "social:" + topic, origin); michael@0: return; michael@0: } michael@0: if (topic == "provider-enabled") { michael@0: Social._updateProviderCache(providers); michael@0: Social._updateWorkerState(true); michael@0: Services.obs.notifyObservers(null, "social:" + topic, origin); michael@0: return; michael@0: } michael@0: if (topic == "provider-disabled") { michael@0: // a provider was removed from the list of providers, that does not michael@0: // affect worker state for other providers michael@0: Social._updateProviderCache(providers); michael@0: Social._updateWorkerState(providers.length > 0); michael@0: Services.obs.notifyObservers(null, "social:" + topic, origin); michael@0: return; michael@0: } michael@0: if (topic == "provider-update") { michael@0: // a provider has self-updated its manifest, we need to update our cache michael@0: // and reload the provider. michael@0: Social._updateProviderCache(providers); michael@0: let provider = Social._getProviderFromOrigin(origin); michael@0: provider.reload(); michael@0: } michael@0: }); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _updateWorkerState: function(enable) { michael@0: [p.enabled = enable for (p of Social.providers) if (p.enabled != enable)]; michael@0: }, michael@0: michael@0: // Called to update our cache of providers and set the current provider michael@0: _updateProviderCache: function (providers) { michael@0: this.providers = providers; michael@0: Services.obs.notifyObservers(null, "social:providers-changed", null); michael@0: }, michael@0: michael@0: get enabled() { michael@0: return !this._disabledForSafeMode && this.providers.length > 0; michael@0: }, michael@0: michael@0: toggleNotifications: function SocialNotifications_toggle() { michael@0: let prefValue = Services.prefs.getBoolPref("social.toast-notifications.enabled"); michael@0: Services.prefs.setBoolPref("social.toast-notifications.enabled", !prefValue); michael@0: }, michael@0: michael@0: _getProviderFromOrigin: function (origin) { michael@0: for (let p of this.providers) { michael@0: if (p.origin == origin) { michael@0: return p; michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: getManifestByOrigin: function(origin) { michael@0: return SocialService.getManifestByOrigin(origin); michael@0: }, michael@0: michael@0: installProvider: function(doc, data, installCallback) { michael@0: SocialService.installProvider(doc, data, installCallback); michael@0: }, michael@0: michael@0: uninstallProvider: function(origin, aCallback) { michael@0: SocialService.uninstallProvider(origin, aCallback); michael@0: }, michael@0: michael@0: // Activation functionality michael@0: activateFromOrigin: function (origin, callback) { michael@0: // For now only "builtin" providers can be activated. It's OK if the michael@0: // provider has already been activated - we still get called back with it. michael@0: SocialService.addBuiltinProvider(origin, callback); michael@0: }, michael@0: michael@0: // Page Marking functionality michael@0: isURIMarked: function(origin, aURI, aCallback) { michael@0: promiseGetAnnotation(aURI).then(function(val) { michael@0: if (val) { michael@0: let providerList = JSON.parse(val); michael@0: val = providerList.indexOf(origin) >= 0; michael@0: } michael@0: aCallback(!!val); michael@0: }).then(null, Cu.reportError); michael@0: }, michael@0: michael@0: markURI: function(origin, aURI, aCallback) { michael@0: // update or set our annotation michael@0: promiseGetAnnotation(aURI).then(function(val) { michael@0: michael@0: let providerList = val ? JSON.parse(val) : []; michael@0: let marked = providerList.indexOf(origin) >= 0; michael@0: if (marked) michael@0: return; michael@0: providerList.push(origin); michael@0: // we allow marking links in a page that may not have been visited yet. michael@0: // make sure there is a history entry for the uri, then annotate it. michael@0: let place = { michael@0: uri: aURI, michael@0: visits: [{ michael@0: visitDate: Date.now() + 1000, michael@0: transitionType: Ci.nsINavHistoryService.TRANSITION_LINK michael@0: }] michael@0: }; michael@0: PlacesUtils.asyncHistory.updatePlaces(place, { michael@0: handleError: function () Cu.reportError("couldn't update history for socialmark annotation"), michael@0: handleResult: function () {}, michael@0: handleCompletion: function () { michael@0: promiseSetAnnotation(aURI, providerList).then(function() { michael@0: if (aCallback) michael@0: schedule(function() { aCallback(true); } ); michael@0: }).then(null, Cu.reportError); michael@0: } michael@0: }); michael@0: }).then(null, Cu.reportError); michael@0: }, michael@0: michael@0: unmarkURI: function(origin, aURI, aCallback) { michael@0: // this should not be called if this.provider or the port is null michael@0: // set our annotation michael@0: promiseGetAnnotation(aURI).then(function(val) { michael@0: let providerList = val ? JSON.parse(val) : []; michael@0: let marked = providerList.indexOf(origin) >= 0; michael@0: if (marked) { michael@0: // remove the annotation michael@0: providerList.splice(providerList.indexOf(origin), 1); michael@0: promiseSetAnnotation(aURI, providerList).then(function() { michael@0: if (aCallback) michael@0: schedule(function() { aCallback(false); } ); michael@0: }).then(null, Cu.reportError); michael@0: } michael@0: }).then(null, Cu.reportError); michael@0: }, michael@0: michael@0: setErrorListener: function(iframe, errorHandler) { michael@0: if (iframe.socialErrorListener) michael@0: return iframe.socialErrorListener; michael@0: return new SocialErrorListener(iframe, errorHandler); michael@0: } michael@0: }; michael@0: michael@0: function schedule(callback) { michael@0: Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); michael@0: } michael@0: michael@0: function CreateSocialStatusWidget(aId, aProvider) { michael@0: if (!aProvider.statusURL) michael@0: return; michael@0: let widget = CustomizableUI.getWidget(aId); michael@0: // The widget is only null if we've created then destroyed the widget. michael@0: // Once we've actually called createWidget the provider will be set to michael@0: // PROVIDER_API. michael@0: if (widget && widget.provider == CustomizableUI.PROVIDER_API) michael@0: return; michael@0: michael@0: CustomizableUI.createWidget({ michael@0: id: aId, michael@0: type: 'custom', michael@0: removable: true, michael@0: defaultArea: CustomizableUI.AREA_NAVBAR, michael@0: onBuild: function(aDocument) { michael@0: let node = aDocument.createElement('toolbarbutton'); michael@0: node.id = this.id; michael@0: node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional social-status-button'); michael@0: node.setAttribute('type', "badged"); michael@0: node.style.listStyleImage = "url(" + (aProvider.icon32URL || aProvider.iconURL) + ")"; michael@0: node.setAttribute("origin", aProvider.origin); michael@0: node.setAttribute("label", aProvider.name); michael@0: node.setAttribute("tooltiptext", aProvider.name); michael@0: node.setAttribute("oncommand", "SocialStatus.showPopup(this);"); michael@0: michael@0: if (PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView)) michael@0: node.setAttribute("disabled", "true"); michael@0: michael@0: return node; michael@0: } michael@0: }); michael@0: }; michael@0: michael@0: function CreateSocialMarkWidget(aId, aProvider) { michael@0: if (!aProvider.markURL) michael@0: return; michael@0: let widget = CustomizableUI.getWidget(aId); michael@0: // The widget is only null if we've created then destroyed the widget. michael@0: // Once we've actually called createWidget the provider will be set to michael@0: // PROVIDER_API. michael@0: if (widget && widget.provider == CustomizableUI.PROVIDER_API) michael@0: return; michael@0: michael@0: CustomizableUI.createWidget({ michael@0: id: aId, michael@0: type: 'custom', michael@0: removable: true, michael@0: defaultArea: CustomizableUI.AREA_NAVBAR, michael@0: onBuild: function(aDocument) { michael@0: let node = aDocument.createElement('toolbarbutton'); michael@0: node.id = this.id; michael@0: node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional social-mark-button'); michael@0: node.setAttribute('type', "socialmark"); michael@0: node.style.listStyleImage = "url(" + (aProvider.unmarkedIcon || aProvider.icon32URL || aProvider.iconURL) + ")"; michael@0: node.setAttribute("origin", aProvider.origin); michael@0: node.setAttribute("oncommand", "this.markCurrentPage();"); michael@0: michael@0: let window = aDocument.defaultView; michael@0: let menuLabel = window.gNavigatorBundle.getFormattedString("social.markpageMenu.label", [aProvider.name]); michael@0: node.setAttribute("label", menuLabel); michael@0: node.setAttribute("tooltiptext", menuLabel); michael@0: michael@0: return node; michael@0: } michael@0: }); michael@0: }; michael@0: michael@0: // Error handling class used to listen for network errors in the social frames michael@0: // and replace them with a social-specific error page michael@0: function SocialErrorListener(iframe, errorHandler) { michael@0: this.setErrorMessage = errorHandler; michael@0: this.iframe = iframe; michael@0: iframe.socialErrorListener = this; michael@0: iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebProgress) michael@0: .addProgressListener(this, michael@0: Ci.nsIWebProgress.NOTIFY_STATE_REQUEST | michael@0: Ci.nsIWebProgress.NOTIFY_LOCATION); michael@0: } michael@0: michael@0: SocialErrorListener.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, michael@0: Ci.nsISupportsWeakReference, michael@0: Ci.nsISupports]), michael@0: michael@0: remove: function() { michael@0: this.iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebProgress) michael@0: .removeProgressListener(this); michael@0: delete this.iframe.socialErrorListener; michael@0: }, michael@0: michael@0: onStateChange: function SPL_onStateChange(aWebProgress, aRequest, aState, aStatus) { michael@0: let failure = false; michael@0: if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) { michael@0: if (aRequest instanceof Ci.nsIHttpChannel) { michael@0: try { michael@0: // Change the frame to an error page on 4xx (client errors) michael@0: // and 5xx (server errors) michael@0: failure = aRequest.responseStatus >= 400 && michael@0: aRequest.responseStatus < 600; michael@0: } catch (e) {} michael@0: } michael@0: } michael@0: michael@0: // Calling cancel() will raise some OnStateChange notifications by itself, michael@0: // so avoid doing that more than once michael@0: if (failure && aStatus != Components.results.NS_BINDING_ABORTED) { michael@0: aRequest.cancel(Components.results.NS_BINDING_ABORTED); michael@0: let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin")); michael@0: provider.errorState = "content-error"; michael@0: this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell) michael@0: .chromeEventHandler); michael@0: } michael@0: }, michael@0: michael@0: onLocationChange: function SPL_onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { michael@0: if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) { michael@0: aRequest.cancel(Components.results.NS_BINDING_ABORTED); michael@0: let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin")); michael@0: if (!provider.errorState) michael@0: provider.errorState = "content-error"; michael@0: schedule(function() { michael@0: this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell) michael@0: .chromeEventHandler); michael@0: }.bind(this)); michael@0: } michael@0: }, michael@0: michael@0: onProgressChange: function SPL_onProgressChange() {}, michael@0: onStatusChange: function SPL_onStatusChange() {}, michael@0: onSecurityChange: function SPL_onSecurityChange() {}, michael@0: }; michael@0: michael@0: michael@0: function sizeSocialPanelToContent(panel, iframe) { michael@0: let doc = iframe.contentDocument; michael@0: if (!doc || !doc.body) { michael@0: return; michael@0: } michael@0: // We need an element to use for sizing our panel. See if the body defines michael@0: // an id for that element, otherwise use the body itself. michael@0: let body = doc.body; michael@0: let bodyId = body.getAttribute("contentid"); michael@0: if (bodyId) { michael@0: body = doc.getElementById(bodyId) || doc.body; michael@0: } michael@0: // offsetHeight/Width don't include margins, so account for that. michael@0: let cs = doc.defaultView.getComputedStyle(body); michael@0: let width = PANEL_MIN_WIDTH; michael@0: let height = PANEL_MIN_HEIGHT; michael@0: // if the panel is preloaded prior to being shown, cs will be null. in that michael@0: // case use the minimum size for the panel until it is shown. michael@0: if (cs) { michael@0: let computedHeight = parseInt(cs.marginTop) + body.offsetHeight + parseInt(cs.marginBottom); michael@0: height = Math.max(computedHeight, height); michael@0: let computedWidth = parseInt(cs.marginLeft) + body.offsetWidth + parseInt(cs.marginRight); michael@0: width = Math.max(computedWidth, width); michael@0: } michael@0: iframe.style.width = width + "px"; michael@0: iframe.style.height = height + "px"; michael@0: // since we do not use panel.sizeTo, we need to adjust the arrow ourselves michael@0: if (panel.state == "open") michael@0: panel.adjustArrowPosition(); michael@0: } michael@0: michael@0: function DynamicResizeWatcher() { michael@0: this._mutationObserver = null; michael@0: } michael@0: michael@0: DynamicResizeWatcher.prototype = { michael@0: start: function DynamicResizeWatcher_start(panel, iframe) { michael@0: this.stop(); // just in case... michael@0: let doc = iframe.contentDocument; michael@0: this._mutationObserver = new iframe.contentWindow.MutationObserver(function(mutations) { michael@0: sizeSocialPanelToContent(panel, iframe); michael@0: }); michael@0: // Observe anything that causes the size to change. michael@0: let config = {attributes: true, characterData: true, childList: true, subtree: true}; michael@0: this._mutationObserver.observe(doc, config); michael@0: // and since this may be setup after the load event has fired we do an michael@0: // initial resize now. michael@0: sizeSocialPanelToContent(panel, iframe); michael@0: }, michael@0: stop: function DynamicResizeWatcher_stop() { michael@0: if (this._mutationObserver) { michael@0: try { michael@0: this._mutationObserver.disconnect(); michael@0: } catch (ex) { michael@0: // may get "TypeError: can't access dead object" which seems strange, michael@0: // but doesn't seem to indicate a real problem, so ignore it... michael@0: } michael@0: this._mutationObserver = null; michael@0: } michael@0: } michael@0: } michael@0: michael@0: michael@0: this.OpenGraphBuilder = { michael@0: generateEndpointURL: function(URLTemplate, pageData) { michael@0: // support for existing oexchange style endpoints by supporting their michael@0: // querystring arguments. parse the query string template and do michael@0: // replacements where necessary the query names may be different than ours, michael@0: // so we could see u=%{url} or url=%{url} michael@0: let [endpointURL, queryString] = URLTemplate.split("?"); michael@0: let query = {}; michael@0: if (queryString) { michael@0: queryString.split('&').forEach(function (val) { michael@0: let [name, value] = val.split('='); michael@0: let p = /%\{(.+)\}/.exec(value); michael@0: if (!p) { michael@0: // preserve non-template query vars michael@0: query[name] = value; michael@0: } else if (pageData[p[1]]) { michael@0: query[name] = pageData[p[1]]; michael@0: } else if (p[1] == "body") { michael@0: // build a body for emailers michael@0: let body = ""; michael@0: if (pageData.title) michael@0: body += pageData.title + "\n\n"; michael@0: if (pageData.description) michael@0: body += pageData.description + "\n\n"; michael@0: if (pageData.text) michael@0: body += pageData.text + "\n\n"; michael@0: body += pageData.url; michael@0: query["body"] = body; michael@0: } michael@0: }); michael@0: } michael@0: var str = []; michael@0: for (let p in query) michael@0: str.push(p + "=" + encodeURIComponent(query[p])); michael@0: if (str.length) michael@0: endpointURL = endpointURL + "?" + str.join("&"); michael@0: return endpointURL; michael@0: }, michael@0: michael@0: getData: function(browser) { michael@0: let res = { michael@0: url: this._validateURL(browser, browser.currentURI.spec), michael@0: title: browser.contentDocument.title, michael@0: previews: [] michael@0: }; michael@0: this._getMetaData(browser, res); michael@0: this._getLinkData(browser, res); michael@0: this._getPageData(browser, res); michael@0: return res; michael@0: }, michael@0: michael@0: _getMetaData: function(browser, o) { michael@0: // query for standardized meta data michael@0: let els = browser.contentDocument michael@0: .querySelectorAll("head > meta[property], head > meta[name]"); michael@0: if (els.length < 1) michael@0: return; michael@0: let url; michael@0: for (let el of els) { michael@0: let value = el.getAttribute("content") michael@0: if (!value) michael@0: continue; michael@0: value = unescapeService.unescape(value.trim()); michael@0: switch (el.getAttribute("property") || el.getAttribute("name")) { michael@0: case "title": michael@0: case "og:title": michael@0: o.title = value; michael@0: break; michael@0: case "description": michael@0: case "og:description": michael@0: o.description = value; michael@0: break; michael@0: case "og:site_name": michael@0: o.siteName = value; michael@0: break; michael@0: case "medium": michael@0: case "og:type": michael@0: o.medium = value; michael@0: break; michael@0: case "og:video": michael@0: url = this._validateURL(browser, value); michael@0: if (url) michael@0: o.source = url; michael@0: break; michael@0: case "og:url": michael@0: url = this._validateURL(browser, value); michael@0: if (url) michael@0: o.url = url; michael@0: break; michael@0: case "og:image": michael@0: url = this._validateURL(browser, value); michael@0: if (url) michael@0: o.previews.push(url); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _getLinkData: function(browser, o) { michael@0: let els = browser.contentDocument michael@0: .querySelectorAll("head > link[rel], head > link[id]"); michael@0: for (let el of els) { michael@0: let url = el.getAttribute("href"); michael@0: if (!url) michael@0: continue; michael@0: url = this._validateURL(browser, unescapeService.unescape(url.trim())); michael@0: switch (el.getAttribute("rel") || el.getAttribute("id")) { michael@0: case "shorturl": michael@0: case "shortlink": michael@0: o.shortUrl = url; michael@0: break; michael@0: case "canonicalurl": michael@0: case "canonical": michael@0: o.url = url; michael@0: break; michael@0: case "image_src": michael@0: o.previews.push(url); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: // scrape through the page for data we want michael@0: _getPageData: function(browser, o) { michael@0: if (o.previews.length < 1) michael@0: o.previews = this._getImageUrls(browser); michael@0: }, michael@0: michael@0: _validateURL: function(browser, url) { michael@0: let uri = Services.io.newURI(browser.currentURI.resolve(url), null, null); michael@0: if (["http", "https", "ftp", "ftps"].indexOf(uri.scheme) < 0) michael@0: return null; michael@0: uri.userPass = ""; michael@0: return uri.spec; michael@0: }, michael@0: michael@0: _getImageUrls: function(browser) { michael@0: let l = []; michael@0: let els = browser.contentDocument.querySelectorAll("img"); michael@0: for (let el of els) { michael@0: let content = el.getAttribute("src"); michael@0: if (content) { michael@0: l.push(this._validateURL(browser, unescapeService.unescape(content))); michael@0: // we don't want a billion images michael@0: if (l.length > 5) michael@0: break; michael@0: } michael@0: } michael@0: return l; michael@0: } michael@0: };