// Copyright 2017 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/**
 * @fileoverview A base class to safely parse and transform an HTML string
 * using an inert DOM, which avoids executing scripts and loading images. Note:
 * this class does *not* guarantee that the output does not contain scripts and
 * images that eventually execute once the output is inserted into an active DOM
 * document. If any subclass claims to produce SafeHtml output, it must be
 * reviewed separately.
 * @supported IE 10+, Chrome 26+, Firefox 22+, Safari 7.1+, Opera 15+
 */

goog.module('goog.html.sanitizer.SafeDomTreeProcessor');
goog.module.declareLegacyNamespace();

var ElementWeakMap = goog.require('goog.html.sanitizer.ElementWeakMap');
var NodeType = goog.require('goog.dom.NodeType');
var TagName = goog.require('goog.dom.TagName');
var googDom = goog.require('goog.dom');
var googLog = goog.require('goog.log');
var noclobber = goog.require('goog.html.sanitizer.noclobber');
var userAgent = goog.require('goog.userAgent');

/** @const {?googLog.Logger} */
var logger = googLog.getLogger('goog.html.sanitizer.SafeDomTreeProcessor');

/**
 * Whether the HTML sanitizer is supported. For now mainly exclude
 * IE9 or below, for which we know the sanitizer is insecure or broken.
 * @const {boolean}
 */
var SAFE_PARSING_SUPPORTED =
    !userAgent.IE || userAgent.isDocumentModeOrHigher(10);

/**
 * Parses a string of unsanitized HTML and provides an iterator over the
 * resulting DOM tree nodes. The parsing operation is inert (that is,
 * it does not cause execution of any active content or cause the browser to
 * issue any requests). The returned iterator is guaranteed to iterate over a
 * parent element before iterating over any of its children.
 * @param {string} html
 * @return {!TreeWalker}
 */
function getDomTreeWalker(html) {
  var iteratorParent;
  // Use a <template> element if possible.
  var templateElement = document.createElement('template');
  if ('content' in templateElement) {
    templateElement.innerHTML = html;
    iteratorParent = templateElement.content;
  } else {
    // In browsers where <template> is not implemented, use an inert
    // HTMLDocument.
    var doc = document.implementation.createHTMLDocument('x');
    iteratorParent = doc.body;
    doc.body.innerHTML = html;
  }
  return document.createTreeWalker(
      iteratorParent, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
      null /* filter */, false /* entityReferenceExpansion */);
}

/**
 * Constructs a {@link SafeDomTreeProcessor} object that safely parses an input
 * string into a DOM tree using an inert document, and creates a new tree based
 * on the original tree, optionally transforming it in the process. The
 * transformation is not specified in this abstract class; subclasses are
 * supposed to override its protected methods to define a transformation that
 * allows tags and attributes, drops entire subtrees, modifies tag names or
 * attributes, etc.
 * @constructor @struct @abstract
 */
var SafeDomTreeProcessor = function() {};

/**
 * Parses an HTML string and walks the resulting DOM forest to apply the
 * transformation function and generate a new forest. Returns the string
 * representation of the forest.
 * @param {string} html
 * @return {string}
 * @protected @final
 */
SafeDomTreeProcessor.prototype.processToString = function(html) {
  if (!SAFE_PARSING_SUPPORTED) {
    return '';
  }

  var newTree = this.processToTree(html);
  if (noclobber.getElementAttributes(newTree).length > 0) {
    // We want to preserve the outer SPAN tag, because the processor has
    // attached attributes to it. To do so, we make a new SPAN tag the parent of
    // the existing root span tag, so that the rest of the function will remove
    // that one instead.
    var newRoot = googDom.createElement(TagName.SPAN);
    newRoot.appendChild(newTree);
    newTree = newRoot;
  }
  // The XMLSerializer will add a spurious xmlns attribute to the root node.
  var serializedNewTree = new XMLSerializer().serializeToString(newTree);
  // Remove the outer span before returning the string representation of the
  // processed copy.
  return serializedNewTree.slice(
      serializedNewTree.indexOf('>') + 1, serializedNewTree.lastIndexOf('</'));
};

/**
 * Parses an HTML string and walks the resulting DOM forest to apply the
 * transformation function and generate a copy of the forest. Returns the forest
 * wrapped in a common SPAN parent, so that the result is always a tree.
 * @param {string} html
 * @return {!HTMLSpanElement}
 * @protected @final
 */
SafeDomTreeProcessor.prototype.processToTree = function(html) {
  if (!SAFE_PARSING_SUPPORTED) {
    return googDom.createElement(TagName.SPAN);
  }
  var newRoot = googDom.createElement(TagName.SPAN);
  // Allow subclasses to attach properties to the root.
  this.processRoot(newRoot);

  // Allow subclasses to pre-process the HTML string before performing the main
  // tree-based transformation.
  html = this.preProcessHtml(html);
  var originalTreeWalker = getDomTreeWalker(html);

  // Mapping from original nodes to new nodes, used to find the parent to which
  // a newly processed node should be attached.
  var elementMap = ElementWeakMap.newWeakMap();

  var originalNode;
  while (originalNode = originalTreeWalker.nextNode()) {
    // Make a copy of the node, potentially dropping it or changing its content,
    // tag name, etc.
    var newNode = this.createNode_(originalNode);
    if (!newNode) {
      // The transformation function chose not to copy over the node. We delete
      // the children so that the current treeWalker will stop iterating on
      // them.
      googDom.removeChildren(originalNode);
      continue;
    }
    if (noclobber.isNodeElement(newNode)) {
      elementMap.set(originalNode, newNode);
    }

    // Finds the new parent to which newNode should be appended. The tree is
    // copied top-down, so the parent of the current node has already been
    // copied and placed into the new tree. The new parent is either the root
    // of the new tree or a node found using originalToNewElementMap.
    var originalParent = noclobber.getParentNode(originalNode);
    var isParentRoot = false;
    if (originalParent) {
      var originalParentNodeType = noclobber.getNodeType(originalParent);
      var originalParentNodeName =
          noclobber.getNodeName(originalParent).toLowerCase();
      var originalGrandParent = noclobber.getParentNode(originalParent);
      // The following checks if newParent is an immediate child of the inert
      // parent template element.
      if (originalParentNodeType == NodeType.DOCUMENT_FRAGMENT &&
          !originalGrandParent) {
        isParentRoot = true;
      } else if (originalParentNodeName == 'body' && originalGrandParent) {
        // The following checks if newParent is an immediate child of the
        // inert parent HtmlDocument.
        var dirtyGreatGrandParent =
            noclobber.getParentNode(originalGrandParent);
        if (dirtyGreatGrandParent &&
            !noclobber.getParentNode(dirtyGreatGrandParent)) {
          isParentRoot = true;
        }
      }
      var newParent = null;
      if (isParentRoot || !originalParent) {
        newParent = newRoot;
      } else if (noclobber.isNodeElement(originalParent)) {
        newParent = elementMap.get(originalParent);
      }
      if (newParent.content) {
        newParent = newParent.content;
      }
      newParent.appendChild(newNode);
    }
  }
  if (elementMap.clear) {
    // Clear the map. On browsers that don't support WeakMap, entries are not
    // automatically cleaned up.
    elementMap.clear();
  }
  return newRoot;
};

/**
 * Creates the root SPAN element for the new tree. This function can be
 * overridden to add attributes to the tag. Note that if any attributes are
 * added to the element, then {@link processToString} will not strip it from the
 * generated string to preserve the attributes.
 * @param {!HTMLSpanElement} newRoot
 * @protected @abstract
 */
SafeDomTreeProcessor.prototype.processRoot = function(newRoot) {};

/**
 * Pre-processes the input html before the main tree-based transformation.
 * @param {string} html
 * @return {string}
 * @protected @abstract
 */
SafeDomTreeProcessor.prototype.preProcessHtml = function(html) {};

/**
 * Returns a new node based on the transformation of an original node, or null
 * if the node and all its children should not be copied over to the new tree.
 * @param {!Node} originalNode
 * @return {?Node}
 * @private
 */
SafeDomTreeProcessor.prototype.createNode_ = function(originalNode) {
  var nodeType = noclobber.getNodeType(originalNode);
  switch (nodeType) {
    case NodeType.TEXT:
      return this.createTextNode(originalNode);
    case NodeType.ELEMENT:
      return this.createElement_(noclobber.assertNodeIsElement(originalNode));
    default:
      googLog.warning(logger, 'Dropping unknown node type: ' + nodeType);
      return null;
  }
};

/**
 * Creates a new text node from the original text node, or null if the node
 * should not be copied over to the new tree.
 * @param {!Node} originalNode
 * @return {?Node}
 * @protected @abstract
 */
SafeDomTreeProcessor.prototype.createTextNode = function(originalNode) {};

/**
 * Creates a new element from the original element, potentially applying
 * transformations to the element's tagname and attributes.
 * @param {!Element} originalElement
 * @return {?Element}
 * @private
 */
SafeDomTreeProcessor.prototype.createElement_ = function(originalElement) {
  if (noclobber.getNodeName(originalElement).toUpperCase() == 'TEMPLATE') {
    // Processing TEMPLATE tags is not supported, they are automatically
    // dropped.
    return null;
  }
  var newElement = this.createElementWithoutAttributes(originalElement);
  if (!newElement) {
    return null;
  }
  // Copy over element attributes, applying a transformation on each attribute.
  this.processElementAttributes_(originalElement, newElement);
  return newElement;
};

/**
 * Creates a new element from the original element. This function should only
 * either create a new element (optionally changing the tag name from the
 * original element) or return null to prevent the entire subtree from appearing
 * in the output. Note that TEMPLATE tags and their contents are automatically
 * dropped, and this function is not called to decide whether to keep them or
 * not.
 * @param {!Element} originalElement
 * @return {?Element}
 * @protected @abstract
 */
SafeDomTreeProcessor.prototype.createElementWithoutAttributes = function(
    originalElement) {};

/**
 * Copies over the attributes of an original node to its corresponding new node
 * generated with {@link processNode}.
 * @param {!Element} originalElement
 * @param {!Element} newElement
 * @private
 */
SafeDomTreeProcessor.prototype.processElementAttributes_ = function(
    originalElement, newElement) {
  var attributes = noclobber.getElementAttributes(originalElement);
  if (attributes == null) {
    return;
  }
  for (var i = 0, attribute; attribute = attributes[i]; i++) {
    if (attribute.specified) {
      var newValue = this.processElementAttribute(originalElement, attribute);
      if (!goog.isNull(newValue)) {
        noclobber.setElementAttribute(newElement, attribute.name, newValue);
      }
    }
  }
};

/**
 * Returns the new value for an attribute, or null if the attribute should be
 * dropped.
 * @param {!Element} element
 * @param {!Attr} attribute
 * @return {?string}
 * @protected @abstract
 */
SafeDomTreeProcessor.prototype.processElementAttribute = function(
    element, attribute) {};

/** @const {boolean} */
SafeDomTreeProcessor.SAFE_PARSING_SUPPORTED = SAFE_PARSING_SUPPORTED;
exports = SafeDomTreeProcessor;
