Loading editor/src/htmlimport.cpp +90 −0 Changes for editor/src/htmlimport.cpp: 90 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -2093,6 +2093,89 @@ void extractGlobalCssFromStylesheet(const libhtmlpp::CSSStyleSheet &sheet, } } // True for any Unicode codepoint XML 1.0 allows in character data/attribute // values -- #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]. // Notably excludes the C0 control characters (other than tab/LF/CR) and the // noncharacters U+FFFE/U+FFFF, which are illegal in XML even as a numeric // character reference -- there is no way to escape them into a well-formed // document, only to remove them. bool isXmlLegalCodepoint(unsigned int cp) { return cp == 0x9 || cp == 0xA || cp == 0xD || (cp >= 0x20 && cp <= 0xD7FF) || (cp >= 0xE000 && cp <= 0xFFFD) || (cp >= 0x10000 && cp <= 0x10FFFF); } // Strips codepoints isXmlLegalCodepoint rejects from @p s, decoding as UTF-8 // so a stripped character can't split a multi-byte sequence and corrupt the // surrounding text; a malformed/truncated UTF-8 byte is dropped on its own // rather than resyncing further. Needed because real-world fetched content -- // a minified external JS bundle was found doing exactly this -- occasionally // embeds a raw noncharacter codepoint (e.g. U+FFFE, used as a compact literal // for a reversed-BOM check) directly in a string literal: valid JS, valid // UTF-8, but once it lands in an XML attribute (GlobalJs, GlobalCss, ...) it // makes the whole generated WebEditor document fail to parse back on the // very next load/publish -- tinyxml2's printer escapes the standard special // characters but has no reason to know about XML's separate, stricter // "legal character" rule. std::string sanitizeXmlText(const std::string &s) { std::string out; out.reserve(s.size()); size_t i = 0, n = s.size(); while (i < n) { unsigned char c = static_cast<unsigned char>(s[i]); unsigned int cp; size_t len; if (c < 0x80) { cp = c; len = 1; } else if ((c & 0xE0) == 0xC0 && i + 1 < n) { cp = c & 0x1F; len = 2; } else if ((c & 0xF0) == 0xE0 && i + 2 < n) { cp = c & 0x0F; len = 3; } else if ((c & 0xF8) == 0xF0 && i + 3 < n) { cp = c & 0x07; len = 4; } else { ++i; continue; } bool validCont = true; for (size_t k = 1; k < len; ++k) { unsigned char cc = static_cast<unsigned char>(s[i + k]); if ((cc & 0xC0) != 0x80) { validCont = false; break; } cp = (cp << 6) | (cc & 0x3F); } if (!validCont) { ++i; continue; } if (isXmlLegalCodepoint(cp)) out.append(s, i, len); i += len; } return out; } // Recursively strips XML-illegal codepoints (see sanitizeXmlText) from every // attribute value and text node under @p el, in place. Run once, on the // fully-built tree right before printing, rather than at each individual // SetAttribute/SetText call site: cheap relative to the rest of an import of // this size, and one guaranteed choke point beats keeping dozens of call // sites throughout this file each individually safe. void sanitizeXmlTree(tinyxml2::XMLElement *el) { for (const tinyxml2::XMLAttribute *attr = el->FirstAttribute(); attr; ) { const tinyxml2::XMLAttribute *next = attr->Next(); std::string val = attr->Value(); std::string clean = sanitizeXmlText(val); if (clean != val) el->SetAttribute(attr->Name(), clean.c_str()); attr = next; } for (tinyxml2::XMLNode *child = el->FirstChild(); child; ) { tinyxml2::XMLNode *next = child->NextSibling(); if (tinyxml2::XMLElement *childEl = child->ToElement()) { sanitizeXmlTree(childEl); } else if (tinyxml2::XMLText *textNode = child->ToText()) { std::string val = textNode->Value(); std::string clean = sanitizeXmlText(val); if (clean != val) textNode->SetValue(clean.c_str()); } child = next; } } } // namespace std::vector<blogi::htmlimport::ImportNode> Loading Loading @@ -2442,6 +2525,13 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, xmlRoot->SetAttribute("GlobalJs", globalJs.c_str()); } // Last step before printing: strip any XML-illegal codepoints that made // it into an attribute or text node (see sanitizeXmlTree's doc comment). // Every widget attribute/text value in this tree ultimately traces back // to fetched/scraped content, so this is checked once, tree-wide, rather // than at each individual SetAttribute/SetText call site above. sanitizeXmlTree(xmlRoot); tinyxml2::XMLPrinter printer; xmlDoc.Print(&printer); outXml = printer.CStr(); Loading tests/htmlimport_smoke.cpp +20 −0 Changes for tests/htmlimport_smoke.cpp: 20 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -390,6 +390,26 @@ int main(){ "display:none on an Image maps to hidden=\"true\" (previously silently dropped)"); } // --- XML-illegal codepoints in fetched/inline JS don't break the // generated document --- // A real minified external JS bundle was found embedding a raw U+FFFE // (a noncharacter, used as a compact literal for a reversed-BOM check) // directly in a string literal -- valid JS, valid UTF-8, but XML 1.0 // forbids that codepoint even via a numeric character reference, so it // made the entire generated WebEditor document fail to parse back on // the next load/publish. std::cout << "=== XML-illegal codepoint in inline script ===" << std::endl; { std::string html = "<script>var x = \"\xef\xbf\xbe\";</script><p>hi</p>"; std::string xml = importXml(html); check(xml.find("\xef\xbf\xbe") == std::string::npos, "raw U+FFFE from an inline script is stripped from GlobalJs"); tinyxml2::XMLDocument doc; check(doc.Parse(xml.c_str()) == tinyxml2::XML_SUCCESS, "output XML is well-formed even when source JS contains an XML-illegal codepoint"); } // --- Summary --- std::cout << "\n=== " << passCount << "/" << testCount << " tests passed ===" << std::endl; Loading Loading
editor/src/htmlimport.cpp +90 −0 Changes for editor/src/htmlimport.cpp: 90 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -2093,6 +2093,89 @@ void extractGlobalCssFromStylesheet(const libhtmlpp::CSSStyleSheet &sheet, } } // True for any Unicode codepoint XML 1.0 allows in character data/attribute // values -- #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]. // Notably excludes the C0 control characters (other than tab/LF/CR) and the // noncharacters U+FFFE/U+FFFF, which are illegal in XML even as a numeric // character reference -- there is no way to escape them into a well-formed // document, only to remove them. bool isXmlLegalCodepoint(unsigned int cp) { return cp == 0x9 || cp == 0xA || cp == 0xD || (cp >= 0x20 && cp <= 0xD7FF) || (cp >= 0xE000 && cp <= 0xFFFD) || (cp >= 0x10000 && cp <= 0x10FFFF); } // Strips codepoints isXmlLegalCodepoint rejects from @p s, decoding as UTF-8 // so a stripped character can't split a multi-byte sequence and corrupt the // surrounding text; a malformed/truncated UTF-8 byte is dropped on its own // rather than resyncing further. Needed because real-world fetched content -- // a minified external JS bundle was found doing exactly this -- occasionally // embeds a raw noncharacter codepoint (e.g. U+FFFE, used as a compact literal // for a reversed-BOM check) directly in a string literal: valid JS, valid // UTF-8, but once it lands in an XML attribute (GlobalJs, GlobalCss, ...) it // makes the whole generated WebEditor document fail to parse back on the // very next load/publish -- tinyxml2's printer escapes the standard special // characters but has no reason to know about XML's separate, stricter // "legal character" rule. std::string sanitizeXmlText(const std::string &s) { std::string out; out.reserve(s.size()); size_t i = 0, n = s.size(); while (i < n) { unsigned char c = static_cast<unsigned char>(s[i]); unsigned int cp; size_t len; if (c < 0x80) { cp = c; len = 1; } else if ((c & 0xE0) == 0xC0 && i + 1 < n) { cp = c & 0x1F; len = 2; } else if ((c & 0xF0) == 0xE0 && i + 2 < n) { cp = c & 0x0F; len = 3; } else if ((c & 0xF8) == 0xF0 && i + 3 < n) { cp = c & 0x07; len = 4; } else { ++i; continue; } bool validCont = true; for (size_t k = 1; k < len; ++k) { unsigned char cc = static_cast<unsigned char>(s[i + k]); if ((cc & 0xC0) != 0x80) { validCont = false; break; } cp = (cp << 6) | (cc & 0x3F); } if (!validCont) { ++i; continue; } if (isXmlLegalCodepoint(cp)) out.append(s, i, len); i += len; } return out; } // Recursively strips XML-illegal codepoints (see sanitizeXmlText) from every // attribute value and text node under @p el, in place. Run once, on the // fully-built tree right before printing, rather than at each individual // SetAttribute/SetText call site: cheap relative to the rest of an import of // this size, and one guaranteed choke point beats keeping dozens of call // sites throughout this file each individually safe. void sanitizeXmlTree(tinyxml2::XMLElement *el) { for (const tinyxml2::XMLAttribute *attr = el->FirstAttribute(); attr; ) { const tinyxml2::XMLAttribute *next = attr->Next(); std::string val = attr->Value(); std::string clean = sanitizeXmlText(val); if (clean != val) el->SetAttribute(attr->Name(), clean.c_str()); attr = next; } for (tinyxml2::XMLNode *child = el->FirstChild(); child; ) { tinyxml2::XMLNode *next = child->NextSibling(); if (tinyxml2::XMLElement *childEl = child->ToElement()) { sanitizeXmlTree(childEl); } else if (tinyxml2::XMLText *textNode = child->ToText()) { std::string val = textNode->Value(); std::string clean = sanitizeXmlText(val); if (clean != val) textNode->SetValue(clean.c_str()); } child = next; } } } // namespace std::vector<blogi::htmlimport::ImportNode> Loading Loading @@ -2442,6 +2525,13 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, xmlRoot->SetAttribute("GlobalJs", globalJs.c_str()); } // Last step before printing: strip any XML-illegal codepoints that made // it into an attribute or text node (see sanitizeXmlTree's doc comment). // Every widget attribute/text value in this tree ultimately traces back // to fetched/scraped content, so this is checked once, tree-wide, rather // than at each individual SetAttribute/SetText call site above. sanitizeXmlTree(xmlRoot); tinyxml2::XMLPrinter printer; xmlDoc.Print(&printer); outXml = printer.CStr(); Loading
tests/htmlimport_smoke.cpp +20 −0 Changes for tests/htmlimport_smoke.cpp: 20 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -390,6 +390,26 @@ int main(){ "display:none on an Image maps to hidden=\"true\" (previously silently dropped)"); } // --- XML-illegal codepoints in fetched/inline JS don't break the // generated document --- // A real minified external JS bundle was found embedding a raw U+FFFE // (a noncharacter, used as a compact literal for a reversed-BOM check) // directly in a string literal -- valid JS, valid UTF-8, but XML 1.0 // forbids that codepoint even via a numeric character reference, so it // made the entire generated WebEditor document fail to parse back on // the next load/publish. std::cout << "=== XML-illegal codepoint in inline script ===" << std::endl; { std::string html = "<script>var x = \"\xef\xbf\xbe\";</script><p>hi</p>"; std::string xml = importXml(html); check(xml.find("\xef\xbf\xbe") == std::string::npos, "raw U+FFFE from an inline script is stripped from GlobalJs"); tinyxml2::XMLDocument doc; check(doc.Parse(xml.c_str()) == tinyxml2::XML_SUCCESS, "output XML is well-formed even when source JS contains an XML-illegal codepoint"); } // --- Summary --- std::cout << "\n=== " << passCount << "/" << testCount << " tests passed ===" << std::endl; Loading