events: Escape plain bodies in replies

Replies generate an HTML body even if the reply itself only consists of
plain text. In order to convert the plain text to HTML, it has to be
escaped, which did not happen previously.
This commit is contained in:
Xiretza 2022-11-03 13:16:01 +01:00 committed by GitHub
parent 8d0f817f48
commit 69c807bdc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 81 additions and 23 deletions

View File

@ -1,5 +1,10 @@
# [unreleased]
Bug fixes:
* HTML-relevant characters (`<`, `>`, etc) in plaintext replies are now escaped
during creation of the rich reply
Breaking changes:
* Remove deprecated `EventType` enum

View File

@ -48,22 +48,8 @@ fn get_message_quote_fallbacks(original_message: &OriginalRoomMessageEvent) -> (
}
}
fn formatted_or_plain_body(
formatted: Option<&FormattedBody>,
body: &str,
#[cfg(feature = "unstable-sanitize")] is_reply: bool,
) -> String {
if let Some(formatted_body) = formatted {
#[cfg(feature = "unstable-sanitize")]
if is_reply {
sanitize_html(&formatted_body.body, HtmlSanitizerMode::Strict, RemoveReplyFallback::Yes)
} else {
formatted_body.body.clone()
}
#[cfg(not(feature = "unstable-sanitize"))]
formatted_body.body.clone()
} else {
/// Converts a plaintext body to HTML, escaping any characters that would cause problems.
fn escape_html_entities(body: &str) -> String {
let mut escaped_body = String::with_capacity(body.len());
for c in body.chars() {
// Escape reserved HTML entities and new lines.
@ -83,6 +69,25 @@ fn formatted_or_plain_body(
}
}
escaped_body
}
fn formatted_or_plain_body(
formatted: Option<&FormattedBody>,
body: &str,
#[cfg(feature = "unstable-sanitize")] is_reply: bool,
) -> String {
if let Some(formatted_body) = formatted {
#[cfg(feature = "unstable-sanitize")]
if is_reply {
sanitize_html(&formatted_body.body, HtmlSanitizerMode::Strict, RemoveReplyFallback::Yes)
} else {
formatted_body.body.clone()
}
#[cfg(not(feature = "unstable-sanitize"))]
formatted_body.body.clone()
} else {
escape_html_entities(body)
}
}
@ -97,7 +102,7 @@ fn formatted_or_plain_body(
/// [HTML tags and attributes]: https://spec.matrix.org/v1.4/client-server-api/#mroommessage-msgtypes
/// [rich reply fallbacks]: https://spec.matrix.org/v1.4/client-server-api/#fallbacks-for-rich-replies
pub fn plain_and_formatted_reply_body(
body: impl fmt::Display,
body: &str,
formatted: Option<impl fmt::Display>,
original_message: &OriginalRoomMessageEvent,
) -> (String, String) {
@ -106,7 +111,7 @@ pub fn plain_and_formatted_reply_body(
let plain = format!("{quoted}\n{body}");
let html = match formatted {
Some(formatted) => format!("{quoted_html}{formatted}"),
None => format!("{quoted_html}{body}"),
None => format!("{quoted_html}{}", escape_html_entities(body)),
};
(plain, html)

View File

@ -8,8 +8,9 @@ use ruma_common::{
key::verification::VerificationMethod,
room::{
message::{
AudioMessageEventContent, KeyVerificationRequestEventContent, MessageType,
OriginalRoomMessageEvent, RoomMessageEventContent, TextMessageEventContent,
AudioMessageEventContent, ForwardThread, KeyVerificationRequestEventContent,
MessageType, OriginalRoomMessageEvent, RoomMessageEventContent,
TextMessageEventContent,
},
MediaSource,
},
@ -434,6 +435,53 @@ fn content_deserialization_failure() {
assert_matches!(from_json_value::<RoomMessageEventContent>(json_data), Err(_));
}
#[test]
fn escape_tags_in_plain_reply_body() {
let first_message = OriginalRoomMessageEvent {
content: RoomMessageEventContent::text_plain("Usage: cp <source> <destination>"),
event_id: event_id!("$143273582443PhrSn:example.org").to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)),
room_id: room_id!("!testroomid:example.org").to_owned(),
sender: user_id!("@user:example.org").to_owned(),
unsigned: MessageLikeUnsigned::default(),
};
let second_message = RoomMessageEventContent::text_plain("Usage: rm <path>")
.make_reply_to(&first_message, ForwardThread::Yes);
let body = assert_matches!(
first_message.content.msgtype,
MessageType::Text(TextMessageEventContent { body, formatted: None, .. }) => body
);
assert_eq!(body, "Usage: cp <source> <destination>");
let (body, formatted) = assert_matches!(
second_message.msgtype,
MessageType::Text(TextMessageEventContent { body, formatted, .. }) => (body, formatted)
);
assert_eq!(
body,
"\
> <@user:example.org> Usage: cp <source> <destination>\n\
Usage: rm <path>\
"
);
let formatted = formatted.unwrap();
assert_eq!(
formatted.body,
"\
<mx-reply>\
<blockquote>\
<a href=\"https://matrix.to/#/!testroomid:example.org/$143273582443PhrSn:example.org\">In reply to</a> \
<a href=\"https://matrix.to/#/@user:example.org\">@user:example.org</a>\
<br>\
Usage: cp &lt;source&gt; &lt;destination&gt;\
</blockquote>\
</mx-reply>\
Usage: rm &lt;path&gt;\
"
);
}
#[test]
#[cfg(feature = "unstable-sanitize")]
fn reply_sanitize() {