Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .maestro/enrichedInput/flows/link_href_attribute_escaping.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
appId: swmansion.enriched.example
---
# Regression test for href="..." attribute-injection: a URL containing " (or
# &) must be HTML-attribute-escaped on serialize, and the escaped output must
# decode back to the original, un-escaped URL when re-parsed.
- launchApp

- tapOn:
id: 'toggle-screen-button'

- tapOn:
id: 'editor-input'

- inputText: 'Hello '

- runFlow:
file: '../subflows/insert_link.yaml'
env:
LINK_TEXT: 'test'
LINK_URL: 'https://example.com/a&b"c'

- extendedWaitUntil:
notVisible:
id: 'link-modal-submit-button'
timeout: 10000

- tapOn:
id: 'toggle-html-button'

# the raw " and & must not reach the output unescaped (that's the
# attribute-injection bug: it would break out of href="...")
- assertVisible:
id: 'html-output'
text: '.*href="https://example.com/a&b"c".*'

- assertNotVisible:
id: 'html-output'
text: '.*href="https://example.com/a&b"c".*'

# Restart the app before the round-trip check below. On iOS, presenting a
# second Modal right after dismissing one (LinkModal -> ValueModal) can leave
# the accessibility snapshot stuck empty indefinitely - the screen still
# renders correctly, but Maestro can't see any element in it. A full relaunch
# resets the native view/accessibility state; losing the editor content here
# is fine since the next step overwrites it anyway via "Set Value".
- launchApp

- tapOn:
id: 'toggle-screen-button'

# Feed the escaped HTML back in. The round trip is verified via the HTML
# output box rather than by reopening the link modal to read LinkData.url:
# reopening it is a second Modal in this same session and hits the exact
# accessibility-snapshot issue above. Checking the re-serialized HTML instead
# proves the same thing - decode must turn "&b"c" back into the raw
# "&b"c" that re-encode then escapes right back to "&b"c" - without
# ever presenting a second Modal.
- runFlow:
file: '../subflows/set_editor_value.yaml'
env:
VALUE: '<html><p><a href="https://example.com/a&amp;b&quot;c">test</a></p></html>'

- tapOn:
id: 'toggle-html-button'

- assertVisible:
id: 'html-output'
text: '.*href="https://example.com/a&amp;b&quot;c".*'

# if decode left the entities un-decoded, re-encoding would double-escape them
- assertNotVisible:
id: 'html-output'
text: '.*href="https://example.com/a&amp;amp;.*'
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions .maestro/enrichedText/flows/image_attribute_entity_decoding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
appId: swmansion.enriched.example
---
# Entities inside src="..." must decode too - regression test for the src
# attribute-escaping fix.
- launchApp

- tapOn:
id: 'toggle-screen-button'

- tapOn:
id: 'toggle-enriched-text-screen-button'

- runFlow:
file: '../subflows/set_enriched_text_value.yaml'
env:
VALUE: '<html><img src="https://example.com/img&amp;x.png" width="100" height="100"/></html>'

- tapOn:
id: 'enriched-text'
point: '5%,50%'

- assertVisible:
text: 'You pressed the image:.*img&x.png.*'

- assertNotVisible:
text: '.*&amp;.*'
26 changes: 26 additions & 0 deletions .maestro/enrichedText/flows/link_attribute_entity_decoding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
appId: swmansion.enriched.example
---
# Entities inside href="..." must decode too, not just inside text content -
# regression test for the href attribute-escaping fix.
- launchApp

- tapOn:
id: 'toggle-screen-button'

- tapOn:
id: 'toggle-enriched-text-screen-button'

- runFlow:
file: '../subflows/set_enriched_text_value.yaml'
env:
VALUE: '<html><p><a href="https://example.com/a&amp;b&quot;c">link</a></p></html>'

- tapOn:
id: 'enriched-text'
point: '5%,50%'

- assertVisible:
text: 'You pressed the link: https://example.com/a&b"c'

- assertNotVisible:
text: '.*&(amp|quot);.*'
27 changes: 27 additions & 0 deletions .maestro/enrichedText/flows/mention_attribute_entity_decoding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
appId: swmansion.enriched.example
---
# Entities inside mention attributes (a JSON blob spliced into key="value"
# pairs) must decode too - regression test for the mention attribute-escaping
# fix.
- launchApp

- tapOn:
id: 'toggle-screen-button'

- tapOn:
id: 'toggle-enriched-text-screen-button'

- runFlow:
file: '../subflows/set_enriched_text_value.yaml'
env:
VALUE: '<html><p><mention text="@John Doe" indicator="@" id="1&amp;2">@John Doe</mention></p></html>'

- tapOn:
id: 'enriched-text'
point: '5%,50%'

- assertVisible:
text: 'You pressed the mention:.*"id":"1&2".*'

- assertNotVisible:
text: '.*&amp;.*'
23 changes: 23 additions & 0 deletions .maestro/enrichedText/flows/named_html_entities.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
appId: swmansion.enriched.example
---
# Issue #818 - named character references (&nbsp;, &quot;, &apos;, ...) are
# decoded instead of being rendered literally.
- launchApp

- tapOn:
id: 'toggle-screen-button'

- tapOn:
id: 'toggle-enriched-text-screen-button'

- runFlow:
file: '../subflows/set_enriched_text_value.yaml'
env:
VALUE: "<html><p>Hello&nbsp;world &quot;q&quot; &apos;a&apos; &mdash;</p></html>"

# '.' matches the decoded U+00A0 between "Hello" and "world"
- assertVisible:
text: "Hello.world \"q\" 'a' —"

- assertNotVisible:
text: '.*&(nbsp|quot|apos|mdash);.*'
2 changes: 2 additions & 0 deletions apps/example/src/screens/TestScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LinkModal } from '../components/LinkModal';
import { MentionPopup } from '../components/MentionPopup';
import { ImageModal } from '../components/ImageModal';
import { ValueModal } from '../components/ValueModal';
import { HtmlSection } from '../components/HtmlSection';
import { useEditorState } from '../hooks/useEditorState';
import { useKeyboardHeight } from '../hooks/useKeyboardHeight';
import {
Expand Down Expand Up @@ -111,6 +112,7 @@ export function TestScreen({
layout="grid"
/>
</View>
<HtmlSection currentHtml={editor.currentHtml} />
<View style={styles.buttonRow}>
<Button
title="Set Value"
Expand Down
24 changes: 17 additions & 7 deletions cpp/parser/GumboNormalizer.c
Original file line number Diff line number Diff line change
Expand Up @@ -309,12 +309,13 @@ static const char *find_css_value(const char *style, size_t style_len,
return NULL;
}

static void append_style_prop(buffer_t *out, const char *prefix,
const char *val, size_t val_len, bool *first) {
if (!*first)
buffer_append_str(out, "; ");
*first = false;
buffer_append_str(out, prefix);
/* Escapes the characters that would otherwise let a value break out of a
* double-quoted HTML attribute. Gumbo's GumboAttribute.value is already
* character-reference-decoded, so every attribute value written to `out`
* must go through this before being re-parsed downstream - otherwise it's
* decoded a second time there. */
static void append_escaped_attr_value(buffer_t *out, const char *val,
size_t val_len) {
for (size_t i = 0; i < val_len; i++) {
if (val[i] == '&')
buffer_append_str(out, "&amp;");
Expand All @@ -329,6 +330,15 @@ static void append_style_prop(buffer_t *out, const char *prefix,
}
}

static void append_style_prop(buffer_t *out, const char *prefix,
const char *val, size_t val_len, bool *first) {
if (!*first)
buffer_append_str(out, "; ");
*first = false;
buffer_append_str(out, prefix);
append_escaped_attr_value(out, val, val_len);
}

static bool css_val_contains(const char *val, size_t val_len,
const char *needle) {
size_t nlen = strlen(needle);
Expand Down Expand Up @@ -501,7 +511,7 @@ static void emit_one_attr(buffer_t *out, GumboElement *el,
buffer_append_str(out, " ");
buffer_append_str(out, attr_name);
buffer_append_str(out, "=\"");
buffer_append_str(out, val);
append_escaped_attr_value(out, val, strlen(val));
buffer_append_str(out, "\"");
}
}
Expand Down
2 changes: 2 additions & 0 deletions ios/extensions/StringExtension.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
+ (NSString *)fromCppString:(std::string)string;
+ (NSString *)stringByEscapingHtml:(NSString *)html;
+ (NSDictionary *)getEscapedCharactersInfoFrom:(NSString *)text;
+ (NSString *)stringByEscapingHtmlAttribute:(NSString *)value;
+ (NSString *)stringByUnescapingHtmlAttribute:(NSString *)value;
@end

@interface NSMutableString (StringExtension)
Expand Down
Loading
Loading