]> Untitled Git - lemmy.git/blob - crates/api_crud/src/site/update.rs
c9f97e8dfab9d0c7f295a3ba3207dc3792d478a3
[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, 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(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(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(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(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(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(context.pool(), allowed).await?;
125     let blocked = data.blocked_instances.clone();
126     FederationBlockList::replace(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(context.pool())
141         .await
142         .map_err(|e| LemmyError::from_error_message(e, "couldnt_set_all_registrations_accepted"))?;
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(context.pool())
151         .await
152         .map_err(|e| LemmyError::from_error_message(e, "couldnt_set_all_email_verified"))?;
153     }
154
155     let new_taglines = data.taglines.clone();
156     let taglines = Tagline::replace(context.pool(), local_site.id, new_taglines).await?;
157
158     let site_view = SiteView::read_local(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
224   #[test]
225   fn test_validate_invalid_update_payload() {
226     let invalid_payloads = [
227       (
228         "EditSite name matches LocalSite slur filter",
229         "slurs",
230         &generate_local_site(
231           Some(String::from("(foo|bar)")),
232           true,
233           false,
234           None::<String>,
235           RegistrationMode::Open,
236         ),
237         &generate_edit_site(
238           Some(String::from("foo site_name")),
239           None::<String>,
240           None::<String>,
241           None::<ListingType>,
242           None::<String>,
243           None::<bool>,
244           None::<bool>,
245           None::<String>,
246           None::<RegistrationMode>,
247         ),
248       ),
249       (
250         "EditSite name matches new slur filter",
251         "slurs",
252         &generate_local_site(
253           Some(String::from("(foo|bar)")),
254           true,
255           false,
256           None::<String>,
257           RegistrationMode::Open,
258         ),
259         &generate_edit_site(
260           Some(String::from("zeta site_name")),
261           None::<String>,
262           None::<String>,
263           None::<ListingType>,
264           Some(String::from("(zeta|alpha)")),
265           None::<bool>,
266           None::<bool>,
267           None::<String>,
268           None::<RegistrationMode>,
269         ),
270       ),
271       (
272         "EditSite listing type is Subscribed, which is invalid",
273         "invalid_default_post_listing_type",
274         &generate_local_site(
275           None::<String>,
276           true,
277           false,
278           None::<String>,
279           RegistrationMode::Open,
280         ),
281         &generate_edit_site(
282           Some(String::from("site_name")),
283           None::<String>,
284           None::<String>,
285           Some(ListingType::Subscribed),
286           None::<String>,
287           None::<bool>,
288           None::<bool>,
289           None::<String>,
290           None::<RegistrationMode>,
291         ),
292       ),
293       (
294         "EditSite is both private and federated",
295         "cant_enable_private_instance_and_federation_together",
296         &generate_local_site(
297           None::<String>,
298           true,
299           false,
300           None::<String>,
301           RegistrationMode::Open,
302         ),
303         &generate_edit_site(
304           Some(String::from("site_name")),
305           None::<String>,
306           None::<String>,
307           None::<ListingType>,
308           None::<String>,
309           Some(true),
310           Some(true),
311           None::<String>,
312           None::<RegistrationMode>,
313         ),
314       ),
315       (
316         "LocalSite is private, but EditSite also makes it federated",
317         "cant_enable_private_instance_and_federation_together",
318         &generate_local_site(
319           None::<String>,
320           true,
321           false,
322           None::<String>,
323           RegistrationMode::Open,
324         ),
325         &generate_edit_site(
326           Some(String::from("site_name")),
327           None::<String>,
328           None::<String>,
329           None::<ListingType>,
330           None::<String>,
331           None::<bool>,
332           Some(true),
333           None::<String>,
334           None::<RegistrationMode>,
335         ),
336       ),
337       (
338         "EditSite requires application, but neither it nor LocalSite has an application question",
339         "application_question_required",
340         &generate_local_site(
341           None::<String>,
342           true,
343           false,
344           None::<String>,
345           RegistrationMode::Open,
346         ),
347         &generate_edit_site(
348           Some(String::from("site_name")),
349           None::<String>,
350           None::<String>,
351           None::<ListingType>,
352           None::<String>,
353           None::<bool>,
354           None::<bool>,
355           None::<String>,
356           Some(RegistrationMode::RequireApplication),
357         ),
358       ),
359     ];
360
361     invalid_payloads.iter().enumerate().for_each(
362       |(
363          idx,
364          &(reason, expected_err, local_site, edit_site),
365        )| {
366         match validate_update_payload(local_site, edit_site) {
367           Ok(_) => {
368             panic!(
369               "Got Ok, but validation should have failed with error: {} for reason: {}. invalid_payloads.nth({})",
370               expected_err, reason, idx
371             )
372           }
373           Err(error) => {
374             assert!(
375               error.message.eq(&Some(String::from(expected_err))),
376               "Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})",
377               error.message,
378               expected_err,
379               reason,
380               idx
381             )
382           }
383         }
384       },
385     );
386   }
387
388   #[test]
389   fn test_validate_valid_update_payload() {
390     let valid_payloads = [
391       (
392         "No changes between LocalSite and EditSite",
393         &generate_local_site(
394           None::<String>,
395           true,
396           false,
397           None::<String>,
398           RegistrationMode::Open,
399         ),
400         &generate_edit_site(
401           None::<String>,
402           None::<String>,
403           None::<String>,
404           None::<ListingType>,
405           None::<String>,
406           None::<bool>,
407           None::<bool>,
408           None::<String>,
409           None::<RegistrationMode>,
410         ),
411       ),
412       (
413         "EditSite allows clearing and changing values",
414         &generate_local_site(
415           None::<String>,
416           true,
417           false,
418           None::<String>,
419           RegistrationMode::Open,
420         ),
421         &generate_edit_site(
422           Some(String::from("site_name")),
423           Some(String::new()),
424           Some(String::new()),
425           Some(ListingType::All),
426           Some(String::new()),
427           Some(false),
428           Some(true),
429           Some(String::new()),
430           Some(RegistrationMode::Open),
431         ),
432       ),
433       (
434         "EditSite name passes slur filter regex",
435         &generate_local_site(
436           Some(String::from("(foo|bar)")),
437           true,
438           false,
439           None::<String>,
440           RegistrationMode::Open,
441         ),
442         &generate_edit_site(
443           Some(String::from("foo site_name")),
444           None::<String>,
445           None::<String>,
446           None::<ListingType>,
447           Some(String::new()),
448           None::<bool>,
449           None::<bool>,
450           None::<String>,
451           None::<RegistrationMode>,
452         ),
453       ),
454       (
455         "LocalSite has application question and EditSite now requires applications,",
456         &generate_local_site(
457           None::<String>,
458           true,
459           false,
460           Some(String::from("question")),
461           RegistrationMode::Open,
462         ),
463         &generate_edit_site(
464           Some(String::from("site_name")),
465           None::<String>,
466           None::<String>,
467           None::<ListingType>,
468           None::<String>,
469           None::<bool>,
470           None::<bool>,
471           None::<String>,
472           Some(RegistrationMode::RequireApplication),
473         ),
474       ),
475     ];
476
477     valid_payloads
478       .iter()
479       .enumerate()
480       .for_each(|(idx, &(reason, local_site, edit_site))| {
481         assert!(
482           validate_update_payload(local_site, edit_site).is_ok(),
483           "Got Err, but should have got Ok for reason: {}. valid_payloads.nth({})",
484           reason,
485           idx
486         );
487       })
488   }
489
490   fn generate_local_site(
491     site_slur_filter_regex: Option<String>,
492     site_is_private: bool,
493     site_is_federated: bool,
494     site_application_question: Option<String>,
495     site_registration_mode: RegistrationMode,
496   ) -> LocalSite {
497     LocalSite {
498       id: Default::default(),
499       site_id: Default::default(),
500       site_setup: true,
501       enable_downvotes: false,
502       enable_nsfw: false,
503       community_creation_admin_only: false,
504       require_email_verification: false,
505       application_question: site_application_question,
506       private_instance: site_is_private,
507       default_theme: String::new(),
508       default_post_listing_type: ListingType::All,
509       legal_information: None,
510       hide_modlog_mod_names: false,
511       application_email_admins: false,
512       slur_filter_regex: site_slur_filter_regex,
513       actor_name_max_length: 0,
514       federation_enabled: site_is_federated,
515       captcha_enabled: false,
516       captcha_difficulty: String::new(),
517       published: Default::default(),
518       updated: None,
519       registration_mode: site_registration_mode,
520       reports_email_admins: false,
521     }
522   }
523
524   // Allow the test helper function to have too many arguments.
525   // It's either this or generate the entire struct each time for testing.
526   #[allow(clippy::too_many_arguments)]
527   fn generate_edit_site(
528     site_name: Option<String>,
529     site_description: Option<String>,
530     site_sidebar: Option<String>,
531     site_listing_type: Option<ListingType>,
532     site_slur_filter_regex: Option<String>,
533     site_is_private: Option<bool>,
534     site_is_federated: Option<bool>,
535     site_application_question: Option<String>,
536     site_registration_mode: Option<RegistrationMode>,
537   ) -> EditSite {
538     EditSite {
539       name: site_name,
540       sidebar: site_sidebar,
541       description: site_description,
542       icon: None,
543       banner: None,
544       enable_downvotes: None,
545       enable_nsfw: None,
546       community_creation_admin_only: None,
547       require_email_verification: None,
548       application_question: site_application_question,
549       private_instance: site_is_private,
550       default_theme: None,
551       default_post_listing_type: site_listing_type,
552       legal_information: None,
553       application_email_admins: None,
554       hide_modlog_mod_names: None,
555       discussion_languages: None,
556       slur_filter_regex: site_slur_filter_regex,
557       actor_name_max_length: None,
558       rate_limit_message: None,
559       rate_limit_message_per_second: None,
560       rate_limit_post: None,
561       rate_limit_post_per_second: None,
562       rate_limit_register: None,
563       rate_limit_register_per_second: None,
564       rate_limit_image: None,
565       rate_limit_image_per_second: None,
566       rate_limit_comment: None,
567       rate_limit_comment_per_second: None,
568       rate_limit_search: None,
569       rate_limit_search_per_second: None,
570       federation_enabled: site_is_federated,
571       federation_debug: None,
572       captcha_enabled: None,
573       captcha_difficulty: None,
574       allowed_instances: None,
575       blocked_instances: None,
576       taglines: None,
577       registration_mode: site_registration_mode,
578       reports_email_admins: None,
579       auth: Default::default(),
580     }
581   }
582 }