tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

utils.js (18259B)


      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 
      5 "use strict";
      6 
      7 loader.lazyRequireGetter(
      8  this,
      9  "colorUtils",
     10  "resource://devtools/shared/css/color.js",
     11  true
     12 );
     13 loader.lazyRequireGetter(
     14  this,
     15  "AsyncUtils",
     16  "resource://devtools/shared/async-utils.js"
     17 );
     18 loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js");
     19 loader.lazyRequireGetter(
     20  this,
     21  "DevToolsUtils",
     22  "resource://devtools/shared/DevToolsUtils.js"
     23 );
     24 loader.lazyRequireGetter(
     25  this,
     26  "nodeFilterConstants",
     27  "resource://devtools/shared/dom-node-filter-constants.js"
     28 );
     29 loader.lazyRequireGetter(
     30  this,
     31  "getAdjustedQuads",
     32  "resource://devtools/shared/layout/utils.js",
     33  true
     34 );
     35 loader.lazyRequireGetter(
     36  this,
     37  "CssLogic",
     38  "resource://devtools/server/actors/inspector/css-logic.js",
     39  true
     40 );
     41 loader.lazyRequireGetter(
     42  this,
     43  "getBackgroundFor",
     44  "resource://devtools/server/actors/accessibility/audit/contrast.js",
     45  true
     46 );
     47 loader.lazyRequireGetter(
     48  this,
     49  ["loadSheetForBackgroundCalculation", "removeSheetForBackgroundCalculation"],
     50  "resource://devtools/server/actors/utils/accessibility.js",
     51  true
     52 );
     53 loader.lazyRequireGetter(
     54  this,
     55  "getTextProperties",
     56  "resource://devtools/shared/accessibility.js",
     57  true
     58 );
     59 
     60 const XHTML_NS = "http://www.w3.org/1999/xhtml";
     61 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     62 const IMAGE_FETCHING_TIMEOUT = 500;
     63 
     64 /**
     65 * Returns the properly cased version of the node's tag name, which can be
     66 * used when displaying said name in the UI.
     67 *
     68 * @param  {Node} rawNode
     69 *         Node for which we want the display name
     70 * @return {string}
     71 *         Properly cased version of the node tag name
     72 */
     73 const getNodeDisplayName = function (rawNode) {
     74  const { implementedPseudoElement } = rawNode;
     75  if (implementedPseudoElement) {
     76    if (
     77      implementedPseudoElement.startsWith("::view-transition") &&
     78      rawNode.hasAttribute("name")
     79    ) {
     80      return `${implementedPseudoElement}(${rawNode.getAttribute("name")})`;
     81    }
     82 
     83    return implementedPseudoElement;
     84  }
     85 
     86  if (rawNode.nodeName && !rawNode.localName) {
     87    // The localName & prefix APIs have been moved from the Node interface to the Element
     88    // interface. Use Node.nodeName as a fallback.
     89    return rawNode.nodeName;
     90  }
     91  return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
     92 };
     93 
     94 /**
     95 * Returns flex and grid information about a DOM node.
     96 * In particular is it a grid flex/container and/or item?
     97 *
     98 * @param  {DOMNode} node
     99 *         The node for which then information is required
    100 * @return {object}
    101 *         An object like { grid: { isContainer, isItem }, flex: { isContainer, isItem } }
    102 */
    103 function getNodeGridFlexType(node) {
    104  return {
    105    grid: getNodeGridType(node),
    106    flex: getNodeFlexType(node),
    107  };
    108 }
    109 
    110 function getNodeFlexType(node) {
    111  return {
    112    isContainer: node.getAsFlexContainer && !!node.getAsFlexContainer(),
    113    isItem: !!node.parentFlexElement,
    114  };
    115 }
    116 
    117 function getNodeGridType(node) {
    118  return {
    119    isContainer: node.hasGridFragments && node.hasGridFragments(),
    120    isItem: !!findGridParentContainerForNode(node),
    121  };
    122 }
    123 
    124 function nodeDocument(node) {
    125  if (Cu.isDeadWrapper(node)) {
    126    return null;
    127  }
    128  return (
    129    node.ownerDocument || (node.nodeType == Node.DOCUMENT_NODE ? node : null)
    130  );
    131 }
    132 
    133 function isNodeDead(node) {
    134  return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
    135 }
    136 
    137 function isInXULDocument(el) {
    138  const doc = nodeDocument(el);
    139  return doc?.documentElement && doc.documentElement.namespaceURI === XUL_NS;
    140 }
    141 
    142 /**
    143 * This DeepTreeWalker filter skips whitespace text nodes and anonymous content (unless
    144 * we want them visible in the markup view, e.g. ::before, ::after, ::marker, …),
    145 * plus anonymous content in XUL document (needed to show all elements in the browser toolbox).
    146 */
    147 function standardTreeWalkerFilter(node) {
    148  // There are a few native anonymous content that we want to show in markup
    149  if (
    150    node.nodeName === "_moz_generated_content_marker" ||
    151    node.nodeName === "_moz_generated_content_before" ||
    152    node.nodeName === "_moz_generated_content_after" ||
    153    node.nodeName === "_moz_generated_content_backdrop"
    154  ) {
    155    return nodeFilterConstants.FILTER_ACCEPT;
    156  }
    157 
    158  // Ignore empty whitespace text nodes that do not impact the layout.
    159  if (isWhitespaceTextNode(node)) {
    160    return nodeHasSize(node)
    161      ? nodeFilterConstants.FILTER_ACCEPT
    162      : nodeFilterConstants.FILTER_SKIP;
    163  }
    164 
    165  if (node.isNativeAnonymous && !isInXULDocument(node)) {
    166    const nodeTypeAttribute = node.getAttribute && node.getAttribute("type");
    167    // The ::view-transition pseudo element node has a <div type=":-moz-snapshot-containing-block">
    168    // parent element that we don't want to display in the markup view.
    169    // Instead, we want to directly display the ::view-transition pseudo-element.
    170    if (nodeTypeAttribute === ":-moz-snapshot-containing-block") {
    171      // FILTER_ACCEPT_CHILDREN means that the node won't be returned, but its children
    172      // will be instead
    173      return nodeFilterConstants.FILTER_ACCEPT_CHILDREN;
    174    }
    175 
    176    // Display all the ::view-transition* nodes
    177    if (nodeTypeAttribute && nodeTypeAttribute.startsWith(":view-transition")) {
    178      return nodeFilterConstants.FILTER_ACCEPT;
    179    }
    180 
    181    // Ignore all other native anonymous roots inside a non-XUL document.
    182    // We need to do this to skip things like form controls, scrollbars,
    183    // video controls, etc (see bug 1187482).
    184    return nodeFilterConstants.FILTER_SKIP;
    185  }
    186 
    187  return nodeFilterConstants.FILTER_ACCEPT;
    188 }
    189 
    190 /**
    191 * This DeepTreeWalker filter ignores anonymous content.
    192 */
    193 function noAnonymousContentTreeWalkerFilter(node) {
    194  // Ignore all native anonymous content inside a non-XUL document.
    195  // We need to do this to skip things like form controls, scrollbars,
    196  // video controls, etc (see bug 1187482).
    197  if (!isInXULDocument(node) && node.isNativeAnonymous) {
    198    return nodeFilterConstants.FILTER_SKIP;
    199  }
    200 
    201  return nodeFilterConstants.FILTER_ACCEPT;
    202 }
    203 /**
    204 * This DeepTreeWalker filter is like standardTreeWalkerFilter except that
    205 * it also includes all anonymous content (like internal form controls).
    206 */
    207 function allAnonymousContentTreeWalkerFilter(node) {
    208  // Ignore empty whitespace text nodes that do not impact the layout.
    209  if (isWhitespaceTextNode(node)) {
    210    return nodeHasSize(node)
    211      ? nodeFilterConstants.FILTER_ACCEPT
    212      : nodeFilterConstants.FILTER_SKIP;
    213  }
    214  return nodeFilterConstants.FILTER_ACCEPT;
    215 }
    216 
    217 /**
    218 * Is the given node a text node composed of whitespace only?
    219 *
    220 * @param {DOMNode} node
    221 * @return {boolean}
    222 */
    223 function isWhitespaceTextNode(node) {
    224  return node.nodeType == Node.TEXT_NODE && !/[^\s]/.exec(node.nodeValue);
    225 }
    226 
    227 /**
    228 * Does the given node have non-0 width and height?
    229 *
    230 * @param {DOMNode} node
    231 * @return {boolean}
    232 */
    233 function nodeHasSize(node) {
    234  if (!node.getBoxQuads) {
    235    return false;
    236  }
    237 
    238  const quads = node.getBoxQuads({
    239    createFramesForSuppressedWhitespace: false,
    240  });
    241  return quads.some(quad => {
    242    const bounds = quad.getBounds();
    243    return bounds.width && bounds.height;
    244  });
    245 }
    246 
    247 /**
    248 * Returns a promise that is settled once the given HTMLImageElement has
    249 * finished loading.
    250 *
    251 * @param {HTMLImageElement} image - The image element.
    252 * @param {number} timeout - Maximum amount of time the image is allowed to load
    253 * before the waiting is aborted. Ignored if flags.testing is set.
    254 *
    255 * @return {Promise} that is fulfilled once the image has loaded. If the image
    256 * fails to load or the load takes too long, the promise is rejected.
    257 */
    258 function ensureImageLoaded(image, timeout) {
    259  const { HTMLImageElement } = image.ownerGlobal;
    260  if (!(image instanceof HTMLImageElement)) {
    261    return Promise.reject("image must be an HTMLImageELement");
    262  }
    263 
    264  if (image.complete) {
    265    // The image has already finished loading.
    266    return Promise.resolve();
    267  }
    268 
    269  // This image is still loading.
    270  const onLoad = AsyncUtils.listenOnce(image, "load");
    271 
    272  // Reject if loading fails.
    273  const onError = AsyncUtils.listenOnce(image, "error").then(() => {
    274    return Promise.reject("Image '" + image.src + "' failed to load.");
    275  });
    276 
    277  // Don't timeout when testing. This is never settled.
    278  let onAbort = new Promise(() => {});
    279 
    280  if (!flags.testing) {
    281    // Tests are not running. Reject the promise after given timeout.
    282    onAbort = DevToolsUtils.waitForTime(timeout).then(() => {
    283      return Promise.reject("Image '" + image.src + "' took too long to load.");
    284    });
    285  }
    286 
    287  // See which happens first.
    288  return Promise.race([onLoad, onError, onAbort]);
    289 }
    290 
    291 /**
    292 * Given an <img> or <canvas> element, return the image data-uri. If @param node
    293 * is an <img> element, the method waits a while for the image to load before
    294 * the data is generated. If the image does not finish loading in a reasonable
    295 * time (IMAGE_FETCHING_TIMEOUT milliseconds) the process aborts.
    296 *
    297 * @param {HTMLImageElement|HTMLCanvasElement} node - The <img> or <canvas>
    298 * element, or Image() object. Other types cause the method to reject.
    299 * @param {number} maxDim - Optionally pass a maximum size you want the longest
    300 * side of the image to be resized to before getting the image data.
    301 
    302 * @return {Promise} A promise that is fulfilled with an object containing the
    303 * data-uri and size-related information:
    304 * { data: "...",
    305 *   size: {
    306 *     naturalWidth: 400,
    307 *     naturalHeight: 300,
    308 *     resized: true }
    309 *  }.
    310 *
    311 * If something goes wrong, the promise is rejected.
    312 */
    313 const imageToImageData = async function (node, maxDim) {
    314  const { HTMLCanvasElement, HTMLImageElement } = node.ownerGlobal;
    315 
    316  const isImg = node instanceof HTMLImageElement;
    317  const isCanvas = node instanceof HTMLCanvasElement;
    318 
    319  if (!isImg && !isCanvas) {
    320    throw new Error("node is not a <canvas> or <img> element.");
    321  }
    322 
    323  if (isImg) {
    324    // Ensure that the image is ready.
    325    await ensureImageLoaded(node, IMAGE_FETCHING_TIMEOUT);
    326  }
    327 
    328  // Get the image resize ratio if a maxDim was provided
    329  let resizeRatio = 1;
    330  const imgWidth = node.naturalWidth || node.width;
    331  const imgHeight = node.naturalHeight || node.height;
    332  const imgMax = Math.max(imgWidth, imgHeight);
    333  if (maxDim && imgMax > maxDim) {
    334    resizeRatio = maxDim / imgMax;
    335  }
    336 
    337  // Extract the image data
    338  let imageData;
    339  // The image may already be a data-uri, in which case, save ourselves the
    340  // trouble of converting via the canvas.drawImage.toDataURL method, but only
    341  // if the image doesn't need resizing
    342  if (isImg && node.src.startsWith("data:") && resizeRatio === 1) {
    343    imageData = node.src;
    344  } else {
    345    // Create a canvas to copy the rawNode into and get the imageData from
    346    const canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas");
    347    canvas.width = imgWidth * resizeRatio;
    348    canvas.height = imgHeight * resizeRatio;
    349    const ctx = canvas.getContext("2d");
    350 
    351    // Copy the rawNode image or canvas in the new canvas and extract data
    352    ctx.drawImage(node, 0, 0, canvas.width, canvas.height);
    353    imageData = canvas.toDataURL("image/png");
    354  }
    355 
    356  return {
    357    data: imageData,
    358    size: {
    359      naturalWidth: imgWidth,
    360      naturalHeight: imgHeight,
    361      resized: resizeRatio !== 1,
    362    },
    363  };
    364 };
    365 
    366 /**
    367 * Finds the computed background color of the closest parent with a set background color.
    368 *
    369 * @param  {DOMNode}  node
    370 *         Node for which we want to find closest background color.
    371 * @return {string}
    372 *         String with the background color of the form rgba(r, g, b, a). Defaults to
    373 *         rgba(255, 255, 255, 1) if no background color is found.
    374 */
    375 function getClosestBackgroundColor(node) {
    376  let current = node;
    377 
    378  while (current) {
    379    const computedStyle = CssLogic.getComputedStyle(current);
    380    if (computedStyle) {
    381      const currentStyle = computedStyle.getPropertyValue("background-color");
    382      if (InspectorUtils.isValidCSSColor(currentStyle)) {
    383        const currentCssColor = new colorUtils.CssColor(currentStyle);
    384        if (!currentCssColor.isTransparent()) {
    385          return currentCssColor.rgba;
    386        }
    387      }
    388    }
    389 
    390    current = current.parentNode;
    391  }
    392 
    393  return "rgba(255, 255, 255, 1)";
    394 }
    395 
    396 /**
    397 * Finds the background image of the closest parent where it is set.
    398 *
    399 * @param  {DOMNode}  node
    400 *         Node for which we want to find the background image.
    401 * @return {string}
    402 *         String with the value of the background iamge property. Defaults to "none" if
    403 *         no background image is found.
    404 */
    405 function getClosestBackgroundImage(node) {
    406  let current = node;
    407 
    408  while (current) {
    409    const computedStyle = CssLogic.getComputedStyle(current);
    410    if (computedStyle) {
    411      const currentBackgroundImage =
    412        computedStyle.getPropertyValue("background-image");
    413      if (currentBackgroundImage !== "none") {
    414        return currentBackgroundImage;
    415      }
    416    }
    417 
    418    current = current.parentNode;
    419  }
    420 
    421  return "none";
    422 }
    423 
    424 /**
    425 * If the provided node is a grid item, then return its parent grid.
    426 *
    427 * @param  {DOMNode} node
    428 *         The node that is supposedly a grid item.
    429 * @return {DOMNode|null}
    430 *         The parent grid if found, null otherwise.
    431 */
    432 function findGridParentContainerForNode(node) {
    433  try {
    434    while ((node = node.parentNode)) {
    435      const display = node.ownerGlobal.getComputedStyle(node).display;
    436 
    437      if (display.includes("grid")) {
    438        return node;
    439      } else if (display === "contents") {
    440        // Continue walking up the tree since the parent node is a content element.
    441        continue;
    442      }
    443 
    444      break;
    445    }
    446  } catch (e) {
    447    // Getting the parentNode can fail when the supplied node is in shadow DOM.
    448  }
    449 
    450  return null;
    451 }
    452 
    453 /**
    454 * Finds the background color range for the parent of a single text node
    455 * (i.e. for multi-colored backgrounds with gradients, images) or a single
    456 * background color for single-colored backgrounds. Defaults to the closest
    457 * background color if an error is encountered.
    458 *
    459 * @param  {object}
    460 *         Node actor containing the following properties:
    461 *         {DOMNode} rawNode
    462 *         Node for which we want to calculate the color contrast.
    463 *         {WalkerActor} walker
    464 *         Walker actor used to check whether the node is the parent elm of a single text node.
    465 * @return {object}
    466 *         Object with one or more of the following properties:
    467 *         {Array|null} value
    468 *         RGBA array for single-colored background. Null for multi-colored backgrounds.
    469 *         {Array|null} min
    470 *         RGBA array for the min luminance color in a multi-colored background.
    471 *         Null for single-colored backgrounds.
    472 *         {Array|null} max
    473 *         RGBA array for the max luminance color in a multi-colored background.
    474 *         Null for single-colored backgrounds.
    475 */
    476 async function getBackgroundColor({ rawNode: node, walker }) {
    477  // Fall back to calculating contrast against closest bg if:
    478  // - not element node
    479  // - more than one child
    480  // Avoid calculating bounds and creating doc walker by returning early.
    481  if (
    482    node.nodeType != Node.ELEMENT_NODE ||
    483    node.childNodes.length > 1 ||
    484    !node.firstChild
    485  ) {
    486    return {
    487      value: getClosestBackgroundColorInRGBA(node),
    488    };
    489  }
    490 
    491  const quads = getAdjustedQuads(node.ownerGlobal, node.firstChild, "content");
    492 
    493  // Fall back to calculating contrast against closest bg if there are no bounds for text node.
    494  // Avoid creating doc walker by returning early.
    495  if (quads.length === 0 || !quads[0].bounds) {
    496    return {
    497      value: getClosestBackgroundColorInRGBA(node),
    498    };
    499  }
    500 
    501  const bounds = quads[0].bounds;
    502 
    503  const docWalker = walker.getDocumentWalker(node);
    504  const firstChild = docWalker.firstChild();
    505 
    506  // Fall back to calculating contrast against closest bg if:
    507  // - more than one child
    508  // - unique child is not a text node
    509  if (
    510    !firstChild ||
    511    docWalker.nextSibling() ||
    512    firstChild.nodeType !== Node.TEXT_NODE
    513  ) {
    514    return {
    515      value: getClosestBackgroundColorInRGBA(node),
    516    };
    517  }
    518 
    519  // Try calculating complex backgrounds for node
    520  const win = node.ownerGlobal;
    521  loadSheetForBackgroundCalculation(win);
    522  const computedStyle = CssLogic.getComputedStyle(node);
    523  const props = computedStyle ? getTextProperties(computedStyle) : null;
    524 
    525  // Fall back to calculating contrast against closest bg if there are no text props.
    526  if (!props) {
    527    return {
    528      value: getClosestBackgroundColorInRGBA(node),
    529    };
    530  }
    531 
    532  const bgColor = await getBackgroundFor(node, {
    533    bounds,
    534    win,
    535    convertBoundsRelativeToViewport: false,
    536    size: props.size,
    537    isBoldText: props.isBoldText,
    538  });
    539  removeSheetForBackgroundCalculation(win);
    540 
    541  return (
    542    bgColor || {
    543      value: getClosestBackgroundColorInRGBA(node),
    544    }
    545  );
    546 }
    547 
    548 /**
    549 *
    550 * @param {DOMNode} node: The node we want the background color of
    551 * @returns {Array[r,g,b,a]}
    552 */
    553 function getClosestBackgroundColorInRGBA(node) {
    554  const { r, g, b, a } = InspectorUtils.colorToRGBA(
    555    getClosestBackgroundColor(node)
    556  );
    557  return [r, g, b, a];
    558 }
    559 /**
    560 * Indicates if a document is ready (i.e. if it's not loading anymore)
    561 *
    562 * @param {HTMLDocument} document: The document we want to check
    563 * @returns {boolean}
    564 */
    565 function isDocumentReady(document) {
    566  if (!document) {
    567    return false;
    568  }
    569 
    570  const { readyState } = document;
    571  if (readyState == "interactive" || readyState == "complete") {
    572    return true;
    573  }
    574 
    575  // A document might stay forever in uninitialized state.
    576  // If the target actor is not currently loading a document,
    577  // assume the document is ready.
    578  const webProgress = document.defaultView.docShell.QueryInterface(
    579    Ci.nsIWebProgress
    580  );
    581  return !webProgress.isLoadingDocument;
    582 }
    583 
    584 module.exports = {
    585  allAnonymousContentTreeWalkerFilter,
    586  isDocumentReady,
    587  isWhitespaceTextNode,
    588  findGridParentContainerForNode,
    589  getBackgroundColor,
    590  getClosestBackgroundColor,
    591  getClosestBackgroundImage,
    592  getNodeDisplayName,
    593  getNodeGridFlexType,
    594  imageToImageData,
    595  isNodeDead,
    596  nodeDocument,
    597  standardTreeWalkerFilter,
    598  noAnonymousContentTreeWalkerFilter,
    599 };