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

voilà quelque chose qui me pose problème. J'ai un script côté client local qui doit permettre à un utilisateur de récupérer une page web distante et de rechercher cette page résultante pour les formulaires. Pour ce faire (sans regex), je dois analyser le document en un objet DOM entièrement traversable.

quelques limites que je voudrais souligner:

  • Je ne veux pas utiliser les bibliothèques (comme jQuery). Il y a trop de météorisation pour ce J'ai besoin de le faire ici.
  • en aucun cas les scripts de la page distante ne doivent être exécutés (pour des raisons de sécurité).
  • DOM Api, comme getElementsByTagName , doivent être disponibles.
  • il suffit de travailler dans Internet Explorer, mais au moins en 7.
  • faisons comme si je n'avais 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 une chaîne de document HTML complète (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é d'extraire les noeuds <head> et <body> du HTML et de les ajouter à un élément <HTML> à l'intérieur du fragment, toujours pas de chance.

quelqu'un a une idée?

41
demandé sur dda 2011-09-19 21:15:06

7 réponses

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

DocumentFragment ne met pas en œuvre les méthodes DOM. L'utilisation de document.createElement en conjonction avec innerHTML supprime les balises <head> et <body> (même si l'élément créé est un élément racine, <html> ). Par conséquent, la solution devrait être recherchée ailleurs. J'ai créé un cross-browser fonction string-to-DOM, qui utilise un inline-frame invisible.

toutes les ressources externes et les scripts seront désactivés. Voir explication du code pour plus de renseignements.

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

la fonction sanitiseHTML est basée sur ma fonction replace_all_rel_by_abs (voir cette réponse ). La fonction sanitiseHTML est entièrement réécrite afin d'obtenir un maximum d'efficacité et de fiabilité.

de plus, un nouvel ensemble de RegExps est ajouté pour supprimer tous les scripts et les gestionnaires d'événements (y compris CSS expression() , IE7-). Pour s'assurer que toutes les étiquettes sont analysées comme prévu, les étiquettes ajustées sont préfixées par <!--'"--> . Ce préfixe est nécessaire pour analyser correctement les "event handlers" imbriqués en conjonction avec les guillemets: <a id="><input onclick="<div onmousemove=evil()>"> .

ces RegExps sont créés dynamiquement en utilisant une fonction interne cr / cri ( C reate R eplace [ I nline]). Ces fonctions acceptent une liste d'arguments et créent et exécutent un remplacement avancé. Pour s'assurer que les entités HTML ne brisent pas un RegExp ( refresh dans <meta http-equiv=refresh> pourrait être écrit de différentes façons), les regexp créés dynamiquement sont partiellement construit par la fonction ae ( Un ny E entité).

Les remplacements sont effectués par la fonction by (remplacer par ). Dans cette implémentation, by ajoute data- avant tous les attributs appariés.

  1. tous les événements <script>//<[CDATA[ .. //]]></script> sont rayés. Cette étape est nécessaire, car les sections CDATA permettent </script> chaînes à l'intérieur du code. Après ce remplacement a été exécuté, il est sûr d'aller au prochain remplacement:
  2. les autres étiquettes <script>...</script> sont retirées.
  3. l'étiquette <meta http-equiv=refresh .. > est retirée
  4. Tous et des écouteurs d'événement externe pointeurs/attributs ( href , src , url() ) sont préfixés par data- , comme décrit précédemment.

  5. Un IFrame l'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 ajouté au document, de sorte que le DOM peut être accédé. document.write() sont utilisés pour écrire HTML à 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 la html chaîne.

  6. si une fonction de rappel a été spécifiée, la fonction sera appelée avec deux arguments. L'argument first est une référence à l'objet document généré. L'argument second est une fonction qui détruit l'arbre DOM généré lorsqu'il est appelé. Cette fonction devrait être appelée quand vous n'avez plus besoin de l'arbre.

    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 comme les arguments mentionnés précédemment.

Notes complémentaires

  • le paramétrage de la propriété designMode à" On " empêchera un cadre d'exécuter des scripts (non pris en charge dans Chrome). Si vous devez conserver les étiquettes <script> pour une raison spécifique, vous pouvez utiliser iframe.designMode = "On" au lieu de la fonction de strip-tease de script.
  • Je n'ai pas pu trouver de source fiable pour le htmlfile activeXObject . Selon cette source , htmlfile est plus lent que les IFrames, et plus sensible aux fuites de mémoire.



  • tous les attributs affectés ( href , src , ...) sont préfixés par data- . Un exemple d'obtention/modification de ces attributs est affiché 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ées. En conséquence, la page peut sembler complètement différente:

    <link rel="stylesheet" href="main.css" /> pas de styles externes

    <script>document.body.bgColor="red";</script> aucun style scénarisé

    <img src="128x128.png" /> pas d'image: la taille de l'élément peut être complètement différente.

exemples

sanitiseHTML(html)

Coller ce bookmarklet dans la barre. Il offrira une option pour injecter une textarea, montrant la chaîne HTML assainie.

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

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

références notables

78
répondu Rob W 2017-05-23 12:32:23

Je ne suis pas sûr de savoir pourquoi vous jouez avec documentFragments, vous pouvez simplement définir le texte HTML comme le innerHTML d'un nouvel élément div. Alors vous pouvez utiliser cet élément div pour getElementsByTagName etc sans ajouter le 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 aurez encore l'envelopper dans un div pour obtenir les fonctions DOM vous êtes après:

function makeDocumentFragment(htmlText) {
    var range = document.createRange();
    var frag = range.createContextualFragment(htmlText);
    var d = document.createElement('div');
    d.appendChild(frag);
    return d;
}
4
répondu Chris Baker 2011-09-19 19:13:46

Je ne sais pas si IE supporte 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.
2
répondu Eli Grey 2011-09-25 19:35:41

en supposant que le HTML est valide XML aussi, vous pouvez utiliser loadXML ()

1
répondu Dr.Molle 2011-09-19 18:33:04

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

vous pouvez avoir besoin d'utiliser une bibliothèque comme jsdom , qui fournit une implémentation du DOM et à travers lequel vous pouvez rechercher en utilisant getElementsByTagName et D'autres Dom APIs. Et vous pouvez le configurer pour ne pas exécuter les scripts. Oui, c'est "lourd" et je ne sais pas si ça marche dans IE 7.

0
répondu Javier Pedemonte 2011-09-23 20:51:28

vient de se promener à travers cette page, je suis un peu en retard pour être d'une utilité quelconque :) mais ce qui suit devrait aider n'importe qui avec un problème similaire à l'avenir... cependant IE7 / 8 devrait vraiment être ignoré maintenant et il ya beaucoup de meilleures méthodes prises en charge par les navigateurs plus modernes.

les travaux suivants à travers presque tout ce que j'ai testé - les deux seuls côtés vers le bas sont:

  1. j'ai ajouté getElementById et getElementsByName les fonctions à l'élément div racine, de sorte que ceux-ci ne apparaissent pas comme prévu plus loin dans l'arbre (sauf si le code est modifié pour répondre à cela) .

  2. le doctype sera ignoré - mais je ne pense pas que cela fera beaucoup de différence car mon expérience est que le doctype n'aura pas d'effet sur la façon dont le dom est structuré, juste comment il est rendu (ce qui évidemment n'arrivera pas avec cette méthode) .

Fondamentalement, le système repose sur le fait que <tag> et <namespace:tag> sont traités différemment par les useragents. Comme a été trouvé certaines étiquettes spéciales ne peuvent pas exister dans un élément div, et donc ils sont enlevés. Les éléments marqués d'un nom peuvent être placés n'importe où (à moins qu'il y ait une DTD indiquant le contraire) . Alors que ces tags d'espace de noms ne se comporteront pas réellement comme les vrais tags en question, considérant que nous sommes seulement vraiment les utiliser pour leur position structurelle dans le document ne pose pas vraiment de problème.

le balisage et le code sont les suivants:

<!DOCTYPE html>
<html>
<head>
<script>

  /// function for parsing HTML source to a dom structure
  /// Tested in Mac OSX, Win 7, Win XP with FF, IE 7/8/9, 
  /// Chrome, Safari & Opera.
  function parseHTML(src){

    /// create a random div, this will be our root
    var div = document.createElement('div'),
        /// specificy our namespace prefix
        ns = 'faux:',
        /// state which tags we will treat as "special"
        stn = ['html','head','body','title'];
        /// the reg exp for replacing the special tags
        re = new RegExp('<(/?)('+stn.join('|')+')([^>]*)?>','gi'),
        /// remember the getElementsByTagName function before we override it
        gtn = div.getElementsByTagName;

    /// a quick function to namespace certain tag names
    var nspace = function(tn){
      if ( stn.indexOf ) {
        return stn.indexOf(tn) != -1 ? ns + tn : tn;
      }
      else {
        return ('|'+stn.join('|')+'|').indexOf(tn) != -1 ? ns + tn : tn;
      }
    };

    /// search and replace our source so that special tags are namespaced
    /// &nbsp; required for IE7/8 to render tags before first text found
    /// <faux:check /> tag added so we can test how namespaces work
    src = '&nbsp;<'+ns+'check />' + src.replace(re,'<'+ns+'>');
    /// inject to the div
    div.innerHTML = src;
    /// quick test to see how we support namespaces in TagName searches
    if ( !div.getElementsByTagName(ns+'check').length ) {
      ns = '';
    }

    /// create our replacement getByName and getById functions
    var createGetElementByAttr = function(attr, collect){
      var func = function(a,w){
        var i,c,e,f,l,o; w = w||[];
        if ( this.nodeType == 1 ) {
          if ( this.getAttribute(attr) == a ) {
            if ( collect ) {
              w.push(this);
            }
            else {
              return this;
            }
          }
        }
        else {
          return false;
        }
        if ( (c = this.childNodes) && (l = c.length) ) {
          for( i=0; i<l; i++ ){
            if( (e = c[i]) && (e.nodeType == 1) ) {
              if ( (f = func.call( e, a, w )) && !collect ) {
                return f;
              }
            }
          }
        }
        return (w.length?w:false);
      }
      return func;
    }

    /// apply these replacement functions to the div container, obviously 
    /// you could add these to prototypes for browsers the support element 
    /// constructors. For other browsers you could step each element and 
    /// apply the functions through-out the node tree... however this would  
    /// be quite messy, far better just to always call from the root node - 
    /// or use div.getElementsByTagName.call( localElement, 'tag' );
    div.getElementsByTagName = function(t){return gtn.call(this,nspace(t));}
    div.getElementsByName    = createGetElementByAttr('name', true);
    div.getElementById       = createGetElementByAttr('id', false);

    /// return the final element
    return div;
  }

  window.onload = function(){

    /// parse the HTML source into a node tree
    var dom = parseHTML( document.getElementById('source').innerHTML );

    /// test some look ups :)
    var a = dom.getElementsByTagName('head'),
        b = dom.getElementsByTagName('title'),
        c = dom.getElementsByTagName('script'),
        d = dom.getElementById('body');

    /// alert the result
    alert(a[0].innerHTML);
    alert(b[0].innerHTML);
    alert(c[0].innerHTML);
    alert(d.innerHTML);

  }
</script>
</head>
<body>
  <xmp id="source">
    <!DOCTYPE html>
    <html>
    <head>
      <!-- Comment //-->
      <meta charset="utf-8">
      <meta name="robots" content="index, follow">
      <title>An example</title>
      <link href="test.css" />
      <script>alert('of parsing..');</script>
    </head>
    <body id="body">
      <b>in a similar way to createDocumentFragment</b>
    </body>
    </html>
  </xmp>
</body>
</html>
0
répondu Pebbl 2012-10-13 01:15:48

utiliser l'ensemble des DOM HTML capacités sans déclenchement de la demande, sans avoir à traiter avec des incompatibilités:

var doc = document.cloneNode();
if (!doc.documentElement) {
    doc.appendChild(doc.createElement('html'));
    doc.documentElement.appendChild(doc.createElement('head'));
    doc.documentElement.appendChild(doc.createElement('body'));
}

prêt ! doc est un document html, mais il n'est pas en ligne.

0
répondu Jérémy Lal 2018-01-25 14:41:45