2 site::{application_question_check, site_default_post_listing_type_check},
5 use actix_web::web::Data;
6 use lemmy_api_common::{
8 site::{EditSite, SiteResponse},
11 local_site_rate_limit_to_rate_limit_config,
12 local_user_view_from_jwt,
16 use lemmy_db_schema::{
18 actor_language::SiteLanguage,
19 federation_allowlist::FederationAllowList,
20 federation_blocklist::FederationBlockList,
21 local_site::{LocalSite, LocalSiteUpdateForm},
22 local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
23 local_user::LocalUser,
24 site::{Site, SiteUpdateForm},
28 utils::{diesel_option_overwrite, diesel_option_overwrite_to_url, naive_now},
31 use lemmy_db_views::structs::SiteView;
33 error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
35 slurs::check_slurs_opt,
37 build_and_check_regex,
38 check_site_visibility_valid,
40 site_description_length_check,
41 site_name_length_check,
46 #[async_trait::async_trait(?Send)]
47 impl PerformCrud for EditSite {
48 type Response = SiteResponse;
50 #[tracing::instrument(skip(context))]
51 async fn perform(&self, context: &Data<LemmyContext>) -> Result<SiteResponse, LemmyError> {
52 let data: &EditSite = self;
53 let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
54 let site_view = SiteView::read_local(&mut context.pool()).await?;
55 let local_site = site_view.local_site;
56 let site = site_view.site;
58 // Make sure user is an admin; other types of users should not update site data...
59 is_admin(&local_user_view)?;
61 validate_update_payload(&local_site, data)?;
63 if let Some(discussion_languages) = data.discussion_languages.clone() {
64 SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
67 let name = sanitize_html_opt(&data.name);
68 let sidebar = sanitize_html_opt(&data.sidebar);
69 let description = sanitize_html_opt(&data.description);
71 let site_form = SiteUpdateForm::builder()
73 .sidebar(diesel_option_overwrite(sidebar))
74 .description(diesel_option_overwrite(description))
75 .icon(diesel_option_overwrite_to_url(&data.icon)?)
76 .banner(diesel_option_overwrite_to_url(&data.banner)?)
77 .updated(Some(Some(naive_now())))
80 Site::update(&mut context.pool(), site.id, &site_form)
82 // Ignore errors for all these, so as to not throw errors if no update occurs
83 // Diesel will throw an error for empty update forms
86 let application_question = sanitize_html_opt(&data.application_question);
87 let default_theme = sanitize_html_opt(&data.default_theme);
88 let legal_information = sanitize_html_opt(&data.legal_information);
90 let local_site_form = LocalSiteUpdateForm::builder()
91 .enable_downvotes(data.enable_downvotes)
92 .registration_mode(data.registration_mode)
93 .enable_nsfw(data.enable_nsfw)
94 .community_creation_admin_only(data.community_creation_admin_only)
95 .require_email_verification(data.require_email_verification)
96 .application_question(diesel_option_overwrite(application_question))
97 .private_instance(data.private_instance)
98 .default_theme(default_theme)
99 .default_post_listing_type(data.default_post_listing_type)
100 .legal_information(diesel_option_overwrite(legal_information))
101 .application_email_admins(data.application_email_admins)
102 .hide_modlog_mod_names(data.hide_modlog_mod_names)
103 .updated(Some(Some(naive_now())))
104 .slur_filter_regex(diesel_option_overwrite(data.slur_filter_regex.clone()))
105 .actor_name_max_length(data.actor_name_max_length)
106 .federation_enabled(data.federation_enabled)
107 .captcha_enabled(data.captcha_enabled)
108 .captcha_difficulty(data.captcha_difficulty.clone())
109 .reports_email_admins(data.reports_email_admins)
112 let update_local_site = LocalSite::update(&mut context.pool(), &local_site_form)
116 let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
117 .message(data.rate_limit_message)
118 .message_per_second(data.rate_limit_message_per_second)
119 .post(data.rate_limit_post)
120 .post_per_second(data.rate_limit_post_per_second)
121 .register(data.rate_limit_register)
122 .register_per_second(data.rate_limit_register_per_second)
123 .image(data.rate_limit_image)
124 .image_per_second(data.rate_limit_image_per_second)
125 .comment(data.rate_limit_comment)
126 .comment_per_second(data.rate_limit_comment_per_second)
127 .search(data.rate_limit_search)
128 .search_per_second(data.rate_limit_search_per_second)
131 LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form)
135 // Replace the blocked and allowed instances
136 let allowed = data.allowed_instances.clone();
137 FederationAllowList::replace(&mut context.pool(), allowed).await?;
138 let blocked = data.blocked_instances.clone();
139 FederationBlockList::replace(&mut context.pool(), blocked).await?;
141 // TODO can't think of a better way to do this.
142 // If the server suddenly requires email verification, or required applications, no old users
143 // will be able to log in. It really only wants this to be a requirement for NEW signups.
144 // So if it was set from false, to true, you need to update all current users columns to be verified.
146 let old_require_application =
147 local_site.registration_mode == RegistrationMode::RequireApplication;
148 let new_require_application = update_local_site
150 .map(|ols| ols.registration_mode == RegistrationMode::RequireApplication)
152 if !old_require_application && new_require_application {
153 LocalUser::set_all_users_registration_applications_accepted(&mut context.pool())
155 .with_lemmy_type(LemmyErrorType::CouldntSetAllRegistrationsAccepted)?;
158 let new_require_email_verification = update_local_site
160 .map(|ols| ols.require_email_verification)
162 if !local_site.require_email_verification && new_require_email_verification {
163 LocalUser::set_all_users_email_verified(&mut context.pool())
165 .with_lemmy_type(LemmyErrorType::CouldntSetAllEmailVerified)?;
168 let new_taglines = data.taglines.clone();
169 let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
171 let site_view = SiteView::read_local(&mut context.pool()).await?;
173 let rate_limit_config =
174 local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
176 .settings_updated_channel()
177 .send(rate_limit_config)
180 let res = SiteResponse {
189 fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> LemmyResult<()> {
190 // Check that the slur regex compiles, and return the regex if valid...
191 // Prioritize using new slur regex from the request; if not provided, use the existing regex.
192 let slur_regex = build_and_check_regex(
196 .or(local_site.slur_filter_regex.as_deref()),
199 if let Some(name) = &edit_site.name {
200 // The name doesn't need to be updated, but if provided it cannot be blanked out...
201 site_name_length_check(name)?;
202 check_slurs_opt(&edit_site.name, &slur_regex)?;
205 if let Some(desc) = &edit_site.description {
206 site_description_length_check(desc)?;
207 check_slurs_opt(&edit_site.description, &slur_regex)?;
210 site_default_post_listing_type_check(&edit_site.default_post_listing_type)?;
212 check_site_visibility_valid(
213 local_site.private_instance,
214 local_site.federation_enabled,
215 &edit_site.private_instance,
216 &edit_site.federation_enabled,
219 // Ensure that the sidebar has fewer than the max num characters...
220 is_valid_body_field(&edit_site.sidebar, false)?;
222 application_question_check(
223 &local_site.application_question,
224 &edit_site.application_question,
227 .unwrap_or(local_site.registration_mode),
233 #![allow(clippy::unwrap_used)]
234 #![allow(clippy::indexing_slicing)]
236 use crate::site::update::validate_update_payload;
237 use lemmy_api_common::site::EditSite;
238 use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode};
239 use lemmy_utils::error::LemmyErrorType;
242 fn test_validate_invalid_update_payload() {
243 let invalid_payloads = [
245 "EditSite name matches LocalSite slur filter",
246 LemmyErrorType::Slurs,
247 &generate_local_site(
248 Some(String::from("(foo|bar)")),
252 RegistrationMode::Open,
255 Some(String::from("foo site_name")),
263 None::<RegistrationMode>,
267 "EditSite name matches new slur filter",
268 LemmyErrorType::Slurs,
269 &generate_local_site(
270 Some(String::from("(foo|bar)")),
274 RegistrationMode::Open,
277 Some(String::from("zeta site_name")),
281 Some(String::from("(zeta|alpha)")),
285 None::<RegistrationMode>,
289 "EditSite listing type is Subscribed, which is invalid",
290 LemmyErrorType::InvalidDefaultPostListingType,
291 &generate_local_site(
296 RegistrationMode::Open,
299 Some(String::from("site_name")),
302 Some(ListingType::Subscribed),
307 None::<RegistrationMode>,
311 "EditSite is both private and federated",
312 LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
313 &generate_local_site(
318 RegistrationMode::Open,
321 Some(String::from("site_name")),
329 None::<RegistrationMode>,
333 "LocalSite is private, but EditSite also makes it federated",
334 LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
335 &generate_local_site(
340 RegistrationMode::Open,
343 Some(String::from("site_name")),
351 None::<RegistrationMode>,
355 "EditSite requires application, but neither it nor LocalSite has an application question",
356 LemmyErrorType::ApplicationQuestionRequired,
357 &generate_local_site(
362 RegistrationMode::Open,
365 Some(String::from("site_name")),
373 Some(RegistrationMode::RequireApplication),
378 invalid_payloads.iter().enumerate().for_each(
381 &(reason, ref expected_err, local_site, edit_site),
383 match validate_update_payload(local_site, edit_site) {
386 "Got Ok, but validation should have failed with error: {} for reason: {}. invalid_payloads.nth({})",
387 expected_err, reason, idx
392 error.error_type.eq(&expected_err.clone()),
393 "Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})",
406 fn test_validate_valid_update_payload() {
407 let valid_payloads = [
409 "No changes between LocalSite and EditSite",
410 &generate_local_site(
415 RegistrationMode::Open,
426 None::<RegistrationMode>,
430 "EditSite allows clearing and changing values",
431 &generate_local_site(
436 RegistrationMode::Open,
439 Some(String::from("site_name")),
442 Some(ListingType::All),
447 Some(RegistrationMode::Open),
451 "EditSite name passes slur filter regex",
452 &generate_local_site(
453 Some(String::from("(foo|bar)")),
457 RegistrationMode::Open,
460 Some(String::from("foo site_name")),
468 None::<RegistrationMode>,
472 "LocalSite has application question and EditSite now requires applications,",
473 &generate_local_site(
477 Some(String::from("question")),
478 RegistrationMode::Open,
481 Some(String::from("site_name")),
489 Some(RegistrationMode::RequireApplication),
497 .for_each(|(idx, &(reason, local_site, edit_site))| {
499 validate_update_payload(local_site, edit_site).is_ok(),
500 "Got Err, but should have got Ok for reason: {}. valid_payloads.nth({})",
507 fn generate_local_site(
508 site_slur_filter_regex: Option<String>,
509 site_is_private: bool,
510 site_is_federated: bool,
511 site_application_question: Option<String>,
512 site_registration_mode: RegistrationMode,
515 id: Default::default(),
516 site_id: Default::default(),
518 enable_downvotes: false,
520 community_creation_admin_only: false,
521 require_email_verification: false,
522 application_question: site_application_question,
523 private_instance: site_is_private,
524 default_theme: String::new(),
525 default_post_listing_type: ListingType::All,
526 legal_information: None,
527 hide_modlog_mod_names: false,
528 application_email_admins: false,
529 slur_filter_regex: site_slur_filter_regex,
530 actor_name_max_length: 0,
531 federation_enabled: site_is_federated,
532 captcha_enabled: false,
533 captcha_difficulty: String::new(),
534 published: Default::default(),
536 registration_mode: site_registration_mode,
537 reports_email_admins: false,
541 // Allow the test helper function to have too many arguments.
542 // It's either this or generate the entire struct each time for testing.
543 #[allow(clippy::too_many_arguments)]
544 fn generate_edit_site(
545 site_name: Option<String>,
546 site_description: Option<String>,
547 site_sidebar: Option<String>,
548 site_listing_type: Option<ListingType>,
549 site_slur_filter_regex: Option<String>,
550 site_is_private: Option<bool>,
551 site_is_federated: Option<bool>,
552 site_application_question: Option<String>,
553 site_registration_mode: Option<RegistrationMode>,
557 sidebar: site_sidebar,
558 description: site_description,
561 enable_downvotes: None,
563 community_creation_admin_only: None,
564 require_email_verification: None,
565 application_question: site_application_question,
566 private_instance: site_is_private,
568 default_post_listing_type: site_listing_type,
569 legal_information: None,
570 application_email_admins: None,
571 hide_modlog_mod_names: None,
572 discussion_languages: None,
573 slur_filter_regex: site_slur_filter_regex,
574 actor_name_max_length: None,
575 rate_limit_message: None,
576 rate_limit_message_per_second: None,
577 rate_limit_post: None,
578 rate_limit_post_per_second: None,
579 rate_limit_register: None,
580 rate_limit_register_per_second: None,
581 rate_limit_image: None,
582 rate_limit_image_per_second: None,
583 rate_limit_comment: None,
584 rate_limit_comment_per_second: None,
585 rate_limit_search: None,
586 rate_limit_search_per_second: None,
587 federation_enabled: site_is_federated,
588 federation_debug: None,
589 captcha_enabled: None,
590 captcha_difficulty: None,
591 allowed_instances: None,
592 blocked_instances: None,
594 registration_mode: site_registration_mode,
595 reports_email_admins: None,
596 auth: Default::default(),