Подтвердить что ты не робот

Как обнаружить ошибки синтаксического анализа XML при использовании Javascript DOMParser в кросс-браузере?

Кажется, что все основные браузеры реализуют API DOMParser, так что XML может быть проанализирован в DOM, а затем запрошен с использованием XPath, getElementsByTagName и т.д.

Однако обнаружение ошибок синтаксического анализа кажется более сложным. DOMParser.prototype.parseFromString всегда возвращает действительную DOM. Когда возникает ошибка синтаксического анализа, возвращаемый DOM содержит элемент <parsererror>, но он немного отличается в каждом крупном браузере.

Пример JavaScript:

xmlText = '<root xmlns="http://default" xmlns:other="http://other"><child><otherr:grandchild/></child></root>';
parser = new DOMParser();
dom = parser.parseFromString(xmlText, 'text/xml');
console.log((new XMLSerializer()).serializeToString(dom));

Результат в Opera:

DOM root - это элемент <parsererror>.

<?xml version="1.0"?><parsererror xmlns="http://www.mozilla.org/newlayout/xml/parsererror.xml">Error<sourcetext>Unknown source</sourcetext></parsererror>

Результат в Firefox:

DOM root - это элемент <parsererror>.

<?xml-stylesheet href="chrome://global/locale/intl.css" type="text/css"?>
<parsererror xmlns="http://www.mozilla.org/newlayout/xml/parsererror.xml">XML Parsing Error: prefix not bound to a namespace
Location: http://fiddle.jshell.net/_display/
Line Number 1, Column 64:<sourcetext>&lt;root xmlns="http://default" xmlns:other="http://other"&gt;&lt;child&gt;&lt;otherr:grandchild/&gt;&lt;/child&gt;&lt;/root&gt;
---------------------------------------------------------------^</sourcetext></parsererror>

Результат в Safari:

Элемент <root> корректно анализирует, но содержит вложенный <parsererror> в другое пространство имен, чем элемент Opera и Firefox <parsererror>.

<root xmlns="http://default" xmlns:other="http://other"><parsererror xmlns="http://www.w3.org/1999/xhtml" style="display: block; white-space: pre; border: 2px solid #c77; padding: 0 1em 0 1em; margin: 1em; background-color: #fdd; color: black"><h3>This page contains the following errors:</h3><div style="font-family:monospace;font-size:12px">error on line 1 at column 50: Namespace prefix otherr on grandchild is not defined
</div><h3>Below is a rendering of the page up to the first error.</h3></parsererror><child><otherr:grandchild/></child></root>

Я пропустил простой, кросс-браузерный способ обнаружения ошибки XML-анализа в любом месте документа XML? Или я должен запросить DOM для каждого из возможных элементов <parsererror>, которые могут генерировать различные браузеры?

4b9b3361

Ответ 1

Это лучшее решение, которое я придумал.

Я пытаюсь проанализировать строку, которая преднамеренно неверна, и наблюдать за пространством имен результирующего элемента <parsererror>. Затем при анализе фактического XML я могу использовать getElementsByTagNameNS для обнаружения того же типа <parsererror> и выбросить Javascript Error.

// My function that parses a string into an XML DOM, throwing an Error if XML parsing fails
function parseXml(xmlString) {
    var parser = new DOMParser();
    // attempt to parse the passed-in xml
    var dom = parser.parseFromString(xmlString, 'text/xml');
    if(isParseError(dom)) {
        throw new Error('Error parsing XML');
    }
    return dom;
}

function isParseError(parsedDocument) {
    // parser and parsererrorNS could be cached on startup for efficiency
    var parser = new DOMParser(),
        errorneousParse = parser.parseFromString('<', 'text/xml'),
        parsererrorNS = errorneousParse.getElementsByTagName("parsererror")[0].namespaceURI;

    if (parsererrorNS === 'http://www.w3.org/1999/xhtml') {
        // In PhantomJS the parseerror element doesn't seem to have a special namespace, so we are just guessing here :(
        return parsedDocument.getElementsByTagName("parsererror").length > 0;
    }

    return parsedDocument.getElementsByTagNameNS(parsererrorNS, 'parsererror').length > 0;
};

Обратите внимание, что это решение не включает специальную оболочку, необходимую для Internet Explorer. Однако в IE все гораздо проще. XML анализируется с помощью метода loadXML, который возвращает true или false, если синтаксический анализ был успешным или неудачным, соответственно. См. http://www.w3schools.com/xml/xml_parser.asp для примера.

Ответ 2

Когда я впервые пришел сюда, я подтвердил оригинальный ответ (cspotcode), однако он не работает в Firefox. Полученное пространство имен всегда "нулевое" из-за структуры подготовленного документа. Я сделал небольшое исследование (проверьте код здесь). Идея состоит в том, чтобы использовать не

invalidXml.childNodes[0].namespaceURI

но

invalidXml.getElementsByTagName("parsererror")[0].namespaceURI

И затем выберите элемент parsererror по пространству имен, как в исходном ответе. Однако, если у вас есть действующий XML-документ с тегом <parsererror> в том же пространстве имен, который используется браузером, вы оказываете ложную тревогу. Итак, здесь эвристика, чтобы проверить, успешно ли ваш XML-анализ:

function tryParseXML(xmlString) {
    var parser = new DOMParser();
    var parsererrorNS = parser.parseFromString('INVALID', 'text/xml').getElementsByTagName("parsererror")[0].namespaceURI;
    var dom = parser.parseFromString(xmlString, 'text/xml');
    if(dom.getElementsByTagNameNS(parsererrorNS, 'parsererror').length > 0) {
        throw new Error('Error parsing XML');
    }
    return dom;
}

Почему бы не реализовать исключения в DOMParser?

Интересная вещь, которую стоит упомянуть в текущем контексте: если вы попытаетесь получить XML файл с XMLHttpRequest, анализируемый DOM будет сохранен в свойстве responseXML или null, если содержимое файла XML было недействительным. Не исключение, а не parsererror или другой конкретный индикатор. Просто null.

Ответ 3

В современных браузерах DOMParser, по-видимому, имеет два возможных поведения при использовании некорректного XML:

  1. Полностью <parsererror> полученный документ - верните документ <parsererror> с подробностями ошибки. Firefox и Edge, кажется, всегда используют этот подход; браузеры из семейства Chrome делают это в большинстве случаев.

  2. Вернуть полученный документ с одним дополнительным <parsererror> вставленным в качестве первого дочернего элемента корневого элемента. Парсер Chrome делает это в тех случаях, когда он может создать корневой элемент, несмотря на обнаружение ошибок в исходном XML. Вставленный <parsererror> может иметь или не иметь пространство имен. Остальная часть документа, кажется, оставлена без изменений, включая комментарии и т.д. Обратитесь к xml_errors.cc - найдите XMLErrors::InsertErrorMessageBlock.

Для (1) способ обнаружения ошибки состоит в том, чтобы добавить узел к исходной строке, проанализировать его, проверить, существует ли узел в результирующем документе, а затем удалить его. Насколько мне известно, единственный способ достичь этого без потенциального влияния на результат - добавить инструкцию обработки или комментарий в конец источника.

Пример:

let key = 'a'+Math.random().toString(32);

let doc = (new DOMParser).parseFromString(src+'<?${key}?>', 'application/xml');

let lastNode = doc.lastChild;
if (!(lastNode instanceof ProcessingInstruction)
    || lastNode.target !== key
    || lastNode.data !== '')
{
    /* the XML was malformed */
} else {
    /* the XML was well-formed */
    doc.removeChild(lastNode);
}

Если происходит случай (2), ошибка не будет обнаружена вышеуказанным методом, поэтому требуется еще один шаг.

Мы можем использовать тот факт, что вставлен только один <parsererror>, даже если в разных местах в источнике обнаружено несколько ошибок. Снова проанализировав исходную строку, к этому времени с добавленной синтаксической ошибкой мы можем убедиться, что <parsererror> поведение (2), а затем проверить, изменилось ли количество элементов <parsererror> - если нет, то первый результат parseFromString уже содержал true <parsererror>.

Пример:

let errCount = doc.documentElement.getElementsByTagName('parsererror').length;
if (errCount !== 0) {
    let doc2 = parser.parseFromString(src+'<?', 'application/xml');
    if (doc2.documentElement.getElementsByTagName('parsererror').length === errCount) {
        /* the XML was malformed */
    }
}

Я собрал тестовую страницу, чтобы проверить этот подход: https://github.com/Cauterite/domparser-tests.

Он проверяет весь набор XML W3C Conformance Test Suite, а также несколько дополнительных примеров, чтобы убедиться, что он может отличить документы, содержащие элементы <parsererror> от фактических ошибок, допущенных DOMParser. Только несколько тестовых случаев исключаются, поскольку они содержат недопустимые последовательности Юникода.

Чтобы было ясно, это только проверка, идентичен ли результат XMLHttpRequest.responseXML для данного документа.

Вы можете запустить тесты самостоятельно по адресу https://cauterite.github.io/domparser-tests/index.html, но имейте в виду, что он использует ECMAScript 2018.

На момент написания все тесты проходили в последних версиях Firefox, Chrome, Safari и Firefox для Android. Opera на основе Edge и Presto должна пройти, поскольку их DOMParsers, похоже, ведут себя как Firefox, а текущая Opera должна пройти, поскольку она является форком Chromium.


Пожалуйста, дайте мне знать, если вы можете найти контрпримеры или возможные улучшения.

Для ленивых, вот полная функция:

const tryParseXml = function(src) {
    /* returns an XMLDocument, or null if 'src' is malformed */

    let key = 'a'+Math.random().toString(32);

    let parser = new DOMParser;

    let doc = null;
    try {
        doc = parser.parseFromString(
            src+'<?${key}?>', 'application/xml');
    } catch (_) {}

    if (!(doc instanceof XMLDocument)) {
        return null;
    }

    let lastNode = doc.lastChild;
    if (!(lastNode instanceof ProcessingInstruction)
        || lastNode.target !== key
        || lastNode.data !== '')
    {
        return null;
    }

    doc.removeChild(lastNode);

    let errElemCount =
        doc.documentElement.getElementsByTagName('parsererror').length;
    if (errElemCount !== 0) {
        let errDoc = null;
        try {
            errDoc = parser.parseFromString(
                src+'<?', 'application/xml');
        } catch (_) {}

        if (!(errDoc instanceof XMLDocument)
            || errDoc.documentElement.getElementsByTagName('parsererror').length
                === errElemCount)
        {
            return null;
        }
    }

    return doc;
}

Ответ 4

Моя веб-платформа использует HTML5 в качестве XML (application/xhtml + xml), и ничего недопустимого сохранить нельзя. Недавно я определил, что я теряю код, потому что он был искажен при переключении между Rich Editor и XML Editor. Выявление искаженных ошибок в различных механизмах рендеринга не является одинаковым, хотя и не слишком сложным. Gecko по-прежнему будет загрязнять console ошибкой в формате XML, хотя все механизмы рендеринга будут работать так, как нужно. Проверено в:

  • Gecko/Waterfox 56
  • Presto/Opera 12.1
  • Трайдент/IE 11
  • WebKit/Safari 12.1
  • Blink/Chrome 55/75

Я также включил функции id_(), entities() и xml_add(), которые в значительной степени предотвращают искажение символов Юникода, если база данных не соответствует требованиям. Начиная с 2019 года, вы захотите использовать MariaDB и установить кодировку для базы данных на utf8mb4_unicode_520_ci. Моя функция entities() очень агрессивна (кодирует очень малые числовые объекты в Unicode). У Рика Джеймса есть действительно глубокая страница сравнения MySQL utf8 Collations, которая явно совместима с MariaDB. В какой-то момент 520 будет заменено, поэтому я рекомендую добавить ежегодное (годовое) напоминание, чтобы проверить, какая кодировка является самой высокой.

Хотя все это будет охватывать почти все, когда вы импортируете XML в DOM, браузеры не будут проверять наличие дублирующихся атрибутов/значений id! На моей платформе я просто удаляю слой страницы в большинстве случаев. Я также отмечаю, если импортируемая страница имеет один и тот же id два или более раз. Если ваш код содержит один и тот же id дважды, браузер выберет для первого или второго экземпляра. Это может быть очень невыносимо, если вы считаете, что какая-то другая часть вашего кода содержит ошибки. Strict всегда превосходит свободный код, а чистый JavaScript всегда превосходит фреймворки и библиотеки.

try
{
 if (!id_('xml_temp')) {xml_add('after', 'editor_rich', '<div class="hidden" id="xml_temp"></div>');}
 var f = id_('xml_temp').appendChild(new DOMParser().parseFromString(entities('<div xmlns="http://www.w3.org/1999/xhtml">'+id_('post_xml').value+'</div>'),'application/xml').childNodes[0]);
}
catch (err) {var f = false}

if (!f || f.childNodes.length == 0 || f.childNodes[0].nodeName == 'parsererror') {dialog.alert(error);}
else
{
 //Proceed with compliant XML.
}

Необходимые условия, которые мой код использует на моей платформе.

function id_(id) {return (document.getElementById(id)) ? document.getElementById(id) : false;}


function entities(s)
{
 var i = 0;
 var r = '';

 while (i<=s.length)
 {
  if (!isNaN(s.charCodeAt(i)))
  {
   if (s.charCodeAt(i)<127) {r += s.charAt(i);}
   else {r += '&#'+s.charCodeAt(i)+';';}
  }
  i++;
 }

 return r;
}

function xml_add(pos, e, xml)
{
 e = (typeof e == 'string' && id_(e)) ? id_(e) : e;

 if (e.nodeName)
 {
  if (pos=='after') {e.parentNode.insertBefore(document.importNode(new DOMParser().parseFromString(xml,'application/xml').childNodes[0],true),e.nextSibling);}
  else if (pos=='before') {e.parentNode.insertBefore(document.importNode(new DOMParser().parseFromString(xml,'application/xml').childNodes[0],true),e);}
  else if (pos=='inside') {e.appendChild(document.importNode(new DOMParser().parseFromString(xml,'application/xml').childNodes[0],true));}
  else if (pos=='replace') {e.parentNode.replaceChild(document.importNode(new DOMParser().parseFromString(xml,'application/xml').childNodes[0],true),e);}
  //Add fragment and have it returned.
 }
}