1use std::error::Error;
4use std::convert::TryFrom;
5use std::collections::HashMap;
6use std::sync::{Arc, Mutex};
7
8use async_trait::async_trait;
9use reqwest::{Method, StatusCode};
10use reqwest::header::CONTENT_TYPE;
11use minidom::Element;
12use url::Url;
13use csscolorparser::Color;
14
15use crate::resource::Resource;
16use crate::utils::{find_elem, find_elems};
17use crate::calendar::remote_calendar::RemoteCalendar;
18use crate::calendar::SupportedComponents;
19use crate::traits::CalDavSource;
20use crate::traits::BaseCalendar;
21use crate::traits::DavCalendar;
22
23
24static DAVCLIENT_BODY: &str = r#"
25 <d:propfind xmlns:d="DAV:">
26 <d:prop>
27 <d:current-user-principal />
28 </d:prop>
29 </d:propfind>
30"#;
31
32static HOMESET_BODY: &str = r#"
33 <d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
34 <d:self/>
35 <d:prop>
36 <c:calendar-home-set />
37 </d:prop>
38 </d:propfind>
39"#;
40
41static CAL_BODY: &str = r#"
42 <d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
43 <d:prop>
44 <d:displayname />
45 <E:calendar-color xmlns:E="http://apple.com/ns/ical/"/>
46 <d:resourcetype />
47 <c:supported-calendar-component-set />
48 </d:prop>
49 </d:propfind>
50"#;
51
52
53
54pub(crate) async fn sub_request(resource: &Resource, method: &str, body: String, depth: u32) -> Result<String, Box<dyn Error>> {
55 let method = method.parse()
56 .expect("invalid method name");
57
58 let res = reqwest::Client::new()
59 .request(method, resource.url().clone())
60 .header("Depth", depth)
61 .header(CONTENT_TYPE, "application/xml")
62 .basic_auth(resource.username(), Some(resource.password()))
63 .body(body)
64 .send()
65 .await?;
66
67 if res.status().is_success() == false {
68 return Err(format!("Unexpected HTTP status code {:?}", res.status()).into());
69 }
70
71 let text = res.text().await?;
72 Ok(text)
73}
74
75pub(crate) async fn sub_request_and_extract_elem(resource: &Resource, body: String, items: &[&str]) -> Result<String, Box<dyn Error>> {
76 let text = sub_request(resource, "PROPFIND", body, 0).await?;
77
78 let mut current_element: &Element = &text.parse()?;
79 for item in items {
80 current_element = match find_elem(¤t_element, item) {
81 Some(elem) => elem,
82 None => return Err(format!("missing element {}", item).into()),
83 }
84 }
85 Ok(current_element.text())
86}
87
88pub(crate) async fn sub_request_and_extract_elems(resource: &Resource, method: &str, body: String, item: &str) -> Result<Vec<Element>, Box<dyn Error>> {
89 let text = sub_request(resource, method, body, 1).await?;
90
91 let element: &Element = &text.parse()?;
92 Ok(find_elems(&element, item)
93 .iter()
94 .map(|elem| (*elem).clone())
95 .collect()
96 )
97}
98
99
100#[derive(Debug)]
102pub struct Client {
103 resource: Resource,
104
105 cached_replies: Mutex<CachedReplies>,
108}
109
110
111#[derive(Debug, Default)]
112struct CachedReplies {
113 principal: Option<Resource>,
114 calendar_home_set: Option<Resource>,
115 calendars: Option<HashMap<Url, Arc<Mutex<RemoteCalendar>>>>,
116}
117
118impl Client {
119 pub fn new<S: AsRef<str>, T: ToString, U: ToString>(url: S, username: T, password: U) -> Result<Self, Box<dyn Error>> {
121 let url = Url::parse(url.as_ref())?;
122
123 Ok(Self{
124 resource: Resource::new(url, username.to_string(), password.to_string()),
125 cached_replies: Mutex::new(CachedReplies::default()),
126 })
127 }
128
129 async fn get_principal(&self) -> Result<Resource, Box<dyn Error>> {
131 if let Some(p) = &self.cached_replies.lock().unwrap().principal {
132 return Ok(p.clone());
133 }
134
135 let href = sub_request_and_extract_elem(&self.resource, DAVCLIENT_BODY.into(), &["current-user-principal", "href"]).await?;
136 let principal_url = self.resource.combine(&href);
137 self.cached_replies.lock().unwrap().principal = Some(principal_url.clone());
138 log::debug!("Principal URL is {}", href);
139
140 return Ok(principal_url);
141 }
142
143 async fn get_cal_home_set(&self) -> Result<Resource, Box<dyn Error>> {
145 if let Some(h) = &self.cached_replies.lock().unwrap().calendar_home_set {
146 return Ok(h.clone());
147 }
148 let principal_url = self.get_principal().await?;
149
150 let href = sub_request_and_extract_elem(&principal_url, HOMESET_BODY.into(), &["calendar-home-set", "href"]).await?;
151 let chs_url = self.resource.combine(&href);
152 self.cached_replies.lock().unwrap().calendar_home_set = Some(chs_url.clone());
153 log::debug!("Calendar home set URL is {:?}", href);
154
155 Ok(chs_url)
156 }
157
158 async fn populate_calendars(&self) -> Result<(), Box<dyn Error>> {
159 let cal_home_set = self.get_cal_home_set().await?;
160
161 let reps = sub_request_and_extract_elems(&cal_home_set, "PROPFIND", CAL_BODY.to_string(), "response").await?;
162 let mut calendars = HashMap::new();
163 for rep in reps {
164 let display_name = find_elem(&rep, "displayname").map(|e| e.text()).unwrap_or("<no name>".to_string());
165 log::debug!("Considering calendar {}", display_name);
166
167 let resource_types = match find_elem(&rep, "resourcetype") {
169 None => continue,
170 Some(rt) => rt,
171 };
172 let mut found_calendar_type = false;
173 for resource_type in resource_types.children() {
174 if resource_type.name() == "calendar" {
175 found_calendar_type = true;
176 break;
177 }
178 }
179 if found_calendar_type == false {
180 continue;
181 }
182
183 let el_supported_comps = match find_elem(&rep, "supported-calendar-component-set") {
185 None => continue,
186 Some(comps) => comps,
187 };
188 if el_supported_comps.children().count() == 0 {
189 continue;
190 }
191
192 let calendar_href = match find_elem(&rep, "href") {
193 None => {
194 log::warn!("Calendar {} has no URL! Ignoring it.", display_name);
195 continue;
196 },
197 Some(h) => h.text(),
198 };
199
200 let this_calendar_url = self.resource.combine(&calendar_href);
201
202 let supported_components = match crate::calendar::SupportedComponents::try_from(el_supported_comps.clone()) {
203 Err(err) => {
204 log::warn!("Calendar {} has invalid supported components ({})! Ignoring it.", display_name, err);
205 continue;
206 },
207 Ok(sc) => sc,
208 };
209
210 let this_calendar_color = find_elem(&rep, "calendar-color")
211 .and_then(|col| {
212 col.texts().next()
213 .and_then(|t| csscolorparser::parse(t).ok())
214 });
215
216 let this_calendar = RemoteCalendar::new(display_name, this_calendar_url, supported_components, this_calendar_color);
217 log::info!("Found calendar {}", this_calendar.name());
218 calendars.insert(this_calendar.url().clone(), Arc::new(Mutex::new(this_calendar)));
219 }
220
221 let mut replies = self.cached_replies.lock().unwrap();
222 replies.calendars = Some(calendars);
223 Ok(())
224 }
225
226}
227
228#[async_trait]
229impl CalDavSource<RemoteCalendar> for Client {
230 async fn get_calendars(&self) -> Result<HashMap<Url, Arc<Mutex<RemoteCalendar>>>, Box<dyn Error>> {
231 self.populate_calendars().await?;
232
233 match &self.cached_replies.lock().unwrap().calendars {
234 Some(cals) => {
235 return Ok(cals.clone())
236 },
237 None => return Err("No calendars available".into())
238 };
239 }
240
241 async fn get_calendar(&self, url: &Url) -> Option<Arc<Mutex<RemoteCalendar>>> {
242 if let Err(err) = self.populate_calendars().await {
243 log::warn!("Unable to fetch calendars: {}", err);
244 return None;
245 }
246
247 self.cached_replies.lock().unwrap()
248 .calendars
249 .as_ref()
250 .and_then(|cals| cals.get(url))
251 .map(|cal| cal.clone())
252 }
253
254 async fn create_calendar(&mut self, url: Url, name: String, supported_components: SupportedComponents, color: Option<Color>) -> Result<Arc<Mutex<RemoteCalendar>>, Box<dyn Error>> {
255 self.populate_calendars().await?;
256
257 match self.cached_replies.lock().unwrap().calendars.as_ref() {
258 None => return Err("No calendars have been fetched".into()),
259 Some(cals) => {
260 if cals.contains_key(&url) {
261 return Err("This calendar already exists".into());
262 }
263 },
264 }
265
266 let creation_body = calendar_body(name, supported_components, color);
267
268 let response = reqwest::Client::new()
269 .request(Method::from_bytes(b"MKCALENDAR").unwrap(), url.clone())
270 .header(CONTENT_TYPE, "application/xml")
271 .basic_auth(self.resource.username(), Some(self.resource.password()))
272 .body(creation_body)
273 .send()
274 .await?;
275
276 let status = response.status();
277 if status != StatusCode::CREATED {
278 return Err(format!("Unexpected HTTP status code. Expected CREATED, got {}", status.as_u16()).into());
279 }
280
281 self.get_calendar(&url).await.ok_or(format!("Unable to insert calendar {:?}", url).into())
282 }
283}
284
285fn calendar_body(name: String, supported_components: SupportedComponents, color: Option<Color>) -> String {
286 let color_property = match color {
287 None => "".to_string(),
288 Some(color) => format!("<D:calendar-color xmlns:D=\"http://apple.com/ns/ical/\">{}FF</D:calendar-color>", color.to_hex_string().to_ascii_uppercase()),
289 };
290
291 format!(r#"<?xml version="1.0" encoding="utf-8" ?>
293 <B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
294 <A:set xmlns:A="DAV:">
295 <A:prop>
296 <A:displayname>{}</A:displayname>
297 {}
298 {}
299 </A:prop>
300 </A:set>
301 </B:mkcalendar>
302 "#,
303 name,
304 color_property,
305 supported_components.to_xml_string(),
306 )
307}