]> Untitled Git - lemmy.git/blob - crates/api_crud/src/site/update.rs
Sanitize html (#3708)
[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::{
10     is_admin,
11     local_site_rate_limit_to_rate_limit_config,
12     local_user_view_from_jwt,
13     sanitize_html_opt,
14   },
15 };
16 use lemmy_db_schema::{
17   source::{
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},
25     tagline::Tagline,
26   },
27   traits::Crud,
28   utils::{diesel_option_overwrite, diesel_option_overwrite_to_url, naive_now},
29   RegistrationMode,
30 };
31 use lemmy_db_views::structs::SiteView;
32 use lemmy_utils::{
33   error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
34   utils::{
35     slurs::check_slurs_opt,
36     validation::{
37       build_and_check_regex,
38       check_site_visibility_valid,
39       is_valid_body_field,
40       site_description_length_check,
41       site_name_length_check,
42     },
43   },
44 };
45
46 #[async_trait::async_trait(?Send)]
47 impl PerformCrud for EditSite {
48   type Response = SiteResponse;
49
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;
57
58     // Make sure user is an admin; other types of users should not update site data...
59     is_admin(&local_user_view)?;
60
61     validate_update_payload(&local_site, data)?;
62
63     if let Some(discussion_languages) = data.discussion_languages.clone() {
64       SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
65     }
66
67     let name = sanitize_html_opt(&data.name);
68     let sidebar = sanitize_html_opt(&data.sidebar);
69     let description = sanitize_html_opt(&data.description);
70
71     let site_form = SiteUpdateForm::builder()
72       .name(name)
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())))
78       .build();
79
80     Site::update(&mut context.pool(), site.id, &site_form)
81       .await
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
84       .ok();
85
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);
89
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)
110       .build();
111
112     let update_local_site = LocalSite::update(&mut context.pool(), &local_site_form)
113       .await
114       .ok();
115
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)
129       .build();
130
131     LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form)
132       .await
133       .ok();
134
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?;
140
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.
145
146     let old_require_application =
147       local_site.registration_mode == RegistrationMode::RequireApplication;
148     let new_require_application = update_local_site
149       .as_ref()
150       .map(|ols| ols.registration_mode == RegistrationMode::RequireApplication)
151       .unwrap_or(false);
152     if !old_require_application && new_require_application {
153       LocalUser::set_all_users_registration_applications_accepted(&mut context.pool())
154         .await
155         .with_lemmy_type(LemmyErrorType::CouldntSetAllRegistrationsAccepted)?;
156     }
157
158     let new_require_email_verification = update_local_site
159       .as_ref()
160       .map(|ols| ols.require_email_verification)
161       .unwrap_or(false);
162     if !local_site.require_email_verification && new_require_email_verification {
163       LocalUser::set_all_users_email_verified(&mut context.pool())
164         .await
165         .with_lemmy_type(LemmyErrorType::CouldntSetAllEmailVerified)?;
166     }
167
168     let new_taglines = data.taglines.clone();
169     let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
170
171     let site_view = SiteView::read_local(&mut context.pool()).await?;
172
173     let rate_limit_config =
174       local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
175     context
176       .settings_updated_channel()
177       .send(rate_limit_config)
178       .await?;
179
180     let res = SiteResponse {
181       site_view,
182       taglines,
183     };
184
185     Ok(res)
186   }
187 }
188
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(
193     &edit_site
194       .slur_filter_regex
195       .as_deref()
196       .or(local_site.slur_filter_regex.as_deref()),
197   )?;
198
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)?;
203   }
204
205   if let Some(desc) = &edit_site.description {
206     site_description_length_check(desc)?;
207     check_slurs_opt(&edit_site.description, &slur_regex)?;
208   }
209
210   site_default_post_listing_type_check(&edit_site.default_post_listing_type)?;
211
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,
217   )?;
218
219   // Ensure that the sidebar has fewer than the max num characters...
220   is_valid_body_field(&edit_site.sidebar, false)?;
221
222   application_question_check(
223     &local_site.application_question,
224     &edit_site.application_question,
225     edit_site
226       .registration_mode
227       .unwrap_or(local_site.registration_mode),
228   )
229 }
230
231 #[cfg(test)]
232 mod tests {
233   #![allow(clippy::unwrap_used)]
234   #![allow(clippy::indexing_slicing)]
235
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;
240
241   #[test]
242   fn test_validate_invalid_update_payload() {
243     let invalid_payloads = [
244       (
245         "EditSite name matches LocalSite slur filter",
246         LemmyErrorType::Slurs,
247         &generate_local_site(
248           Some(String::from("(foo|bar)")),
249           true,
250           false,
251           None::<String>,
252           RegistrationMode::Open,
253         ),
254         &generate_edit_site(
255           Some(String::from("foo site_name")),
256           None::<String>,
257           None::<String>,
258           None::<ListingType>,
259           None::<String>,
260           None::<bool>,
261           None::<bool>,
262           None::<String>,
263           None::<RegistrationMode>,
264         ),
265       ),
266       (
267         "EditSite name matches new slur filter",
268         LemmyErrorType::Slurs,
269         &generate_local_site(
270           Some(String::from("(foo|bar)")),
271           true,
272           false,
273           None::<String>,
274           RegistrationMode::Open,
275         ),
276         &generate_edit_site(
277           Some(String::from("zeta site_name")),
278           None::<String>,
279           None::<String>,
280           None::<ListingType>,
281           Some(String::from("(zeta|alpha)")),
282           None::<bool>,
283           None::<bool>,
284           None::<String>,
285           None::<RegistrationMode>,
286         ),
287       ),
288       (
289         "EditSite listing type is Subscribed, which is invalid",
290         LemmyErrorType::InvalidDefaultPostListingType,
291         &generate_local_site(
292           None::<String>,
293           true,
294           false,
295           None::<String>,
296           RegistrationMode::Open,
297         ),
298         &generate_edit_site(
299           Some(String::from("site_name")),
300           None::<String>,
301           None::<String>,
302           Some(ListingType::Subscribed),
303           None::<String>,
304           None::<bool>,
305           None::<bool>,
306           None::<String>,
307           None::<RegistrationMode>,
308         ),
309       ),
310       (
311         "EditSite is both private and federated",
312         LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
313         &generate_local_site(
314           None::<String>,
315           true,
316           false,
317           None::<String>,
318           RegistrationMode::Open,
319         ),
320         &generate_edit_site(
321           Some(String::from("site_name")),
322           None::<String>,
323           None::<String>,
324           None::<ListingType>,
325           None::<String>,
326           Some(true),
327           Some(true),
328           None::<String>,
329           None::<RegistrationMode>,
330         ),
331       ),
332       (
333         "LocalSite is private, but EditSite also makes it federated",
334         LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
335         &generate_local_site(
336           None::<String>,
337           true,
338           false,
339           None::<String>,
340           RegistrationMode::Open,
341         ),
342         &generate_edit_site(
343           Some(String::from("site_name")),
344           None::<String>,
345           None::<String>,
346           None::<ListingType>,
347           None::<String>,
348           None::<bool>,
349           Some(true),
350           None::<String>,
351           None::<RegistrationMode>,
352         ),
353       ),
354       (
355         "EditSite requires application, but neither it nor LocalSite has an application question",
356         LemmyErrorType::ApplicationQuestionRequired,
357         &generate_local_site(
358           None::<String>,
359           true,
360           false,
361           None::<String>,
362           RegistrationMode::Open,
363         ),
364         &generate_edit_site(
365           Some(String::from("site_name")),
366           None::<String>,
367           None::<String>,
368           None::<ListingType>,
369           None::<String>,
370           None::<bool>,
371           None::<bool>,
372           None::<String>,
373           Some(RegistrationMode::RequireApplication),
374         ),
375       ),
376     ];
377
378     invalid_payloads.iter().enumerate().for_each(
379       |(
380          idx,
381          &(reason, ref expected_err, local_site, edit_site),
382        )| {
383         match validate_update_payload(local_site, edit_site) {
384           Ok(_) => {
385             panic!(
386               "Got Ok, but validation should have failed with error: {} for reason: {}. invalid_payloads.nth({})",
387               expected_err, reason, idx
388             )
389           }
390           Err(error) => {
391             assert!(
392               error.error_type.eq(&expected_err.clone()),
393               "Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})",
394               error.error_type,
395               expected_err,
396               reason,
397               idx
398             )
399           }
400         }
401       },
402     );
403   }
404
405   #[test]
406   fn test_validate_valid_update_payload() {
407     let valid_payloads = [
408       (
409         "No changes between LocalSite and EditSite",
410         &generate_local_site(
411           None::<String>,
412           true,
413           false,
414           None::<String>,
415           RegistrationMode::Open,
416         ),
417         &generate_edit_site(
418           None::<String>,
419           None::<String>,
420           None::<String>,
421           None::<ListingType>,
422           None::<String>,
423           None::<bool>,
424           None::<bool>,
425           None::<String>,
426           None::<RegistrationMode>,
427         ),
428       ),
429       (
430         "EditSite allows clearing and changing values",
431         &generate_local_site(
432           None::<String>,
433           true,
434           false,
435           None::<String>,
436           RegistrationMode::Open,
437         ),
438         &generate_edit_site(
439           Some(String::from("site_name")),
440           Some(String::new()),
441           Some(String::new()),
442           Some(ListingType::All),
443           Some(String::new()),
444           Some(false),
445           Some(true),
446           Some(String::new()),
447           Some(RegistrationMode::Open),
448         ),
449       ),
450       (
451         "EditSite name passes slur filter regex",
452         &generate_local_site(
453           Some(String::from("(foo|bar)")),
454           true,
455           false,
456           None::<String>,
457           RegistrationMode::Open,
458         ),
459         &generate_edit_site(
460           Some(String::from("foo site_name")),
461           None::<String>,
462           None::<String>,
463           None::<ListingType>,
464           Some(String::new()),
465           None::<bool>,
466           None::<bool>,
467           None::<String>,
468           None::<RegistrationMode>,
469         ),
470       ),
471       (
472         "LocalSite has application question and EditSite now requires applications,",
473         &generate_local_site(
474           None::<String>,
475           true,
476           false,
477           Some(String::from("question")),
478           RegistrationMode::Open,
479         ),
480         &generate_edit_site(
481           Some(String::from("site_name")),
482           None::<String>,
483           None::<String>,
484           None::<ListingType>,
485           None::<String>,
486           None::<bool>,
487           None::<bool>,
488           None::<String>,
489           Some(RegistrationMode::RequireApplication),
490         ),
491       ),
492     ];
493
494     valid_payloads
495       .iter()
496       .enumerate()
497       .for_each(|(idx, &(reason, local_site, edit_site))| {
498         assert!(
499           validate_update_payload(local_site, edit_site).is_ok(),
500           "Got Err, but should have got Ok for reason: {}. valid_payloads.nth({})",
501           reason,
502           idx
503         );
504       })
505   }
506
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,
513   ) -> LocalSite {
514     LocalSite {
515       id: Default::default(),
516       site_id: Default::default(),
517       site_setup: true,
518       enable_downvotes: false,
519       enable_nsfw: 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(),
535       updated: None,
536       registration_mode: site_registration_mode,
537       reports_email_admins: false,
538     }
539   }
540
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>,
554   ) -> EditSite {
555     EditSite {
556       name: site_name,
557       sidebar: site_sidebar,
558       description: site_description,
559       icon: None,
560       banner: None,
561       enable_downvotes: None,
562       enable_nsfw: None,
563       community_creation_admin_only: None,
564       require_email_verification: None,
565       application_question: site_application_question,
566       private_instance: site_is_private,
567       default_theme: None,
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,
593       taglines: None,
594       registration_mode: site_registration_mode,
595       reports_email_admins: None,
596       auth: Default::default(),
597     }
598   }
599 }