diff --git a/Cargo.lock b/Cargo.lock
index 3f82443..b697fa8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2411,18 +2411,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
-version = "1.0.148"
+version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
+checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.148"
+version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
+checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4"
dependencies = [
"proc-macro2",
"quote",
@@ -2860,9 +2860,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
-version = "1.22.0"
+version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
+checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
dependencies = [
"autocfg",
"bytes",
@@ -2873,7 +2873,7 @@ dependencies = [
"pin-project-lite 0.2.9",
"socket2",
"tokio-macros",
- "winapi",
+ "windows-sys",
]
[[package]]
@@ -3011,9 +3011,9 @@ dependencies = [
[[package]]
name = "typenum"
-version = "1.15.0"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "ucd-trie"
diff --git a/Cargo.toml b/Cargo.toml
index 0156190..64ea791 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,12 +23,12 @@ petname = { version = "1.1.3", optional = true, default-features = false, featur
rand = "0.8.5"
rand_core = { version = "0.6.4", features = ["std"] }
rpassword = "7.2.0"
-serde = { version = "1.0.148", features = ["derive", "rc"] }
+serde = { version = "1.0.149", features = ["derive", "rc"] }
sha2 = "0.10.6"
sled = "0.34.7"
tera = { version = "1.17.1", features = ["builtins", "date-locale"] }
tide = { version = "0.16.0", default-features = false, features = ["h1-server", "cookies", "logger"] }
-tokio = { version = "1.22.0", features = ["macros", "rt-multi-thread"] }
+tokio = { version = "1.23.0", features = ["macros", "rt-multi-thread"] }
toml_edit = { version = "0.15.0", features = ["easy"] }
typed-sled = "0.2.0"
unic-langid = { version = "0.9.1", features = ["macros"] }
diff --git a/locales/en.ftl b/locales/en.ftl
index 0cf2300..8ce2d08 100644
--- a/locales/en.ftl
+++ b/locales/en.ftl
@@ -18,4 +18,6 @@ comment_form-email = Your email:
comment_form-edit_button = Edit comment
comment_form-new_button = Post comment
comment_form-text = Your comment:
+new_comment-success = Your comment has been published. You can edit it with this link.
+new_comment-success_pending = Your comment has been saved. It will be reviewed by an administrator before being published. You can edit it with this link.
title = Comments
diff --git a/src/db.rs b/src/db.rs
index b76a626..544137e 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -36,12 +36,41 @@ pub fn load_dbs(path: Option<&Path>) -> Dbs {
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Comment {
- pub topic_hash: TopicHash,
pub author: String,
pub email: Option,
pub last_edit_time: Option,
+ pub mutation_token: MutationToken,
pub post_time: u64,
pub text: String,
+ pub topic_hash: TopicHash,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
+pub struct MutationToken(pub [u8; 16]);
+
+impl MutationToken {
+ pub fn new() -> Self {
+ Self(rand::random())
+ }
+
+ pub fn to_base64(&self) -> String {
+ base64::encode_config(self.0, base64::URL_SAFE_NO_PAD)
+ }
+
+ pub fn from_base64(s: &str) -> Result {
+ std::panic::catch_unwind(|| {
+ let mut buf = [0; 16];
+ base64::decode_config_slice(s, base64::URL_SAFE_NO_PAD, &mut buf)?;
+ Ok(Self(buf))
+ })
+ .map_err(|_| base64::DecodeError::InvalidLength)?
+ }
+}
+
+impl AsRef<[u8]> for MutationToken {
+ fn as_ref(&self) -> &[u8] {
+ &self.0
+ }
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
@@ -82,12 +111,12 @@ impl CommentId {
}
pub fn from_base64(s: &str) -> Result {
- let mut buf = [0; 16];
- std::panic::catch_unwind(move || {
- base64::decode_config_slice(s, base64::URL_SAFE_NO_PAD, &mut buf)
+ std::panic::catch_unwind(|| {
+ let mut buf = [0; 16];
+ base64::decode_config_slice(s, base64::URL_SAFE_NO_PAD, &mut buf)?;
+ Ok(Self(buf))
})
.map_err(|_| base64::DecodeError::InvalidLength)?
- .map(|_| Self(buf))
}
}
@@ -112,6 +141,17 @@ mod test {
assert_eq!(iter.next(), Some(Ok(((123, 456), ()))));
}
+ #[test]
+ fn test_comment_id_base64() {
+ for _ in 0..10 {
+ let comment_id = CommentId::new();
+ assert_eq!(
+ CommentId::from_base64(&comment_id.to_base64()),
+ Ok(comment_id)
+ );
+ }
+ }
+
#[test]
fn test_from_base64_dont_panic() {
assert_eq!(CommentId::from_base64("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), Err(base64::DecodeError::InvalidLength));
diff --git a/src/helpers.rs b/src/helpers.rs
index 93c09bc..7ccd231 100644
--- a/src/helpers.rs
+++ b/src/helpers.rs
@@ -217,6 +217,31 @@ pub fn check_comment(
}
}
+pub fn check_can_edit_comment<'a>(
+ config: &Config,
+ comment: &Comment,
+ mutation_token: &MutationToken,
+) -> Result<(), &'a str> {
+ if &comment.mutation_token != mutation_token {
+ return Err("bad mutation token");
+ }
+
+ let time = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ if time
+ > comment
+ .post_time
+ .saturating_add(config.comment_edit_timeout)
+ {
+ return Err("mutation timeout expired");
+ }
+
+ Ok(())
+}
+
#[cfg(test)]
mod test {
use super::*;
@@ -228,12 +253,13 @@ mod test {
author: String::from("Emmanuel Goldstein"),
email: None,
last_edit_time: None,
+ mutation_token: MutationToken::new(),
post_time: 42,
text: String::from("Hello world!"),
};
let dbs = load_dbs(None);
- let comment_id = new_pending_comment(&comment, &dbs).unwrap();
+ let comment_id = new_pending_comment(&comment, None, &dbs).unwrap();
let mut iter = dbs.comment.iter();
assert_eq!(iter.next(), Some(Ok((comment_id.clone(), comment.clone()))));
@@ -248,7 +274,7 @@ mod test {
comment.post_time,
comment_id.clone()
),
- ()
+ None
)))
);
assert_eq!(iter.next(), None);
diff --git a/src/locales.rs b/src/locales.rs
index a23738d..757d3bf 100644
--- a/src/locales.rs
+++ b/src/locales.rs
@@ -28,6 +28,8 @@ impl Locales {
.iter()
.map(|(lang, raw)| {
let mut bundle = FluentBundle::new_concurrent(vec![lang.clone()]);
+ // We don't want dangerous zero-width bidi chars everywhere! (issue #26)
+ bundle.set_use_isolating(false);
bundle
.add_resource(
FluentResource::try_new(raw.to_string()).unwrap_or_else(|e| {
@@ -68,7 +70,6 @@ impl Locales {
NegotiationStrategy::Filtering,
) {
if let Some(bundle) = self.bundles.get(prefered_lang.as_ref()) {
- println!("got bundle");
if let Some(message) = bundle.get_message(key) {
let mut errors = Vec::new();
let ret = bundle.format_pattern(message.value().unwrap(), args, &mut errors);
diff --git a/src/server.rs b/src/server.rs
index ab0880f..b9acac4 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -39,6 +39,43 @@ pub async fn run_server(
}
});
app.at(&format!("{}t/:topic", config.root_url)).post({
+ let dbs = dbs.clone();
+ let notify_send = notify_send.clone();
+ move |req: tide::Request<()>| {
+ handle_post_comments(
+ req,
+ config,
+ templates,
+ dbs.clone(),
+ locales,
+ notify_send.clone(),
+ )
+ }
+ });
+ app.at(&format!(
+ "{}t/:topic/edit/:comment_id/:mutation_token",
+ config.root_url
+ ))
+ .get({
+ let dbs = dbs.clone();
+ move |req: tide::Request<()>| {
+ let client_langs = get_client_langs(&req);
+ serve_edit_comment(
+ req,
+ config,
+ templates,
+ dbs.clone(),
+ client_langs,
+ Context::new(),
+ 200,
+ )
+ }
+ });
+ app.at(&format!(
+ "{}t/:topic/edit/:comment_id/:mutation_token",
+ config.root_url
+ ))
+ .post({
let dbs = dbs.clone();
move |req: tide::Request<()>| {
handle_post_comments(
@@ -63,6 +100,53 @@ pub async fn run_server(
app.listen(config.listen).await.unwrap();
}
+async fn serve_edit_comment<'a>(
+ req: tide::Request<()>,
+ config: &Config,
+ templates: &Templates,
+ dbs: Dbs,
+ client_langs: Vec,
+ mut context: Context,
+ status_code: u16,
+) -> tide::Result {
+ let (Ok(comment_id_str), Ok(mutation_token_str)) = (req.param("comment_id"), req.param("mutation_token")) else {
+ context.insert("log", &["no comment id or no token"]);
+ return serve_comments(req, config, templates, dbs, client_langs, context, 400).await;
+ };
+
+ let (Ok(comment_id), Ok(mutation_token)) = (CommentId::from_base64(comment_id_str), MutationToken::from_base64(mutation_token_str)) else {
+ context.insert("log", &["badly encoded comment id or token"]);
+ return serve_comments(req, config, templates, dbs, client_langs, context, 400).await;
+ };
+
+ let Some(comment) = dbs.comment.get(&comment_id).unwrap() else {
+ context.insert("log", &["not found comment"]);
+ return serve_comments(req, config, templates, dbs, client_langs, context, 404).await;
+ };
+
+ if let Err(e) = helpers::check_can_edit_comment(config, &comment, &mutation_token) {
+ context.insert("log", &[e]);
+ return serve_comments(req, config, templates, dbs, client_langs, context, 403).await;
+ }
+
+ context.insert("edit_comment", &comment_id.to_base64());
+ context.insert("edit_comment_mutation_token", &mutation_token.to_base64());
+ context.insert("edit_comment_author", &comment.author);
+ context.insert("edit_comment_email", &comment.email);
+ context.insert("edit_comment_text", &comment.text);
+
+ serve_comments(
+ req,
+ config,
+ templates,
+ dbs,
+ client_langs,
+ context,
+ status_code,
+ )
+ .await
+}
+
async fn serve_comments<'a>(
req: tide::Request<()>,
config: &Config,
@@ -337,17 +421,43 @@ async fn handle_post_comments(
Some(query.comment.email)
},
last_edit_time: None,
+ mutation_token: MutationToken::new(),
post_time: time,
text: query.comment.text,
};
- helpers::new_pending_comment(&comment, client_addr, &dbs)
- .map_err(|e| error!("Adding pending comment: {:?}", e))
- .ok();
- notify_send
- .send(Notification {
- topic: topic.to_string(),
- })
- .ok();
+ match helpers::new_pending_comment(&comment, client_addr, &dbs) {
+ Ok(comment_id) => {
+ notify_send
+ .send(Notification {
+ topic: topic.to_string(),
+ })
+ .ok();
+ context.insert(
+ "log",
+ &[locales
+ .tr(
+ &client_langs,
+ if config.comment_approve {
+ "new_comment-success_pending"
+ } else {
+ "new_comment-success"
+ },
+ Some(&FluentArgs::from_iter([(
+ "edit_link",
+ format!(
+ "{}t/{}/edit/{}/{}",
+ &config.root_url,
+ topic,
+ comment_id.to_base64(),
+ comment.mutation_token.to_base64(),
+ ),
+ )])),
+ )
+ .unwrap()],
+ );
+ }
+ Err(e) => error!("Adding pending comment: {:?}", e),
+ }
} else {
context.insert("new_comment_author", &query.comment.author);
context.insert("new_comment_email", &query.comment.email);
@@ -356,51 +466,69 @@ async fn handle_post_comments(
context.insert("new_comment_errors", &errors);
}
CommentQuery::EditComment(query) => {
- if !admin {
- return Err(tide::Error::from_str(403, "Forbidden"));
- }
-
helpers::check_comment(config, locales, &client_langs, &query.comment, &mut errors);
- let comment_id = if let Ok(comment_id) = CommentId::from_base64(&query.id) {
- comment_id
- } else {
+ let Ok(comment_id) = CommentId::from_base64(&query.id) else {
return Err(tide::Error::from_str(400, "Invalid comment id"));
};
- let mut comment = if let Some(comment) = dbs.comment.get(&comment_id).unwrap() {
- comment
- } else {
+ let Some(mut comment) = dbs.comment.get(&comment_id).unwrap() else {
return Err(tide::Error::from_str(404, "Not found"));
};
- // We're admin
- /*if let Some(client_addr) = &client_addr {
- if let Some(antispam_timeout) =
- helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
- {
- let client_langs = get_client_langs(&req);
- errors.push(
- locales
- .tr(
- &client_langs,
- "error-antispam",
- Some(&FluentArgs::from_iter([(
- "antispam_timeout",
- antispam_timeout,
- )])),
- )
- .unwrap()
- .into_owned(),
- );
+ let mutation_token = if admin {
+ None
+ } else {
+ 'mutation_token: {
+ let Ok(mutation_token_str) = req.param("mutation_token") else {
+ errors.push("no mutation token".into());
+ break 'mutation_token None;
+ };
+
+ let Ok(mutation_token) = MutationToken::from_base64(mutation_token_str) else {
+ errors.push("badly encoded token".into());
+ break 'mutation_token None;
+ };
+
+ if let Err(e) =
+ helpers::check_can_edit_comment(config, &comment, &mutation_token)
+ {
+ errors.push(e.to_string());
+ }
+
+ Some(mutation_token)
}
- }*/
+ };
+
+ if !admin {
+ if let Some(client_addr) = &client_addr {
+ if let Some(antispam_timeout) =
+ helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
+ {
+ let client_langs = get_client_langs(&req);
+ errors.push(
+ locales
+ .tr(
+ &client_langs,
+ "error-antispam",
+ Some(&FluentArgs::from_iter([(
+ "antispam_timeout",
+ antispam_timeout,
+ )])),
+ )
+ .unwrap()
+ .into_owned(),
+ );
+ }
+ }
+ }
if errors.is_empty() {
- // We're admin
- /*if let Some(client_addr) = &client_addr {
- helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
- }*/
+ if !admin {
+ if let Some(client_addr) = &client_addr {
+ helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
+ }
+ }
let time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -423,9 +551,16 @@ async fn handle_post_comments(
dbs.comment.insert(&comment_id, &comment).unwrap();
} else {
context.insert("edit_comment", &comment_id.to_base64());
+ if let Some(mutation_token) = &mutation_token {
+ context.insert("edit_comment_mutation_token", &mutation_token.to_base64());
+ }
context.insert("edit_comment_author", &query.comment.author);
context.insert("edit_comment_email", &query.comment.email);
context.insert("edit_comment_text", &query.comment.text);
+ context.insert("edit_comment_errors", &errors);
+
+ return serve_edit_comment(req, config, templates, dbs, client_langs, context, 400)
+ .await;
}
context.insert("edit_comment_errors", &errors);
}
diff --git a/templates/comments.html b/templates/comments.html
index 3960fa5..4740e56 100644
--- a/templates/comments.html
+++ b/templates/comments.html
@@ -5,6 +5,14 @@
{{ tr(l=l,k="title")|safe }}
+ {% if log %}
+
+ {% for log_msg in log %}
+ - {{ log_msg | safe }}
+ {% endfor %}
+
+ {% endif %}
+
{% if comments_pending %}