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