]> Untitled Git - lemmy.git/blob - crates/api_crud/src/site/update.rs
e5c0bc5e224bffadad252c08a33c519a4f7b87a0
[lemmy.git] / crates / api_crud / src / site / update.rs
1 use crate::{
2   site::{application_question_check, site_default_post_listing_type_check},
3   PerformCrud,
4 };
5 use actix_web::web::Data;
6 use lemmy_api_common::{
7   context::LemmyContext,
8   site::{EditSite, SiteResponse},
9   utils::{is_admin, local_site_rate_limit_to_rate_limit_config, local_user_view_from_jwt},
10 };
11 use lemmy_db_schema::{
12   source::{
13     actor_language::SiteLanguage,
14     federation_allowlist::FederationAllowList,
15     federation_blocklist::FederationBlockList,
16     local_site::{LocalSite, LocalSiteUpdateForm},
17     local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
18     local_user::LocalUser,
19     site::{Site, SiteUpdateForm},
20     tagline::Tagline,
21   },
22   traits::Crud,
23   utils::{diesel_option_overwrite, diesel_option_overwrite_to_url, naive_now},
24   RegistrationMode,
25 };
26 use lemmy_db_views::structs::SiteView;
27 use lemmy_utils::{
28   error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
29   utils::{
30     slurs::check_slurs_opt,
31     validation::{
32       build_and_check_regex,
33       check_site_visibility_valid,
34       is_valid_body_field,
35       site_description_length_check,
36       site_name_length_check,
37     },
38   },
39 };
40
41 #[async_trait::async_trait(?Send)]
42 impl PerformCrud for EditSite {
43   type Response = SiteResponse;
44
45   #[tracing::instrument(skip(context))]
46   async fn perform(&self, context: &Data<LemmyContext>) -> Result<SiteResponse, LemmyError> {
47     let data: &EditSite = self;
48     let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
49     let site_view = SiteView::read_local(&mut context.pool()).await?;
50     let local_site = site_view.local_site;
51     let site = site_view.site;
52
53     // Make sure user is an admin; other types of users should not update site data...
54     is_admin(&local_user_view)?;
55
56     validate_update_payload(&local_site, data)?;
57
58     if let Some(discussion_languages) = data.discussion_languages.clone() {
59       SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
60     }
61
62     let site_form = SiteUpdateForm::builder()
63       .name(data.name.clone())
64       .sidebar(diesel_option_overwrite(&data.sidebar))
65       .description(diesel_option_overwrite(&data.description))
66       .icon(diesel_option_overwrite_to_url(&data.icon)?)
67       .banner(diesel_option_overwrite_to_url(&data.banner)?)
68       .updated(Some(Some(naive_now())))
69       .build();
70
71     Site::update(&mut context.pool(), site.id, &site_form)
72       .await
73       // Ignore errors for all these, so as to not throw errors if no update occurs
74       // Diesel will throw an error for empty update forms
75       .ok();
76
77     let local_site_form = LocalSiteUpdateForm::builder()
78       .enable_downvotes(data.enable_downvotes)
79       .registration_mode(data.registration_mode)
80       .enable_nsfw(data.enable_nsfw)
81       .community_creation_admin_only(data.community_creation_admin_only)
82       .require_email_verification(data.require_email_verification)
83       .application_question(diesel_option_overwrite(&data.application_question))
84       .private_instance(data.private_instance)
85       .default_theme(data.default_theme.clone())
86       .default_post_listing_type(data.default_post_listing_type)
87       .legal_information(diesel_option_overwrite(&data.legal_information))
88       .application_email_admins(data.application_email_admins)
89       .hide_modlog_mod_names(data.hide_modlog_mod_names)
90       .updated(Some(Some(naive_now())))
91       .slur_filter_regex(diesel_option_overwrite(&data.slur_filter_regex))
92       .actor_name_max_length(data.actor_name_max_length)
93       .federation_enabled(data.federation_enabled)
94       .captcha_enabled(data.captcha_enabled)
95       .captcha_difficulty(data.captcha_difficulty.clone())
96       .reports_email_admins(data.reports_email_admins)
97       .build();
98
99     let update_local_site = LocalSite::update(&mut context.pool(), &local_site_form)
100       .await
101       .ok();
102
103     let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
104       .message(data.rate_limit_message)
105       .message_per_second(data.rate_limit_message_per_second)
106       .post(data.rate_limit_post)
107       .post_per_second(data.rate_limit_post_per_second)
108       .register(data.rate_limit_register)
109       .register_per_second(data.rate_limit_register_per_second)
110       .image(data.rate_limit_image)
111       .image_per_second(data.rate_limit_image_per_second)
112       .comment(data.rate_limit_comment)
113       .comment_per_second(data.rate_limit_comment_per_second)
114       .search(data.rate_limit_search)
115       .search_per_second(data.rate_limit_search_per_second)
116       .build();
117
118     LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form)
119       .await
120       .ok();
121
122     // Replace the blocked and allowed instances
123     let allowed = data.allowed_instances.clone();
124     FederationAllowList::replace(&mut context.pool(), allowed).await?;
125     let blocked = data.blocked_instances.clone();
126     FederationBlockList::replace(&mut context.pool(), blocked).await?;
127
128     // TODO can't think of a better way to do this.
129     // If the server suddenly requires email verification, or required applications, no old users
130     // will be able to log in. It really only wants this to be a requirement for NEW signups.
131     // So if it was set from false, to true, you need to update all current users columns to be verified.
132
133     let old_require_application =
134       local_site.registration_mode == RegistrationMode::RequireApplication;
135     let new_require_application = update_local_site
136       .as_ref()
137       .map(|ols| ols.registration_mode == RegistrationMode::RequireApplication)
138       .unwrap_or(false);
139     if !old_require_application && new_require_application {
140       LocalUser::set_all_users_registration_applications_accepted(&mut context.pool())
141         .await
142         .with_lemmy_type(LemmyErrorType::CouldntSetAllRegistrationsAccepted)?;
143     }
144
145     let new_require_email_verification = update_local_site
146       .as_ref()
147       .map(|ols| ols.require_email_verification)
148       .unwrap_or(false);
149     if !local_site.require_email_verification && new_require_email_verification {
150       LocalUser::set_all_users_email_verified(&mut context.pool())
151         .await
152         .with_lemmy_type(LemmyErrorType::CouldntSetAllEmailVerified)?;
153     }
154
155     let new_taglines = data.taglines.clone();
156     let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
157
158     let site_view = SiteView::read_local(&mut context.pool()).await?;
159
160     let rate_limit_config =
161       local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
162     context
163       .settings_updated_channel()
164       .send(rate_limit_config)
165       .await?;
166
167     let res = SiteResponse {
168       site_view,
169       taglines,
170     };
171
172     Ok(res)
173   }
174 }
175
176 fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> LemmyResult<()> {
177   // Check that the slur regex compiles, and return the regex if valid...
178   // Prioritize using new slur regex from the request; if not provided, use the existing regex.
179   let slur_regex = build_and_check_regex(
180     &edit_site
181       .slur_filter_regex
182       .as_deref()
183       .or(local_site.slur_filter_regex.as_deref()),
184   )?;
185
186   if let Some(name) = &edit_site.name {
187     // The name doesn't need to be updated, but if provided it cannot be blanked out...
188     site_name_length_check(name)?;
189     check_slurs_opt(&edit_site.name, &slur_regex)?;
190   }
191
192   if let Some(desc) = &edit_site.description {
193     site_description_length_check(desc)?;
194     check_slurs_opt(&edit_site.description, &slur_regex)?;
195   }
196
197   site_default_post_listing_type_check(&edit_site.default_post_listing_type)?;
198
199   check_site_visibility_valid(
200     local_site.private_instance,
201     local_site.federation_enabled,
202     &edit_site.private_instance,
203     &edit_site.federation_enabled,
204   )?;
205
206   // Ensure that the sidebar has fewer than the max num characters...
207   is_valid_body_field(&edit_site.sidebar, false)?;
208
209   application_question_check(
210     &local_site.application_question,
211     &edit_site.application_question,
212     edit_site
213       .registration_mode
214       .unwrap_or(local_site.registration_mode),
215   )
216 }
217
218 #[cfg(test)]
219 mod tests {
220   use crate::site::update::validate_update_payload;
221   use lemmy_api_common::site::EditSite;
222   use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode};
223   use lemmy_utils::error::LemmyErrorType;
224
225   #[test]
226   fn test_validate_invalid_update_payload() {
227     let invalid_payloads = [
228       (
229         "EditSite name matches LocalSite slur filter",
230         LemmyErrorType::Slurs,
231         &generate_local_site(
232           Some(String::from("(foo|bar)")),
233           true,
234           false,
235           None::<String>,
236           RegistrationMode::Open,
237         ),
238         &generate_edit_site(
239           Some(String::from("foo site_name")),
240           None::<String>,
241           None::<String>,
242           None::<ListingType>,
243           None::<String>,
244           None::<bool>,
245           None::<bool>,
246           None::<String>,
247           None::<RegistrationMode>,
248         ),
249       ),
250       (
251         "EditSite name matches new slur filter",
252         LemmyErrorType::Slurs,
253         &generate_local_site(
254           Some(String::from("(foo|bar)")),
255           true,
256           false,
257           None::<String>,
258           RegistrationMode::Open,
259         ),
260         &generate_edit_site(
261           Some(String::from("zeta site_name")),
262           None::<String>,
263           None::<String>,
264           None::<ListingType>,
265           Some(String::from("(zeta|alpha)")),
266           None::<bool>,
267           None::<bool>,
268           None::<String>,
269           None::<RegistrationMode>,
270         ),
271       ),
272       (
273         "EditSite listing type is Subscribed, which is invalid",
274         LemmyErrorType::InvalidDefaultPostListingType,
275         &generate_local_site(
276           None::<String>,
277           true,
278           false,
279           None::<String>,
280           RegistrationMode::Open,
281         ),
282         &generate_edit_site(
283           Some(String::from("site_name")),
284           None::<String>,
285           None::<String>,
286           Some(ListingType::Subscribed),
287           None::<String>,
288           None::<bool>,
289           None::<bool>,
290           None::<String>,
291           None::<RegistrationMode>,
292         ),
293       ),
294       (
295         "EditSite is both private and federated",
296         LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
297         &generate_local_site(
298           None::<String>,
299           true,
300           false,
301           None::<String>,
302           RegistrationMode::Open,
303         ),
304         &generate_edit_site(
305           Some(String::from("site_name")),
306           None::<String>,
307           None::<String>,
308           None::<ListingType>,
309           None::<String>,
310           Some(true),
311           Some(true),
312           None::<String>,
313           None::<RegistrationMode>,
314         ),
315       ),
316       (
317         "LocalSite is private, but EditSite also makes it federated",
318         LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
319         &generate_local_site(
320           None::<String>,
321           true,
322           false,
323           None::<String>,
324           RegistrationMode::Open,
325         ),
326         &generate_edit_site(
327           Some(String::from("site_name")),
328           None::<String>,
329           None::<String>,
330           None::<ListingType>,
331           None::<String>,
332           None::<bool>,
333           Some(true),
334           None::<String>,
335           None::<RegistrationMode>,
336         ),
337       ),
338       (
339         "EditSite requires application, but neither it nor LocalSite has an application question",
340         LemmyErrorType::ApplicationQuestionRequired,
341         &generate_local_site(
342           None::<String>,
343           true,
344           false,
345           None::<String>,
346           RegistrationMode::Open,
347         ),
348         &generate_edit_site(
349           Some(String::from("site_name")),
350           None::<String>,
351           None::<String>,
352           None::<ListingType>,
353           None::<String>,
354           None::<bool>,
355           None::<bool>,
356           None::<String>,
357           Some(RegistrationMode::RequireApplication),
358         ),
359       ),
360     ];
361
362     invalid_payloads.iter().enumerate().for_each(
363       |(
364          idx,
365          &(reason, ref expected_err, local_site, edit_site),
366        )| {
367         match validate_update_payload(local_site, edit_site) {
368           Ok(_) => {
369             panic!(
370               "Got Ok, but validation should have failed with error: {} for reason: {}. invalid_payloads.nth({})",
371               expected_err, reason, idx
372             )
373           }
374           Err(error) => {
375             assert!(
376               error.error_type.eq(&expected_err.clone()),
377               "Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})",
378               error.error_type,
379               expected_err,
380               reason,
381               idx
382             )
383           }
384         }
385       },
386     );
387   }
388
389   #[test]
390   fn test_validate_valid_update_payload() {
391     let valid_payloads = [
392       (
393         "No changes between LocalSite and EditSite",
394         &generate_local_site(
395           None::<String>,
396           true,
397           false,
398           None::<String>,
399           RegistrationMode::Open,
400         ),
401         &generate_edit_site(
402           None::<String>,
403           None::<String>,
404           None::<String>,
405           None::<ListingType>,
406           None::<String>,
407           None::<bool>,
408           None::<bool>,
409           None::<String>,
410           None::<RegistrationMode>,
411         ),
412       ),
413       (
414         "EditSite allows clearing and changing values",
415         &generate_local_site(
416           None::<String>,
417           true,
418           false,
419           None::<String>,
420           RegistrationMode::Open,
421         ),
422         &generate_edit_site(
423           Some(String::from("site_name")),
424           Some(String::new()),
425           Some(String::new()),
426           Some(ListingType::All),
427           Some(String::new()),
428           Some(false),
429           Some(true),
430           Some(String::new()),
431           Some(RegistrationMode::Open),
432         ),
433       ),
434       (
435         "EditSite name passes slur filter regex",
436         &generate_local_site(
437           Some(String::from("(foo|bar)")),
438           true,
439           false,
440           None::<String>,
441           RegistrationMode::Open,
442         ),
443         &generate_edit_site(
444           Some(String::from("foo site_name")),
445           None::<String>,
446           None::<String>,
447           None::<ListingType>,
448           Some(String::new()),
449           None::<bool>,
450           None::<bool>,
451           None::<String>,
452           None::<RegistrationMode>,
453         ),
454       ),
455       (
456         "LocalSite has application question and EditSite now requires applications,",
457         &generate_local_site(
458           None::<String>,
459           true,
460           false,
461           Some(String::from("question")),
462           RegistrationMode::Open,
463         ),
464         &generate_edit_site(
465           Some(String::from("site_name")),
466           None::<String>,
467           None::<String>,
468           None::<ListingType>,
469           None::<String>,
470           None::<bool>,
471           None::<bool>,
472           None::<String>,
473           Some(RegistrationMode::RequireApplication),
474         ),
475       ),
476     ];
477
478     valid_payloads
479       .iter()
480       .enumerate()
481       .for_each(|(idx, &(reason, local_site, edit_site))| {
482         assert!(
483           validate_update_payload(local_site, edit_site).is_ok(),
484           "Got Err, but should have got Ok for reason: {}. valid_payloads.nth({})",
485           reason,
486           idx
487         );
488       })
489   }
490
491   fn generate_local_site(
492     site_slur_filter_regex: Option<String>,
493     site_is_private: bool,
494     site_is_federated: bool,
495     site_application_question: Option<String>,
496     site_registration_mode: RegistrationMode,
497   ) -> LocalSite {
498     LocalSite {
499       id: Default::default(),
500       site_id: Default::default(),
501       site_setup: true,
502       enable_downvotes: false,
503       enable_nsfw: false,
504       community_creation_admin_only: false,
505       require_email_verification: false,
506       application_question: site_application_question,
507       private_instance: site_is_private,
508       default_theme: String::new(),
509       default_post_listing_type: ListingType::All,
510       legal_information: None,
511       hide_modlog_mod_names: false,
512       application_email_admins: false,
513       slur_filter_regex: site_slur_filter_regex,
514       actor_name_max_length: 0,
515       federation_enabled: site_is_federated,
516       captcha_enabled: false,
517       captcha_difficulty: String::new(),
518       published: Default::default(),
519       updated: None,
520       registration_mode: site_registration_mode,
521       reports_email_admins: false,
522     }
523   }
524
525   // Allow the test helper function to have too many arguments.
526   // It's either this or generate the entire struct each time for testing.
527   #[allow(clippy::too_many_arguments)]
528   fn generate_edit_site(
529     site_name: Option<String>,
530     site_description: Option<String>,
531     site_sidebar: Option<String>,
532     site_listing_type: Option<ListingType>,
533     site_slur_filter_regex: Option<String>,
534     site_is_private: Option<bool>,
535     site_is_federated: Option<bool>,
536     site_application_question: Option<String>,
537     site_registration_mode: Option<RegistrationMode>,
538   ) -> EditSite {
539     EditSite {
540       name: site_name,
541       sidebar: site_sidebar,
542       description: site_description,
543       icon: None,
544       banner: None,
545       enable_downvotes: None,
546       enable_nsfw: None,
547       community_creation_admin_only: None,
548       require_email_verification: None,
549       application_question: site_application_question,
550       private_instance: site_is_private,
551       default_theme: None,
552       default_post_listing_type: site_listing_type,
553       legal_information: None,
554       application_email_admins: None,
555       hide_modlog_mod_names: None,
556       discussion_languages: None,
557       slur_filter_regex: site_slur_filter_regex,
558       actor_name_max_length: None,
559       rate_limit_message: None,
560       rate_limit_message_per_second: None,
561       rate_limit_post: None,
562       rate_limit_post_per_second: None,
563       rate_limit_register: None,
564       rate_limit_register_per_second: None,
565       rate_limit_image: None,
566       rate_limit_image_per_second: None,
567       rate_limit_comment: None,
568       rate_limit_comment_per_second: None,
569       rate_limit_search: None,
570       rate_limit_search_per_second: None,
571       federation_enabled: site_is_federated,
572       federation_debug: None,
573       captcha_enabled: None,
574       captcha_difficulty: None,
575       allowed_instances: None,
576       blocked_instances: None,
577       taglines: None,
578       registration_mode: site_registration_mode,
579       reports_email_admins: None,
580       auth: Default::default(),
581     }
582   }
583 }