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