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