diff --git a/core/src/models.rs b/core/src/models.rs index e4fe69a..f3c1e1d 100644 --- a/core/src/models.rs +++ b/core/src/models.rs @@ -58,6 +58,15 @@ impl DbVal { } } +impl std::borrow::Borrow for DbVal +where + T: Sized, +{ + fn borrow(&self) -> &T { + &self.0 + } +} + impl std::ops::Deref for DbVal { type Target = T; fn deref(&self) -> &T { diff --git a/web/src/lists.rs b/web/src/lists.rs index c0197af..fa756db 100644 --- a/web/src/lists.rs +++ b/web/src/lists.rs @@ -118,6 +118,9 @@ pub async fn list( url: ListPath(list.id.to_string().into()).to_crumb(), }, ]; + let list_owners = db.list_owners(list.pk)?; + let mut list_obj = MailingList::from(list.clone()); + list_obj.set_safety(list_owners.as_slice(), &state.conf.administrators); let context = minijinja::context! { canonical_url => ListPath::from(&list).to_crumb(), page_title => &list.name, @@ -128,7 +131,7 @@ pub async fn list( months, hists => &hist, posts => posts_ctx, - list => Value::from_object(MailingList::from(list)), + list => Value::from_object(list_obj), current_user => auth.current_user, user_context, messages => session.drain_messages(), @@ -196,11 +199,16 @@ pub async fn list_post( url: ListPostPath(list.id.to_string().into(), msg_id.to_string()).to_crumb(), }, ]; + + let list_owners = db.list_owners(list.pk)?; + let mut list_obj = MailingList::from(list.clone()); + list_obj.set_safety(list_owners.as_slice(), &state.conf.administrators); + let context = minijinja::context! { canonical_url => ListPostPath(ListPathIdentifier::from(list.id.clone()), msg_id.to_string()).to_crumb(), page_title => subject_ref, description => &list.description, - list => Value::from_object(MailingList::from(list)), + list => Value::from_object(list_obj), pk => post.pk, body => &body_text, from => &envelope.field_from_to_string(), @@ -300,6 +308,9 @@ pub async fn list_edit( url: ListEditPath(ListPathIdentifier::from(list.id.clone())).to_crumb(), }, ]; + let list_owners = db.list_owners(list.pk)?; + let mut list_obj = MailingList::from(list.clone()); + list_obj.set_safety(list_owners.as_slice(), &state.conf.administrators); let context = minijinja::context! { canonical_url => ListEditPath(ListPathIdentifier::from(list.id.clone())).to_crumb(), page_title => format!("Edit {} settings", list.name), @@ -310,7 +321,7 @@ pub async fn list_edit( post_count, subs_count, sub_requests_count, - list => Value::from_object(MailingList::from(list)), + list => Value::from_object(list_obj), current_user => auth.current_user, messages => session.drain_messages(), crumbs, @@ -661,11 +672,14 @@ pub async fn list_subscribers( url: ListEditSubscribersPath(list.id.to_string().into()).to_crumb(), }, ]; + let list_owners = db.list_owners(list.pk)?; + let mut list_obj = MailingList::from(list.clone()); + list_obj.set_safety(list_owners.as_slice(), &state.conf.administrators); let context = minijinja::context! { canonical_url => ListEditSubscribersPath(ListPathIdentifier::from(list.id.clone())).to_crumb(), page_title => format!("Subscribers of {}", list.name), subs, - list => Value::from_object(MailingList::from(list)), + list => Value::from_object(list_obj), current_user => auth.current_user, messages => session.drain_messages(), crumbs, @@ -744,11 +758,13 @@ pub async fn list_candidates( url: ListEditCandidatesPath(list.id.to_string().into()).to_crumb(), }, ]; + let mut list_obj: MailingList = MailingList::from(list.clone()); + list_obj.set_safety(list_owners.as_slice(), &state.conf.administrators); let context = minijinja::context! { canonical_url => ListEditCandidatesPath(ListPathIdentifier::from(list.id.clone())).to_crumb(), page_title => format!("Requests of {}", list.name), subs, - list => Value::from_object(MailingList::from(list)), + list => Value::from_object(list_obj), current_user => auth.current_user, messages => session.drain_messages(), crumbs, diff --git a/web/src/main.rs b/web/src/main.rs index df233ad..e80c06d 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -198,13 +198,14 @@ async fn root( .earliest() .map(|d| d.to_string()) }); + let list_owners = db.list_owners(list.pk)?; + let mut list_obj = MailingList::from(list.clone()); + list_obj.set_safety(list_owners.as_slice(), &state.conf.administrators); Ok(minijinja::context! { - name => &list.name, newest, posts => &posts, months => &months, - description => &list.description.as_deref().unwrap_or_default(), - list => Value::from_object(MailingList::from(list.clone())), + list => Value::from_object(list_obj), }) }) .collect::, mailpot::Error>>()?; diff --git a/web/src/minijinja_utils.rs b/web/src/minijinja_utils.rs index ce77da1..b7a3d65 100644 --- a/web/src/minijinja_utils.rs +++ b/web/src/minijinja_utils.rs @@ -21,6 +21,8 @@ use std::fmt::Write; +use mailpot::models::ListOwner; + use super::*; mod compressed; @@ -98,6 +100,29 @@ pub struct MailingList { #[serde(serialize_with = "super::utils::to_safe_string_opt")] pub archive_url: Option, pub inner: DbVal, + #[serde(default)] + pub is_description_html_safe: bool, +} + +impl MailingList { + /// Set whether it's safe to not escape the list's description field. + /// + /// If anyone can display arbitrary html in the server, that's bad. + /// + /// Note: uses `Borrow` so that it can use both `DbVal` and + /// `ListOwner` slices. + pub fn set_safety>( + &mut self, + owners: &[O], + administrators: &[String], + ) { + if owners.is_empty() || administrators.is_empty() { + return; + } + self.is_description_html_safe = owners + .iter() + .any(|o| administrators.contains(&o.borrow().address)); + } } impl From> for MailingList { @@ -124,6 +149,7 @@ impl From> for MailingList { topics, archive_url, inner: val, + is_description_html_safe: false, } } } @@ -181,9 +207,18 @@ impl minijinja::value::StructObject for MailingList { "name" => Some(Value::from_serializable(&self.name)), "id" => Some(Value::from_serializable(&self.id)), "address" => Some(Value::from_serializable(&self.address)), + "description" if self.is_description_html_safe => { + self.description.as_ref().map_or_else( + || Some(Value::from_serializable(&self.description)), + |d| Some(Value::from_safe_string(d.clone())), + ) + } "description" => Some(Value::from_serializable(&self.description)), "topics" => Some(Value::from_serializable(&self.topics)), "archive_url" => Some(Value::from_serializable(&self.archive_url)), + "is_description_html_safe" => { + Some(Value::from_serializable(&self.is_description_html_safe)) + } _ => None, } } @@ -198,6 +233,7 @@ impl minijinja::value::StructObject for MailingList { "description", "topics", "archive_url", + "is_description_html_safe", ][..], ) } @@ -782,4 +818,46 @@ mod tests { 0)(24, 0)(25, 0)(26, 0)(27, 0)(28, 0)(29, 0)(30, 0)" ); } + + #[test] + fn test_list_html_safe() { + let mut list = MailingList { + pk: 0, + name: String::new(), + id: String::new(), + address: String::new(), + description: None, + topics: vec![], + archive_url: None, + inner: DbVal( + mailpot::models::MailingList { + pk: 0, + name: String::new(), + id: String::new(), + address: String::new(), + description: None, + topics: vec![], + archive_url: None, + }, + 0, + ), + is_description_html_safe: false, + }; + + let mut list_owners = vec![ListOwner { + pk: 0, + list: 0, + address: "admin@example.com".to_string(), + name: None, + }]; + let administrators = vec!["admin@example.com".to_string()]; + list.set_safety(&list_owners, &administrators); + assert!(list.is_description_html_safe); + list.set_safety::(&[], &[]); + assert!(list.is_description_html_safe); + list.is_description_html_safe = false; + list_owners[0].address = "user@example.com".to_string(); + list.set_safety(&list_owners, &administrators); + assert!(!list.is_description_html_safe); + } } diff --git a/web/src/settings.rs b/web/src/settings.rs index 033614b..20e38fb 100644 --- a/web/src/settings.rs +++ b/web/src/settings.rs @@ -302,6 +302,9 @@ pub async fn user_list_subscription( }, ]; + let list_owners = db.list_owners(list.pk)?; + let mut list = crate::minijinja_utils::MailingList::from(list); + list.set_safety(list_owners.as_slice(), &state.conf.administrators); let context = minijinja::context! { page_title => "Subscription settings", user => user, diff --git a/web/src/templates/lists.html b/web/src/templates/lists.html index e1f8c45..a6d5698 100644 --- a/web/src/templates/lists.html +++ b/web/src/templates/lists.html @@ -5,7 +5,7 @@
{% for l in lists %}
{{ l.list.name }}
-
{{ l.list.description if l.list.description else "no description" }} | {{ l.posts|length }} post{{ l.posts|length|pluralize("","s") }}{% if l.newest %} | {% endif %}{% if l.list.topics|length > 0 %}Topics: {{ l.list.topics() }}{% endif %}
+
{{ l.list.description if l.list.description else "

no description

"|safe }}

{{ l.posts|length }} post{{ l.posts|length|pluralize("","s") }}{% if l.newest %} | {% endif %}{% if l.list.topics|length > 0 %}Topics: {{ l.list.topics() }}{% endif %}
{% endfor %}
diff --git a/web/src/templates/lists/edit.html b/web/src/templates/lists/edit.html index 396bee5..02c3ef3 100644 --- a/web/src/templates/lists/edit.html +++ b/web/src/templates/lists/edit.html @@ -5,7 +5,11 @@ {{ list.name }} {{ list.address }} {% if list.description %} -

{{ list.description }}

+ {% if list.is_description_html_safe %} + {{ list.description|safe}} + {% else %} +

{{ list.description }}

+ {% endif %} {% endif %} {% if list.archive_url %}

{{ list.archive_url }}

diff --git a/web/src/templates/lists/list.html b/web/src/templates/lists/list.html index 4844855..e6aef5c 100644 --- a/web/src/templates/lists/list.html +++ b/web/src/templates/lists/list.html @@ -5,7 +5,7 @@ {% endif %} {% if list.description %} -

List description: {{ list.description }}

+

{{ list.description }}

{% else %}

No list description.

{% endif %} @@ -17,12 +17,14 @@ +
{% else %}
+
{% endif %} {% endif %} {% if preamble %} diff --git a/web/src/templates/settings_subscription.html b/web/src/templates/settings_subscription.html index cd2d708..abe7289 100644 --- a/web/src/templates/settings_subscription.html +++ b/web/src/templates/settings_subscription.html @@ -4,7 +4,9 @@
{{ list.name }} {{ list.address }}
- {% if list.description %} + {% if list.is_description_html_safe %} + {{ list.description|safe}} + {% else %}

{{ list.description }}

{% endif %} {% if list.archive_url %}