Najszybsza metoda zmiany znaczenia tagów HTML jako encji HTML?

99

Piszę rozszerzenie Chrome, która polega robi wiele z następujących stanowisk: odkażające łańcuchów, które mogą zawierać znaczniki HTML, konwertując <, >i &do &lt;, &gt;i &amp;, odpowiednio.

(Innymi słowy, to samo, co PHP htmlspecialchars(str, ENT_NOQUOTES)- nie sądzę, aby istniała potrzeba konwertowania znaków podwójnego cudzysłowu).

To najszybsza funkcja, jaką do tej pory znalazłem:

function safe_tags(str) {
    return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') ;
}

Ale wciąż jest duże opóźnienie, kiedy muszę przepuścić kilka tysięcy strun za jednym razem.

Czy ktoś może to poprawić? Dotyczy to głównie łańcuchów zawierających od 10 do 150 znaków, jeśli to robi różnicę.

(Jeden pomysł, jaki miałem, to nie zawracać sobie głowy kodowaniem znaku większego niż - czy byłoby z tym jakieś realne niebezpieczeństwo?)

callum
źródło
2
Czemu? W większości przypadków, gdy chcesz to zrobić, chcesz wstawić dane do DOM, w takim przypadku powinieneś zapomnieć o ucieczce z niego i po prostu utworzyć z niego textNode.
Quentin
1
@David Dorward: być może chciał wyczyścić dane POST, a serwer nie przekazuje danych poprawnie.
Lie Ryan
4
@Lie - jeśli tak, to rozwiązaniem jest „Ze względu na Pete'a napraw serwer, ponieważ masz dużą dziurę w XSS”
Quentin
2
@David Dorward: możliwe, że nie ma on kontroli nad serwerem. Niedawno znalazłem się w takiej sytuacji, w której pisałem skrypt greasemonkey, aby obejść kilka rzeczy, których nie lubię w witrynie mojej uczelni; Musiałem wykonać POST na serwerze, nad którym nie mam kontroli, i wyczyścić dane POST za pomocą javascript (ponieważ surowe dane pochodzą z bogatego pola tekstowego, a więc ma mnóstwo tagów html, które nie wykonują podróży w obie strony na serwerze) . Administrator sieci zignorował moją prośbę o naprawienie strony internetowej, więc nie miałem innego wyjścia.
Lie Ryan
1
Mam przypadek użycia, w którym muszę wyświetlić komunikat o błędzie w div. Komunikat o błędzie może zawierać HTML i znaki nowej linii. Chcę wyjść z kodu HTML i zamienić znaki nowej linii na <br>. Następnie umieść wynik w div do wyświetlenia.
mozey

Odpowiedzi:

84

Możesz spróbować przekazać funkcję zwrotną, aby wykonać zamianę:

var tagsToReplace = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;'
};

function replaceTag(tag) {
    return tagsToReplace[tag] || tag;
}

function safe_tags_replace(str) {
    return str.replace(/[&<>]/g, replaceTag);
}

Oto test wydajności: http://jsperf.com/encode-html-entities do porównania z replacewielokrotnym wywoływaniem funkcji i przy użyciu metody DOM zaproponowanej przez Dmitrija.

Twoja droga wydaje się szybsza ...

Dlaczego jednak tego potrzebujesz?

Martijn
źródło
2
Nie ma potrzeby ucieczki >.
6
Właściwie, jeśli umieścisz wartość ucieczki w atrybucie elementu html, musisz zmienić znaczenie symbolu>. W przeciwnym razie spowodowałoby uszkodzenie tagu dla tego elementu html.
Zlatin Zlatev
1
W normalnym tekście znaki ucieczki są rzadkie. Lepiej zadzwonić do wymiany tylko w razie potrzeby, jeśli zależy Ci na maksymalnej prędkości:if (/[<>&"]/.test(str) { ... }
Witalij
3
@callum: Nie. Nie jestem zainteresowany wyliczaniem przypadków, w których myślę, że „coś może pójść nie tak” (nie tylko dlatego, że to nieoczekiwane / zapomniane przypadki cię skrzywdzą i kiedy najmniej się tego spodziewasz). Interesuje mnie kodowanie zgodnie ze standardami (aby nieoczekiwane / zapomniane przypadki nie zaszkodziły z definicji ). Nie mogę podkreślić, jakie to ważne. >jest znakiem specjalnym w HTML, więc ucieknij przed nim. Proste. :)
Wyścigi lekkości na orbicie
4
@LightnessRacesinOrbit Jest to istotne, ponieważ pytanie brzmi, jaka jest najszybsza możliwa metoda. Jeśli można pominąć >wymianę, przyspieszy to.
callum
104

Oto jeden sposób, w jaki możesz to zrobić:

var escape = document.createElement('textarea');
function escapeHTML(html) {
    escape.textContent = html;
    return escape.innerHTML;
}

function unescapeHTML(html) {
    escape.innerHTML = html;
    return escape.textContent;
}

Oto demo.

Projektant stron internetowych
źródło
Przeprojektowano wersję demonstracyjną. Oto wersja pełnoekranowa: jsfiddle.net/Daniel_Hug/qPUEX/show/light
Web_Designer
13
Nie wiem jak / co / dlaczego - ale to genialne.
rob_james
4
Wygląda na to, że wykorzystuje istniejący kod elementu TextArea do zmiany znaczenia literału. Bardzo fajnie, myślę, że ta mała sztuczka pomoże znaleźć inny dom.
Ajax
3
@jazkat Nie używam tej funkcji. Używam zmiennej ucieczki, którą definiuję w przykładzie.
Web_Designer,
2
ale czy to traci białe miejsce itp.
Andrew
31

Metoda Martijna jako funkcja prototypowa:

String.prototype.escape = function() {
    var tagsToReplace = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;'
    };
    return this.replace(/[&<>]/g, function(tag) {
        return tagsToReplace[tag] || tag;
    });
};

var a = "<abc>";
var b = a.escape(); // "&lt;abc&gt;"
Aram Kocharyan
źródło
12
Dodajmy do Stringtego, że powinno to być escapeHtml, ponieważ ogólnie nie jest to znak ucieczki dla String. Zgadza się String.escapeHtml, ale String.escapenasuwa pytanie „po co uciec?”
Lawrence Dol
3
Tak, dobry pomysł. Obecnie odeszłam od rozszerzania prototypu, aby uniknąć konfliktów.
Aram Kocharyan
1
Jeśli Twoja przeglądarka obsługuje Symbol, możesz użyć tego zamiast zanieczyszczania przestrzeni nazw ciągu-klucza. var escape = nowy Symbol ("ucieczka"); String.prototype [escape] = function () {...}; "tekst" [escape] ();
Ajax
12

Jeszcze szybszym / krótszym rozwiązaniem jest:

escaped = new Option(html).innerHTML

Jest to związane z pewnym dziwnym śladem JavaScript, w którym element Option zachowuje konstruktor, który wykonuje tego rodzaju ucieczki automatycznie.

Kredyt dla https://github.com/jasonmoo/t.js/blob/master/t.js

Todd
źródło
1
Zgrabna jedna linijka, ale najwolniejsza metoda po wyrażeniu regularnym. Ponadto tekst tutaj może mieć usunięte spacje, zgodnie ze specyfikacją
ShortFuse
Zwróć uwagę, że łącze @ ShortFuse „najwolniejsza metoda” powoduje, że w moim systemie zabraknie pamięci RAM (przy ok. 6 GB wolnego miejsca), a Firefox wydaje się zatrzymywać alokację tuż przed wyczerpaniem pamięci, więc zamiast zabijać problematyczny proces, Linux będzie siedział tam i na to pozwoli mocne wyłączenie.
Luc
11

Kod źródłowy AngularJS ma również wersję wewnątrz angular-sanitize.js .

var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
    // Match everything outside of normal chars and " (quote character)
    NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
/**
 * Escapes all potentially dangerous characters, so that the
 * resulting string can be safely inserted into attribute or
 * element text.
 * @param value
 * @returns {string} escaped text
 */
function encodeEntities(value) {
  return value.
    replace(/&/g, '&amp;').
    replace(SURROGATE_PAIR_REGEXP, function(value) {
      var hi = value.charCodeAt(0);
      var low = value.charCodeAt(1);
      return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
    }).
    replace(NON_ALPHANUMERIC_REGEXP, function(value) {
      return '&#' + value.charCodeAt(0) + ';';
    }).
    replace(/</g, '&lt;').
    replace(/>/g, '&gt;');
}
Kevin Hakanson
źródło
1
Wow, to wyrażenie inne niż alfa jest intensywne. Nie sądzę, | w wyrażeniu jest jednak potrzebne.
Ajax
9

Skrypt „wszystko w jednym”:

// HTML entities Encode/Decode

function htmlspecialchars(str) {
    var map = {
        "&": "&amp;",
        "<": "&lt;",
        ">": "&gt;",
        "\"": "&quot;",
        "'": "&#39;" // ' -> &apos; for XML only
    };
    return str.replace(/[&<>"']/g, function(m) { return map[m]; });
}
function htmlspecialchars_decode(str) {
    var map = {
        "&amp;": "&",
        "&lt;": "<",
        "&gt;": ">",
        "&quot;": "\"",
        "&#39;": "'"
    };
    return str.replace(/(&amp;|&lt;|&gt;|&quot;|&#39;)/g, function(m) { return map[m]; });
}
function htmlentities(str) {
    var textarea = document.createElement("textarea");
    textarea.innerHTML = str;
    return textarea.innerHTML;
}
function htmlentities_decode(str) {
    var textarea = document.createElement("textarea");
    textarea.innerHTML = str;
    return textarea.value;
}

http://pastebin.com/JGCVs0Ts

baptx
źródło
Nie głosowałem przeciw, ale wszystkie zamiany stylu regex nie zakodują Unicode ... Więc każdy, kto używa obcego języka, będzie rozczarowany. Wspomniana powyżej sztuczka <textarea> jest naprawdę fajna i obsługuje wszystko szybko i bezpiecznie.
Ajax
Wyrażenie regularne działa dobrze dla mnie z wieloma znakami Unicode innymi niż łacińskie. Nie spodziewałbym się niczego innego. Jak myślisz, jak to nie zadziała? Czy myślisz o jednobajtowych stronach kodowych, które wymagają jednostek HTML? Do tego służą trzecia i czwarta funkcja, a wyraźnie nie pierwsza i druga. Podoba mi się różnicowanie.
ygoe
@LonelyPixel Nie sądzę, żeby zobaczył Twój komentarz, jeśli o nim nie wspomnisz („Tylko jeden dodatkowy użytkownik może zostać powiadomiony; właściciel posta zawsze zostanie powiadomiony”)
baptx
W ogóle nie wiedziałem, że istnieją powiadomienia ukierunkowane. @ Ajax, zobacz mój komentarz powyżej.
ygoe
@LonelyPixel Teraz widzę. Z jakiegoś powodu nie sądziłem, że w tej odpowiedzi istnieje zamiennik stylu textarea. Rzeczywiście, myślałem o dużych wartościach Unicode z podwójnym kodem, jak mandaryński. Chodzi mi o to, że byłoby możliwe uczynienie wyrażenia regularnego wystarczająco inteligentnym, ale kiedy spojrzysz na skróty, które mogą obrać sprzedawcy przeglądarek, poczułbym się całkiem dobrze, zakładając, że textarea będzie znacznie szybsza (niż całkowicie kompetentne wyrażenie regularne). Czy ktoś opublikował test porównawczy tej odpowiedzi? Przysięgałem, że widziałem.
Ajax
2

function encode(r) {
  return r.replace(/[\x26\x0A\x3c\x3e\x22\x27]/g, function(r) {
	return "&#" + r.charCodeAt(0) + ";";
  });
}

test.value=encode('How to encode\nonly html tags &<>\'" nice & fast!');

/*
 \x26 is &ampersand (it has to be first),
 \x0A is newline,
 \x22 is ",
 \x27 is ',
 \x3c is <,
 \x3e is >
*/
<textarea id=test rows=11 cols=55>www.WHAK.com</textarea>

Dave Brown
źródło
1

Nie jestem do końca pewien co do szybkości, ale jeśli szukasz prostoty, sugerowałbym użycie funkcji ucieczki lodash / podkreślenia .

gilmatic
źródło
0

Metoda Martijna jako pojedyncza funkcja z obsługą znaku ( używanego w javascript ):

function escapeHTML(html) {
    var fn=function(tag) {
        var charsToReplace = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&#34;'
        };
        return charsToReplace[tag] || tag;
    }
    return html.replace(/[&<>"]/g, fn);
}
iman
źródło
0

Dodam XMLSerializerdo stosu. Zapewnia najszybszy wynik bez korzystania z buforowania obiektów (nie w serializatorze ani w węźle tekstowym).

function serializeTextNode(text) {
  return new XMLSerializer().serializeToString(document.createTextNode(text));
}

Dodatkową zaletą jest to, że obsługuje atrybuty, które są serializowane inaczej niż węzły tekstowe:

function serializeAttributeValue(value) {
  const attr = document.createAttribute('a');
  attr.value = value;
  return new XMLSerializer().serializeToString(attr);
}

Możesz zobaczyć, co faktycznie zastępuje, sprawdzając specyfikację, zarówno dla węzłów tekstowych , jak i dla wartości atrybutów . Pełna dokumentacja zawiera więcej typów węzłów, ale koncepcja jest taka sama.

Jeśli chodzi o wydajność, jest najszybszy, gdy nie jest buforowany. Jeśli zezwolisz na buforowanie, wywołanie innerHTMLelementu HTMLElement z podrzędnym węzłem tekstowym jest najszybsze. Regex byłby najwolniejszy (co udowodniono w innych komentarzach). Oczywiście XMLSerializer mógłby być szybszy na innych przeglądarkach, ale w moich (ograniczonych) testach a innerHTMLjest najszybszy.


Najszybsza pojedyncza linia:

new XMLSerializer().serializeToString(document.createTextNode(text));

Najszybszy z buforowaniem:

const cachedElementParent = document.createElement('div');
const cachedChildTextNode = document.createTextNode('');
cachedElementParent.appendChild(cachedChildTextNode);

function serializeTextNode(text) {
  cachedChildTextNode.nodeValue = text;
  return cachedElementParent.innerHTML;
}

https://jsperf.com/htmlentityencode/1

ShortFuse
źródło
-3

Trochę za późno na przedstawienie, ale co jest złego w używaniu encodeURIComponent () i decodeURIComponent () ?

suncat100
źródło
1
Ci robią coś zupełnie niezwiązanego
callum
1
Być może największe nadużycie słowa „całkowicie”, jakie kiedykolwiek słyszałem. Na przykład, w odniesieniu do głównego pytania tematu, można go użyć do zdekodowania ciągu html (oczywiście z jakiegoś powodu do przechowywania), niezależnie od tagów HTML, a następnie łatwo zakodować go z powrotem do html, kiedy i jeśli jest to wymagane.
suncat100