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
This commit is contained in:
Tony Antonov 2020-07-10 19:15:53 -06:00 committed by GitHub
parent 7a9a973c89
commit 8d24659892
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 46 additions and 5 deletions

View file

@ -158,12 +158,17 @@ pub fn is_valid_community_name(name: &str) -> bool {
VALID_COMMUNITY_NAME_REGEX.is_match(name) 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)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
is_email_regex, is_email_regex,
is_valid_community_name, is_valid_community_name,
is_valid_username, is_valid_username,
is_valid_post_title,
remove_slurs, remove_slurs,
scrape_text_for_mentions, scrape_text_for_mentions,
slur_check, slur_check,
@ -204,6 +209,15 @@ mod tests {
assert!(!is_valid_community_name("")); 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] #[test]
fn test_slur_filter() { fn test_slur_filter() {
let test = let test =
@ -249,6 +263,7 @@ lazy_static! {
static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap(); static ref MENTIONS_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap();
static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").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_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!( pub static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
"^group:([a-z0-9_]{{3, 20}})@{}$", "^group:([a-z0-9_]{{3, 20}})@{}$",
Settings::get().hostname Settings::get().hostname

View file

@ -28,7 +28,7 @@ use lemmy_db::{
Saveable, Saveable,
SortType, 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 serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -135,6 +135,10 @@ impl Perform for Oper<CreatePost> {
} }
} }
if !is_valid_post_title(&data.name) {
return Err(APIError::err("invalid_post_title").into());
}
let user_id = claims.id; let user_id = claims.id;
// Check for a community ban // Check for a community ban
@ -156,7 +160,7 @@ impl Perform for Oper<CreatePost> {
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await; fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.trim().to_owned(),
url: data.url.to_owned(), url: data.url.to_owned(),
body: data.body.to_owned(), body: data.body.to_owned(),
community_id: data.community_id, community_id: data.community_id,
@ -516,6 +520,10 @@ impl Perform for Oper<EditPost> {
} }
} }
if !is_valid_post_title(&data.name) {
return Err(APIError::err("invalid_post_title").into());
}
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()), Err(_e) => return Err(APIError::err("not_logged_in").into()),
@ -565,7 +573,7 @@ impl Perform for Oper<EditPost> {
let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.trim().to_owned(),
url: data.url.to_owned(), url: data.url.to_owned(),
body: data.body.to_owned(), body: data.body.to_owned(),
creator_id: data.creator_id.to_owned(), creator_id: data.creator_id.to_owned(),

View file

@ -35,6 +35,7 @@ import {
setupTippy, setupTippy,
hostname, hostname,
pictrsDeleteToast, pictrsDeleteToast,
validTitle,
} from '../utils'; } from '../utils';
import autosize from 'autosize'; import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js'; import Tribute from 'tributejs/src/Tribute.js';
@ -271,12 +272,19 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
value={this.state.postForm.name} value={this.state.postForm.name}
id="post-title" id="post-title"
onInput={linkEvent(this, this.handlePostNameChange)} onInput={linkEvent(this, this.handlePostNameChange)}
class="form-control" class={`form-control ${
!validTitle(this.state.postForm.name) && 'is-invalid'
}`}
required required
rows={2} rows={2}
minLength={3} minLength={3}
maxLength={MAX_POST_TITLE_LENGTH} maxLength={MAX_POST_TITLE_LENGTH}
/> />
{!validTitle(this.state.postForm.name) && (
<div class="invalid-feedback">
{i18n.t('invalid_post_title')}
</div>
)}
{this.state.suggestedPosts.length > 0 && ( {this.state.suggestedPosts.length > 0 && (
<> <>
<div class="my-1 text-muted small font-weight-bold"> <div class="my-1 text-muted small font-weight-bold">

9
ui/src/utils.ts vendored
View file

@ -986,3 +986,12 @@ function canUseWebP() {
// // very old browser like IE 8, canvas not supported // // very old browser like IE 8, canvas not supported
// return false; // 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);
}

View file

@ -268,5 +268,6 @@
"block_leaving": "Are you sure you want to leave?", "block_leaving": "Are you sure you want to leave?",
"what_is": "What is", "what_is": "What is",
"cake_day_title": "Cake day:", "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"
} }