/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

/**
 * A loose JavaScript version of Magento\Framework\Escaper
 *
 * Due to differences in how XML/HTML is processed in PHP vs JS there are a couple of minor differences in behavior
 * from the PHP counterpart.
 *
 * The first difference is that the default invocation of escapeHtml without allowedTags will double-escape existing
 * entities as the intention of such an invocation is that the input isn't supposed to contain any HTML.
 *
 * The second difference is that escapeHtml will not escape quotes. Since the input is actually being processed by the
 * DOM there is no chance of quotes being mixed with HTML syntax. And, since escapeHtml is not
 * intended to be used with raw injection into a HTML attribute, this is acceptable.
 *
 * @api
 */
define([], function () {
    'use strict';

    return {
        neverAllowedElements: ['script', 'img', 'embed', 'iframe', 'video', 'source', 'object', 'audio'],
        generallyAllowedAttributes: ['id', 'class', 'href', 'title', 'style'],
        forbiddenAttributesByElement: {
            a: ['style']
        },

        /**
         * Escape a string for safe injection into HTML
         *
         * @param {String} data
         * @param {Array|null} allowedTags
         * @returns {String}
         */
        escapeHtml: function (data, allowedTags) {
            var domParser = new DOMParser(),
                fragment = domParser.parseFromString('<div></div>', 'text/html');

            fragment = fragment.body.childNodes[0];
            allowedTags = typeof allowedTags === 'object' && allowedTags.length ? allowedTags : null;

            if (allowedTags) {
                fragment.innerHTML = data || '';
                allowedTags = this._filterProhibitedTags(allowedTags);

                this._removeComments(fragment);
                this._removeNotAllowedElements(fragment, allowedTags);
                this._removeNotAllowedAttributes(fragment);

                return fragment.innerHTML;
            }

            fragment.textContent = data || '';

            return fragment.innerHTML;
        },

        /**
         * Remove the always forbidden tags from a list of provided tags
         *
         * @param {Array} tags
         * @returns {Array}
         * @private
         */
        _filterProhibitedTags: function (tags) {
            return tags.filter(function (n) {
                return this.neverAllowedElements.indexOf(n) === -1;
            }.bind(this));
        },

        /**
         * Remove comment nodes from the given node
         *
         * @param {Node} node
         * @private
         */
        _removeComments: function (node) {
            var treeWalker = node.ownerDocument.createTreeWalker(
                    node,
                    NodeFilter.SHOW_COMMENT,
                    function () {
                        return NodeFilter.FILTER_ACCEPT;
                    },
                    false
                ),
                nodesToRemove = [];

            while (treeWalker.nextNode()) {
                nodesToRemove.push(treeWalker.currentNode);
            }

            nodesToRemove.forEach(function (nodeToRemove) {
                nodeToRemove.parentNode.removeChild(nodeToRemove);
            });
        },

        /**
         * Strip the given node of all disallowed tags while permitting any nested text nodes
         *
         * @param {Node} node
         * @param {Array|null} allowedTags
         * @private
         */
        _removeNotAllowedElements: function (node, allowedTags) {
            var treeWalker = node.ownerDocument.createTreeWalker(
                    node,
                    NodeFilter.SHOW_ELEMENT,
                    function (currentNode) {
                        return allowedTags.indexOf(currentNode.nodeName.toLowerCase()) === -1 ?
                            NodeFilter.FILTER_ACCEPT
                            // SKIP instead of REJECT because REJECT also rejects child nodes
                            : NodeFilter.FILTER_SKIP;
                    },
                    false
                ),
                nodesToRemove = [];

            while (treeWalker.nextNode()) {
                if (allowedTags.indexOf(treeWalker.currentNode.nodeName.toLowerCase()) === -1) {
                    nodesToRemove.push(treeWalker.currentNode);
                }
            }

            nodesToRemove.forEach(function (nodeToRemove) {
                nodeToRemove.parentNode.replaceChild(
                    node.ownerDocument.createTextNode(nodeToRemove.textContent),
                    nodeToRemove
                );
            });
        },

        /**
         * Remove any invalid attributes from the given node
         *
         * @param {Node} node
         * @private
         */
        _removeNotAllowedAttributes: function (node) {
            var treeWalker = node.ownerDocument.createTreeWalker(
                    node,
                    NodeFilter.SHOW_ELEMENT,
                    function () {
                        return NodeFilter.FILTER_ACCEPT;
                    },
                    false
                ),
                i,
                attribute,
                nodeName,
                attributesToRemove = [];

            while (treeWalker.nextNode()) {
                for (i = 0; i < treeWalker.currentNode.attributes.length; i++) {
                    attribute = treeWalker.currentNode.attributes[i];
                    nodeName = treeWalker.currentNode.nodeName.toLowerCase();

                    if (this.generallyAllowedAttributes.indexOf(attribute.name) === -1 || // eslint-disable-line max-depth,max-len
                        this._checkHrefValue(attribute) ||
                        this.forbiddenAttributesByElement[nodeName] &&
                        this.forbiddenAttributesByElement[nodeName].indexOf(attribute.name) !== -1
                    ) {
                        attributesToRemove.push(attribute);
                    }
                }
            }

            attributesToRemove.forEach(function (attributeToRemove) {
                attributeToRemove.ownerElement.removeAttribute(attributeToRemove.name);
            });
        },

        /**
         * Check that attribute contains script content
         *
         * @param {Object} attribute
         * @private
         */
        _checkHrefValue: function (attribute) {
            return attribute.nodeName.toLowerCase() === 'href' &&
                attribute.nodeValue.toLowerCase().replace(/\s/g, '').startsWith('javascript');
        }
    };
});
