X-Git-Url: http://these/git/?a=blobdiff_plain;f=crates%2Fapi_crud%2Fsrc%2Fsite%2Fcreate.rs;h=f7beb254bf23e393c7f2673cc87915bd3ea480ab;hb=70fae9d68d65b1e4d153e30d3c065cc315b75eaf;hp=3ee2874b4f1d8c70e68e96702c6495b1746e041e;hpb=b41f7f3eca1b7d618e3a2f32186a6b058a6e8b6f;p=lemmy.git diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 3ee2874b..f7beb254 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -1,99 +1,592 @@ -use crate::PerformCrud; -use actix_web::web::Data; +use crate::site::{application_question_check, site_default_post_listing_type_check}; +use activitypub_federation::http_signatures::generate_actor_keypair; +use actix_web::web::{Data, Json}; use lemmy_api_common::{ - blocking, - check_image_has_local_domain, - get_local_user_view_from_jwt, - is_admin, - site::*, - site_description_length_check, + context::LemmyContext, + site::{CreateSite, SiteResponse}, + utils::{ + generate_site_inbox_url, + is_admin, + local_site_rate_limit_to_rate_limit_config, + local_user_view_from_jwt, + sanitize_html, + sanitize_html_opt, + }, }; -use lemmy_apub::generate_site_inbox_url; use lemmy_db_schema::{ - diesel_option_overwrite, - diesel_option_overwrite_to_url, - naive_now, newtypes::DbUrl, - source::site::{Site, SiteForm}, + source::{ + local_site::{LocalSite, LocalSiteUpdateForm}, + local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, + site::{Site, SiteUpdateForm}, + tagline::Tagline, + }, traits::Crud, + utils::{diesel_option_overwrite, diesel_option_overwrite_to_url, naive_now}, }; -use lemmy_db_views::site_view::SiteView; +use lemmy_db_views::structs::SiteView; use lemmy_utils::{ - apub::generate_actor_keypair, - settings::structs::Settings, - utils::{check_slurs, check_slurs_opt}, - ConnectionId, - LemmyError, + error::{LemmyError, LemmyErrorType, LemmyResult}, + utils::{ + slurs::{check_slurs, check_slurs_opt}, + validation::{ + build_and_check_regex, + check_site_visibility_valid, + is_valid_body_field, + site_description_length_check, + site_name_length_check, + }, + }, }; -use lemmy_websocket::LemmyContext; use url::Url; -#[async_trait::async_trait(?Send)] -impl PerformCrud for CreateSite { - type Response = SiteResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &CreateSite = self; - - let read_site = Site::read_local_site; - if blocking(context.pool(), read_site).await?.is_ok() { - return Err(LemmyError::from_message("site_already_exists")); - }; - - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let sidebar = diesel_option_overwrite(&data.sidebar); - let description = diesel_option_overwrite(&data.description); - let icon = diesel_option_overwrite_to_url(&data.icon)?; - let banner = diesel_option_overwrite_to_url(&data.banner)?; - - check_slurs(&data.name, &context.settings().slur_regex())?; - check_slurs_opt(&data.description, &context.settings().slur_regex())?; - check_image_has_local_domain(icon.as_ref().unwrap_or(&None))?; - check_image_has_local_domain(banner.as_ref().unwrap_or(&None))?; - - // Make sure user is an admin - is_admin(&local_user_view)?; - - if let Some(Some(desc)) = &description { - site_description_length_check(desc)?; +#[tracing::instrument(skip(context))] +pub async fn create_site( + data: Json, + context: Data, +) -> Result, LemmyError> { + let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?; + let local_site = LocalSite::read(&mut context.pool()).await?; + + // Make sure user is an admin; other types of users should not create site data... + is_admin(&local_user_view)?; + + validate_create_payload(&local_site, &data)?; + + let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into(); + let inbox_url = Some(generate_site_inbox_url(&actor_id)?); + let keypair = generate_actor_keypair()?; + let name = sanitize_html(&data.name); + let sidebar = sanitize_html_opt(&data.sidebar); + let description = sanitize_html_opt(&data.description); + + let site_form = SiteUpdateForm::builder() + .name(Some(name)) + .sidebar(diesel_option_overwrite(sidebar)) + .description(diesel_option_overwrite(description)) + .icon(diesel_option_overwrite_to_url(&data.icon)?) + .banner(diesel_option_overwrite_to_url(&data.banner)?) + .actor_id(Some(actor_id)) + .last_refreshed_at(Some(naive_now())) + .inbox_url(inbox_url) + .private_key(Some(Some(keypair.private_key))) + .public_key(Some(keypair.public_key)) + .build(); + + let site_id = local_site.site_id; + + Site::update(&mut context.pool(), site_id, &site_form).await?; + + let application_question = sanitize_html_opt(&data.application_question); + let default_theme = sanitize_html_opt(&data.default_theme); + let legal_information = sanitize_html_opt(&data.legal_information); + + let local_site_form = LocalSiteUpdateForm::builder() + // Set the site setup to true + .site_setup(Some(true)) + .enable_downvotes(data.enable_downvotes) + .registration_mode(data.registration_mode) + .enable_nsfw(data.enable_nsfw) + .community_creation_admin_only(data.community_creation_admin_only) + .require_email_verification(data.require_email_verification) + .application_question(diesel_option_overwrite(application_question)) + .private_instance(data.private_instance) + .default_theme(default_theme) + .default_post_listing_type(data.default_post_listing_type) + .legal_information(diesel_option_overwrite(legal_information)) + .application_email_admins(data.application_email_admins) + .hide_modlog_mod_names(data.hide_modlog_mod_names) + .updated(Some(Some(naive_now()))) + .slur_filter_regex(diesel_option_overwrite(data.slur_filter_regex.clone())) + .actor_name_max_length(data.actor_name_max_length) + .federation_enabled(data.federation_enabled) + .captcha_enabled(data.captcha_enabled) + .captcha_difficulty(data.captcha_difficulty.clone()) + .build(); + + LocalSite::update(&mut context.pool(), &local_site_form).await?; + + let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder() + .message(data.rate_limit_message) + .message_per_second(data.rate_limit_message_per_second) + .post(data.rate_limit_post) + .post_per_second(data.rate_limit_post_per_second) + .register(data.rate_limit_register) + .register_per_second(data.rate_limit_register_per_second) + .image(data.rate_limit_image) + .image_per_second(data.rate_limit_image_per_second) + .comment(data.rate_limit_comment) + .comment_per_second(data.rate_limit_comment_per_second) + .search(data.rate_limit_search) + .search_per_second(data.rate_limit_search_per_second) + .build(); + + LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?; + + let site_view = SiteView::read_local(&mut context.pool()).await?; + + let new_taglines = data.taglines.clone(); + let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; + + let rate_limit_config = + local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); + context + .settings_updated_channel() + .send(rate_limit_config) + .await?; + + Ok(Json(SiteResponse { + site_view, + taglines, + })) +} + +fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> LemmyResult<()> { + // Make sure the site hasn't already been set up... + if local_site.site_setup { + Err(LemmyErrorType::SiteAlreadyExists)?; + }; + + // Check that the slur regex compiles, and returns the regex if valid... + // Prioritize using new slur regex from the request; if not provided, use the existing regex. + let slur_regex = build_and_check_regex( + &create_site + .slur_filter_regex + .as_deref() + .or(local_site.slur_filter_regex.as_deref()), + )?; + + site_name_length_check(&create_site.name)?; + check_slurs(&create_site.name, &slur_regex)?; + + if let Some(desc) = &create_site.description { + site_description_length_check(desc)?; + check_slurs_opt(&create_site.description, &slur_regex)?; + } + + site_default_post_listing_type_check(&create_site.default_post_listing_type)?; + + check_site_visibility_valid( + local_site.private_instance, + local_site.federation_enabled, + &create_site.private_instance, + &create_site.federation_enabled, + )?; + + // Ensure that the sidebar has fewer than the max num characters... + is_valid_body_field(&create_site.sidebar, false)?; + + application_question_check( + &local_site.application_question, + &create_site.application_question, + create_site + .registration_mode + .unwrap_or(local_site.registration_mode), + ) +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + #![allow(clippy::indexing_slicing)] + + use crate::site::create::validate_create_payload; + use lemmy_api_common::site::CreateSite; + use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode}; + use lemmy_utils::error::LemmyErrorType; + + #[test] + fn test_validate_invalid_create_payload() { + let invalid_payloads = [ + ( + "CreateSite attempted on set up LocalSite", + LemmyErrorType::SiteAlreadyExists, + &generate_local_site( + true, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + None::, + None::, + None::, + None::, + ), + ), + ( + "CreateSite name matches LocalSite slur filter", + LemmyErrorType::Slurs, + &generate_local_site( + false, + Some(String::from("(foo|bar)")), + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("foo site_name"), + None::, + None::, + None::, + None::, + None::, + None::, + None::, + None::, + ), + ), + ( + "CreateSite name matches new slur filter", + LemmyErrorType::Slurs, + &generate_local_site( + false, + Some(String::from("(foo|bar)")), + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("zeta site_name"), + None::, + None::, + None::, + Some(String::from("(zeta|alpha)")), + None::, + None::, + None::, + None::, + ), + ), + ( + "CreateSite listing type is Subscribed, which is invalid", + LemmyErrorType::InvalidDefaultPostListingType, + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + Some(ListingType::Subscribed), + None::, + None::, + None::, + None::, + None::, + ), + ), + ( + "CreateSite is both private and federated", + LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + Some(true), + Some(true), + None::, + None::, + ), + ), + ( + "LocalSite is private, but CreateSite also makes it federated", + LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether, + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + None::, + Some(true), + None::, + None::, + ), + ), + ( + "CreateSite requires application, but neither it nor LocalSite has an application question", + LemmyErrorType::ApplicationQuestionRequired, + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + None::, + None::, + None::, + Some(RegistrationMode::RequireApplication), + ), + ), + ]; + + invalid_payloads.iter().enumerate().for_each( + |( + idx, + &(reason, ref expected_err, local_site, create_site), + )| { + match validate_create_payload( + local_site, + create_site, + ) { + Ok(_) => { + panic!( + "Got Ok, but validation should have failed with error: {} for reason: {}. invalid_payloads.nth({})", + expected_err, reason, idx + ) + } + Err(error) => { + assert!( + error.error_type.eq(&expected_err.clone()), + "Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})", + error.error_type, + expected_err, + reason, + idx + ) + } + } + }, + ); + } + + #[test] + fn test_validate_valid_create_payload() { + let valid_payloads = [ + ( + "No changes between LocalSite and CreateSite", + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + None::, + None::, + None::, + None::, + ), + ), + ( + "CreateSite allows clearing and changing values", + &generate_local_site( + false, + None::, + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + Some(String::new()), + Some(String::new()), + Some(ListingType::All), + Some(String::new()), + Some(false), + Some(true), + Some(String::new()), + Some(RegistrationMode::Open), + ), + ), + ( + "CreateSite clears existing slur filter regex", + &generate_local_site( + false, + Some(String::from("(foo|bar)")), + true, + false, + None::, + RegistrationMode::Open, + ), + &generate_create_site( + String::from("foo site_name"), + None::, + None::, + None::, + Some(String::new()), + None::, + None::, + None::, + None::, + ), + ), + ( + "LocalSite has application question and CreateSite now requires applications,", + &generate_local_site( + false, + None::, + true, + false, + Some(String::from("question")), + RegistrationMode::Open, + ), + &generate_create_site( + String::from("site_name"), + None::, + None::, + None::, + None::, + None::, + None::, + None::, + Some(RegistrationMode::RequireApplication), + ), + ), + ]; + + valid_payloads + .iter() + .enumerate() + .for_each(|(idx, &(reason, local_site, edit_site))| { + assert!( + validate_create_payload(local_site, edit_site).is_ok(), + "Got Err, but should have got Ok for reason: {}. valid_payloads.nth({})", + reason, + idx + ); + }) + } + + fn generate_local_site( + site_setup: bool, + site_slur_filter_regex: Option, + site_is_private: bool, + site_is_federated: bool, + site_application_question: Option, + site_registration_mode: RegistrationMode, + ) -> LocalSite { + LocalSite { + id: Default::default(), + site_id: Default::default(), + site_setup, + enable_downvotes: false, + enable_nsfw: false, + community_creation_admin_only: false, + require_email_verification: false, + application_question: site_application_question, + private_instance: site_is_private, + default_theme: String::new(), + default_post_listing_type: ListingType::All, + legal_information: None, + hide_modlog_mod_names: false, + application_email_admins: false, + slur_filter_regex: site_slur_filter_regex, + actor_name_max_length: 0, + federation_enabled: site_is_federated, + captcha_enabled: false, + captcha_difficulty: String::new(), + published: Default::default(), + updated: None, + registration_mode: site_registration_mode, + reports_email_admins: false, } + } - let actor_id: DbUrl = Url::parse(&Settings::get().get_protocol_and_hostname())?.into(); - let inbox_url = Some(generate_site_inbox_url(&actor_id)?); - let keypair = generate_actor_keypair()?; - let site_form = SiteForm { - name: data.name.to_owned(), - sidebar, - description, - icon, - banner, - enable_downvotes: data.enable_downvotes, - open_registration: data.open_registration, - enable_nsfw: data.enable_nsfw, - community_creation_admin_only: data.community_creation_admin_only, - actor_id: Some(actor_id), - last_refreshed_at: Some(naive_now()), - inbox_url, - private_key: Some(Some(keypair.private_key)), - public_key: Some(keypair.public_key), - default_theme: data.default_theme.clone(), - ..SiteForm::default() - }; - - let create_site = move |conn: &'_ _| Site::create(conn, &site_form); - blocking(context.pool(), create_site) - .await? - .map_err(|e| LemmyError::from_error_message(e, "site_already_exists"))?; - - let site_view = blocking(context.pool(), SiteView::read_local).await??; - - Ok(SiteResponse { site_view }) + // Allow the test helper function to have too many arguments. + // It's either this or generate the entire struct each time for testing. + #[allow(clippy::too_many_arguments)] + fn generate_create_site( + site_name: String, + site_description: Option, + site_sidebar: Option, + site_listing_type: Option, + site_slur_filter_regex: Option, + site_is_private: Option, + site_is_federated: Option, + site_application_question: Option, + site_registration_mode: Option, + ) -> CreateSite { + CreateSite { + name: site_name, + sidebar: site_sidebar, + description: site_description, + icon: None, + banner: None, + enable_downvotes: None, + enable_nsfw: None, + community_creation_admin_only: None, + require_email_verification: None, + application_question: site_application_question, + private_instance: site_is_private, + default_theme: None, + default_post_listing_type: site_listing_type, + legal_information: None, + application_email_admins: None, + hide_modlog_mod_names: None, + discussion_languages: None, + slur_filter_regex: site_slur_filter_regex, + actor_name_max_length: None, + rate_limit_message: None, + rate_limit_message_per_second: None, + rate_limit_post: None, + rate_limit_post_per_second: None, + rate_limit_register: None, + rate_limit_register_per_second: None, + rate_limit_image: None, + rate_limit_image_per_second: None, + rate_limit_comment: None, + rate_limit_comment_per_second: None, + rate_limit_search: None, + rate_limit_search_per_second: None, + federation_enabled: site_is_federated, + federation_debug: None, + captcha_enabled: None, + captcha_difficulty: None, + allowed_instances: None, + blocked_instances: None, + taglines: None, + registration_mode: site_registration_mode, + auth: Default::default(), + } } }