browser/modules/Social.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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 };

mercurial