Bug 1822393 - Consume GeckoView directly in Android Components for CI builds. r=owlis...
[gecko.git] / browser / actors / PageInfoChild.sys.mjs
blobaee59ef295b438608c36dba49d80ce1478303f99
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 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
9   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
10   setTimeout: "resource://gre/modules/Timer.sys.mjs",
11 });
13 export class PageInfoChild extends JSWindowActorChild {
14   async receiveMessage(message) {
15     let window = this.contentWindow;
16     let document = window.document;
18     //Handles two different types of messages: one for general info (PageInfo:getData)
19     //and one for media info (PageInfo:getMediaData)
20     switch (message.name) {
21       case "PageInfo:getData": {
22         return Promise.resolve({
23           metaViewRows: this.getMetaInfo(document),
24           docInfo: this.getDocumentInfo(document),
25           windowInfo: this.getWindowInfo(window),
26         });
27       }
28       case "PageInfo:getMediaData": {
29         return Promise.resolve({
30           mediaItems: await this.getDocumentMedia(document),
31         });
32       }
33       case "PageInfo:getPartitionKey": {
34         return Promise.resolve({
35           partitionKey: await this.getPartitionKey(document),
36         });
37       }
38     }
40     return undefined;
41   }
43   getPartitionKey(document) {
44     let partitionKey = document.cookieJarSettings.partitionKey;
45     return partitionKey;
46   }
48   getMetaInfo(document) {
49     let metaViewRows = [];
51     // Get the meta tags from the page.
52     let metaNodes = document.getElementsByTagName("meta");
54     for (let metaNode of metaNodes) {
55       metaViewRows.push([
56         metaNode.name ||
57           metaNode.httpEquiv ||
58           metaNode.getAttribute("property"),
59         metaNode.content,
60       ]);
61     }
63     return metaViewRows;
64   }
66   getWindowInfo(window) {
67     let windowInfo = {};
68     windowInfo.isTopWindow = window == window.top;
70     let hostName = null;
71     try {
72       hostName = Services.io.newURI(window.location.href).displayHost;
73     } catch (exception) {}
75     windowInfo.hostName = hostName;
76     return windowInfo;
77   }
79   getDocumentInfo(document) {
80     let docInfo = {};
81     docInfo.title = document.title;
82     docInfo.location = document.location.toString();
83     try {
84       docInfo.location = Services.io.newURI(
85         document.location.toString()
86       ).displaySpec;
87     } catch (exception) {}
88     docInfo.referrer = document.referrer;
89     try {
90       if (document.referrer) {
91         docInfo.referrer = Services.io.newURI(document.referrer).displaySpec;
92       }
93     } catch (exception) {}
94     docInfo.compatMode = document.compatMode;
95     docInfo.contentType = document.contentType;
96     docInfo.characterSet = document.characterSet;
97     docInfo.lastModified = document.lastModified;
98     docInfo.principal = document.nodePrincipal;
99     docInfo.cookieJarSettings = lazy.E10SUtils.serializeCookieJarSettings(
100       document.cookieJarSettings
101     );
103     let documentURIObject = {};
104     documentURIObject.spec = document.documentURIObject.spec;
105     docInfo.documentURIObject = documentURIObject;
107     docInfo.isContentWindowPrivate =
108       lazy.PrivateBrowsingUtils.isContentWindowPrivate(document.ownerGlobal);
110     return docInfo;
111   }
113   /**
114    * Returns an array that stores all mediaItems found in the document
115    * Calls getMediaItems for all nodes within the constructed tree walker and forms
116    * resulting array.
117    */
118   async getDocumentMedia(document) {
119     let nodeCount = 0;
120     let content = document.ownerGlobal;
121     let iterator = document.createTreeWalker(
122       document,
123       content.NodeFilter.SHOW_ELEMENT
124     );
126     let totalMediaItems = [];
128     while (iterator.nextNode()) {
129       let mediaItems = this.getMediaItems(document, iterator.currentNode);
131       if (++nodeCount % 500 == 0) {
132         // setTimeout every 500 elements so we don't keep blocking the content process.
133         await new Promise(resolve => lazy.setTimeout(resolve, 10));
134       }
135       totalMediaItems.push(...mediaItems);
136     }
138     return totalMediaItems;
139   }
141   getMediaItems(document, elem) {
142     // Check for images defined in CSS (e.g. background, borders)
143     let computedStyle = elem.ownerGlobal.getComputedStyle(elem);
144     // A node can have multiple media items associated with it - for example,
145     // multiple background images.
146     let mediaItems = [];
147     let content = document.ownerGlobal;
149     let addMedia = (url, type, alt, el, isBg, altNotProvided = false) => {
150       let element = this.serializeElementInfo(document, url, el, isBg);
151       mediaItems.push({
152         url,
153         type,
154         alt,
155         altNotProvided,
156         element,
157         isBg,
158       });
159     };
161     if (computedStyle) {
162       let addImgFunc = (type, urls) => {
163         for (let url of urls) {
164           addMedia(url, type, "", elem, true, true);
165         }
166       };
167       // FIXME: This is missing properties. See the implementation of
168       // getCSSImageURLs for a list of properties.
169       //
170       // If you don't care about the message you can also pass "all" here and
171       // get all the ones the browser knows about.
172       addImgFunc("bg-img", computedStyle.getCSSImageURLs("background-image"));
173       addImgFunc(
174         "border-img",
175         computedStyle.getCSSImageURLs("border-image-source")
176       );
177       addImgFunc("list-img", computedStyle.getCSSImageURLs("list-style-image"));
178       addImgFunc("cursor", computedStyle.getCSSImageURLs("cursor"));
179     }
181     // One swi^H^H^Hif-else to rule them all.
182     if (content.HTMLImageElement.isInstance(elem)) {
183       addMedia(
184         elem.currentSrc,
185         "img",
186         elem.getAttribute("alt"),
187         elem,
188         false,
189         !elem.hasAttribute("alt")
190       );
191     } else if (content.SVGImageElement.isInstance(elem)) {
192       try {
193         // Note: makeURLAbsolute will throw if either the baseURI is not a valid URI
194         //       or the URI formed from the baseURI and the URL is not a valid URI.
195         if (elem.href.baseVal) {
196           let href = Services.io.newURI(
197             elem.href.baseVal,
198             null,
199             Services.io.newURI(elem.baseURI)
200           ).spec;
201           addMedia(href, "img", "", elem, false);
202         }
203       } catch (e) {}
204     } else if (content.HTMLVideoElement.isInstance(elem)) {
205       addMedia(elem.currentSrc, "video", "", elem, false);
206     } else if (content.HTMLAudioElement.isInstance(elem)) {
207       addMedia(elem.currentSrc, "audio", "", elem, false);
208     } else if (content.HTMLLinkElement.isInstance(elem)) {
209       if (elem.rel && /\bicon\b/i.test(elem.rel)) {
210         addMedia(elem.href, "link", "", elem, false);
211       }
212     } else if (
213       content.HTMLInputElement.isInstance(elem) ||
214       content.HTMLButtonElement.isInstance(elem)
215     ) {
216       if (elem.type.toLowerCase() == "image") {
217         addMedia(
218           elem.src,
219           "input",
220           elem.getAttribute("alt"),
221           elem,
222           false,
223           !elem.hasAttribute("alt")
224         );
225       }
226     } else if (content.HTMLObjectElement.isInstance(elem)) {
227       addMedia(elem.data, "object", this.getValueText(elem), elem, false);
228     } else if (content.HTMLEmbedElement.isInstance(elem)) {
229       addMedia(elem.src, "embed", "", elem, false);
230     }
232     return mediaItems;
233   }
235   /**
236    * Set up a JSON element object with all the instanceOf and other infomation that
237    * makePreview in pageInfo.js uses to figure out how to display the preview.
238    */
240   serializeElementInfo(document, url, item, isBG) {
241     let result = {};
242     let content = document.ownerGlobal;
244     let imageText;
245     if (
246       !isBG &&
247       !content.SVGImageElement.isInstance(item) &&
248       !content.ImageDocument.isInstance(document)
249     ) {
250       imageText = item.title || item.alt;
252       if (!imageText && !content.HTMLImageElement.isInstance(item)) {
253         imageText = this.getValueText(item);
254       }
255     }
257     result.imageText = imageText;
258     result.longDesc = item.longDesc;
259     result.numFrames = 1;
261     if (
262       content.HTMLObjectElement.isInstance(item) ||
263       content.HTMLEmbedElement.isInstance(item) ||
264       content.HTMLLinkElement.isInstance(item)
265     ) {
266       result.mimeType = item.type;
267     }
269     if (
270       !result.mimeType &&
271       !isBG &&
272       item instanceof Ci.nsIImageLoadingContent
273     ) {
274       // Interface for image loading content.
275       let imageRequest = item.getRequest(
276         Ci.nsIImageLoadingContent.CURRENT_REQUEST
277       );
278       if (imageRequest) {
279         result.mimeType = imageRequest.mimeType;
280         let image =
281           !(imageRequest.imageStatus & imageRequest.STATUS_ERROR) &&
282           imageRequest.image;
283         if (image) {
284           result.numFrames = image.numFrames;
285         }
286       }
287     }
289     // If we have a data url, get the MIME type from the url.
290     if (!result.mimeType && url.startsWith("data:")) {
291       let dataMimeType = /^data:(image\/[^;,]+)/i.exec(url);
292       if (dataMimeType) {
293         result.mimeType = dataMimeType[1].toLowerCase();
294       }
295     }
297     result.HTMLLinkElement = content.HTMLLinkElement.isInstance(item);
298     result.HTMLInputElement = content.HTMLInputElement.isInstance(item);
299     result.HTMLImageElement = content.HTMLImageElement.isInstance(item);
300     result.HTMLObjectElement = content.HTMLObjectElement.isInstance(item);
301     result.SVGImageElement = content.SVGImageElement.isInstance(item);
302     result.HTMLVideoElement = content.HTMLVideoElement.isInstance(item);
303     result.HTMLAudioElement = content.HTMLAudioElement.isInstance(item);
305     if (isBG) {
306       // Items that are showing this image as a background
307       // image might not necessarily have a width or height,
308       // so we'll dynamically generate an image and send up the
309       // natural dimensions.
310       let img = content.document.createElement("img");
311       img.src = url;
312       result.naturalWidth = img.naturalWidth;
313       result.naturalHeight = img.naturalHeight;
314     } else if (!content.SVGImageElement.isInstance(item)) {
315       // SVG items do not have integer values for height or width,
316       // so we must handle them differently in order to correctly
317       // serialize
319       // Otherwise, we can use the current width and height
320       // of the image.
321       result.width = item.width;
322       result.height = item.height;
323     }
325     if (content.SVGImageElement.isInstance(item)) {
326       result.SVGImageElementWidth = item.width.baseVal.value;
327       result.SVGImageElementHeight = item.height.baseVal.value;
328     }
330     result.baseURI = item.baseURI;
332     return result;
333   }
335   // Other Misc Stuff
336   // Modified from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html
337   // parse a node to extract the contents of the node
338   getValueText(node) {
339     let valueText = "";
340     let content = node.ownerGlobal;
342     // Form input elements don't generally contain information that is useful to our callers, so return nothing.
343     if (
344       content.HTMLInputElement.isInstance(node) ||
345       content.HTMLSelectElement.isInstance(node) ||
346       content.HTMLTextAreaElement.isInstance(node)
347     ) {
348       return valueText;
349     }
351     // Otherwise recurse for each child.
352     let length = node.childNodes.length;
354     for (let i = 0; i < length; i++) {
355       let childNode = node.childNodes[i];
356       let nodeType = childNode.nodeType;
358       // Text nodes are where the goods are.
359       if (nodeType == content.Node.TEXT_NODE) {
360         valueText += " " + childNode.nodeValue;
361       } else if (nodeType == content.Node.ELEMENT_NODE) {
362         // And elements can have more text inside them.
363         // Images are special, we want to capture the alt text as if the image weren't there.
364         if (content.HTMLImageElement.isInstance(childNode)) {
365           valueText += " " + this.getAltText(childNode);
366         } else {
367           valueText += " " + this.getValueText(childNode);
368         }
369       }
370     }
372     return this.stripWS(valueText);
373   }
375   // Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html.
376   // Traverse the tree in search of an img or area element and grab its alt tag.
377   getAltText(node) {
378     let altText = "";
380     if (node.alt) {
381       return node.alt;
382     }
383     let length = node.childNodes.length;
384     for (let i = 0; i < length; i++) {
385       if ((altText = this.getAltText(node.childNodes[i]) != undefined)) {
386         // stupid js warning...
387         return altText;
388       }
389     }
390     return "";
391   }
393   // Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html.
394   // Strip leading and trailing whitespace, and replace multiple consecutive whitespace characters with a single space.
395   stripWS(text) {
396     let middleRE = /\s+/g;
397     let endRE = /(^\s+)|(\s+$)/g;
399     text = text.replace(middleRE, " ");
400     return text.replace(endRE, "");
401   }