]> Untitled Git - lemmy.git/blob - crates/api_crud/src/site/create.rs
Sanitize html (#3708)
[lemmy.git] / crates / api_crud / src / site / create.rs
1 use crate::{
2   site::{application_question_check, site_default_post_listing_type_check},
3   PerformCrud,
4 };
5 use activitypub_federation::http_signatures::generate_actor_keypair;
6 use actix_web::web::Data;
7 use lemmy_api_common::{
8   context::LemmyContext,
9   site::{CreateSite, SiteResponse},
10   utils::{
11     generate_site_inbox_url,
12     is_admin,
13     local_site_rate_limit_to_rate_limit_config,
14     local_user_view_from_jwt,
15     sanitize_html,
16     sanitize_html_opt,
17   },
18 };
19 use lemmy_db_schema::{
20   newtypes::DbUrl,
21   source::{
22     local_site::{LocalSite, LocalSiteUpdateForm},
23     local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
24     site::{Site, SiteUpdateForm},
25     tagline::Tagline,
26   },
27   traits::Crud,
28   utils::{diesel_option_overwrite, diesel_option_overwrite_to_url, naive_now},
29 };
30 use lemmy_db_views::structs::SiteView;
31 use lemmy_utils::{
32   error::{LemmyError, LemmyErrorType, LemmyResult},
33   utils::{
34     slurs::{check_slurs, check_slurs_opt},
35     validation::{
36       build_and_check_regex,
37       check_site_visibility_valid,
38       is_valid_body_field,
39       site_description_length_check,
40       site_name_length_check,
41     },
42   },
43 };
44 use url::Url;
45
46 #[async_trait::async_trait(?Send)]
47 impl PerformCrud for CreateSite {
48   type Response = SiteResponse;
49
50   #[tracing::instrument(skip(context))]
51   async fn perform(&self, context: &Data<LemmyContext>) -> Result<SiteResponse, LemmyError> {
52     let data: &CreateSite = self;
53     let local_user_view = local_user_view_from_jwt(&data.auth, context).await?;
54     let local_site = LocalSite::read(&mut context.pool()).await?;
55
56     // Make sure user is an admin; other types of users should not create site data...
57     is_admin(&local_user_view)?;
58
59     validate_create_payload(&local_site, data)?;
60
61     let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
62     let inbox_url = Some(generate_site_inbox_url(&actor_id)?);
63     let keypair = generate_actor_keypair()?;
64     let name = sanitize_html(&data.name);
65     let sidebar = sanitize_html_opt(&data.sidebar);
66     let description = sanitize_html_opt(&data.description);
67
68     let site_form = SiteUpdateForm::builder()
69       .name(Some(name))
70       .sidebar(diesel_option_overwrite(sidebar))
71       .description(diesel_option_overwrite(description))
72       .icon(diesel_option_overwrite_to_url(&data.icon)?)
73       .banner(diesel_option_overwrite_to_url(&data.banner)?)
74       .actor_id(Some(actor_id))
75       .last_refreshed_at(Some(naive_now()))
76       .inbox_url(inbox_url)
77       .private_key(Some(Some(keypair.private_key)))
78       .public_key(Some(keypair.public_key))
79       .build();
80
81     let site_id = local_site.site_id;
82
83     Site::update(&mut context.pool(), site_id, &site_form).await?;
84
85     let application_question = sanitize_html_opt(&data.application_question);
86     let default_theme = sanitize_html_opt(&data.default_theme);
87     let legal_information = sanitize_html_opt(&data.legal_information);
88
89     let local_site_form = LocalSiteUpdateForm::builder()
90       // Set the site setup to true
91       .site_setup(Some(true))
92       .enable_downvotes(data.enable_downvotes)
93       .registration_mode(data.registration_mode)
94       .enable_nsfw(data.enable_nsfw)
95       .community_creation_admin_only(data.community_creation_admin_only)
96       .require_email_verification(data.require_email_verification)
97       .application_question(diesel_option_overwrite(application_question))
98       .private_instance(data.private_instance)
99       .default_theme(default_theme)
100       .default_post_listing_type(data.default_post_listing_type)
101       .legal_information(diesel_option_overwrite(legal_information))
102       .application_email_admins(data.application_email_admins)
103       .hide_modlog_mod_names(data.hide_modlog_mod_names)
104       .updated(Some(Some(naive_now())))
105       .slur_filter_regex(diesel_option_overwrite(data.slur_filter_regex.clone()))
106       .actor_name_max_length(data.actor_name_max_length)
107       .federation_enabled(data.federation_enabled)
108       .captcha_enabled(data.captcha_enabled)
109       .captcha_difficulty(data.captcha_difficulty.clone())
110       .build();
111
112     LocalSite::update(&mut context.pool(), &local_site_form).await?;
113
114     let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm::builder()
115       .message(data.rate_limit_message)
116       .message_per_second(data.rate_limit_message_per_second)
117       .post(data.rate_limit_post)
118       .post_per_second(data.rate_limit_post_per_second)
119       .register(data.rate_limit_register)
120       .register_per_second(data.rate_limit_register_per_second)
121       .image(data.rate_limit_image)
122       .image_per_second(data.rate_limit_image_per_second)
123       .comment(data.rate_limit_comment)
124       .comment_per_second(data.rate_limit_comment_per_second)
125       .search(data.rate_limit_search)
126       .search_per_second(data.rate_limit_search_per_second)
127       .build();
128
129     LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?;
130
131     let site_view = SiteView::read_local(&mut context.pool()).await?;
132
133     let new_taglines = data.taglines.clone();
134     let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
135
136     let rate_limit_config =
137       local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
138     context
139       .settings_updated_channel()
140       .send(rate_limit_config)
141       .await?;
142
143     Ok(SiteResponse {
144       site_view,
145       taglines,
146     })
147   }
148 }
149
150 fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> LemmyResult<()> {
151   // Make sure the site hasn't already been set up...
152   if local_site.site_setup {
153     return Err(LemmyErrorType::SiteAlreadyExists)?;
154   };
155
156   // Check that the slur regex compiles, and returns the regex if valid...
157   // Prioritize using new slur regex from the request; if not provided, use the existing regex.
158   let slur_regex = build_and_check_regex(
159     &create_site
160       .slur_filter_regex
161       .as_deref()
162       .or(local_site.slur_filter_regex.as_deref()),
163   )?;
164
165   site_name_length_check(&create_site.name)?;
166   check_slurs(&create_site.name, &slur_regex)?;
167
168   if let Some(desc) = &create_site.description {
169     site_description_length_check(desc)?;
170     check_slurs_opt(&create_site.description, &slur_regex)?;
171   }
172
173   site_default_post_listing_type_check(&create_site.default_post_listing_type)?;
174
175   check_site_visibility_valid(
176     local_site.private_instance,
177     local_site.federation_enabled,
178     &create_site.private_instance,
179     &create_site.federation_enabled,
180   )?;
181
182   // Ensure that the sidebar has fewer than the max num characters...
183   is_valid_body_field(&create_site.sidebar, false)?;
184
185   application_question_check(
186     &local_site.application_question,
187     &create_site.application_question,
188     create_site
189       .registration_mode
190       .unwrap_or(local_site.registration_mode),
191   )
192 }
193
194 #[cfg(test)]
195 mod tests {
196   #![allow(clippy::unwrap_used)]
197   #![allow(clippy::indexing_slicing)]
198
199   use crate::site::create::validate_create_payload;
200   use lemmy_api_common::site::CreateSite;
201   use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode};
202   use lemmy_utils::error::LemmyErrorType;
203
204   #[test]
205   fn test_validate_invalid_create_payload() {
206     let invalid_payloads = [
207       (
208         "CreateSite attempted on set up LocalSite",
209         LemmyErrorType::SiteAlreadyExists,
210         &generate_local_site(
211           true,
212           None::<String>,
213           true,
214           false,
215           None::<String>,
216           RegistrationMode::Open,
217         ),
218         &generate_create_site(
219           String::from("site_name"),
220           None::<String>,
221           None::<String>,
222           None::<ListingType>,
223           None::<String>,
224           None::<bool>,
225           None::<bool>,
226           None::<String>,
227           None::<RegistrationMode>,
228         ),
229       ),
230       (
231         "CreateSite name matches LocalSite slur filter",
232         LemmyErrorType::Slurs,
233         &generate_local_site(
234           false,
235           Some(String::from("(foo|bar)")),
236           true,
237           false,
238           None::<String>,
239           RegistrationMode::Open,
240         ),
241         &generate_create_site(
242           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         "CreateSite name matches new slur filter",
255         LemmyErrorType::Slurs,
256         &generate_local_site(
257           false,
258           Some(String::from("(foo|bar)")),
259           true,
260           false,
261           None::<String>,
262           RegistrationMode::Open,
263         ),
264         &generate_create_site(
265           String::from("zeta site_name"),
266           None::<String>,
267           None::<String>,
268           None::<ListingType>,
269           Some(String::from("(zeta|alpha)")),
270           None::<bool>,
271           None::<bool>,
272           None::<String>,
273           None::<RegistrationMode>,
274         ),
275       ),
276       (
277         "CreateSite listing type is Subscribed, which is invalid",
278         LemmyErrorType::InvalidDefaultPostListingType,
279         &generate_local_site(
280           false,
281           None::<String>,
282           true,
283           false,
284           None::<String>,
285           RegistrationMode::Open,
286         ),
287         &generate_create_site(
288           String::from("site_name"),
289           None::<String>,
290           None::<String>,
291           Some(ListingType::Subscribed),
292           None::<String>,
293           None::<bool>,
294           None::<bool>,
295           None::<String>,
296           None::<RegistrationMode>,
297         ),
298       ),
299       (
300         "CreateSite is both private and federated",
301         LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
302         &generate_local_site(
303           false,
304           None::<String>,
305           true,
306           false,
307           None::<String>,
308           RegistrationMode::Open,
309         ),
310         &generate_create_site(
311           String::from("site_name"),
312           None::<String>,
313           None::<String>,
314           None::<ListingType>,
315           None::<String>,
316           Some(true),
317           Some(true),
318           None::<String>,
319           None::<RegistrationMode>,
320         ),
321       ),
322       (
323         "LocalSite is private, but CreateSite also makes it federated",
324         LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
325         &generate_local_site(
326           false,
327           None::<String>,
328           true,
329           false,
330           None::<String>,
331           RegistrationMode::Open,
332         ),
333         &generate_create_site(
334           String::from("site_name"),
335           None::<String>,
336           None::<String>,
337           None::<ListingType>,
338           None::<String>,
339           None::<bool>,
340           Some(true),
341           None::<String>,
342           None::<RegistrationMode>,
343         ),
344       ),
345       (
346         "CreateSite requires application, but neither it nor LocalSite has an application question",
347         LemmyErrorType::ApplicationQuestionRequired,
348         &generate_local_site(
349           false,
350           None::<String>,
351           true,
352           false,
353           None::<String>,
354           RegistrationMode::Open,
355         ),
356         &generate_create_site(
357           String::from("site_name"),
358           None::<String>,
359           None::<String>,
360           None::<ListingType>,
361           None::<String>,
362           None::<bool>,
363           None::<bool>,
364           None::<String>,
365           Some(RegistrationMode::RequireApplication),
366         ),
367       ),
368     ];
369
370     invalid_payloads.iter().enumerate().for_each(
371       |(
372          idx,
373          &(reason, ref expected_err, local_site, create_site),
374        )| {
375         match validate_create_payload(
376           local_site,
377           create_site,
378         ) {
379           Ok(_) => {
380             panic!(
381               "Got Ok, but validation should have failed with error: {} for reason: {}. invalid_payloads.nth({})",
382               expected_err, reason, idx
383             )
384           }
385           Err(error) => {
386             assert!(
387               error.error_type.eq(&expected_err.clone()),
388               "Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})",
389               error.error_type,
390               expected_err,
391               reason,
392               idx
393             )
394           }
395         }
396       },
397     );
398   }
399
400   #[test]
401   fn test_validate_valid_create_payload() {
402     let valid_payloads = [
403       (
404         "No changes between LocalSite and CreateSite",
405         &generate_local_site(
406           false,
407           None::<String>,
408           true,
409           false,
410           None::<String>,
411           RegistrationMode::Open,
412         ),
413         &generate_create_site(
414           String::from("site_name"),
415           None::<String>,
416           None::<String>,
417           None::<ListingType>,
418           None::<String>,
419           None::<bool>,
420           None::<bool>,
421           None::<String>,
422           None::<RegistrationMode>,
423         ),
424       ),
425       (
426         "CreateSite allows clearing and changing values",
427         &generate_local_site(
428           false,
429           None::<String>,
430           true,
431           false,
432           None::<String>,
433           RegistrationMode::Open,
434         ),
435         &generate_create_site(
436           String::from("site_name"),
437           Some(String::new()),
438           Some(String::new()),
439           Some(ListingType::All),
440           Some(String::new()),
441           Some(false),
442           Some(true),
443           Some(String::new()),
444           Some(RegistrationMode::Open),
445         ),
446       ),
447       (
448         "CreateSite clears existing slur filter regex",
449         &generate_local_site(
450           false,
451           Some(String::from("(foo|bar)")),
452           true,
453           false,
454           None::<String>,
455           RegistrationMode::Open,
456         ),
457         &generate_create_site(
458           String::from("foo site_name"),
459           None::<String>,
460           None::<String>,
461           None::<ListingType>,
462           Some(String::new()),
463           None::<bool>,
464           None::<bool>,
465           None::<String>,
466           None::<RegistrationMode>,
467         ),
468       ),
469       (
470         "LocalSite has application question and CreateSite now requires applications,",
471         &generate_local_site(
472           false,
473           None::<String>,
474           true,
475           false,
476           Some(String::from("question")),
477           RegistrationMode::Open,
478         ),
479         &generate_create_site(
480           String::from("site_name"),
481           None::<String>,
482           None::<String>,
483           None::<ListingType>,
484           None::<String>,
485           None::<bool>,
486           None::<bool>,
487           None::<String>,
488           Some(RegistrationMode::RequireApplication),
489         ),
490       ),
491     ];
492
493     valid_payloads
494       .iter()
495       .enumerate()
496       .for_each(|(idx, &(reason, local_site, edit_site))| {
497         assert!(
498           validate_create_payload(local_site, edit_site).is_ok(),
499           "Got Err, but should have got Ok for reason: {}. valid_payloads.nth({})",
500           reason,
501           idx
502         );
503       })
504   }
505
506   fn generate_local_site(
507     site_setup: bool,
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,
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_create_site(
545     site_name: 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   ) -> CreateSite {
555     CreateSite {
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       auth: Default::default(),
596     }
597   }
598 }