diff --git a/migrations/V0017__actor_profile__increase_display_name_length.sql b/migrations/V0017__actor_profile__increase_display_name_length.sql new file mode 100644 index 0000000..ad1a8fe --- /dev/null +++ b/migrations/V0017__actor_profile__increase_display_name_length.sql @@ -0,0 +1 @@ +ALTER TABLE actor_profile ALTER COLUMN display_name TYPE VARCHAR(200); diff --git a/migrations/schema.sql b/migrations/schema.sql index ae22c40..7c6947e 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -1,7 +1,7 @@ CREATE TABLE actor_profile ( id UUID PRIMARY KEY, username VARCHAR(100) NOT NULL, - display_name VARCHAR(100), + display_name VARCHAR(200), acct VARCHAR(200) UNIQUE NOT NULL, bio TEXT, bio_source TEXT, diff --git a/src/activitypub/receiver.rs b/src/activitypub/receiver.rs index 2a56a59..1541bd7 100644 --- a/src/activitypub/receiver.rs +++ b/src/activitypub/receiver.rs @@ -135,10 +135,25 @@ pub enum ImportError { #[error(transparent)] FetchError(#[from] FetchError), + #[error(transparent)] + ValidationError(#[from] ValidationError), + #[error(transparent)] DatabaseError(#[from] DatabaseError), } +impl From for HttpError { + fn from(error: ImportError) -> Self { + match error { + ImportError::FetchError(error) => { + HttpError::ValidationError(error.to_string()) + }, + ImportError::ValidationError(error) => error.into(), + ImportError::DatabaseError(error) => error.into(), + } + } +} + async fn get_or_fetch_profile_by_actor_id( db_client: &impl GenericClient, instance: &Instance, @@ -187,6 +202,7 @@ pub async fn import_profile_by_actor_address( }; }; log::info!("fetched profile {}", profile_data.acct); + profile_data.clean()?; let profile = create_profile(db_client, &profile_data).await?; Ok(profile) } @@ -328,14 +344,14 @@ pub async fn process_note( &actor_address, ).await { Ok(profile) => profile, - Err(ImportError::DatabaseError(error)) => { - return Err(error.into()); - }, Err(ImportError::FetchError(error)) => { // Ignore mention if fetcher fails log::warn!("{}", error); continue; }, + Err(other_error) => { + return Err(other_error.into()); + }, } }, Err(other_error) => return Err(other_error.into()), diff --git a/src/mastodon_api/statuses/views.rs b/src/mastodon_api/statuses/views.rs index 7ffd799..fd7580e 100644 --- a/src/mastodon_api/statuses/views.rs +++ b/src/mastodon_api/statuses/views.rs @@ -58,7 +58,7 @@ async fn create_status( let current_user = get_current_user(db_client, auth.token()).await?; let instance = config.instance(); let mut post_data = PostCreateData::from(data.into_inner()); - post_data.validate()?; + post_data.clean()?; // Mentions let mention_map = find_mentioned_profiles( db_client, diff --git a/src/models/posts/types.rs b/src/models/posts/types.rs index 800284e..436e17c 100644 --- a/src/models/posts/types.rs +++ b/src/models/posts/types.rs @@ -205,7 +205,7 @@ pub struct PostCreateData { impl PostCreateData { /// Validate and clean post data. - pub fn validate(&mut self) -> Result<(), ValidationError> { + pub fn clean(&mut self) -> Result<(), ValidationError> { let content_safe = clean_html(&self.content); let content_trimmed = content_safe.trim(); if content_trimmed.is_empty() { @@ -233,7 +233,7 @@ mod tests { object_id: None, created_at: None, }; - assert_eq!(post_data_1.validate().is_ok(), false); + assert_eq!(post_data_1.clean().is_ok(), false); } #[test] @@ -249,7 +249,7 @@ mod tests { object_id: None, created_at: None, }; - assert_eq!(post_data_2.validate().is_ok(), true); + assert_eq!(post_data_2.clean().is_ok(), true); assert_eq!(post_data_2.content.as_str(), "test"); } } diff --git a/src/models/profiles/mod.rs b/src/models/profiles/mod.rs index 0333ab5..a35947b 100644 --- a/src/models/profiles/mod.rs +++ b/src/models/profiles/mod.rs @@ -1,2 +1,3 @@ pub mod queries; pub mod types; +pub mod validators; diff --git a/src/models/profiles/types.rs b/src/models/profiles/types.rs index 225d74e..9541602 100644 --- a/src/models/profiles/types.rs +++ b/src/models/profiles/types.rs @@ -11,6 +11,10 @@ use uuid::Uuid; use crate::activitypub::views::get_actor_url; use crate::errors::{ConversionError, ValidationError}; use crate::utils::html::clean_html; +use super::validators::{ + validate_username, + validate_display_name, +}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ExtraField { @@ -137,6 +141,14 @@ pub struct ProfileCreateData { pub actor: Option, } +impl ProfileCreateData { + pub fn clean(&self) -> Result<(), ValidationError> { + validate_username(&self.username)?; + validate_display_name(self.display_name.as_ref())?; + Ok(()) + } +} + pub struct ProfileUpdateData { pub display_name: Option, pub bio: Option, @@ -169,7 +181,7 @@ impl ProfileUpdateData { unique_labels.dedup(); if unique_labels.len() < self.extra_fields.len() { return Err(ValidationError("duplicate labels")); - } + }; Ok(()) } } diff --git a/src/models/profiles/validators.rs b/src/models/profiles/validators.rs new file mode 100644 index 0000000..6580c0f --- /dev/null +++ b/src/models/profiles/validators.rs @@ -0,0 +1,46 @@ +use crate::errors::ValidationError; + +pub fn validate_username(username: &str) -> Result<(), ValidationError> { + if username.len() > 100 { + return Err(ValidationError("username is too long")); + }; + Ok(()) +} + +pub fn validate_display_name(display_name: Option<&String>) + -> Result<(), ValidationError> +{ + if let Some(display_name) = display_name { + if display_name.len() > 200 { + return Err(ValidationError("display name is too long")); + }; + }; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_username() { + let result_1 = validate_username("test"); + assert!(result_1.is_ok()); + let result_2 = validate_username(&"x".repeat(101)); + assert!(result_2.is_err()); + } + + #[test] + fn test_validate_display_name() { + let display_name = "test".to_string(); + let result_1 = validate_display_name(Some(&display_name)); + assert!(result_1.is_ok()); + + let result_2 = validate_display_name(None); + assert!(result_2.is_ok()); + + let display_name = "x".repeat(201); + let result_3 = validate_display_name(Some(&display_name)); + assert!(result_3.is_err()); + } +} diff --git a/src/models/users/types.rs b/src/models/users/types.rs index 50e8617..66a9abd 100644 --- a/src/models/users/types.rs +++ b/src/models/users/types.rs @@ -6,6 +6,7 @@ use uuid::Uuid; use crate::errors::ValidationError; use crate::models::profiles::types::DbActorProfile; +use crate::models::profiles::validators::validate_username; #[derive(FromSql)] #[postgres(name = "user_account")] @@ -53,7 +54,7 @@ pub struct UserCreateData { pub invite_code: Option, } -fn validate_username(username: &str) -> Result<(), ValidationError> { +fn validate_local_username(username: &str) -> Result<(), ValidationError> { // The username regexp should not allow domain names and IP addresses let username_regexp = Regex::new(r"^[a-z0-9_]+$").unwrap(); if !username_regexp.is_match(username) { @@ -66,6 +67,7 @@ impl UserCreateData { /// Validate and clean. pub fn clean(&self) -> Result<(), ValidationError> { validate_username(&self.username)?; + validate_local_username(&self.username)?; Ok(()) } } @@ -75,10 +77,10 @@ mod tests { use super::*; #[test] - fn test_validate_username() { - let result_1 = validate_username("name_1"); + fn test_validate_local_username() { + let result_1 = validate_local_username("name_1"); assert_eq!(result_1.is_ok(), true); - let result_2 = validate_username("name&"); + let result_2 = validate_local_username("name&"); assert_eq!(result_2.is_ok(), false); } }