web: make list description string safe for html if any owner is an admin
parent
211700ad9a
commit
17e1a79cef
|
@ -58,6 +58,15 @@ impl<T> DbVal<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> std::borrow::Borrow<T> for DbVal<T>
|
||||||
|
where
|
||||||
|
T: Sized,
|
||||||
|
{
|
||||||
|
fn borrow(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> std::ops::Deref for DbVal<T> {
|
impl<T> std::ops::Deref for DbVal<T> {
|
||||||
type Target = T;
|
type Target = T;
|
||||||
fn deref(&self) -> &T {
|
fn deref(&self) -> &T {
|
||||||
|
|
|
@ -118,6 +118,9 @@ pub async fn list(
|
||||||
url: ListPath(list.id.to_string().into()).to_crumb(),
|
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! {
|
let context = minijinja::context! {
|
||||||
canonical_url => ListPath::from(&list).to_crumb(),
|
canonical_url => ListPath::from(&list).to_crumb(),
|
||||||
page_title => &list.name,
|
page_title => &list.name,
|
||||||
|
@ -128,7 +131,7 @@ pub async fn list(
|
||||||
months,
|
months,
|
||||||
hists => &hist,
|
hists => &hist,
|
||||||
posts => posts_ctx,
|
posts => posts_ctx,
|
||||||
list => Value::from_object(MailingList::from(list)),
|
list => Value::from_object(list_obj),
|
||||||
current_user => auth.current_user,
|
current_user => auth.current_user,
|
||||||
user_context,
|
user_context,
|
||||||
messages => session.drain_messages(),
|
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(),
|
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! {
|
let context = minijinja::context! {
|
||||||
canonical_url => ListPostPath(ListPathIdentifier::from(list.id.clone()), msg_id.to_string()).to_crumb(),
|
canonical_url => ListPostPath(ListPathIdentifier::from(list.id.clone()), msg_id.to_string()).to_crumb(),
|
||||||
page_title => subject_ref,
|
page_title => subject_ref,
|
||||||
description => &list.description,
|
description => &list.description,
|
||||||
list => Value::from_object(MailingList::from(list)),
|
list => Value::from_object(list_obj),
|
||||||
pk => post.pk,
|
pk => post.pk,
|
||||||
body => &body_text,
|
body => &body_text,
|
||||||
from => &envelope.field_from_to_string(),
|
from => &envelope.field_from_to_string(),
|
||||||
|
@ -300,6 +308,9 @@ pub async fn list_edit(
|
||||||
url: ListEditPath(ListPathIdentifier::from(list.id.clone())).to_crumb(),
|
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! {
|
let context = minijinja::context! {
|
||||||
canonical_url => ListEditPath(ListPathIdentifier::from(list.id.clone())).to_crumb(),
|
canonical_url => ListEditPath(ListPathIdentifier::from(list.id.clone())).to_crumb(),
|
||||||
page_title => format!("Edit {} settings", list.name),
|
page_title => format!("Edit {} settings", list.name),
|
||||||
|
@ -310,7 +321,7 @@ pub async fn list_edit(
|
||||||
post_count,
|
post_count,
|
||||||
subs_count,
|
subs_count,
|
||||||
sub_requests_count,
|
sub_requests_count,
|
||||||
list => Value::from_object(MailingList::from(list)),
|
list => Value::from_object(list_obj),
|
||||||
current_user => auth.current_user,
|
current_user => auth.current_user,
|
||||||
messages => session.drain_messages(),
|
messages => session.drain_messages(),
|
||||||
crumbs,
|
crumbs,
|
||||||
|
@ -661,11 +672,14 @@ pub async fn list_subscribers(
|
||||||
url: ListEditSubscribersPath(list.id.to_string().into()).to_crumb(),
|
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! {
|
let context = minijinja::context! {
|
||||||
canonical_url => ListEditSubscribersPath(ListPathIdentifier::from(list.id.clone())).to_crumb(),
|
canonical_url => ListEditSubscribersPath(ListPathIdentifier::from(list.id.clone())).to_crumb(),
|
||||||
page_title => format!("Subscribers of {}", list.name),
|
page_title => format!("Subscribers of {}", list.name),
|
||||||
subs,
|
subs,
|
||||||
list => Value::from_object(MailingList::from(list)),
|
list => Value::from_object(list_obj),
|
||||||
current_user => auth.current_user,
|
current_user => auth.current_user,
|
||||||
messages => session.drain_messages(),
|
messages => session.drain_messages(),
|
||||||
crumbs,
|
crumbs,
|
||||||
|
@ -744,11 +758,13 @@ pub async fn list_candidates(
|
||||||
url: ListEditCandidatesPath(list.id.to_string().into()).to_crumb(),
|
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! {
|
let context = minijinja::context! {
|
||||||
canonical_url => ListEditCandidatesPath(ListPathIdentifier::from(list.id.clone())).to_crumb(),
|
canonical_url => ListEditCandidatesPath(ListPathIdentifier::from(list.id.clone())).to_crumb(),
|
||||||
page_title => format!("Requests of {}", list.name),
|
page_title => format!("Requests of {}", list.name),
|
||||||
subs,
|
subs,
|
||||||
list => Value::from_object(MailingList::from(list)),
|
list => Value::from_object(list_obj),
|
||||||
current_user => auth.current_user,
|
current_user => auth.current_user,
|
||||||
messages => session.drain_messages(),
|
messages => session.drain_messages(),
|
||||||
crumbs,
|
crumbs,
|
||||||
|
|
|
@ -198,13 +198,14 @@ async fn root(
|
||||||
.earliest()
|
.earliest()
|
||||||
.map(|d| d.to_string())
|
.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! {
|
Ok(minijinja::context! {
|
||||||
name => &list.name,
|
|
||||||
newest,
|
newest,
|
||||||
posts => &posts,
|
posts => &posts,
|
||||||
months => &months,
|
months => &months,
|
||||||
description => &list.description.as_deref().unwrap_or_default(),
|
list => Value::from_object(list_obj),
|
||||||
list => Value::from_object(MailingList::from(list.clone())),
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, mailpot::Error>>()?;
|
.collect::<Result<Vec<_>, mailpot::Error>>()?;
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use mailpot::models::ListOwner;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
mod compressed;
|
mod compressed;
|
||||||
|
@ -98,6 +100,29 @@ pub struct MailingList {
|
||||||
#[serde(serialize_with = "super::utils::to_safe_string_opt")]
|
#[serde(serialize_with = "super::utils::to_safe_string_opt")]
|
||||||
pub archive_url: Option<String>,
|
pub archive_url: Option<String>,
|
||||||
pub inner: DbVal<mailpot::models::MailingList>,
|
pub inner: DbVal<mailpot::models::MailingList>,
|
||||||
|
#[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<ListOwner>` and
|
||||||
|
/// `ListOwner` slices.
|
||||||
|
pub fn set_safety<O: std::borrow::Borrow<ListOwner>>(
|
||||||
|
&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<DbVal<mailpot::models::MailingList>> for MailingList {
|
impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
||||||
|
@ -124,6 +149,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
||||||
topics,
|
topics,
|
||||||
archive_url,
|
archive_url,
|
||||||
inner: val,
|
inner: val,
|
||||||
|
is_description_html_safe: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,9 +207,18 @@ impl minijinja::value::StructObject for MailingList {
|
||||||
"name" => Some(Value::from_serializable(&self.name)),
|
"name" => Some(Value::from_serializable(&self.name)),
|
||||||
"id" => Some(Value::from_serializable(&self.id)),
|
"id" => Some(Value::from_serializable(&self.id)),
|
||||||
"address" => Some(Value::from_serializable(&self.address)),
|
"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)),
|
"description" => Some(Value::from_serializable(&self.description)),
|
||||||
"topics" => Some(Value::from_serializable(&self.topics)),
|
"topics" => Some(Value::from_serializable(&self.topics)),
|
||||||
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
||||||
|
"is_description_html_safe" => {
|
||||||
|
Some(Value::from_serializable(&self.is_description_html_safe))
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,6 +233,7 @@ impl minijinja::value::StructObject for MailingList {
|
||||||
"description",
|
"description",
|
||||||
"topics",
|
"topics",
|
||||||
"archive_url",
|
"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)"
|
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::<ListOwner>(&[], &[]);
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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! {
|
let context = minijinja::context! {
|
||||||
page_title => "Subscription settings",
|
page_title => "Subscription settings",
|
||||||
user => user,
|
user => user,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<dl class="lists" aria-label="list of mailing lists">
|
<dl class="lists" aria-label="list of mailing lists">
|
||||||
{% for l in lists %}
|
{% for l in lists %}
|
||||||
<dt aria-label="mailing list name"><a href="{{ list_path(l.list.id) }}">{{ l.list.name }}</a></dt>
|
<dt aria-label="mailing list name"><a href="{{ list_path(l.list.id) }}">{{ l.list.name }}</a></dt>
|
||||||
<dd><span aria-label="mailing list description"{% if not l.list.description %} class="no-description"{% endif %}>{{ l.list.description if l.list.description else "no description" }}</span> | {{ l.posts|length }} post{{ l.posts|length|pluralize("","s") }}{% if l.newest %} | <time datetime="{{ l.newest }}">{{ l.newest }}</time>{% endif %}{% if l.list.topics|length > 0 %}<br aria-hidden="true"><br aria-hidden="true"><span><em>Topics</em>:</span> {{ l.list.topics() }}{% endif %}</dd>
|
<dd><span aria-label="mailing list description"{% if not l.list.description %} class="no-description"{% endif %}>{{ l.list.description if l.list.description else "<p>no description</p>"|safe }}</span><br />{{ l.posts|length }} post{{ l.posts|length|pluralize("","s") }}{% if l.newest %} | <time datetime="{{ l.newest }}">{{ l.newest }}</time>{% endif %}{% if l.list.topics|length > 0 %}<br aria-hidden="true"><br aria-hidden="true"><span><em>Topics</em>:</span> {{ l.list.topics() }}{% endif %}</dd>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,11 @@
|
||||||
{{ list.name }} <a href="mailto:{{ list.address | safe }}"><code>{{ list.address }}</code></a>
|
{{ list.name }} <a href="mailto:{{ list.address | safe }}"><code>{{ list.address }}</code></a>
|
||||||
</address>
|
</address>
|
||||||
{% if list.description %}
|
{% if list.description %}
|
||||||
<p>{{ list.description }}</p>
|
{% if list.is_description_html_safe %}
|
||||||
|
{{ list.description|safe}}
|
||||||
|
{% else %}
|
||||||
|
<p>{{ list.description }}</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if list.archive_url %}
|
{% if list.archive_url %}
|
||||||
<p><a href="{{ list.archive_url }}">{{ list.archive_url }}</a></p>
|
<p><a href="{{ list.archive_url }}">{{ list.archive_url }}</a></p>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<br aria-hidden="true">
|
<br aria-hidden="true">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if list.description %}
|
{% if list.description %}
|
||||||
<p title="mailing list description">List description: {{ list.description }}</p>
|
<p title="mailing list description">{{ list.description }}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p title="mailing list description">No list description.</p>
|
<p title="mailing list description">No list description.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -17,12 +17,14 @@
|
||||||
<input type="hidden" name="list_pk", value="{{ list.pk }}">
|
<input type="hidden" name="list_pk", value="{{ list.pk }}">
|
||||||
<input type="submit" name="unsubscribe" value="Unsubscribe as {{ current_user.address }}">
|
<input type="submit" name="unsubscribe" value="Unsubscribe as {{ current_user.address }}">
|
||||||
</form>
|
</form>
|
||||||
|
<br />
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="post" action="{{ settings_path() }}" class="settings-form">
|
<form method="post" action="{{ settings_path() }}" class="settings-form">
|
||||||
<input type="hidden" name="type", value="subscribe">
|
<input type="hidden" name="type", value="subscribe">
|
||||||
<input type="hidden" name="list_pk", value="{{ list.pk }}">
|
<input type="hidden" name="list_pk", value="{{ list.pk }}">
|
||||||
<input type="submit" name="subscribe" value="Subscribe as {{ current_user.address }}">
|
<input type="submit" name="subscribe" value="Subscribe as {{ current_user.address }}">
|
||||||
</form>
|
</form>
|
||||||
|
<br />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if preamble %}
|
{% if preamble %}
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
<address>
|
<address>
|
||||||
{{ list.name }} <a href="mailto:{{ list.address | safe }}"><code>{{ list.address }}</code></a>
|
{{ list.name }} <a href="mailto:{{ list.address | safe }}"><code>{{ list.address }}</code></a>
|
||||||
</address>
|
</address>
|
||||||
{% if list.description %}
|
{% if list.is_description_html_safe %}
|
||||||
|
{{ list.description|safe}}
|
||||||
|
{% else %}
|
||||||
<p>{{ list.description }}</p>
|
<p>{{ list.description }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if list.archive_url %}
|
{% if list.archive_url %}
|
||||||
|
|
Loading…
Reference in New Issue