1 use crate::error::{LemmyError, LemmyResult};
2 use itertools::Itertools;
3 use once_cell::sync::Lazy;
4 use regex::{Regex, RegexBuilder};
5 use totp_rs::{Secret, TOTP};
8 static VALID_ACTOR_NAME_REGEX: Lazy<Regex> =
9 Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,}$").expect("compile regex"));
10 static VALID_POST_TITLE_REGEX: Lazy<Regex> =
11 Lazy::new(|| Regex::new(r".*\S{3,}.*").expect("compile regex"));
12 static VALID_MATRIX_ID_REGEX: Lazy<Regex> = Lazy::new(|| {
13 Regex::new(r"^@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$").expect("compile regex")
15 // taken from https://en.wikipedia.org/wiki/UTM_parameters
16 static CLEAN_URL_PARAMS_REGEX: Lazy<Regex> = Lazy::new(|| {
17 Regex::new(r"^utm_source|utm_medium|utm_campaign|utm_term|utm_content|gclid|gclsrc|dclid|fbclid$")
18 .expect("compile regex")
21 const BODY_MAX_LENGTH: usize = 10000;
22 const POST_BODY_MAX_LENGTH: usize = 50000;
23 const BIO_MAX_LENGTH: usize = 300;
24 const SITE_NAME_MAX_LENGTH: usize = 20;
25 const SITE_NAME_MIN_LENGTH: usize = 1;
26 const SITE_DESCRIPTION_MAX_LENGTH: usize = 150;
28 fn has_newline(name: &str) -> bool {
32 pub fn is_valid_actor_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> {
33 let check = name.chars().count() <= actor_name_max_length
34 && VALID_ACTOR_NAME_REGEX.is_match(name)
35 && !has_newline(name);
37 Err(LemmyError::from_message("invalid_name"))
43 // Can't do a regex here, reverse lookarounds not supported
44 pub fn is_valid_display_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> {
45 let check = !name.starts_with('@')
46 && !name.starts_with('\u{200b}')
47 && name.chars().count() >= 3
48 && name.chars().count() <= actor_name_max_length
49 && !has_newline(name);
51 Err(LemmyError::from_message("invalid_username"))
57 pub fn is_valid_matrix_id(matrix_id: &str) -> LemmyResult<()> {
58 let check = VALID_MATRIX_ID_REGEX.is_match(matrix_id) && !has_newline(matrix_id);
60 Err(LemmyError::from_message("invalid_matrix_id"))
66 pub fn is_valid_post_title(title: &str) -> LemmyResult<()> {
67 let check = VALID_POST_TITLE_REGEX.is_match(title) && !has_newline(title);
69 Err(LemmyError::from_message("invalid_post_title"))
75 /// This could be post bodies, comments, or any description field
76 pub fn is_valid_body_field(body: &Option<String>, post: bool) -> LemmyResult<()> {
77 if let Some(body) = body {
79 body.chars().count() <= POST_BODY_MAX_LENGTH
81 body.chars().count() <= BODY_MAX_LENGTH
85 Err(LemmyError::from_message("invalid_body_field"))
94 pub fn is_valid_bio_field(bio: &str) -> LemmyResult<()> {
95 max_length_check(bio, BIO_MAX_LENGTH, String::from("bio_length_overflow"))
98 /// Checks the site name length, the limit as defined in the DB.
99 pub fn site_name_length_check(name: &str) -> LemmyResult<()> {
100 min_max_length_check(
102 SITE_NAME_MIN_LENGTH,
103 SITE_NAME_MAX_LENGTH,
104 String::from("site_name_required"),
105 String::from("site_name_length_overflow"),
109 /// Checks the site description length, the limit as defined in the DB.
110 pub fn site_description_length_check(description: &str) -> LemmyResult<()> {
113 SITE_DESCRIPTION_MAX_LENGTH,
114 String::from("site_description_length_overflow"),
118 fn max_length_check(item: &str, max_length: usize, msg: String) -> LemmyResult<()> {
119 if item.len() > max_length {
120 Err(LemmyError::from_message(&msg))
126 fn min_max_length_check(
132 ) -> LemmyResult<()> {
133 if item.len() > max_length {
134 Err(LemmyError::from_message(&max_msg))
135 } else if item.len() < min_length {
136 Err(LemmyError::from_message(&min_msg))
142 /// Attempts to build a regex and check it for common errors before inserting into the DB.
143 pub fn build_and_check_regex(regex_str_opt: &Option<&str>) -> LemmyResult<Option<Regex>> {
144 regex_str_opt.map_or_else(
145 || Ok(None::<Regex>),
147 if regex_str.is_empty() {
148 // If the proposed regex is empty, return as having no regex at all; this is the same
149 // behavior that happens downstream before the write to the database.
150 return Ok(None::<Regex>);
153 RegexBuilder::new(regex_str)
154 .case_insensitive(true)
156 .map_err(|e| LemmyError::from_error_message(e, "invalid_regex"))
158 // NOTE: It is difficult to know, in the universe of user-crafted regex, which ones
159 // may match against any string text. To keep it simple, we'll match the regex
160 // against an innocuous string - a single number - which should help catch a regex
161 // that accidentally matches against all strings.
162 if regex.is_match("1") {
163 return Err(LemmyError::from_message("permissive_regex"));
172 pub fn clean_url_params(url: &Url) -> Url {
173 let mut url_out = url.clone();
174 if url.query().is_some() {
177 .filter(|q| !CLEAN_URL_PARAMS_REGEX.is_match(&q.0))
178 .map(|q| format!("{}={}", q.0, q.1))
180 url_out.set_query(Some(&new_query));
185 pub fn check_totp_2fa_valid(
186 totp_secret: &Option<String>,
187 totp_token: &Option<String>,
190 ) -> LemmyResult<()> {
191 // Check only if they have a totp_secret in the DB
192 if let Some(totp_secret) = totp_secret {
193 // Throw an error if their token is missing
194 let token = totp_token
196 .ok_or_else(|| LemmyError::from_message("missing_totp_token"))?;
198 let totp = build_totp_2fa(site_name, username, totp_secret)?;
200 let check_passed = totp.check_current(token)?;
202 return Err(LemmyError::from_message("incorrect_totp token"));
209 pub fn generate_totp_2fa_secret() -> String {
210 Secret::generate_secret().to_string()
213 pub fn build_totp_2fa(site_name: &str, username: &str, secret: &str) -> Result<TOTP, LemmyError> {
214 let sec = Secret::Raw(secret.as_bytes().to_vec());
217 .map_err(|_| LemmyError::from_message("Couldnt parse totp secret"))?;
220 totp_rs::Algorithm::SHA256,
225 Some(site_name.to_string()),
226 username.to_string(),
228 .map_err(|e| LemmyError::from_error_message(e, "Couldnt generate TOTP"))
231 pub fn check_site_visibility_valid(
232 current_private_instance: bool,
233 current_federation_enabled: bool,
234 new_private_instance: &Option<bool>,
235 new_federation_enabled: &Option<bool>,
236 ) -> LemmyResult<()> {
237 let private_instance = new_private_instance.unwrap_or(current_private_instance);
238 let federation_enabled = new_federation_enabled.unwrap_or(current_federation_enabled);
240 if private_instance && federation_enabled {
241 return Err(LemmyError::from_message(
242 "cant_enable_private_instance_and_federation_together",
251 use super::build_totp_2fa;
252 use crate::utils::validation::{
253 build_and_check_regex,
254 check_site_visibility_valid,
256 generate_totp_2fa_secret,
259 is_valid_display_name,
262 site_description_length_check,
263 site_name_length_check,
265 SITE_DESCRIPTION_MAX_LENGTH,
266 SITE_NAME_MAX_LENGTH,
271 fn test_clean_url_params() {
272 let url = Url::parse("https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&username=randomuser&id=123").unwrap();
273 let cleaned = clean_url_params(&url);
274 let expected = Url::parse("https://example.com/path/123?username=randomuser&id=123").unwrap();
275 assert_eq!(expected.to_string(), cleaned.to_string());
277 let url = Url::parse("https://example.com/path/123").unwrap();
278 let cleaned = clean_url_params(&url);
279 assert_eq!(url.to_string(), cleaned.to_string());
284 assert!(is_valid_post_title("hi").is_err());
285 assert!(is_valid_post_title("him").is_ok());
286 assert!(is_valid_post_title("n\n\n\n\nanother").is_err());
287 assert!(is_valid_post_title("hello there!\n this is a test.").is_err());
288 assert!(is_valid_post_title("hello there! this is a test.").is_ok());
292 fn test_valid_actor_name() {
293 let actor_name_max_length = 20;
294 assert!(is_valid_actor_name("Hello_98", actor_name_max_length).is_ok());
295 assert!(is_valid_actor_name("ten", actor_name_max_length).is_ok());
296 assert!(is_valid_actor_name("Hello-98", actor_name_max_length).is_err());
297 assert!(is_valid_actor_name("a", actor_name_max_length).is_err());
298 assert!(is_valid_actor_name("", actor_name_max_length).is_err());
302 fn test_valid_display_name() {
303 let actor_name_max_length = 20;
304 assert!(is_valid_display_name("hello @there", actor_name_max_length).is_ok());
305 assert!(is_valid_display_name("@hello there", actor_name_max_length).is_err());
307 // Make sure zero-space with an @ doesn't work
309 is_valid_display_name(&format!("{}@my name is", '\u{200b}'), actor_name_max_length).is_err()
314 fn test_valid_post_title() {
315 assert!(is_valid_post_title("Post Title").is_ok());
316 assert!(is_valid_post_title(" POST TITLE 😃😃😃😃😃").is_ok());
317 assert!(is_valid_post_title("\n \n \n \n ").is_err()); // tabs/spaces/newlines
321 fn test_valid_matrix_id() {
322 assert!(is_valid_matrix_id("@dess:matrix.org").is_ok());
323 assert!(is_valid_matrix_id("dess:matrix.org").is_err());
324 assert!(is_valid_matrix_id(" @dess:matrix.org").is_err());
325 assert!(is_valid_matrix_id("@dess:matrix.org t").is_err());
329 fn test_build_totp() {
330 let generated_secret = generate_totp_2fa_secret();
331 let totp = build_totp_2fa("lemmy", "my_name", &generated_secret);
332 assert!(totp.is_ok());
336 fn test_valid_site_name() {
338 (0..SITE_NAME_MAX_LENGTH).map(|_| 'A').collect::<String>(),
341 let invalid_names = [
343 &(0..SITE_NAME_MAX_LENGTH + 1)
345 .collect::<String>(),
346 "site_name_length_overflow",
348 (&String::new(), "site_name_required"),
351 valid_names.iter().for_each(|valid_name| {
353 site_name_length_check(valid_name).is_ok(),
354 "Expected {} of length {} to be Ok.",
362 .for_each(|&(invalid_name, expected_err)| {
363 let result = site_name_length_check(invalid_name);
365 assert!(result.is_err());
370 .eq(&Some(String::from(expected_err))),
371 "Testing {}, expected error {}",
379 fn test_valid_bio() {
380 assert!(is_valid_bio_field(&(0..BIO_MAX_LENGTH).map(|_| 'A').collect::<String>()).is_ok());
383 is_valid_bio_field(&(0..BIO_MAX_LENGTH + 1).map(|_| 'A').collect::<String>());
386 invalid_result.is_err()
390 .eq(&Some(String::from("bio_length_overflow")))
395 fn test_valid_site_description() {
396 assert!(site_description_length_check(
397 &(0..SITE_DESCRIPTION_MAX_LENGTH)
403 let invalid_result = site_description_length_check(
404 &(0..SITE_DESCRIPTION_MAX_LENGTH + 1)
406 .collect::<String>(),
410 invalid_result.is_err()
414 .eq(&Some(String::from("site_description_length_overflow")))
419 fn test_valid_slur_regex() {
420 let valid_regexes = [&None, &Some(""), &Some("(foo|bar)")];
422 valid_regexes.iter().for_each(|regex| {
423 let result = build_and_check_regex(regex);
425 assert!(result.is_ok(), "Testing regex: {:?}", regex);
430 fn test_too_permissive_slur_regex() {
431 let match_everything_regexes = [
432 (&Some("["), "invalid_regex"),
433 (&Some("(foo|bar|)"), "permissive_regex"),
434 (&Some(".*"), "permissive_regex"),
437 match_everything_regexes
439 .for_each(|&(regex_str, expected_err)| {
440 let result = build_and_check_regex(regex_str);
442 assert!(result.is_err());
447 .eq(&Some(String::from(expected_err))),
448 "Testing regex {:?}, expected error {}",
456 fn test_check_site_visibility_valid() {
457 assert!(check_site_visibility_valid(true, true, &None, &None).is_err());
458 assert!(check_site_visibility_valid(true, false, &None, &Some(true)).is_err());
459 assert!(check_site_visibility_valid(false, true, &Some(true), &None).is_err());
460 assert!(check_site_visibility_valid(false, false, &Some(true), &Some(true)).is_err());
461 assert!(check_site_visibility_valid(true, false, &None, &None).is_ok());
462 assert!(check_site_visibility_valid(false, true, &None, &None).is_ok());
463 assert!(check_site_visibility_valid(false, false, &Some(true), &None).is_ok());
464 assert!(check_site_visibility_valid(false, false, &None, &Some(true)).is_ok());