42 votes

Puis-je charger un document HTML entier dans un fragment de document dans Internet Explorer?

Voici quelque chose que j'ai eu un peu de difficulté avec. J'ai un local de script côté client qui doit permettre à un utilisateur de récupérer une page web distante et de recherche que la page de résultat pour les formulaires. Pour ce faire (sans regex), j'ai besoin de parser le document en un traversable objet DOM.

Certaines limitations, je tiens à souligner que:

  • Je ne veux pas utiliser des bibliothèques (comme jQuery). Il y a trop de météorisation pour ce que je dois faire ici.
  • En aucun cas, les scripts à partir de la télécommande page exécuté (pour des raisons de sécurité).
  • DOM Api, comme l' getElementsByTagName, doivent être disponibles.
  • Il doit uniquement fonctionner dans Internet Explorer, mais dans 7 à tout le moins.
  • Faisons semblant de croire que je n'ai pas accès à un serveur. Je fais, mais je ne peux pas l'utiliser pour cela.

Ce que j'ai essayé

En supposant que j'ai un complet en HTML chaîne de document (y compris la déclaration DOCTYPE) dans la variable html, voici ce que j'ai essayé jusqu'à présent:

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

J'ai aussi essayé de l'extraction de l' <head> et <body>des nœuds de l'HTML et de les ajouter à un <HTML> élément à l'intérieur du fragment, toujours pas de chance.

Quelqu'un aurait-il des idées?

79voto

Rob W Points 125904

Violon: http://jsfiddle.net/JFSKe/6/

DocumentFragment ne pas mettre en œuvre les méthodes du DOM. À l'aide de document.createElement en conjonction avec d' innerHTML supprime l' <head> et <body> balises (même lors de la création de l'élément est un élément racine, <html>). Par conséquent, la solution doit être recherchée ailleurs. J'ai créé un cross-browser de chaînes en fonction DOM, ce qui rend l'utilisation d'un invisible inline-cadre.

Toutes les ressources externes et les scripts sont désactivés. Voir Explication du code pour plus d'informations.

Code

/*
 @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);
}

Explication du code

L' sanitiseHTML fonction est basée sur mon replace_all_rel_by_abs de la fonction (voir cette réponse). L' sanitiseHTML la fonction est complètement réécrite si, afin d'atteindre un maximum d'efficacité et de fiabilité.

En outre, un nouvel ensemble d'expressions régulières sont ajoutés à supprimer tous les scripts et les gestionnaires d'événements (y compris CSS, expression(), IE7-). Assurez-vous que toutes les balises sont analysées comme prévu, l'adaptation des balises sont préfixés par <!--'"-->. Ce préfixe est nécessaire d'analyser correctement imbriquées "event handlers" en collaboration avec des intermédiaires citations: <a id="><input onclick="<div onmousemove=evil()>">.

Ces expressions régulières sont créées dynamiquement à l'aide d'une fonction interne cr/cri (Créer Replace [jenline]). Ces fonctions acceptent une liste d'arguments, et de créer et d'exécuter une avancée RE de remplacement. Pour s'assurer que les entités HTML ne sont pas en cassant une RegExp (refresh en <meta http-equiv=refresh> pourrait être écrit de différentes façons), l'créés dynamiquement les expressions régulières sont partiellement construits en fonction d' ae (Unny Eentité).
La réelle les remplacements sont effectués par la fonction by (remplacer par). Dans cette mise en œuvre, by ajoute data- avant tout égalé attributs.

  1. Tous <script>//<[CDATA[ .. //]]></script> des occurrences sont rayés. Cette étape est nécessaire, car CDATA sections autoriser </script> des chaînes de caractères dans le code. Après ce remplacement a été exécuté, il est sûr d'aller à la prochaine remplacement:
  2. Le reste <script>...</script> balises sont supprimées.
  3. L' <meta http-equiv=refresh .. > balise est supprimée
  4. Tous les écouteurs d'événements externes et des pointeurs/attributs (href, src, url()) sont préfixés par data-, comme décrit précédemment.

  5. Un IFrame objet est créé. Les IFrames sont moins susceptibles de présenter une fuite de mémoire (contrairement à la htmlfile ActiveXObject). L'IFrame devient invisible, et est annexée au document, de sorte que le DOM peut être consulté. document.write() sont utilisés pour écrire le code HTML de l'IFrame. document.open() et document.close() sont utilisés pour vider le contenu du document, de sorte que le document est une copie exacte de l' html chaîne de caractères.

  6. Si une fonction de rappel a été spécifié, la fonction sera appelée avec deux arguments. Le premier argument est une référence à la générées document objet. Le deuxième argument est une fonction, qui détruit l'généré DOM arbre lorsqu'il est appelé. Cette fonction doit être appelée lorsque vous n'avez pas besoin de l'arbre plus.
    Si la fonction de rappel n'est pas spécifié, la fonction renvoie un objet composé de deux propriétés (doc et destroy), qui se comportent de la même que précédemment mentionné arguments.

Notes supplémentaires

  • Réglage de l' designMode de la propriété de "On" va s'arrêter un cadre de l'exécution de scripts (non pris en charge dans Chrome). Si vous avez de préserver l' <script> balises pour une raison spécifique, vous pouvez utiliser iframe.designMode = "On" à la place du script de décapage de la fonctionnalité.
  • Je n'étais pas en mesure de trouver une source fiable pour l' htmlfile activeXObject. Selon cette source, htmlfile est plus lent que les IFrames, et plus sensibles à des fuites de mémoire.

  • Tous les attributs affectés (href, src, ...) sont préfixés par data-. Un exemple de contracter ou de la modification de ces attributs est indiqué pour data-href:
    elem.getAttribute("data-href") et elem.setAttribute("data-href", "...")
    elem.dataset.href et elem.dataset.href = "...".
  • Les ressources externes ont été désactivés. Comme un résultat, la page peut paraître complètement différents:
    <link rel="stylesheet" href="main.css" /> Pas les styles externes
    <script>document.body.bgColor="red";</script> Aucun script styles
    <img src="128x128.png" /> Pas d'images: la taille de l'élément peut être complètement différente.

Exemples

sanitiseHTML(html)
Coller ce bookmarklet dans la barre. Il permettra d'offrir une option pour injecter un textarea, montrant la variable d'environnement HTML chaîne.

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

Les exemples de Code - 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();

Notable références

4voto

Chris Points 20836

Vous ne savez pas pourquoi vous manipulez documentFragments, vous pouvez simplement définir le texte HTML comme innerHTML d'un nouvel élément div. Ensuite, vous pouvez utiliser cet élément div pour getElementsByTagName etc. sans ajouter la 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'));
 

Si vous êtes vraiment marié à l'idée d'un documentFragment, vous pouvez utiliser ce code, mais vous devrez quand même l'envelopper dans un div pour obtenir les fonctions DOM que vous recherchez:

 function makeDocumentFragment(htmlText) {
    var range = document.createRange();
    var frag = range.createContextualFragment(htmlText);
    var d = document.createElement('div');
    d.appendChild(frag);
    return d;
}
 

2voto

Eli Grey Points 17553

Je ne sais pas si IE prend en charge document.implementation.createHTMLDocument , mais si c'est le cas, utilisez cet algorithme (adapté de mon extension HTML DOMParser ). Notez que le DOCTYPE ne sera pas conservé .:

 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.
 

1voto

Dr.Molle Points 61743

En supposant que le code HTML soit également valide, vous pouvez utiliser loadXML ()

0voto

DocumentFragment ne supporte pas getElementsByTagName - c'est seulement supporté par Document .

Vous devrez peut-être utiliser une bibliothèque telle que jsdom , qui fournit une implémentation du DOM et à travers laquelle vous pouvez effectuer une recherche à l'aide de getElementsByTagName et d'autres API DOM. Et vous pouvez le configurer pour ne pas exécuter de scripts. Oui, c'est «lourd» et je ne sais pas si cela fonctionne dans IE 7.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X