我可以在 Internet Explorer 中将整个 HTML 文档加载到文档片段中吗?

IT技术 javascript html internet-explorer dom
2021-02-03 10:54:22

这是我一直有点困难的事情。我有一个本地客户端脚本,需要允许用户获取远程网页并在生成的页面中搜索表单。为了做到这一点(没有正则表达式),我需要将文档解析为一个完全可遍历的 DOM 对象。

我想强调的一些限制:

  • 我不想使用库(如 jQuery)。我在这里需要做的事情太多了。
  • 在任何情况下都不应执行来自远程页面的脚本(出于安全原因)。
  • DOM API,例如getElementsByTagName,需要可用。
  • 它只需要在 Internet Explorer 中运行,但至少需要在 7 中运行。
  • 假设我无权访问服务器。我有,但我不能用它来做这个。

我试过的

假设我在变量中有一个完整的 HTML 文档字符串(包括 DOCTYPE 声明)html,这是我迄今为止尝试过的:

var frag = document.createDocumentFragment(),
div  = frag.appendChild(document.createElement("div"));

div.outerHTML = html;
//-> results in an empty fragment

div.insertAdjacentHTML("afterEnd", html);
//-> HTML is not added to the fragment

div.innerHTML = html;
//-> Error (expected, but I tried it anyway)

var doc = new ActiveXObject("htmlfile");
doc.write(html);
doc.close();
//-> JavaScript executes

我还尝试从 HTML 中提取<head><body>节点并将它们添加到<HTML>片段内的元素中,但仍然没有运气。

有没有人有任何想法?

6个回答

小提琴http : //jsfiddle.net/JFSKe/6/

DocumentFragment不实现 DOM 方法。使用document.createElement与结合 innerHTML去除<head><body>标签(即使当创建的元素是一个根元素,<html>)。因此,应在别处寻求解决方案。我创建了一个跨浏览器字符串到 DOM 的函数,它利用了一个不可见的内联框架。

所有外部资源和脚本都将被禁用。有关更多信息,请参阅代码说明

代码

/*
 @param String html    The string with HTML which has be converted to a DOM object
 @param func callback  (optional) Callback(HTMLDocument doc, function destroy)
 @returns              undefined if callback exists, else: Object
                        HTMLDocument doc  DOM fetched from Parameter:html
                        function destroy  Removes HTMLDocument doc.         */
function string2dom(html, callback){
    /* Sanitise the string */
    html = sanitiseHTML(html); /*Defined at the bottom of the answer*/

    /* Create an IFrame */
    var iframe = document.createElement("iframe");
    iframe.style.display = "none";
    document.body.appendChild(iframe);

    var doc = iframe.contentDocument || iframe.contentWindow.document;
    doc.open();
    doc.write(html);
    doc.close();

    function destroy(){
        iframe.parentNode.removeChild(iframe);
    }
    if(callback) callback(doc, destroy);
    else return {"doc": doc, "destroy": destroy};
}

/* @name sanitiseHTML
   @param String html  A string representing HTML code
   @return String      A new string, fully stripped of external resources.
                       All "external" attributes (href, src) are prefixed by data- */

function sanitiseHTML(html){
    /* Adds a <!-\"'--> before every matched tag, so that unterminated quotes
        aren't preventing the browser from splitting a tag. Test case:
       '<input style="foo;b:url(0);><input onclick="<input type=button onclick="too() href=;>">' */
    var prefix = "<!--\"'-->";
    /*Attributes should not be prefixed by these characters. This list is not
     complete, but will be sufficient for this function.
      (see http://www.w3.org/TR/REC-xml/#NT-NameChar) */
    var att = "[^-a-z0-9:._]";
    var tag = "<[a-z]";
    var any = "(?:[^<>\"']*(?:\"[^\"]*\"|'[^']*'))*?[^<>]*";
    var etag = "(?:>|(?=<))";

    /*
      @name ae
      @description          Converts a given string in a sequence of the
                             original input and the HTML entity
      @param String string  String to convert
      */
    var entityEnd = "(?:;|(?!\\d))";
    var ents = {" ":"(?:\\s|&nbsp;?|&#0*32"+entityEnd+"|&#x0*20"+entityEnd+")",
                "(":"(?:\\(|&#0*40"+entityEnd+"|&#x0*28"+entityEnd+")",
                ")":"(?:\\)|&#0*41"+entityEnd+"|&#x0*29"+entityEnd+")",
                ".":"(?:\\.|&#0*46"+entityEnd+"|&#x0*2e"+entityEnd+")"};
                /*Placeholder to avoid tricky filter-circumventing methods*/
    var charMap = {};
    var s = ents[" "]+"*"; /* Short-hand space */
    /* Important: Must be pre- and postfixed by < and >. RE matches a whole tag! */
    function ae(string){
        var all_chars_lowercase = string.toLowerCase();
        if(ents[string]) return ents[string];
        var all_chars_uppercase = string.toUpperCase();
        var RE_res = "";
        for(var i=0; i<string.length; i++){
            var char_lowercase = all_chars_lowercase.charAt(i);
            if(charMap[char_lowercase]){
                RE_res += charMap[char_lowercase];
                continue;
            }
            var char_uppercase = all_chars_uppercase.charAt(i);
            var RE_sub = [char_lowercase];
            RE_sub.push("&#0*" + char_lowercase.charCodeAt(0) + entityEnd);
            RE_sub.push("&#x0*" + char_lowercase.charCodeAt(0).toString(16) + entityEnd);
            if(char_lowercase != char_uppercase){
                RE_sub.push("&#0*" + char_uppercase.charCodeAt(0) + entityEnd);   
                RE_sub.push("&#x0*" + char_uppercase.charCodeAt(0).toString(16) + entityEnd);
            }
            RE_sub = "(?:" + RE_sub.join("|") + ")";
            RE_res += (charMap[char_lowercase] = RE_sub);
        }
        return(ents[string] = RE_res);
    }
    /*
      @name by
      @description  second argument for the replace function.
      */
    function by(match, group1, group2){
        /* Adds a data-prefix before every external pointer */
        return group1 + "data-" + group2 
    }
    /*
      @name cr
      @description            Selects a HTML element and performs a
                                  search-and-replace on attributes
      @param String selector  HTML substring to match
      @param String attribute RegExp-escaped; HTML element attribute to match
      @param String marker    Optional RegExp-escaped; marks the prefix
      @param String delimiter Optional RegExp escaped; non-quote delimiters
      @param String end       Optional RegExp-escaped; forces the match to
                                  end before an occurence of <end> when 
                                  quotes are missing
     */
    function cr(selector, attribute, marker, delimiter, end){
        if(typeof selector == "string") selector = new RegExp(selector, "gi");
        marker = typeof marker == "string" ? marker : "\\s*=";
        delimiter = typeof delimiter == "string" ? delimiter : "";
        end = typeof end == "string" ? end : "";
        var is_end = end && "?";
        var re1 = new RegExp("("+att+")("+attribute+marker+"(?:\\s*\"[^\""+delimiter+"]*\"|\\s*'[^'"+delimiter+"]*'|[^\\s"+delimiter+"]+"+is_end+")"+end+")", "gi");
        html = html.replace(selector, function(match){
            return prefix + match.replace(re1, by);
        });
    }
    /* 
      @name cri
      @description            Selects an attribute of a HTML element, and
                               performs a search-and-replace on certain values
      @param String selector  HTML element to match
      @param String attribute RegExp-escaped; HTML element attribute to match
      @param String front     RegExp-escaped; attribute value, prefix to match
      @param String flags     Optional RegExp flags, default "gi"
      @param String delimiter Optional RegExp-escaped; non-quote delimiters
      @param String end       Optional RegExp-escaped; forces the match to
                                  end before an occurence of <end> when 
                                  quotes are missing
     */
    function cri(selector, attribute, front, flags, delimiter, end){
        if(typeof selector == "string") selector = new RegExp(selector, "gi");
        flags = typeof flags == "string" ? flags : "gi";
         var re1 = new RegExp("("+att+attribute+"\\s*=)((?:\\s*\"[^\"]*\"|\\s*'[^']*'|[^\\s>]+))", "gi");

        end = typeof end == "string" ? end + ")" : ")";
        var at1 = new RegExp('(")('+front+'[^"]+")', flags);
        var at2 = new RegExp("(')("+front+"[^']+')", flags);
        var at3 = new RegExp("()("+front+'(?:"[^"]+"|\'[^\']+\'|(?:(?!'+delimiter+').)+)'+end, flags);

        var handleAttr = function(match, g1, g2){
            if(g2.charAt(0) == '"') return g1+g2.replace(at1, by);
            if(g2.charAt(0) == "'") return g1+g2.replace(at2, by);
            return g1+g2.replace(at3, by);
        };
        html = html.replace(selector, function(match){
             return prefix + match.replace(re1, handleAttr);
        });
    }

    /* <meta http-equiv=refresh content="  ; url= " > */
    html = html.replace(new RegExp("<meta"+any+att+"http-equiv\\s*=\\s*(?:\""+ae("refresh")+"\""+any+etag+"|'"+ae("refresh")+"'"+any+etag+"|"+ae("refresh")+"(?:"+ae(" ")+any+etag+"|"+etag+"))", "gi"), "<!-- meta http-equiv=refresh stripped-->");

    /* Stripping all scripts */
    html = html.replace(new RegExp("<script"+any+">\\s*//\\s*<\\[CDATA\\[[\\S\\s]*?]]>\\s*</script[^>]*>", "gi"), "<!--CDATA script-->");
    html = html.replace(/<script[\S\s]+?<\/script\s*>/gi, "<!--Non-CDATA script-->");
    cr(tag+any+att+"on[-a-z0-9:_.]+="+any+etag, "on[-a-z0-9:_.]+"); /* Event listeners */

    cr(tag+any+att+"href\\s*="+any+etag, "href"); /* Linked elements */
    cr(tag+any+att+"src\\s*="+any+etag, "src"); /* Embedded elements */

    cr("<object"+any+att+"data\\s*="+any+etag, "data"); /* <object data= > */
    cr("<applet"+any+att+"codebase\\s*="+any+etag, "codebase"); /* <applet codebase= > */

    /* <param name=movie value= >*/
    cr("<param"+any+att+"name\\s*=\\s*(?:\""+ae("movie")+"\""+any+etag+"|'"+ae("movie")+"'"+any+etag+"|"+ae("movie")+"(?:"+ae(" ")+any+etag+"|"+etag+"))", "value");

    /* <style> and < style=  > url()*/
    cr(/<style[^>]*>(?:[^"']*(?:"[^"]*"|'[^']*'))*?[^'"]*(?:<\/style|$)/gi, "url", "\\s*\\(\\s*", "", "\\s*\\)");
    cri(tag+any+att+"style\\s*="+any+etag, "style", ae("url")+s+ae("(")+s, 0, s+ae(")"), ae(")"));

    /* IE7- CSS expression() */
    cr(/<style[^>]*>(?:[^"']*(?:"[^"]*"|'[^']*'))*?[^'"]*(?:<\/style|$)/gi, "expression", "\\s*\\(\\s*", "", "\\s*\\)");
    cri(tag+any+att+"style\\s*="+any+etag, "style", ae("expression")+s+ae("(")+s, 0, s+ae(")"), ae(")"));
    return html.replace(new RegExp("(?:"+prefix+")+", "g"), prefix);
}

代码说明

sanitiseHTML功能基于我的replace_all_rel_by_abs功能(请参阅此答案)。sanitiseHTML函数被完全重写,以实现最大的效率和可靠性。

此外,还添加了一组新的 RegExp,以删除所有脚本和事件处理程序(包括 CSS expression()、IE7-)。为确保所有标签都按预期解析,调整后的标签以<!--'"-->. 这个前缀对于正确解析嵌套的“事件处理程序”以及未终止的引号是必要的:<a id="><input onclick="<div onmousemove=evil()>">

这些正则表达式是使用内部函数动态创建cr/ criç reate ř E放置[n第])。这些函数接受参数列表,并创建和执行高级 RE 替换。为了确保HTML实体没有违反一个RegExp(refresh<meta http-equiv=refresh>可以用各种方式来写的),动态创建的正则表达式的一部分被构造函数ae一个纽约ê ntity)。
实际的替换是通过函数完成的by(替换)。在这个实现中,在所有匹配的属性之前by添加data-

  1. 所有的<script>//<[CDATA[ .. //]]></script>出现都是条带化的。这一步是必要的,因为CDATA部分允许</script>在代码中使用 字符串。执行此替换后,可以安全地进行下一个替换:
  2. 剩余的<script>...</script>标签被移除。
  3. <meta http-equiv=refresh .. >标记将被删除
  4. 如前所述,所有事件侦听器和外部指针/属性(hrefsrcurl())都以 为前缀data-

  5. 一个IFrame对象被创建。IFrames 不太可能泄漏内存(与 htmlfile ActiveXObject 相反)。IFrame 变得不可见,并附加到文档中,以便可以访问 DOM。document.write()用于将 HTML 写入 IFrame。document.open()document.close()用于清空文档的先前内容,以便生成的文档是给定html字符串的精确副本

  6. 如果已指定回调函数,则将使用两个参数调用该函数。一个参数是对生成document对象的引用第二参数是一个函数被调用时它破坏所生成的DOM树。当您不再需要树时,应调用此函数。
    如果未指定回调函数,该函数将返回一个由两个属性 (docdestroy)组成的对象,其行为与前面提到的参数相同。

补充笔记

  • 将该designMode属性设置为“On”将阻止框架执行脚本(Chrome 不支持)。如果<script>出于特定原因必须保留标签,则可以使用iframe.designMode = "On"代替脚本剥离功能。
  • 我无法找到htmlfile activeXObject. 根据这个来源htmlfile比 IFrames 慢,并且更容易受到内存泄漏的影响。

  • 所有受影响的属性(href, src, ...)都以 为前缀data-获得/改变这些属性中的一个例子示出了用于data-href
    elem.getAttribute("data-href")elem.setAttribute("data-href", "...")
    elem.dataset.hrefelem.dataset.href = "..."
  • 外部资源已被禁用。因此,页面可能看起来完全不同:没有外部样式没有脚本样式没有图像: 元素的大小可能完全不同。
    <link rel="stylesheet" href="main.css" />
    <script>document.body.bgColor="red";</script>
    <img src="128x128.png" />

例子

sanitiseHTML(html)
将此书签粘贴到该位置的栏中。它将提供一个注入 textarea 的选项,显示经过处理的 HTML 字符串。

javascript:void(function(){var s=document.createElement("script");s.src="http://rob.lekensteyn.nl/html-sanitizer.js";document.body.appendChild(s)})();

代码示例 -string2dom(html)

string2dom("<html><head><title>Test</title></head></html>", function(doc, destroy){
    alert(doc.title); /* Alert: "Test" */
    destroy();
});

var test = string2dom("<div id='secret'></div>");
alert(test.doc.getElementById("secret").tagName); /* Alert: "DIV" */
test.destroy();

值得注意的参考资料

请参阅第 3 点(+ 相应的替换函数)和最后的前两个参考。如果您绝对确定某个标签 ( <applet>?) 不会出现,则无需实现它。如果您不必为特定目标保留嵌入元素,则通过 RE 删除它们很容易。例如:.replace(/<object[\S\s]+?<\/object\s*>/gi, "")一些嵌入的对象可能有一个省略的结束标记。在这种情况下,请使用:.replace(/<embed[^>]+>[\S\s]*?<\/embed\s*>/gi, "").replace(/<embed[^>]*>/gi, "")
2021-03-13 10:54:22
@Rob - 您的代码现在似乎根本没有正确清理* 属性。此输入"<html><head><title>Test</title></head><body onload='alert(\"XSS\")'></html>"显示“XSS”警报。我强烈建议您为自己构建一个非常全面的测试套件。
2021-03-15 10:54:22
@Rob - jsfiddle.net/JFSKe/2是对您的消毒剂的微不足道的攻击。我知道至少还有另一种简单的方法可以打败它,而且我什至模糊不清是 XSS 专家。
2021-03-17 10:54:22
我也+1。我已经对片段得出了相同的结论(经过广泛的研究和测试)。有趣的部分是设置designModeon防止脚本执行。无论如何,非常感谢......这更像是我想要的答案。唯一真正的耻辱是许多潜在的漏洞,所以我需要多考虑一下。
2021-03-26 10:54:22
+1 很好的答案。修复<stylesheet>和/或修复是否也有value<style>他们可能有expressions-moz-behaviors
2021-04-08 10:54:22

不确定为什么要弄乱 documentFragments,您可以将 HTML 文本设置为innerHTML新 div 元素的 。然后您可以将该 div 元素用于getElementsByTagNameetc 而无需将 div 添加到 DOM:

var htmlText= '<html><head><title>Test</title></head><body><div id="test_ele1">this is test_ele1 content</div><div id="test_ele2">this is test_ele content2</div></body></html>';

var d = document.createElement('div');
d.innerHTML = htmlText;

console.log(d.getElementsByTagName('div'));

如果你真的很喜欢 documentFragment 的想法,你可以使用这段代码,但你仍然需要将它包装在一个 div 中才能获得你想要的 DOM 函数:

function makeDocumentFragment(htmlText) {
    var range = document.createRange();
    var frag = range.createContextualFragment(htmlText);
    var d = document.createElement('div');
    d.appendChild(frag);
    return d;
}
这会<head>在附加到新创建的 div 之前去除元素。我知道我没有指定我也需要头部的东西,但我确实需要(特别是<link>元素)。我正在处理文档片段,因为如果可能的话,这似乎是最有可能起作用的方法。 createContextualFragment对我没有帮助,IE 不支持它。
2021-03-16 10:54:22
我对此进行了相当多的研究 - 无法访问developer.mozilla.org/En/DOM/DOMImplementation.createDocument 之类的内容,也没有使用 iFrame,确实没有另一种方法可以严格地在客户端执行此操作。不确定 IE 7 中对 Range/createContextualFragment 的支持,但在我查看结果后,我意识到这与将 HTML 插入新的 div 元素没有什么不同。由于文档片段没有您想要的 DOM 函数并且 div 不能有效地包含 HTML/BODY,我不确定您有什么选项。
2021-03-17 10:54:22

我不确定 IE 是否支持document.implementation.createHTMLDocument,但如果支持,请使用此算法(改编自我的DOMParser HTML 扩展)。请注意,不会保留 DOCTYPE。:

var
      doc = document.implementation.createHTMLDocument("")
    , doc_elt = doc.documentElement
    , first_elt
;
doc_elt.innerHTML = your_html_here;
first_elt = doc_elt.firstElementChild;
if ( // are we dealing with an entire document or a fragment?
       doc_elt.childElementCount === 1
    && first_elt.tagName.toLowerCase() === "html"
) {
    doc.replaceChild(first_elt, doc_elt);
}

// doc is an HTML document
// you can now reference stuff like doc.title, etc.
IE 9 支持它,但不幸的是,IE 8 及更低版本不支持。
2021-03-29 10:54:22

假设 HTML 也是有效的 XML,您可以使用loadXML()

不幸的是,我不能假设。加载的 HTML 可以(理论上)来自网络上的任何站点。
2021-03-18 10:54:22

DocumentFragment不支持getElementsByTagName- 只有Document.

您可能需要使用像jsdom这样的库,它提供了 DOM 的实现,您可以通过它来搜索使用getElementsByTagName和其他 DOM API。您可以将其设置为不执行脚本。是的,它很“重”,我不知道它是否适用于 IE 7。

奇怪,但我想我不应该对 IE 不遵循规范感到惊讶。这是一个讨论,暗示createDocumentFragment在 IE 上实际上创建了一个Document而不是DocumentFragment,这将解释为什么它支持getElementsByTagName.
2021-03-28 10:54:22
有趣... IE 支持getElementsByTagName文档片段(这是我在我的问题中基于这一点的内容)。
2021-04-10 10:54:22