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