From 8d2465989230fae7d9aae334a67a726fd6ced912 Mon Sep 17 00:00:00 2001 From: Tony Antonov Date: Fri, 10 Jul 2020 19:15:53 -0600 Subject: [PATCH] Forbid users to use empty titles for posts (#930) - Add a regex that checks if string contains anything but whitespace - Check for whitespace-only titles on post creation and edit - Trim whitespace from titles before saving - Add frontend validation to title --- server/lemmy_utils/src/lib.rs | 15 +++++++++++++++ server/src/api/post.rs | 14 +++++++++++--- ui/src/components/post-form.tsx | 10 +++++++++- ui/src/utils.ts | 9 +++++++++ ui/translations/en.json | 3 ++- 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/server/lemmy_utils/src/lib.rs b/server/lemmy_utils/src/lib.rs index f576ea00..bbee8500 100644 --- a/server/lemmy_utils/src/lib.rs +++ b/server/lemmy_utils/src/lib.rs @@ -158,12 +158,17 @@ pub fn is_valid_community_name(name: &str) -> bool { VALID_COMMUNITY_NAME_REGEX.is_match(name) } +pub fn is_valid_post_title(title: &str) -> bool { + VALID_POST_TITLE_REGEX.is_match(title) +} + #[cfg(test)] mod tests { use crate::{ is_email_regex, is_valid_community_name, is_valid_username, + is_valid_post_title, remove_slurs, scrape_text_for_mentions, slur_check, @@ -204,6 +209,15 @@ mod tests { assert!(!is_valid_community_name("")); } + #[test] + fn test_valid_post_title() { + assert!(is_valid_post_title("Post Title")); + assert!(is_valid_post_title(" POST TITLE 😃😃😃😃😃")); + assert!(!is_valid_post_title("\n \n \n \n ")); // tabs/spaces/newlines + } + + + #[test] fn test_slur_filter() { let test = @@ -249,6 +263,7 @@ lazy_static! { static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P[\w.]+)@(?P[a-zA-Z0-9._:-]+)").unwrap(); static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap(); static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap(); + static ref VALID_POST_TITLE_REGEX: Regex = Regex::new(r".*\S.*").unwrap(); pub static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!( "^group:([a-z0-9_]{{3, 20}})@{}$", Settings::get().hostname diff --git a/server/src/api/post.rs b/server/src/api/post.rs index c56a00df..cbdb976c 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -28,7 +28,7 @@ use lemmy_db::{ Saveable, SortType, }; -use lemmy_utils::{make_apub_endpoint, slur_check, slurs_vec_to_str, EndpointType}; +use lemmy_utils::{is_valid_post_title, make_apub_endpoint, slur_check, slurs_vec_to_str, EndpointType}; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -135,6 +135,10 @@ impl Perform for Oper { } } + if !is_valid_post_title(&data.name) { + return Err(APIError::err("invalid_post_title").into()); + } + let user_id = claims.id; // Check for a community ban @@ -156,7 +160,7 @@ impl Perform for Oper { fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await; let post_form = PostForm { - name: data.name.to_owned(), + name: data.name.trim().to_owned(), url: data.url.to_owned(), body: data.body.to_owned(), community_id: data.community_id, @@ -516,6 +520,10 @@ impl Perform for Oper { } } + if !is_valid_post_title(&data.name) { + return Err(APIError::err("invalid_post_title").into()); + } + let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, Err(_e) => return Err(APIError::err("not_logged_in").into()), @@ -565,7 +573,7 @@ impl Perform for Oper { let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; let post_form = PostForm { - name: data.name.to_owned(), + name: data.name.trim().to_owned(), url: data.url.to_owned(), body: data.body.to_owned(), creator_id: data.creator_id.to_owned(), diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index 247c4cc6..30527510 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -35,6 +35,7 @@ import { setupTippy, hostname, pictrsDeleteToast, + validTitle, } from '../utils'; import autosize from 'autosize'; import Tribute from 'tributejs/src/Tribute.js'; @@ -271,12 +272,19 @@ export class PostForm extends Component { value={this.state.postForm.name} id="post-title" onInput={linkEvent(this, this.handlePostNameChange)} - class="form-control" + class={`form-control ${ + !validTitle(this.state.postForm.name) && 'is-invalid' + }`} required rows={2} minLength={3} maxLength={MAX_POST_TITLE_LENGTH} /> + {!validTitle(this.state.postForm.name) && ( +
+ {i18n.t('invalid_post_title')} +
+ )} {this.state.suggestedPosts.length > 0 && ( <>
diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 2f06b70c..6276519b 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -986,3 +986,12 @@ function canUseWebP() { // // very old browser like IE 8, canvas not supported // return false; } + +export function validTitle(title?: string): boolean { + // Initial title is null, minimum length is taken care of by textarea's minLength={3} + if (title === null || title.length < 3) return true; + + const regex = new RegExp(/.*\S.*/, 'g'); + + return regex.test(title); +} diff --git a/ui/translations/en.json b/ui/translations/en.json index 89f69f69..cb4347f1 100644 --- a/ui/translations/en.json +++ b/ui/translations/en.json @@ -268,5 +268,6 @@ "block_leaving": "Are you sure you want to leave?", "what_is": "What is", "cake_day_title": "Cake day:", - "cake_day_info": "It's {{ creator_name }}'s cake day today!" + "cake_day_info": "It's {{ creator_name }}'s cake day today!", + "invalid_post_title": "Invalid post title" }