Bug 1796551 [wpt PR 36570] - WebKit export of https://bugs.webkit.org/show_bug.cgi...
[gecko.git] / toolkit / actors / ContentMetaChild.jsm
blob1d20edd3d1c14bfaf1a96ca1025e38f4154be18c
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
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const EXPORTED_SYMBOLS = ["ContentMetaChild"];
9 // Debounce time in milliseconds - this should be long enough to account for
10 // sync script tags that could appear between desired meta tags
11 const TIMEOUT_DELAY = 1000;
13 const ACCEPTED_PROTOCOLS = ["http:", "https:"];
15 // Possible description tags, listed in order from least favourable to most favourable
16 const DESCRIPTION_RULES = [
17   "twitter:description",
18   "description",
19   "og:description",
22 // Possible image tags, listed in order from least favourable to most favourable
23 const PREVIEW_IMAGE_RULES = [
24   "thumbnail",
25   "twitter:image",
26   "og:image",
27   "og:image:url",
28   "og:image:secure_url",
32  * Checks if the incoming meta tag has a greater score than the current best
33  * score by checking the index of the meta tag in the list of rules provided.
34  *
35  * @param {Array} aRules
36  *          The list of rules for a given type of meta tag
37  * @param {String} aTag
38  *          The name or property of the incoming meta tag
39  * @param {String} aEntry
40  *          The current best entry for the given meta tag
41  *
42  * @returns {Boolean} true if the incoming meta tag is better than the current
43  *                    best meta tag of that same kind, false otherwise
44  */
45 function shouldExtractMetadata(aRules, aTag, aEntry) {
46   return aRules.indexOf(aTag) > aEntry.currMaxScore;
50  * Ensure that the preview image URL is safe and valid before storing
51  *
52  * @param {URL} aURL
53  *          A URL object that needs to be checked for valid principal and protocol
54  *
55  * @returns {Boolean} true if the preview URL is safe and can be stored, false otherwise
56  */
57 function checkLoadURIStr(aURL) {
58   if (!ACCEPTED_PROTOCOLS.includes(aURL.protocol)) {
59     return false;
60   }
61   try {
62     let ssm = Services.scriptSecurityManager;
63     let principal = ssm.createNullPrincipal({});
64     ssm.checkLoadURIStrWithPrincipal(
65       principal,
66       aURL.href,
67       ssm.DISALLOW_INHERIT_PRINCIPAL
68     );
69   } catch (e) {
70     return false;
71   }
72   return true;
76  * This listens to DOMMetaAdded events and collects relevant metadata about the
77  * meta tag received. Then, it sends the metadata gathered from the meta tags
78  * and the url of the page as it's payload to be inserted into moz_places.
79  */
80 class ContentMetaChild extends JSWindowActorChild {
81   constructor() {
82     super();
84     // Store a mapping of the best description and preview
85     // image collected so far for a given URL.
86     this.metaTags = new Map();
87   }
89   didDestroy() {
90     for (let entry of this.metaTags.values()) {
91       entry.timeout.cancel();
92     }
93   }
95   handleEvent(event) {
96     switch (event.type) {
97       case "DOMContentLoaded":
98         const metaTags = this.contentWindow.document.querySelectorAll("meta");
99         for (let metaTag of metaTags) {
100           this.onMetaTag(metaTag);
101         }
102         break;
103       case "DOMMetaAdded":
104         this.onMetaTag(event.originalTarget);
105         break;
106       default:
107     }
108   }
110   onMetaTag(metaTag) {
111     const window = metaTag.ownerGlobal;
113     // If there's no meta tag, ignore this. Also verify that the window
114     // matches just to be safe.
115     if (!metaTag || !metaTag.ownerDocument || window != this.contentWindow) {
116       return;
117     }
119     const url = metaTag.ownerDocument.documentURI;
121     let name = metaTag.name;
122     let prop = metaTag.getAttributeNS(null, "property");
123     if (!name && !prop) {
124       return;
125     }
127     let tag = name || prop;
129     const entry = this.metaTags.get(url) || {
130       description: { value: null, currMaxScore: -1 },
131       image: { value: null, currMaxScore: -1 },
132       timeout: null,
133     };
135     // Malformed meta tag - do not store it
136     const content = metaTag.getAttributeNS(null, "content");
137     if (!content) {
138       return;
139     }
141     if (shouldExtractMetadata(DESCRIPTION_RULES, tag, entry.description)) {
142       // Extract the description
143       entry.description.value = content;
144       entry.description.currMaxScore = DESCRIPTION_RULES.indexOf(tag);
145     } else if (shouldExtractMetadata(PREVIEW_IMAGE_RULES, tag, entry.image)) {
146       // Extract the preview image
147       let value;
148       try {
149         value = new URL(content, url);
150       } catch (e) {
151         return;
152       }
153       if (value && checkLoadURIStr(value)) {
154         entry.image.value = value.href;
155         entry.image.currMaxScore = PREVIEW_IMAGE_RULES.indexOf(tag);
156       }
157     } else {
158       // We don't care about other meta tags
159       return;
160     }
162     if (!this.metaTags.has(url)) {
163       this.metaTags.set(url, entry);
164     }
166     if (entry.timeout) {
167       entry.timeout.delay = TIMEOUT_DELAY;
168     } else {
169       // We want to debounce incoming meta tags until we're certain we have the
170       // best one for description and preview image, and only store that one
171       entry.timeout = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
172       entry.timeout.initWithCallback(
173         () => {
174           entry.timeout = null;
175           this.metaTags.delete(url);
176           // We try to cancel the timers when we get destroyed, but if
177           // there's a race, catch it:
178           if (!this.manager || this.manager.isClosed) {
179             return;
180           }
182           // Save description and preview image to moz_places
183           this.sendAsyncMessage("Meta:SetPageInfo", {
184             url,
185             description: entry.description.value,
186             previewImageURL: entry.image.value,
187           });
189           // Telemetry for recording the size of page metadata
190           let metadataSize = entry.description.value
191             ? entry.description.value.length
192             : 0;
193           metadataSize += entry.image.value ? entry.image.value.length : 0;
194           Services.telemetry
195             .getHistogramById("PAGE_METADATA_SIZE")
196             .add(metadataSize);
197         },
198         TIMEOUT_DELAY,
199         Ci.nsITimer.TYPE_ONE_SHOT
200       );
201     }
202   }