Merge main into fix-204-contentid-validation
This commit is contained in:
@@ -27,7 +27,7 @@ object RichTextHtml {
|
||||
|
||||
/** The `font-family`/`font-size` declarations of the base-style wrapper (possibly empty). */
|
||||
private fun baseCss(base: RichBaseStyle): String = listOfNotNull(
|
||||
base.fontCss?.let { "font-family:$it" },
|
||||
base.fontCss?.let(::safeFontFamily)?.let { "font-family:$it" },
|
||||
base.fontSizePt?.let { "font-size:${it}pt" },
|
||||
).joinToString(";")
|
||||
|
||||
@@ -197,7 +197,8 @@ private fun appendRun(sb: StringBuilder, content: RichTextContent, a: Int, b: In
|
||||
/** Merges the parameterized styles active on a run into one CSS declaration list (maybe empty). */
|
||||
private fun inlineCss(styles: List<RichStyle>): String {
|
||||
val parts = ArrayList<String>()
|
||||
styles.firstNotNullOfOrNull { it as? RichStyle.FontFamily }?.let { parts.add("font-family:${it.css}") }
|
||||
styles.firstNotNullOfOrNull { it as? RichStyle.FontFamily }?.css?.let(::safeFontFamily)
|
||||
?.let { parts.add("font-family:$it") }
|
||||
styles.firstNotNullOfOrNull { it as? RichStyle.FontSize }?.let { parts.add("font-size:${it.pt}pt") }
|
||||
styles.firstNotNullOfOrNull { it as? RichStyle.FontColor }?.let { parts.add("color:${cssColor(it.argb)}") }
|
||||
styles.firstNotNullOfOrNull { it as? RichStyle.Highlight }
|
||||
@@ -215,3 +216,16 @@ internal fun cssColor(argb: Int): String = "#" + (argb and RGB_MASK).toString(HE
|
||||
internal fun escape(s: String): String = s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
internal fun escapeAttr(s: String): String = escape(s).replace("\"", """)
|
||||
|
||||
/** The characters a font-family stack legitimately uses: letters, digits, spaces, commas, quotes, etc. */
|
||||
private val SAFE_FONT_FAMILY = Regex("[A-Za-z0-9 ,._'\"-]*")
|
||||
|
||||
/**
|
||||
* Emits a `font-family` value only when every character is one a real font stack uses, so a value
|
||||
* that ever carried CSS metacharacters (notably `;` or `:`) can't inject a sibling declaration into
|
||||
* the raw `style` attribute — an unrecognized value is dropped rather than emitted (issue #205,
|
||||
* defense-in-depth: not reachable today, since the picker only offers the fixed `FontRegistry` stacks
|
||||
* and reply/forward flattens sender HTML first). Every bundled `FontRegistry` stack is within this
|
||||
* set, so the built-in fonts round-trip unchanged.
|
||||
*/
|
||||
private fun safeFontFamily(css: String): String? = css.takeIf { SAFE_FONT_FAMILY.matches(it) }
|
||||
|
||||
@@ -254,6 +254,40 @@ class RichTextHtmlTest {
|
||||
assertRoundTrips(content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unsafe inline font-family value is dropped instead of injected raw (issue 205)`() {
|
||||
val content = RichTextContent(
|
||||
text = "x",
|
||||
spans = listOf(
|
||||
RichSpan(0, 1, RichStyle.FontFamily("Arial; color:red")),
|
||||
RichSpan(0, 1, RichStyle.Bold),
|
||||
),
|
||||
)
|
||||
val html = RichTextHtml.toHtml(content)
|
||||
// The smuggled sibling declaration must never reach the emitted style attribute...
|
||||
assertFalse(html.contains("color:red"), html)
|
||||
assertFalse(html.contains("Arial; color:red"), html)
|
||||
assertFalse(html.contains("font-family"), html)
|
||||
// ...but the run's other styling is unaffected.
|
||||
assertTrue(html.contains("<b>x</b>"), html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unsafe base-style font-family is dropped from the wrapper (issue 205)`() {
|
||||
val content = RichTextContent("hi", baseStyle = RichBaseStyle(fontCss = "Arial: red;}", fontSizePt = 12))
|
||||
val html = RichTextHtml.toHtml(content)
|
||||
assertFalse(html.contains("font-family"), html)
|
||||
// The safe font-size declaration in the same wrapper still emits.
|
||||
assertTrue(html.contains("font-size:12pt"), html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a registry font stack with quotes and commas still emits its font-family (issue 205)`() {
|
||||
// The bundled FontRegistry stacks are all within the safe set, so built-in fonts are unaffected.
|
||||
val content = RichTextContent("x", spans = listOf(RichSpan(0, 1, RichStyle.FontFamily("'Inter', sans-serif"))))
|
||||
assertTrue(RichTextHtml.toHtml(content).contains("font-family:'Inter', sans-serif"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `base style wrapper adds no stray text or newlines`() {
|
||||
val restored = RichTextHtml.fromHtml("<div style=\"font-size:12pt\"><p>a<br>b</p></div>")
|
||||
|
||||
Reference in New Issue
Block a user