render.rs
| 1 | //! The `calendar-data` and `address-data` a REPORT returns: the whole |
| 2 | //! object, a part of it, or its recurrence set expanded (RFC 4791, 9.6; |
| 3 | //! RFC 6352, 10.4). |
| 4 | |
| 5 | use std::collections::HashSet; |
| 6 | |
| 7 | use calcard::common::PartialDateTime; |
| 8 | use calcard::icalendar::{ |
| 9 | ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarParameter, |
| 10 | ICalendarParameterName, ICalendarParameterValue, ICalendarProperty, ICalendarValue, |
| 11 | ICalendarValueType, |
| 12 | }; |
| 13 | use calcard::vcard::{VCard, VCardVersion}; |
| 14 | use chrono::{DateTime, Utc}; |
| 15 | use xmltree::Element; |
| 16 | |
| 17 | use crate::expand::expand; |
| 18 | use crate::filter::{TimeRange, time_range}; |
| 19 | use crate::freebusy::period; |
| 20 | use crate::report::Refused; |
| 21 | use crate::xml::{CALDAV, CARDDAV, Name, elements}; |
| 22 | use crate::zone::{Zone, Zones}; |
| 23 | |
| 24 | /// Instances one object may expand into in one response. |
| 25 | const MAX_EXPANDED: usize = 10_000; |
| 26 | |
| 27 | #[derive(Debug, Clone, Default, PartialEq)] |
| 28 | pub struct CalendarData { |
| 29 | /// `None` returns every component and property. |
| 30 | pub comp: Option<CompSelect>, |
| 31 | pub expand: Option<TimeRange>, |
| 32 | pub limit_recurrence: Option<TimeRange>, |
| 33 | pub limit_freebusy: Option<TimeRange>, |
| 34 | } |
| 35 | |
| 36 | #[derive(Debug, Clone, PartialEq)] |
| 37 | pub struct CompSelect { |
| 38 | /// Upper case. |
| 39 | pub name: String, |
| 40 | /// `None` for `allprop`. |
| 41 | pub props: Option<Vec<PropSelect>>, |
| 42 | /// `None` for `allcomp`. |
| 43 | pub comps: Option<Vec<CompSelect>>, |
| 44 | } |
| 45 | |
| 46 | #[derive(Debug, Clone, PartialEq)] |
| 47 | pub struct PropSelect { |
| 48 | pub name: String, |
| 49 | pub novalue: bool, |
| 50 | } |
| 51 | |
| 52 | #[derive(Debug, Clone, Default, PartialEq)] |
| 53 | pub struct AddressData { |
| 54 | /// `None` for `allprop`. |
| 55 | pub props: Option<Vec<PropSelect>>, |
| 56 | pub version: Option<VCardVersion>, |
| 57 | } |
| 58 | |
| 59 | /// The expansion would exceed [`MAX_EXPANDED`] instances. |
| 60 | #[derive(Debug, PartialEq, Eq)] |
| 61 | pub struct TooManyInstances; |
| 62 | |
| 63 | pub fn calendar_request(e: &Element) -> Result<CalendarData, Refused> { |
| 64 | let content_type = e.attributes.get("content-type").map(String::as_str); |
| 65 | let version = e.attributes.get("version").map(String::as_str); |
| 66 | if content_type.is_some_and(|t| t != "text/calendar") || version.is_some_and(|v| v != "2.0") { |
| 67 | return Err(Refused::Condition(Name::new( |
| 68 | CALDAV, |
| 69 | "supported-calendar-data", |
| 70 | ))); |
| 71 | } |
| 72 | let mut d = CalendarData::default(); |
| 73 | for c in elements(e).filter(|c| Name::of(c).ns == CALDAV) { |
| 74 | match c.name.as_str() { |
| 75 | "comp" => d.comp = Some(comp_select(c)?), |
| 76 | "expand" => d.expand = Some(time_range(c)?), |
| 77 | "limit-recurrence-set" => d.limit_recurrence = Some(time_range(c)?), |
| 78 | "limit-freebusy-set" => d.limit_freebusy = Some(time_range(c)?), |
| 79 | _ => {} |
| 80 | } |
| 81 | } |
| 82 | Ok(d) |
| 83 | } |
| 84 | |
| 85 | fn comp_select(e: &Element) -> Result<CompSelect, Refused> { |
| 86 | let name = e.attributes.get("name").ok_or(Refused::Invalid)?; |
| 87 | let has = |local: &str| elements(e).any(|c| Name::of(c).is(CALDAV, local)); |
| 88 | let props = (!has("allprop")).then(|| prop_selects(e, CALDAV)); |
| 89 | let comps = match has("allcomp") { |
| 90 | true => None, |
| 91 | false => Some( |
| 92 | elements(e) |
| 93 | .filter(|c| Name::of(c).is(CALDAV, "comp")) |
| 94 | .map(comp_select) |
| 95 | .collect::<Result<_, _>>()?, |
| 96 | ), |
| 97 | }; |
| 98 | Ok(CompSelect { |
| 99 | name: name.to_ascii_uppercase(), |
| 100 | props, |
| 101 | comps, |
| 102 | }) |
| 103 | } |
| 104 | |
| 105 | fn prop_selects(e: &Element, ns: &str) -> Vec<PropSelect> { |
| 106 | elements(e) |
| 107 | .filter(|c| Name::of(c).is(ns, "prop")) |
| 108 | .filter_map(|c| { |
| 109 | Some(PropSelect { |
| 110 | name: c.attributes.get("name")?.clone(), |
| 111 | novalue: c.attributes.get("novalue").map(String::as_str) == Some("yes"), |
| 112 | }) |
| 113 | }) |
| 114 | .collect() |
| 115 | } |
| 116 | |
| 117 | pub fn address_request(e: &Element) -> Result<AddressData, Refused> { |
| 118 | let unsupported = || Refused::Condition(Name::new(CARDDAV, "supported-address-data")); |
| 119 | if e.attributes |
| 120 | .get("content-type") |
| 121 | .is_some_and(|t| t != "text/vcard") |
| 122 | { |
| 123 | return Err(unsupported()); |
| 124 | } |
| 125 | // RFC 6352, 10.4: without a version, 3.0. |
| 126 | let version = match e.attributes.get("version").map(String::as_str) { |
| 127 | None | Some("3.0") => Some(VCardVersion::V3_0), |
| 128 | Some("4.0") => Some(VCardVersion::V4_0), |
| 129 | Some(_) => return Err(unsupported()), |
| 130 | }; |
| 131 | let all = elements(e).any(|c| Name::of(c).is(CARDDAV, "allprop")); |
| 132 | let props = prop_selects(e, CARDDAV); |
| 133 | Ok(AddressData { |
| 134 | props: (!all && !props.is_empty()).then_some(props), |
| 135 | version, |
| 136 | }) |
| 137 | } |
| 138 | |
| 139 | /// The calendar-data of one object. The stored text is returned unchanged |
| 140 | /// unless the request narrows or expands it. |
| 141 | pub fn calendar_data( |
| 142 | raw: &str, |
| 143 | req: &CalendarData, |
| 144 | floating: &Zone, |
| 145 | ) -> Result<String, TooManyInstances> { |
| 146 | if *req == CalendarData::default() { |
| 147 | return Ok(raw.to_string()); |
| 148 | } |
| 149 | let Ok(mut cal) = ICalendar::parse(raw) else { |
| 150 | return Ok(raw.to_string()); |
| 151 | }; |
| 152 | if let Some(r) = &req.expand { |
| 153 | cal = expanded(&cal, r, floating)?; |
| 154 | } |
| 155 | if let Some(r) = &req.limit_recurrence { |
| 156 | cal = limit_recurrence(&cal, r, floating); |
| 157 | } |
| 158 | if let Some(r) = &req.limit_freebusy { |
| 159 | limit_freebusy(&mut cal, r, floating); |
| 160 | } |
| 161 | if let Some(sel) = &req.comp { |
| 162 | let mut out = Vec::new(); |
| 163 | copy(&cal, 0, Some(sel), &mut out); |
| 164 | cal = ICalendar { components: out }; |
| 165 | } |
| 166 | Ok(cal.to_string()) |
| 167 | } |
| 168 | |
| 169 | /// One component per instance in `range`, in UTC, with a RECURRENCE-ID and |
| 170 | /// without recurrence rules (RFC 4791, 9.6.5). Dates stay dates. |
| 171 | fn expanded( |
| 172 | cal: &ICalendar, |
| 173 | range: &TimeRange, |
| 174 | floating: &Zone, |
| 175 | ) -> Result<ICalendar, TooManyInstances> { |
| 176 | let exp = expand(cal, range.clone(), floating.clone()); |
| 177 | if exp.truncated || exp.instances.len() > MAX_EXPANDED { |
| 178 | return Err(TooManyInstances); |
| 179 | } |
| 180 | let mut out = vec![root(cal)]; |
| 181 | for x in exp.instances { |
| 182 | let src = &cal.components[x.component]; |
| 183 | let date = src |
| 184 | .property(&ICalendarProperty::Dtstart) |
| 185 | .and_then(|e| e.values.first()?.as_partial_date_time()) |
| 186 | .is_some_and(|p| p.hour.is_none()); |
| 187 | let at = copy(cal, x.component, None, &mut out); |
| 188 | let c = &mut out[at as usize]; |
| 189 | c.entries.retain(|e| { |
| 190 | !matches!( |
| 191 | e.name, |
| 192 | ICalendarProperty::Rrule |
| 193 | | ICalendarProperty::Rdate |
| 194 | | ICalendarProperty::Exdate |
| 195 | | ICalendarProperty::Exrule |
| 196 | | ICalendarProperty::Dtstart |
| 197 | | ICalendarProperty::Dtend |
| 198 | | ICalendarProperty::Due |
| 199 | | ICalendarProperty::RecurrenceId |
| 200 | ) |
| 201 | }); |
| 202 | let when = |name: ICalendarProperty, t: DateTime<Utc>| { |
| 203 | let (value, params) = if date { |
| 204 | let day = floating.to_local(t).and_utc().timestamp(); |
| 205 | let param = ICalendarParameter::new( |
| 206 | ICalendarParameterName::Value, |
| 207 | ICalendarParameterValue::Value(ICalendarValueType::Date), |
| 208 | ); |
| 209 | (PartialDateTime::from_date_timestamp(day), vec![param]) |
| 210 | } else { |
| 211 | (PartialDateTime::from_utc_timestamp(t.timestamp()), vec![]) |
| 212 | }; |
| 213 | ICalendarEntry { |
| 214 | name, |
| 215 | params, |
| 216 | values: vec![ICalendarValue::PartialDateTime(Box::new(value))], |
| 217 | } |
| 218 | }; |
| 219 | c.entries.push(when(ICalendarProperty::Dtstart, x.start)); |
| 220 | if src.has_property(&ICalendarProperty::Dtend) { |
| 221 | c.entries.push(when(ICalendarProperty::Dtend, x.end)); |
| 222 | } |
| 223 | if src.has_property(&ICalendarProperty::Due) { |
| 224 | c.entries.push(when(ICalendarProperty::Due, x.end)); |
| 225 | } |
| 226 | if let Some(rid) = x.recurrence_id { |
| 227 | c.entries.push(when(ICalendarProperty::RecurrenceId, rid)); |
| 228 | } |
| 229 | out[0].component_ids.push(at); |
| 230 | } |
| 231 | Ok(ICalendar { components: out }) |
| 232 | } |
| 233 | |
| 234 | /// The masters, and only the overrides that affect `range` (RFC 4791, |
| 235 | /// 9.6.6). |
| 236 | fn limit_recurrence(cal: &ICalendar, range: &TimeRange, floating: &Zone) -> ICalendar { |
| 237 | let exp = expand(cal, range.clone(), floating.clone()); |
| 238 | let used: HashSet<usize> = exp.instances.iter().map(|x| x.component).collect(); |
| 239 | let mut out = vec![root(cal)]; |
| 240 | for &i in cal.components.first().map_or(&[][..], |r| &r.component_ids) { |
| 241 | let c = &cal.components[i as usize]; |
| 242 | let keep = match c.property(&ICalendarProperty::RecurrenceId) { |
| 243 | None => true, |
| 244 | Some(rid) => { |
| 245 | exp.truncated |
| 246 | || used.contains(&(i as usize)) |
| 247 | || rid.parameter(&ICalendarParameterName::Range).is_some() |
| 248 | } |
| 249 | }; |
| 250 | if keep { |
| 251 | let at = copy(cal, i as usize, None, &mut out); |
| 252 | out[0].component_ids.push(at); |
| 253 | } |
| 254 | } |
| 255 | ICalendar { components: out } |
| 256 | } |
| 257 | |
| 258 | /// Drops the FREEBUSY periods outside `range` (RFC 4791, 9.6.7). |
| 259 | fn limit_freebusy(cal: &mut ICalendar, range: &TimeRange, floating: &Zone) { |
| 260 | let zones = Zones::new(cal, floating.clone()); |
| 261 | for c in cal |
| 262 | .components |
| 263 | .iter_mut() |
| 264 | .filter(|c| c.component_type == ICalendarComponentType::VFreebusy) |
| 265 | { |
| 266 | for e in c |
| 267 | .entries |
| 268 | .iter_mut() |
| 269 | .filter(|e| e.name == ICalendarProperty::Freebusy) |
| 270 | { |
| 271 | let tzid = e.tz_id().map(str::to_string); |
| 272 | e.values.retain(|v| { |
| 273 | period(&zones, tzid.as_deref(), v) |
| 274 | .is_some_and(|(s, e)| s < range.end && e > range.start) |
| 275 | }); |
| 276 | } |
| 277 | c.entries |
| 278 | .retain(|e| e.name != ICalendarProperty::Freebusy || !e.values.is_empty()); |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | /// The VCALENDAR with its properties and no components. |
| 283 | fn root(cal: &ICalendar) -> ICalendarComponent { |
| 284 | ICalendarComponent { |
| 285 | component_type: ICalendarComponentType::VCalendar, |
| 286 | entries: cal |
| 287 | .components |
| 288 | .first() |
| 289 | .map(|c| c.entries.clone()) |
| 290 | .unwrap_or_default(), |
| 291 | component_ids: Vec::new(), |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | /// Appends component `i` of `src` and its sub-components, narrowed by `sel`, |
| 296 | /// to `out`. Returns the new index. |
| 297 | fn copy( |
| 298 | src: &ICalendar, |
| 299 | i: usize, |
| 300 | sel: Option<&CompSelect>, |
| 301 | out: &mut Vec<ICalendarComponent>, |
| 302 | ) -> u32 { |
| 303 | let c = &src.components[i]; |
| 304 | let entries = match sel.and_then(|s| s.props.as_ref()) { |
| 305 | None => c.entries.clone(), |
| 306 | Some(props) => c |
| 307 | .entries |
| 308 | .iter() |
| 309 | .filter_map(|e| { |
| 310 | let p = props |
| 311 | .iter() |
| 312 | .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?; |
| 313 | let mut e = e.clone(); |
| 314 | if p.novalue { |
| 315 | e.values.clear(); |
| 316 | } |
| 317 | Some(e) |
| 318 | }) |
| 319 | .collect(), |
| 320 | }; |
| 321 | let at = out.len(); |
| 322 | out.push(ICalendarComponent { |
| 323 | component_type: c.component_type.clone(), |
| 324 | entries, |
| 325 | component_ids: Vec::new(), |
| 326 | }); |
| 327 | for &child in &c.component_ids { |
| 328 | let child = child as usize; |
| 329 | let child_sel = match sel.map(|s| &s.comps) { |
| 330 | None | Some(None) => None, |
| 331 | Some(Some(list)) => { |
| 332 | let name = src.components[child].component_type.as_str(); |
| 333 | match list.iter().find(|s| s.name.eq_ignore_ascii_case(name)) { |
| 334 | Some(s) => Some(s), |
| 335 | None => continue, |
| 336 | } |
| 337 | } |
| 338 | }; |
| 339 | let id = copy(src, child, child_sel, out); |
| 340 | out[at].component_ids.push(id); |
| 341 | } |
| 342 | at as u32 |
| 343 | } |
| 344 | |
| 345 | /// The address-data of one card. The stored text is returned unchanged |
| 346 | /// unless the request narrows it or asks for another version. |
| 347 | pub fn address_data(raw: &str, req: &AddressData) -> String { |
| 348 | if *req == AddressData::default() { |
| 349 | return raw.to_string(); |
| 350 | } |
| 351 | let Ok(card) = VCard::parse(raw) else { |
| 352 | return raw.to_string(); |
| 353 | }; |
| 354 | let version = req.version.or(card.version()).unwrap_or_default(); |
| 355 | if req.props.is_none() && Some(version) == card.version() { |
| 356 | return raw.to_string(); |
| 357 | } |
| 358 | // calcard's 4.0 writer drops a `pref` type, so map before parsing. |
| 359 | let card = match version { |
| 360 | VCardVersion::V4_0 => VCard::parse(apple_forms(raw, version)).unwrap_or(card), |
| 361 | _ => card, |
| 362 | }; |
| 363 | let entries = match &req.props { |
| 364 | None => card.entries, |
| 365 | Some(props) => card |
| 366 | .entries |
| 367 | .into_iter() |
| 368 | .filter_map(|mut e| { |
| 369 | let p = props |
| 370 | .iter() |
| 371 | .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?; |
| 372 | if p.novalue { |
| 373 | e.values.clear(); |
| 374 | } |
| 375 | Some(e) |
| 376 | }) |
| 377 | .collect(), |
| 378 | }; |
| 379 | let mut out = String::new(); |
| 380 | let _ = VCard { entries }.write_to(&mut out, version); |
| 381 | match version { |
| 382 | VCardVersion::V4_0 => out, |
| 383 | _ => apple_forms(&out, version), |
| 384 | } |
| 385 | } |
| 386 | |
| 387 | /// The version a GET asks for in its `Accept` header: 4.0 when named, |
| 388 | /// else 3.0, which RFC 6352 makes the default and Apple Contacts needs. |
| 389 | pub fn accepted_version(accept: Option<&str>) -> VCardVersion { |
| 390 | match accept.is_some_and(|a| a.contains("version=4.0")) { |
| 391 | true => VCardVersion::V4_0, |
| 392 | false => VCardVersion::V3_0, |
| 393 | } |
| 394 | } |
| 395 | |
| 396 | /// vCard 4.0 groups and companies in the vCard 3.0 extensions Apple |
| 397 | /// Contacts reads, and back: `KIND:group` and `MEMBER` as |
| 398 | /// `X-ADDRESSBOOKSERVER-KIND` and `X-ADDRESSBOOKSERVER-MEMBER`, `KIND:org` |
| 399 | /// as `X-ABSHOWAS:COMPANY`, and `PREF=1` as `TYPE=pref`. sabre/vobject |
| 400 | /// converts the same way. |
| 401 | fn apple_forms(text: &str, version: VCardVersion) -> String { |
| 402 | let v3 = version == VCardVersion::V3_0; |
| 403 | let eol = if text.contains("\r\n") { "\r\n" } else { "\n" }; |
| 404 | let mut out = String::with_capacity(text.len()); |
| 405 | for raw in crate::text::logical_lines(text) { |
| 406 | let line = crate::text::unfold(raw); |
| 407 | let start = crate::text::value_start(&line); |
| 408 | let head = &line[..start.saturating_sub(1)]; |
| 409 | let head_name = head.split(';').next().unwrap_or_default(); |
| 410 | let (group, name) = match head_name.rsplit_once('.') { |
| 411 | Some((g, n)) => (&head_name[..=g.len()], n.to_ascii_uppercase()), |
| 412 | None => ("", head_name.to_ascii_uppercase()), |
| 413 | }; |
| 414 | let value = &line[start..]; |
| 415 | let lower = value.trim().to_ascii_lowercase(); |
| 416 | let renamed = match (v3, name.as_str(), lower.as_str()) { |
| 417 | (true, "KIND", "group") => Some(("X-ADDRESSBOOKSERVER-KIND", "group")), |
| 418 | (true, "KIND", "org") => Some(("X-ABSHOWAS", "COMPANY")), |
| 419 | (true, "KIND", "individual") => continue, |
| 420 | (true, "MEMBER", _) => Some(("X-ADDRESSBOOKSERVER-MEMBER", value)), |
| 421 | (false, "X-ADDRESSBOOKSERVER-KIND", "group") => Some(("KIND", "group")), |
| 422 | (false, "X-ABSHOWAS", "company") => Some(("KIND", "org")), |
| 423 | (false, "X-ADDRESSBOOKSERVER-MEMBER", _) => Some(("MEMBER", value)), |
| 424 | _ => None, |
| 425 | }; |
| 426 | let params = pref_params(crate::text::param_parts(&line), v3); |
| 427 | let (name, value) = renamed.unwrap_or((&name, value)); |
| 428 | let mut new = format!("{group}{name}"); |
| 429 | for p in ¶ms { |
| 430 | new.push(';'); |
| 431 | new.push_str(p); |
| 432 | } |
| 433 | new.push(':'); |
| 434 | new.push_str(value); |
| 435 | if new == line { |
| 436 | out.push_str(raw); |
| 437 | } else { |
| 438 | out.push_str(&crate::text::fold(&new, eol)); |
| 439 | } |
| 440 | } |
| 441 | out |
| 442 | } |
| 443 | |
| 444 | /// `PREF=1` as a `pref` type for vCard 3.0, and back for 4.0. |
| 445 | fn pref_params(params: Vec<&str>, v3: bool) -> Vec<String> { |
| 446 | let mut out = Vec::with_capacity(params.len()); |
| 447 | let mut pref = false; |
| 448 | for p in params { |
| 449 | let (key, value) = p.split_once('=').unwrap_or((p, "")); |
| 450 | match (v3, key.to_ascii_uppercase().as_str()) { |
| 451 | (true, "PREF") => pref |= value.trim() == "1", |
| 452 | (false, "TYPE") => { |
| 453 | let (prefs, rest): (Vec<&str>, Vec<&str>) = value |
| 454 | .trim_matches('"') |
| 455 | .split(',') |
| 456 | .partition(|t| t.eq_ignore_ascii_case("pref")); |
| 457 | pref |= !prefs.is_empty(); |
| 458 | if !rest.is_empty() { |
| 459 | out.push(format!("{key}={}", rest.join(","))); |
| 460 | } |
| 461 | } |
| 462 | _ => out.push(p.to_string()), |
| 463 | } |
| 464 | } |
| 465 | if pref { |
| 466 | out.push(if v3 { "TYPE=pref" } else { "PREF=1" }.to_string()); |
| 467 | } |
| 468 | out |
| 469 | } |
| 470 |