]> Untitled Git - lemmy.git/blob - crates/api_crud/src/post/create.rs
Sanitize html (#3708)
[lemmy.git] / crates / api_crud / src / post / create.rs
1 use activitypub_federation::config::Data;
2 use actix_web::web::Json;
3 use lemmy_api_common::{
4   build_response::build_post_response,
5   context::LemmyContext,
6   post::{CreatePost, PostResponse},
7   request::fetch_site_data,
8   send_activity::{ActivityChannel, SendActivityData},
9   utils::{
10     check_community_ban,
11     check_community_deleted_or_removed,
12     generate_local_apub_endpoint,
13     honeypot_check,
14     local_site_to_slur_regex,
15     local_user_view_from_jwt,
16     mark_post_as_read,
17     sanitize_html,
18     sanitize_html_opt,
19     EndpointType,
20   },
21 };
22 use lemmy_db_schema::{
23   impls::actor_language::default_post_language,
24   source::{
25     actor_language::CommunityLanguage,
26     community::Community,
27     local_site::LocalSite,
28     post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
29   },
30   traits::{Crud, Likeable},
31 };
32 use lemmy_db_views_actor::structs::CommunityView;
33 use lemmy_utils::{
34   error::{LemmyError, LemmyErrorExt, LemmyErrorType},
35   spawn_try_task,
36   utils::{
37     slurs::{check_slurs, check_slurs_opt},
38     validation::{check_url_scheme, clean_url_params, is_valid_body_field, is_valid_post_title},
39   },
40   SYNCHRONOUS_FEDERATION,
41 };
42 use tracing::Instrument;
43 use url::Url;
44 use webmention::{Webmention, WebmentionError};
45
46 #[tracing::instrument(skip(context))]
47 pub async fn create_post(
48   data: Json<CreatePost>,
49   context: Data<LemmyContext>,
50 ) -> Result<Json<PostResponse>, LemmyError> {
51   let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
52   let local_site = LocalSite::read(&mut context.pool()).await?;
53
54   let slur_regex = local_site_to_slur_regex(&local_site);
55   check_slurs(&data.name, &slur_regex)?;
56   check_slurs_opt(&data.body, &slur_regex)?;
57   honeypot_check(&data.honeypot)?;
58
59   let data_url = data.url.as_ref();
60   let url = data_url.map(clean_url_params).map(Into::into); // TODO no good way to handle a "clear"
61
62   is_valid_post_title(&data.name)?;
63   is_valid_body_field(&data.body, true)?;
64   check_url_scheme(&data.url)?;
65
66   check_community_ban(
67     local_user_view.person.id,
68     data.community_id,
69     &mut context.pool(),
70   )
71   .await?;
72   check_community_deleted_or_removed(data.community_id, &mut context.pool()).await?;
73
74   let community_id = data.community_id;
75   let community = Community::read(&mut context.pool(), community_id).await?;
76   if community.posting_restricted_to_mods {
77     let community_id = data.community_id;
78     let is_mod = CommunityView::is_mod_or_admin(
79       &mut context.pool(),
80       local_user_view.local_user.person_id,
81       community_id,
82     )
83     .await?;
84     if !is_mod {
85       return Err(LemmyErrorType::OnlyModsCanPostInCommunity)?;
86     }
87   }
88
89   // Fetch post links and pictrs cached image
90   let (metadata_res, thumbnail_url) =
91     fetch_site_data(context.client(), context.settings(), data_url, true).await;
92   let (embed_title, embed_description, embed_video_url) = metadata_res
93     .map(|u| (u.title, u.description, u.embed_video_url))
94     .unwrap_or_default();
95
96   let name = sanitize_html(data.name.trim());
97   let body = sanitize_html_opt(&data.body);
98   let embed_title = sanitize_html_opt(&embed_title);
99   let embed_description = sanitize_html_opt(&embed_description);
100
101   // Only need to check if language is allowed in case user set it explicitly. When using default
102   // language, it already only returns allowed languages.
103   CommunityLanguage::is_allowed_community_language(
104     &mut context.pool(),
105     data.language_id,
106     community_id,
107   )
108   .await?;
109
110   // attempt to set default language if none was provided
111   let language_id = match data.language_id {
112     Some(lid) => Some(lid),
113     None => {
114       default_post_language(
115         &mut context.pool(),
116         community_id,
117         local_user_view.local_user.id,
118       )
119       .await?
120     }
121   };
122
123   let post_form = PostInsertForm::builder()
124     .name(name)
125     .url(url)
126     .body(body)
127     .community_id(data.community_id)
128     .creator_id(local_user_view.person.id)
129     .nsfw(data.nsfw)
130     .embed_title(embed_title)
131     .embed_description(embed_description)
132     .embed_video_url(embed_video_url)
133     .language_id(language_id)
134     .thumbnail_url(thumbnail_url)
135     .build();
136
137   let inserted_post = Post::create(&mut context.pool(), &post_form)
138     .await
139     .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
140
141   let inserted_post_id = inserted_post.id;
142   let protocol_and_hostname = context.settings().get_protocol_and_hostname();
143   let apub_id = generate_local_apub_endpoint(
144     EndpointType::Post,
145     &inserted_post_id.to_string(),
146     &protocol_and_hostname,
147   )?;
148   let updated_post = Post::update(
149     &mut context.pool(),
150     inserted_post_id,
151     &PostUpdateForm::builder().ap_id(Some(apub_id)).build(),
152   )
153   .await
154   .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
155
156   // They like their own post by default
157   let person_id = local_user_view.person.id;
158   let post_id = inserted_post.id;
159   let like_form = PostLikeForm {
160     post_id,
161     person_id,
162     score: 1,
163   };
164
165   PostLike::like(&mut context.pool(), &like_form)
166     .await
167     .with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
168
169   ActivityChannel::submit_activity(SendActivityData::CreatePost(updated_post.clone()), &context)
170     .await?;
171
172   // Mark the post as read
173   mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
174
175   if let Some(url) = updated_post.url.clone() {
176     let task = async move {
177       let mut webmention =
178         Webmention::new::<Url>(updated_post.ap_id.clone().into(), url.clone().into())?;
179       webmention.set_checked(true);
180       match webmention
181         .send()
182         .instrument(tracing::info_span!("Sending webmention"))
183         .await
184       {
185         Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()),
186         Ok(_) => Ok(()),
187         Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention),
188       }
189     };
190     if *SYNCHRONOUS_FEDERATION {
191       task.await?;
192     } else {
193       spawn_try_task(task);
194     }
195   };
196
197   Ok(Json(
198     build_post_response(&context, community_id, person_id, post_id).await?,
199   ))
200 }