]> Untitled Git - lemmy.git/blob - crates/apub/src/objects/instance.rs
Sanitize html (#3708)
[lemmy.git] / crates / apub / src / objects / instance.rs
1 use crate::{
2   check_apub_id_valid_with_strictness,
3   local_site_data_cached,
4   objects::read_from_string_or_source_opt,
5   protocol::{
6     objects::{instance::Instance, LanguageTag},
7     ImageObject,
8     Source,
9   },
10 };
11 use activitypub_federation::{
12   config::Data,
13   fetch::object_id::ObjectId,
14   kinds::actor::ApplicationType,
15   protocol::{values::MediaTypeHtml, verification::verify_domains_match},
16   traits::{Actor, Object},
17 };
18 use chrono::NaiveDateTime;
19 use lemmy_api_common::{
20   context::LemmyContext,
21   utils::{local_site_opt_to_slur_regex, sanitize_html_opt},
22 };
23 use lemmy_db_schema::{
24   newtypes::InstanceId,
25   source::{
26     actor_language::SiteLanguage,
27     instance::Instance as DbInstance,
28     site::{Site, SiteInsertForm},
29   },
30   traits::Crud,
31   utils::{naive_now, DbPool},
32 };
33 use lemmy_utils::{
34   error::LemmyError,
35   utils::{
36     markdown::markdown_to_html,
37     slurs::{check_slurs, check_slurs_opt},
38     time::convert_datetime,
39   },
40 };
41 use std::ops::Deref;
42 use tracing::debug;
43 use url::Url;
44
45 #[derive(Clone, Debug)]
46 pub struct ApubSite(Site);
47
48 impl Deref for ApubSite {
49   type Target = Site;
50   fn deref(&self) -> &Self::Target {
51     &self.0
52   }
53 }
54
55 impl From<Site> for ApubSite {
56   fn from(s: Site) -> Self {
57     ApubSite(s)
58   }
59 }
60
61 #[async_trait::async_trait]
62 impl Object for ApubSite {
63   type DataType = LemmyContext;
64   type Kind = Instance;
65   type Error = LemmyError;
66
67   fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
68     Some(self.last_refreshed_at)
69   }
70
71   #[tracing::instrument(skip_all)]
72   async fn read_from_id(
73     object_id: Url,
74     data: &Data<Self::DataType>,
75   ) -> Result<Option<Self>, LemmyError> {
76     Ok(
77       Site::read_from_apub_id(&mut data.pool(), &object_id.into())
78         .await?
79         .map(Into::into),
80     )
81   }
82
83   async fn delete(self, _data: &Data<Self::DataType>) -> Result<(), LemmyError> {
84     unimplemented!()
85   }
86
87   #[tracing::instrument(skip_all)]
88   async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, LemmyError> {
89     let site_id = self.id;
90     let langs = SiteLanguage::read(&mut data.pool(), site_id).await?;
91     let language = LanguageTag::new_multiple(langs, &mut data.pool()).await?;
92
93     let instance = Instance {
94       kind: ApplicationType::Application,
95       id: self.id().into(),
96       name: self.name.clone(),
97       content: self.sidebar.as_ref().map(|d| markdown_to_html(d)),
98       source: self.sidebar.clone().map(Source::new),
99       summary: self.description.clone(),
100       media_type: self.sidebar.as_ref().map(|_| MediaTypeHtml::Html),
101       icon: self.icon.clone().map(ImageObject::new),
102       image: self.banner.clone().map(ImageObject::new),
103       inbox: self.inbox_url.clone().into(),
104       outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?,
105       public_key: self.public_key(),
106       language,
107       published: convert_datetime(self.published),
108       updated: self.updated.map(convert_datetime),
109     };
110     Ok(instance)
111   }
112
113   #[tracing::instrument(skip_all)]
114   async fn verify(
115     apub: &Self::Kind,
116     expected_domain: &Url,
117     data: &Data<Self::DataType>,
118   ) -> Result<(), LemmyError> {
119     check_apub_id_valid_with_strictness(apub.id.inner(), true, data).await?;
120     verify_domains_match(expected_domain, apub.id.inner())?;
121
122     let local_site_data = local_site_data_cached(&mut data.pool()).await?;
123     let slur_regex = &local_site_opt_to_slur_regex(&local_site_data.local_site);
124     check_slurs(&apub.name, slur_regex)?;
125     check_slurs_opt(&apub.summary, slur_regex)?;
126
127     Ok(())
128   }
129
130   #[tracing::instrument(skip_all)]
131   async fn from_json(apub: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, LemmyError> {
132     let domain = apub.id.inner().domain().expect("group id has domain");
133     let instance = DbInstance::read_or_create(&mut data.pool(), domain.to_string()).await?;
134
135     let sidebar = read_from_string_or_source_opt(&apub.content, &None, &apub.source);
136     let sidebar = sanitize_html_opt(&sidebar);
137     let description = sanitize_html_opt(&apub.summary);
138
139     let site_form = SiteInsertForm {
140       name: apub.name.clone(),
141       sidebar,
142       updated: apub.updated.map(|u| u.clone().naive_local()),
143       icon: apub.icon.clone().map(|i| i.url.into()),
144       banner: apub.image.clone().map(|i| i.url.into()),
145       description,
146       actor_id: Some(apub.id.clone().into()),
147       last_refreshed_at: Some(naive_now()),
148       inbox_url: Some(apub.inbox.clone().into()),
149       public_key: Some(apub.public_key.public_key_pem.clone()),
150       private_key: None,
151       instance_id: instance.id,
152     };
153     let languages = LanguageTag::to_language_id_multiple(apub.language, &mut data.pool()).await?;
154
155     let site = Site::create(&mut data.pool(), &site_form).await?;
156     SiteLanguage::update(&mut data.pool(), languages, &site).await?;
157     Ok(site.into())
158   }
159 }
160
161 impl Actor for ApubSite {
162   fn id(&self) -> Url {
163     self.actor_id.inner().clone()
164   }
165
166   fn public_key_pem(&self) -> &str {
167     &self.public_key
168   }
169
170   fn private_key_pem(&self) -> Option<String> {
171     self.private_key.clone()
172   }
173
174   fn inbox(&self) -> Url {
175     self.inbox_url.clone().into()
176   }
177 }
178
179 /// Try to fetch the instance actor (to make things like instance rules available).
180 pub(in crate::objects) async fn fetch_instance_actor_for_object<T: Into<Url> + Clone>(
181   object_id: &T,
182   context: &Data<LemmyContext>,
183 ) -> Result<InstanceId, LemmyError> {
184   let object_id: Url = object_id.clone().into();
185   let instance_id = Site::instance_actor_id_from_url(object_id);
186   let site = ObjectId::<ApubSite>::from(instance_id.clone())
187     .dereference(context)
188     .await;
189   match site {
190     Ok(s) => Ok(s.instance_id),
191     Err(e) => {
192       // Failed to fetch instance actor, its probably not a lemmy instance
193       debug!("Failed to dereference site for {}: {}", &instance_id, e);
194       let domain = instance_id.domain().expect("has domain");
195       Ok(
196         DbInstance::read_or_create(&mut context.pool(), domain.to_string())
197           .await?
198           .id,
199       )
200     }
201   }
202 }
203
204 pub(crate) async fn remote_instance_inboxes(pool: &mut DbPool<'_>) -> Result<Vec<Url>, LemmyError> {
205   Ok(
206     Site::read_remote_sites(pool)
207       .await?
208       .into_iter()
209       .map(|s| ApubSite::from(s).shared_inbox_or_inbox())
210       .collect(),
211   )
212 }
213
214 #[cfg(test)]
215 pub(crate) mod tests {
216   #![allow(clippy::unwrap_used)]
217   #![allow(clippy::indexing_slicing)]
218
219   use super::*;
220   use crate::{objects::tests::init_context, protocol::tests::file_to_json_object};
221   use lemmy_db_schema::traits::Crud;
222   use serial_test::serial;
223
224   pub(crate) async fn parse_lemmy_instance(context: &Data<LemmyContext>) -> ApubSite {
225     let json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap();
226     let id = Url::parse("https://enterprise.lemmy.ml/").unwrap();
227     ApubSite::verify(&json, &id, context).await.unwrap();
228     let site = ApubSite::from_json(json, context).await.unwrap();
229     assert_eq!(context.request_count(), 0);
230     site
231   }
232
233   #[tokio::test]
234   #[serial]
235   async fn test_parse_lemmy_instance() {
236     let context = init_context().await;
237     let site = parse_lemmy_instance(&context).await;
238
239     assert_eq!(site.name, "Enterprise");
240     assert_eq!(site.description.as_ref().unwrap().len(), 15);
241
242     Site::delete(&mut context.pool(), site.id).await.unwrap();
243   }
244 }