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