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?
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| ?|�*32"+entityEnd+"|�*20"+entityEnd+")",
"(":"(?:\(|�*40"+entityEnd+"|�*28"+entityEnd+")",
")":"(?:\)|�*41"+entityEnd+"|�*29"+entityEnd+")",
".":"(?:\.|�*46"+entityEnd+"|�*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("�*" + char_lowercase.charCodeAt(0) + entityEnd);
RE_sub.push("�*" + char_lowercase.charCodeAt(0).toString(16) + entityEnd);
if(char_lowercase != char_uppercase){
RE_sub.push("�*" + char_uppercase.charCodeAt(0) + entityEnd);
RE_sub.push("�*" + 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.
- tous les événements
<script>//<[CDATA[ .. //]]></script>
sont rayés. Cette étape est nécessaire, car les sectionsCDATA
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: - les autres étiquettes
<script>...</script>
sont retirées. - l'étiquette
<meta http-equiv=refresh .. >
est retirée -
Tous et des écouteurs d'événement externe pointeurs/attributs (
href
,src
,url()
) sont préfixés pardata-
, comme décrit précédemment. -
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()
etdocument.close()
sont utilisés pour vider le contenu du document, de sorte que le document est une copie exacte de lahtml
chaîne. - 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
etdestroy
), 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 utiliseriframe.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 pardata-
. Un exemple d'obtention/modification de ces attributs est affiché pourdata-href
:
elem.getAttribute("data-href")
etelem.setAttribute("data-href", "...")
elem.dataset.href
etelem.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" />
<script>document.body.bgColor="red";</script>
<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
- DONC: JS RE pour changer tout par rapport à des Url absolues - Fonction
sanitiseHTML(html)
est basée sur ma précédemment crééreplace_all_rel_by_abs(html)
de la fonction. - Éléments de contenu Intégré - Une liste complète de la norme d'éléments intégrés
- éléments - Éléments HTML précédents - une liste additionnelle d'éléments (dépréciés) (tels que
<applet>
) - La htmlfile objet ActiveX - "plus Lent que l'iframe sandbox. Fuites mémoire si non gérée "
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;
}
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.
en supposant que le HTML est valide XML aussi, vous pouvez utiliser loadXML ()
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.
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:
-
j'ai ajouté
getElementById
etgetElementsByName
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) . -
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
/// required for IE7/8 to render tags before first text found
/// <faux:check /> tag added so we can test how namespaces work
src = ' <'+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>
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.