Bumping manifests a=b2g-bump
[gecko.git] / browser / modules / Social.jsm
blobcf48542eb5610707ce667d1266997a22d8148d55
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/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["Social", "CreateSocialStatusWidget",
8                          "CreateSocialMarkWidget", "OpenGraphBuilder",
9                          "DynamicResizeWatcher", "sizeSocialPanelToContent"];
11 const Ci = Components.interfaces;
12 const Cc = Components.classes;
13 const Cu = Components.utils;
15 // The minimum sizes for the auto-resize panel code, minimum size necessary to
16 // properly show the error page in the panel.
17 const PANEL_MIN_HEIGHT = 190;
18 const PANEL_MIN_WIDTH = 330;
20 Cu.import("resource://gre/modules/Services.jsm");
21 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
23 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
24   "resource:///modules/CustomizableUI.jsm");
25 XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
26   "resource://gre/modules/SocialService.jsm");
27 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
28   "resource://gre/modules/PlacesUtils.jsm");
29 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
30   "resource://gre/modules/PrivateBrowsingUtils.jsm");
31 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
32   "resource://gre/modules/Promise.jsm");
34 XPCOMUtils.defineLazyServiceGetter(this, "unescapeService",
35                                    "@mozilla.org/feed-unescapehtml;1",
36                                    "nsIScriptableUnescapeHTML");
38 function promiseSetAnnotation(aURI, providerList) {
39   let deferred = Promise.defer();
41   // Delaying to catch issues with asynchronous behavior while waiting
42   // to implement asynchronous annotations in bug 699844.
43   Services.tm.mainThread.dispatch(function() {
44     try {
45       if (providerList && providerList.length > 0) {
46         PlacesUtils.annotations.setPageAnnotation(
47           aURI, "social/mark", JSON.stringify(providerList), 0,
48           PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
49       } else {
50         PlacesUtils.annotations.removePageAnnotation(aURI, "social/mark");
51       }
52     } catch(e) {
53       Cu.reportError("SocialAnnotation failed: " + e);
54     }
55     deferred.resolve();
56   }, Ci.nsIThread.DISPATCH_NORMAL);
58   return deferred.promise;
61 function promiseGetAnnotation(aURI) {
62   let deferred = Promise.defer();
64   // Delaying to catch issues with asynchronous behavior while waiting
65   // to implement asynchronous annotations in bug 699844.
66   Services.tm.mainThread.dispatch(function() {
67     let val = null;
68     try {
69       val = PlacesUtils.annotations.getPageAnnotation(aURI, "social/mark");
70     } catch (ex) { }
72     deferred.resolve(val);
73   }, Ci.nsIThread.DISPATCH_NORMAL);
75   return deferred.promise;
78 this.Social = {
79   initialized: false,
80   lastEventReceived: 0,
81   providers: [],
82   _disabledForSafeMode: false,
84   init: function Social_init() {
85     this._disabledForSafeMode = Services.appinfo.inSafeMode && this.enabled;
86     let deferred = Promise.defer();
88     if (this.initialized) {
89       deferred.resolve(true);
90       return deferred.promise;
91     }
92     this.initialized = true;
93     // if SocialService.hasEnabledProviders, retreive the providers so the
94     // front-end can generate UI
95     if (SocialService.hasEnabledProviders) {
96       // Retrieve the current set of providers, and set the current provider.
97       SocialService.getOrderedProviderList(function (providers) {
98         Social._updateProviderCache(providers);
99         Social._updateWorkerState(SocialService.enabled);
100         deferred.resolve(false);
101       });
102     } else {
103       deferred.resolve(false);
104     }
106     // Register an observer for changes to the provider list
107     SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
108       // An engine change caused by adding/removing a provider should notify.
109       // any providers we receive are enabled in the AddonsManager
110       if (topic == "provider-installed" || topic == "provider-uninstalled") {
111         // installed/uninstalled do not send the providers param
112         Services.obs.notifyObservers(null, "social:" + topic, origin);
113         return;
114       }
115       if (topic == "provider-enabled") {
116         Social._updateProviderCache(providers);
117         Social._updateWorkerState(true);
118         Services.obs.notifyObservers(null, "social:" + topic, origin);
119         return;
120       }
121       if (topic == "provider-disabled") {
122         // a provider was removed from the list of providers, that does not
123         // affect worker state for other providers
124         Social._updateProviderCache(providers);
125         Social._updateWorkerState(providers.length > 0);
126         Services.obs.notifyObservers(null, "social:" + topic, origin);
127         return;
128       }
129       if (topic == "provider-update") {
130         // a provider has self-updated its manifest, we need to update our cache
131         // and reload the provider.
132         Social._updateProviderCache(providers);
133         let provider = Social._getProviderFromOrigin(origin);
134         provider.reload();
135       }
136     });
137     return deferred.promise;
138   },
140   _updateWorkerState: function(enable) {
141     [p.enabled = enable for (p of Social.providers) if (p.enabled != enable)];
142   },
144   // Called to update our cache of providers and set the current provider
145   _updateProviderCache: function (providers) {
146     this.providers = providers;
147     Services.obs.notifyObservers(null, "social:providers-changed", null);
148   },
150   get enabled() {
151     return !this._disabledForSafeMode && this.providers.length > 0;
152   },
154   toggleNotifications: function SocialNotifications_toggle() {
155     let prefValue = Services.prefs.getBoolPref("social.toast-notifications.enabled");
156     Services.prefs.setBoolPref("social.toast-notifications.enabled", !prefValue);
157   },
159   _getProviderFromOrigin: function (origin) {
160     for (let p of this.providers) {
161       if (p.origin == origin) {
162         return p;
163       }
164     }
165     return null;
166   },
168   getManifestByOrigin: function(origin) {
169     return SocialService.getManifestByOrigin(origin);
170   },
172   installProvider: function(data, installCallback, options={}) {
173     SocialService.installProvider(data, installCallback, options);
174   },
176   uninstallProvider: function(origin, aCallback) {
177     SocialService.uninstallProvider(origin, aCallback);
178   },
180   // Activation functionality
181   activateFromOrigin: function (origin, callback) {
182     // It's OK if the provider has already been activated - we still get called
183     // back with it.
184     SocialService.enableProvider(origin, callback);
185   },
187   // Page Marking functionality
188   isURIMarked: function(origin, aURI, aCallback) {
189     promiseGetAnnotation(aURI).then(function(val) {
190       if (val) {
191         let providerList = JSON.parse(val);
192         val = providerList.indexOf(origin) >= 0;
193       }
194       aCallback(!!val);
195     }).then(null, Cu.reportError);
196   },
198   markURI: function(origin, aURI, aCallback) {
199     // update or set our annotation
200     promiseGetAnnotation(aURI).then(function(val) {
202       let providerList = val ? JSON.parse(val) : [];
203       let marked = providerList.indexOf(origin) >= 0;
204       if (marked)
205         return;
206       providerList.push(origin);
207       // we allow marking links in a page that may not have been visited yet.
208       // make sure there is a history entry for the uri, then annotate it.
209       let place = {
210         uri: aURI,
211         visits: [{
212           visitDate: Date.now() + 1000,
213           transitionType: Ci.nsINavHistoryService.TRANSITION_LINK
214         }]
215       };
216       PlacesUtils.asyncHistory.updatePlaces(place, {
217         handleError: function () Cu.reportError("couldn't update history for socialmark annotation"),
218         handleResult: function () {},
219         handleCompletion: function () {
220           promiseSetAnnotation(aURI, providerList).then(function() {
221             if (aCallback)
222               schedule(function() { aCallback(true); } );
223           }).then(null, Cu.reportError);
224         }
225       });
226     }).then(null, Cu.reportError);
227   },
229   unmarkURI: function(origin, aURI, aCallback) {
230     // this should not be called if this.provider or the port is null
231     // set our annotation
232     promiseGetAnnotation(aURI).then(function(val) {
233       let providerList = val ? JSON.parse(val) : [];
234       let marked = providerList.indexOf(origin) >= 0;
235       if (marked) {
236         // remove the annotation
237         providerList.splice(providerList.indexOf(origin), 1);
238         promiseSetAnnotation(aURI, providerList).then(function() {
239           if (aCallback)
240             schedule(function() { aCallback(false); } );
241         }).then(null, Cu.reportError);
242       }
243     }).then(null, Cu.reportError);
244   },
246   setErrorListener: function(iframe, errorHandler) {
247     if (iframe.socialErrorListener)
248       return iframe.socialErrorListener;
249     return new SocialErrorListener(iframe, errorHandler);
250   }
253 function schedule(callback) {
254   Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
257 function CreateSocialStatusWidget(aId, aProvider) {
258   if (!aProvider.statusURL)
259     return;
260   let widget = CustomizableUI.getWidget(aId);
261   // The widget is only null if we've created then destroyed the widget.
262   // Once we've actually called createWidget the provider will be set to
263   // PROVIDER_API.
264   if (widget && widget.provider == CustomizableUI.PROVIDER_API)
265     return;
267   CustomizableUI.createWidget({
268     id: aId,
269     type: 'custom',
270     removable: true,
271     defaultArea: CustomizableUI.AREA_NAVBAR,
272     onBuild: function(aDocument) {
273       let node = aDocument.createElement('toolbarbutton');
274       node.id = this.id;
275       node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional social-status-button badged-button');
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);");
282       if (PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView))
283         node.setAttribute("disabled", "true");
285       return node;
286     }
287   });
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;
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);
313       let window = aDocument.defaultView;
314       let menuLabel = window.gNavigatorBundle.getFormattedString("social.markpageMenu.label", [aProvider.name]);
315       node.setAttribute("label", menuLabel);
316       node.setAttribute("tooltiptext", menuLabel);
317       node.setAttribute("observes", "Social:PageShareOrMark");
319       return node;
320     }
321   });
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   // Force a layout flush by calling .clientTop so that the docShell of this
331   // frame is created for the error listener
332   iframe.clientTop;
333   iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
334                                    .getInterface(Ci.nsIWebProgress)
335                                    .addProgressListener(this,
336                                                         Ci.nsIWebProgress.NOTIFY_STATE_REQUEST |
337                                                         Ci.nsIWebProgress.NOTIFY_LOCATION);
340 SocialErrorListener.prototype = {
341   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
342                                          Ci.nsISupportsWeakReference,
343                                          Ci.nsISupports]),
345   remove: function() {
346     this.iframe.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
347                                      .getInterface(Ci.nsIWebProgress)
348                                      .removeProgressListener(this);
349     delete this.iframe.socialErrorListener;
350   },
352   onStateChange: function SPL_onStateChange(aWebProgress, aRequest, aState, aStatus) {
353     let failure = false;
354     if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
355       if (aRequest instanceof Ci.nsIHttpChannel) {
356         try {
357           // Change the frame to an error page on 4xx (client errors)
358           // and 5xx (server errors).  responseStatus throws if it is not set.
359           failure = aRequest.responseStatus >= 400 &&
360                     aRequest.responseStatus < 600;
361         } catch (e) {
362           failure = aStatus == Components.results.NS_ERROR_CONNECTION_REFUSED;
363         }
364       }
365     }
367     // Calling cancel() will raise some OnStateChange notifications by itself,
368     // so avoid doing that more than once
369     if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
370       aRequest.cancel(Components.results.NS_BINDING_ABORTED);
371       let origin = this.iframe.getAttribute("origin");
372       if (origin) {
373         let provider = Social._getProviderFromOrigin(origin);
374         provider.errorState = "content-error";
375       }
376       this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
377                               .chromeEventHandler);
378     }
379   },
381   onLocationChange: function SPL_onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
382     if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
383       aRequest.cancel(Components.results.NS_BINDING_ABORTED);
384       let origin = this.iframe.getAttribute("origin");
385       if (origin) {
386         let provider = Social._getProviderFromOrigin(origin);
387         if (!provider.errorState)
388           provider.errorState = "content-error";
389       }
390       schedule(function() {
391         this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
392                               .chromeEventHandler);
393       }.bind(this));
394     }
395   },
397   onProgressChange: function SPL_onProgressChange() {},
398   onStatusChange: function SPL_onStatusChange() {},
399   onSecurityChange: function SPL_onSecurityChange() {},
403 function sizeSocialPanelToContent(panel, iframe, requestedSize) {
404   let doc = iframe.contentDocument;
405   if (!doc || !doc.body) {
406     return;
407   }
408   // We need an element to use for sizing our panel.  See if the body defines
409   // an id for that element, otherwise use the body itself.
410   let body = doc.body;
411   let docEl = doc.documentElement;
412   let bodyId = body.getAttribute("contentid");
413   if (bodyId) {
414     body = doc.getElementById(bodyId) || doc.body;
415   }
416   // offsetHeight/Width don't include margins, so account for that.
417   let cs = doc.defaultView.getComputedStyle(body);
418   let width = Math.max(PANEL_MIN_WIDTH, docEl.offsetWidth);
419   let height = Math.max(PANEL_MIN_HEIGHT, docEl.offsetHeight);
420   // if the panel is preloaded prior to being shown, cs will be null.  in that
421   // case use the minimum size for the panel until it is shown.
422   if (cs) {
423     let computedHeight = parseInt(cs.marginTop) + body.offsetHeight + parseInt(cs.marginBottom);
424     height = Math.max(computedHeight, height);
425     let computedWidth = parseInt(cs.marginLeft) + body.offsetWidth + parseInt(cs.marginRight);
426     width = Math.max(computedWidth, width);
427   }
429   // if our scrollHeight is still larger than the iframe, the css calculations
430   // above did not work for this site, increase the height. This can happen if
431   // the site increases its height for additional UI.
432   if (docEl.scrollHeight > iframe.boxObject.height)
433     height = docEl.scrollHeight;
435   // if a size was defined in the manifest use it as a minimum
436   if (requestedSize) {
437     if (requestedSize.height)
438       height = Math.max(height, requestedSize.height);
439     if (requestedSize.width)
440       width = Math.max(width, requestedSize.width);
441   }
443   // add the extra space used by the panel (toolbar, borders, etc) if the iframe
444   // has been loaded
445   if (iframe.boxObject.width && iframe.boxObject.height) {
446     // add extra space the panel needs if any
447     width += panel.boxObject.width - iframe.boxObject.width;
448     height += panel.boxObject.height - iframe.boxObject.height;
449   }
451   // using panel.sizeTo will ignore css transitions, set size via style
452   if (Math.abs(panel.boxObject.width - width) >= 2)
453     panel.style.width = width + "px";
454   if (Math.abs(panel.boxObject.height - height) >= 2)
455     panel.style.height = height + "px";
458 function DynamicResizeWatcher() {
459   this._mutationObserver = null;
462 DynamicResizeWatcher.prototype = {
463   start: function DynamicResizeWatcher_start(panel, iframe, requestedSize) {
464     this.stop(); // just in case...
465     let doc = iframe.contentDocument;
466     this._mutationObserver = new iframe.contentWindow.MutationObserver((mutations) => {
467       sizeSocialPanelToContent(panel, iframe, requestedSize);
468     });
469     // Observe anything that causes the size to change.
470     let config = {attributes: true, characterData: true, childList: true, subtree: true};
471     this._mutationObserver.observe(doc, config);
472     // and since this may be setup after the load event has fired we do an
473     // initial resize now.
474     sizeSocialPanelToContent(panel, iframe, requestedSize);
475   },
476   stop: function DynamicResizeWatcher_stop() {
477     if (this._mutationObserver) {
478       try {
479         this._mutationObserver.disconnect();
480       } catch (ex) {
481         // may get "TypeError: can't access dead object" which seems strange,
482         // but doesn't seem to indicate a real problem, so ignore it...
483       }
484       this._mutationObserver = null;
485     }
486   }
490 this.OpenGraphBuilder = {
491   generateEndpointURL: function(URLTemplate, pageData) {
492     // support for existing oexchange style endpoints by supporting their
493     // querystring arguments. parse the query string template and do
494     // replacements where necessary the query names may be different than ours,
495     // so we could see u=%{url} or url=%{url}
496     let [endpointURL, queryString] = URLTemplate.split("?");
497     let query = {};
498     if (queryString) {
499       queryString.split('&').forEach(function (val) {
500         let [name, value] = val.split('=');
501         let p = /%\{(.+)\}/.exec(value);
502         if (!p) {
503           // preserve non-template query vars
504           query[name] = value;
505         } else if (pageData[p[1]]) {
506           if (p[1] == "previews")
507             query[name] = pageData[p[1]][0];
508           else
509             query[name] = pageData[p[1]];
510         } else if (p[1] == "body") {
511           // build a body for emailers
512           let body = "";
513           if (pageData.title)
514             body += pageData.title + "\n\n";
515           if (pageData.description)
516             body += pageData.description + "\n\n";
517           if (pageData.text)
518             body += pageData.text + "\n\n";
519           body += pageData.url;
520           query["body"] = body;
521         }
522       });
523     }
524     var str = [];
525     for (let p in query)
526        str.push(p + "=" + encodeURIComponent(query[p]));
527     if (str.length)
528       endpointURL = endpointURL + "?" + str.join("&");
529     return endpointURL;
530   },
532   getData: function(aDocument, target) {
533     let res = {
534       url: this._validateURL(aDocument, aDocument.documentURI),
535       title: aDocument.title,
536       previews: []
537     };
538     this._getMetaData(aDocument, res);
539     this._getLinkData(aDocument, res);
540     this._getPageData(aDocument, res);
541     res.microdata = this.getMicrodata(aDocument, target);
542     return res;
543   },
545   getMicrodata: function (aDocument, target) {
546     return getMicrodata(aDocument, target);
547   },
549   _getMetaData: function(aDocument, o) {
550     // query for standardized meta data
551     let els = aDocument.querySelectorAll("head > meta[property], head > meta[name]");
552     if (els.length < 1)
553       return;
554     let url;
555     for (let el of els) {
556       let value = el.getAttribute("content")
557       if (!value)
558         continue;
559       value = unescapeService.unescape(value.trim());
560       let key = el.getAttribute("property") || el.getAttribute("name");
561       if (!key)
562         continue;
563       // There are a wide array of possible meta tags, expressing articles,
564       // products, etc. so all meta tags are passed through but we touch up the
565       // most common attributes.
566       o[key] = value;
567       switch (key) {
568         case "title":
569         case "og:title":
570           o.title = value;
571           break;
572         case "description":
573         case "og:description":
574           o.description = value;
575           break;
576         case "og:site_name":
577           o.siteName = value;
578           break;
579         case "medium":
580         case "og:type":
581           o.medium = value;
582           break;
583         case "og:video":
584           url = this._validateURL(aDocument, value);
585           if (url)
586             o.source = url;
587           break;
588         case "og:url":
589           url = this._validateURL(aDocument, value);
590           if (url)
591             o.url = url;
592           break;
593         case "og:image":
594           url = this._validateURL(aDocument, value);
595           if (url)
596             o.previews.push(url);
597           break;
598       }
599     }
600   },
602   _getLinkData: function(aDocument, o) {
603     let els = aDocument.querySelectorAll("head > link[rel], head > link[id]");
604     for (let el of els) {
605       let url = el.getAttribute("href");
606       if (!url)
607         continue;
608       url = this._validateURL(aDocument, unescapeService.unescape(url.trim()));
609       switch (el.getAttribute("rel") || el.getAttribute("id")) {
610         case "shorturl":
611         case "shortlink":
612           o.shortUrl = url;
613           break;
614         case "canonicalurl":
615         case "canonical":
616           o.url = url;
617           break;
618         case "image_src":
619           o.previews.push(url);
620           break;
621         case "alternate":
622           // expressly for oembed support but we're liberal here and will let
623           // other alternate links through. oembed defines an href, supplied by
624           // the site, where you can fetch additional meta data about a page.
625           // We'll let the client fetch the oembed data themselves, but they
626           // need the data from this link.
627           if (!o.alternate)
628             o.alternate = [];
629           o.alternate.push({
630             "type": el.getAttribute("type"),
631             "href": el.getAttribute("href"),
632             "title": el.getAttribute("title")
633           })
634       }
635     }
636   },
638   // scrape through the page for data we want
639   _getPageData: function(aDocument, o) {
640     if (o.previews.length < 1)
641       o.previews = this._getImageUrls(aDocument);
642   },
644   _validateURL: function(aDocument, url) {
645     let docURI = Services.io.newURI(aDocument.documentURI, null, null);
646     let uri = Services.io.newURI(docURI.resolve(url), null, null);
647     if (["http", "https", "ftp", "ftps"].indexOf(uri.scheme) < 0)
648       return null;
649     uri.userPass = "";
650     return uri.spec;
651   },
653   _getImageUrls: function(aDocument) {
654     let l = [];
655     let els = aDocument.querySelectorAll("img");
656     for (let el of els) {
657       let src = el.getAttribute("src");
658       if (src) {
659         l.push(this._validateURL(aDocument, unescapeService.unescape(src)));
660         // we don't want a billion images
661         if (l.length > 5)
662           break;
663       }
664     }
665     return l;
666   }
669 // getMicrodata (and getObject) based on wg algorythm to convert microdata to json
670 // http://www.whatwg.org/specs/web-apps/current-work/multipage/microdata-2.html#json
671 function  getMicrodata(document, target) {
673   function _getObject(item) {
674     let result = {};
675     if (item.itemType.length)
676       result.types = [i for (i of item.itemType)];
677     if (item.itemId)
678       result.itemId = item.itemid;
679     if (item.properties.length)
680       result.properties = {};
681     for (let elem of item.properties) {
682       let value;
683       if (elem.itemScope)
684         value = _getObject(elem);
685       else if (elem.itemValue)
686         value = elem.itemValue;
687       // handle mis-formatted microdata
688       else if (elem.hasAttribute("content"))
689         value = elem.getAttribute("content");
691       for (let prop of elem.itemProp) {
692         if (!result.properties[prop])
693           result.properties[prop] = [];
694         result.properties[prop].push(value);
695       }
696     }
697     return result;
698   }
700   let result = { items: [] };
701   let elms = target ? [target] : document.getItems();
702   for (let el of elms) {
703     if (el.itemScope)
704       result.items.push(_getObject(el));
705   }
706   return result;