Ответ 1
Fiddle: 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| ?|�*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);
}
Объяснение кода
Функция sanitiseHTML
основана на моей функции replace_all_rel_by_abs
(см. этот ответ). Функция sanitiseHTML
полностью переписана, хотя для достижения максимальной эффективности и надежности.
Кроме того, добавлен новый набор RegExps для удаления всех скриптов и обработчиков событий (включая CSS expression()
, IE7-). Чтобы убедиться, что все теги проанализированы как ожидалось, скорректированные теги имеют префикс <!--'"-->
. Этот префикс необходим для правильного синтаксического анализа вложенных "обработчиков событий" в сочетании с неисчерпаемыми кавычками: <a id="><input onclick="<div onmousemove=evil()>">
.
Эти RegExps динамически создаются с использованием внутренней функции cr
/cri
(C reate R eplace [ I nline]), Эти функции принимают список аргументов, а также создают и выполняют расширенную замену RE. Чтобы убедиться, что объекты HTML не нарушают RegExp (refresh
in <meta http-equiv=refresh>
могут быть записаны различными способами), динамически созданный RegExps частично построен функцией ae
( A ny E).
Фактические замены выполняются функцией by
(замените на). В этой реализации by
добавляет data-
перед всеми согласованными атрибутами.
- Все
<script>//<[CDATA[ .. //]]></script>
вхождения чередуются. Этот шаг необходим, потому что секцииCDATA
допускают строки</script>
внутри кода. После того, как эта замена была выполнена, безопасно перейти к следующей замене: - Остальные теги
<script>...</script>
удаляются. - Тег
<meta http-equiv=refresh .. >
удален -
Все прослушиватели событий и внешние указатели/атрибуты (
href
,src
,url()
) имеют префиксdata-
, как описано ранее. -
Создается объект
IFrame
. IFrames менее склонны к утечке памяти (вопреки htmlfile ActiveXObject). IFrame становится невидимым и добавляется к документу, так что к DOM можно получить доступ.document.write()
используются для записи HTML в IFrame.document.open()
иdocument.close()
используются для удаления предыдущего содержимого документа, так что сгенерированный документ является точной копией данной строкиhtml
. - Если указана функция обратного вызова, функция будет вызываться с двумя аргументами. Первый аргумент - ссылка на сгенерированный объект
document
. Второй аргумент - это функция, которая разрушает сгенерированное дерево DOM при вызове. Эта функция должна вызываться, когда вам больше не нужно дерево.
Если функция обратного вызова не указана, функция возвращает объект, состоящий из двух свойств (doc
иdestroy
), которые ведут себя как и ранее упомянутые аргументы.
Дополнительные примечания
- Установка свойства
designMode
на "On" приведет к остановке выполнения кадром сценариев (не поддерживается в Chrome). Если вам нужно сохранить теги<script>
по определенной причине, вы можете использоватьiframe.designMode = "On"
вместо функции удаления script. - Я не смог найти надежный источник для
htmlfile activeXObject
. Согласно этот источник,htmlfile
медленнее IFrames и более восприимчив к утечкам памяти. - Все затронутые атрибуты (
href
,src
,...) имеют префиксdata-
. Пример получения/изменения этих атрибутов показан дляdata-href
:elem.getAttribute("data-href")
иelem.setAttribute("data-href", "...")
elem.dataset.href
иelem.dataset.href = "..."
. - Внешние ресурсы отключены. В результате страница может выглядеть совершенно по-иному:
<link rel="stylesheet" href="main.css" />
<script>document.body.bgColor="red";</script>
<img src="128x128.png" />
Нет изображений: размер элемента может быть совершенно другим.
Примеры
sanitiseHTML(html)
Вставьте этот букмарклет в строку местоположения. Он предложит вариант для ввода текстового поля, отображающий измененную 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();
Заметные ссылки
- SO: JS RE, чтобы изменить все относительные абсолютные URL - Функция
sanitiseHTML(html)
основана на моей ранее созданной функцииreplace_all_rel_by_abs(html)
. - Элементы - Встроенный контент - Полный список стандартных встроенных элементов
- Элементы - Предыдущие элементы HTML - Дополнительный список (устаревших) элементов (например,
<applet>
) - Объект ActiveX htmlfile - "Медленнее, чем песочницы iframe. Утечка памяти, если не управляется"