From 1d671ad266480db7b3652eb803a2e1218f1cec13 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:31:32 +0100 Subject: [PATCH] caldav: add support for calendar subscriptions --- ...fbce750a4e167ee547abccbefd36a089f399.json} | 12 ++++++++--- ...4e57c1fc7abcc5c7ca6bbb4944554381285c.json} | 12 ++++++++--- ...b978a54d36e4bb12ea77a6d52b9dfa08312c.json} | 12 ++++++++--- .../caldav/src/calendar/methods/mkcalendar.rs | 1 + crates/caldav/src/calendar/resource.rs | 20 +++++++++++++++++-- crates/caldav/src/calendar_object/resource.rs | 2 +- crates/caldav/src/principal/mod.rs | 2 +- crates/carddav/src/address_object/resource.rs | 2 +- crates/carddav/src/addressbook/resource.rs | 2 +- crates/carddav/src/principal/mod.rs | 2 +- crates/dav/src/resource/mod.rs | 4 ++-- crates/dav/src/resources/root.rs | 2 +- crates/dav/src/xml/multistatus.rs | 3 +++ crates/store/src/calendar/calendar.rs | 1 + crates/store_sqlite/migrations/1_init.sql | 1 + crates/store_sqlite/src/calendar_store.rs | 9 +++++---- 16 files changed, 64 insertions(+), 23 deletions(-) rename .sqlx/{query-b66f33bc98029e9bc6427e61d15484a776e8eccb2b72a2fb2d4a5edea90067e5.json => query-38e44b7a271d9be098f00b233100fbce750a4e167ee547abccbefd36a089f399.json} (76%) rename .sqlx/{query-52fcb1f2472a6e14da26a9733b3b9e8a79354f287b3c347bc343f9747f01f134.json => query-7d27bdb54fbc8e65e5482fa9bc974e57c1fc7abcc5c7ca6bbb4944554381285c.json} (76%) rename .sqlx/{query-f387e6ef026d8314e78c0672652fb3d6876598ecde4db98d7a303c9e4c676376.json => query-97d66b4b76bca677badd9087a6bab978a54d36e4bb12ea77a6d52b9dfa08312c.json} (76%) diff --git a/.sqlx/query-b66f33bc98029e9bc6427e61d15484a776e8eccb2b72a2fb2d4a5edea90067e5.json b/.sqlx/query-38e44b7a271d9be098f00b233100fbce750a4e167ee547abccbefd36a089f399.json similarity index 76% rename from .sqlx/query-b66f33bc98029e9bc6427e61d15484a776e8eccb2b72a2fb2d4a5edea90067e5.json rename to .sqlx/query-38e44b7a271d9be098f00b233100fbce750a4e167ee547abccbefd36a089f399.json index 4e2e95d..cafbae4 100644 --- a/.sqlx/query-b66f33bc98029e9bc6427e61d15484a776e8eccb2b72a2fb2d4a5edea90067e5.json +++ b/.sqlx/query-38e44b7a271d9be098f00b233100fbce750a4e167ee547abccbefd36a089f399.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT principal, id, synctoken, \"order\", displayname, description, color, timezone, deleted_at\n FROM calendars\n WHERE (principal, id) = (?, ?)", + "query": "SELECT principal, id, synctoken, \"order\", displayname, description, color, timezone, deleted_at, subscription_url\n FROM calendars\n WHERE (principal, id) = (?, ?)", "describe": { "columns": [ { @@ -47,6 +47,11 @@ "name": "deleted_at", "ordinal": 8, "type_info": "Datetime" + }, + { + "name": "subscription_url", + "ordinal": 9, + "type_info": "Text" } ], "parameters": { @@ -60,9 +65,10 @@ true, true, true, - false, + true, + true, true ] }, - "hash": "b66f33bc98029e9bc6427e61d15484a776e8eccb2b72a2fb2d4a5edea90067e5" + "hash": "38e44b7a271d9be098f00b233100fbce750a4e167ee547abccbefd36a089f399" } diff --git a/.sqlx/query-52fcb1f2472a6e14da26a9733b3b9e8a79354f287b3c347bc343f9747f01f134.json b/.sqlx/query-7d27bdb54fbc8e65e5482fa9bc974e57c1fc7abcc5c7ca6bbb4944554381285c.json similarity index 76% rename from .sqlx/query-52fcb1f2472a6e14da26a9733b3b9e8a79354f287b3c347bc343f9747f01f134.json rename to .sqlx/query-7d27bdb54fbc8e65e5482fa9bc974e57c1fc7abcc5c7ca6bbb4944554381285c.json index b3633c5..10b7c10 100644 --- a/.sqlx/query-52fcb1f2472a6e14da26a9733b3b9e8a79354f287b3c347bc343f9747f01f134.json +++ b/.sqlx/query-7d27bdb54fbc8e65e5482fa9bc974e57c1fc7abcc5c7ca6bbb4944554381285c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT principal, id, synctoken, displayname, \"order\", description, color, timezone, deleted_at\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL", + "query": "SELECT principal, id, synctoken, displayname, \"order\", description, color, timezone, deleted_at, subscription_url\n FROM calendars\n WHERE principal = ? AND deleted_at IS NULL", "describe": { "columns": [ { @@ -47,6 +47,11 @@ "name": "deleted_at", "ordinal": 8, "type_info": "Datetime" + }, + { + "name": "subscription_url", + "ordinal": 9, + "type_info": "Text" } ], "parameters": { @@ -60,9 +65,10 @@ false, true, true, - false, + true, + true, true ] }, - "hash": "52fcb1f2472a6e14da26a9733b3b9e8a79354f287b3c347bc343f9747f01f134" + "hash": "7d27bdb54fbc8e65e5482fa9bc974e57c1fc7abcc5c7ca6bbb4944554381285c" } diff --git a/.sqlx/query-f387e6ef026d8314e78c0672652fb3d6876598ecde4db98d7a303c9e4c676376.json b/.sqlx/query-97d66b4b76bca677badd9087a6bab978a54d36e4bb12ea77a6d52b9dfa08312c.json similarity index 76% rename from .sqlx/query-f387e6ef026d8314e78c0672652fb3d6876598ecde4db98d7a303c9e4c676376.json rename to .sqlx/query-97d66b4b76bca677badd9087a6bab978a54d36e4bb12ea77a6d52b9dfa08312c.json index 037b1f6..12f490a 100644 --- a/.sqlx/query-f387e6ef026d8314e78c0672652fb3d6876598ecde4db98d7a303c9e4c676376.json +++ b/.sqlx/query-97d66b4b76bca677badd9087a6bab978a54d36e4bb12ea77a6d52b9dfa08312c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT principal, id, synctoken, displayname, \"order\", description, color, timezone, deleted_at\n FROM calendars\n WHERE principal = ? AND deleted_at IS NULL", + "query": "SELECT principal, id, synctoken, displayname, \"order\", description, color, timezone, deleted_at, subscription_url\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL", "describe": { "columns": [ { @@ -47,6 +47,11 @@ "name": "deleted_at", "ordinal": 8, "type_info": "Datetime" + }, + { + "name": "subscription_url", + "ordinal": 9, + "type_info": "Text" } ], "parameters": { @@ -60,9 +65,10 @@ false, true, true, - false, + true, + true, true ] }, - "hash": "f387e6ef026d8314e78c0672652fb3d6876598ecde4db98d7a303c9e4c676376" + "hash": "97d66b4b76bca677badd9087a6bab978a54d36e4bb12ea77a6d52b9dfa08312c" } diff --git a/crates/caldav/src/calendar/methods/mkcalendar.rs b/crates/caldav/src/calendar/methods/mkcalendar.rs index 1c5f54a..6e3dd58 100644 --- a/crates/caldav/src/calendar/methods/mkcalendar.rs +++ b/crates/caldav/src/calendar/methods/mkcalendar.rs @@ -79,6 +79,7 @@ pub async fn route_mkcalendar( description: request.calendar_description, deleted_at: None, synctoken: 0, + subscription_url: None, }; match store.get_calendar(&principal, &cal_id).await { diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index e78772d..ad62905 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -15,6 +15,7 @@ use async_trait::async_trait; use derive_more::derive::{From, Into}; use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{Resource, ResourceService}; +use rustical_dav::xml::HrefElement; use rustical_store::auth::User; use rustical_store::{Calendar, CalendarStore}; use serde::{Deserialize, Serialize}; @@ -43,6 +44,7 @@ pub enum CalendarPropName { SupportedReportSet, SyncToken, Getctag, + Source, } #[derive(Default, Deserialize, Serialize, PartialEq)] @@ -82,6 +84,8 @@ pub enum CalendarProp { // CalendarServer #[serde(rename = "CS:getctag", alias = "getctag")] Getctag(String), + #[serde(rename = "CS:source", alias = "source")] + Source(Option), #[serde(other)] #[default] @@ -97,8 +101,12 @@ impl Resource for CalendarResource { type Error = Error; type PrincipalResource = PrincipalResource; - fn get_resourcetype() -> &'static [&'static str] { - &["collection", "C:calendar"] + fn get_resourcetype(&self) -> &'static [&'static str] { + if self.0.subscription_url.is_none() { + &["collection", "C:calendar"] + } else { + &["collection", "CS:subscribed"] + } } fn get_prop( @@ -144,6 +152,9 @@ impl Resource for CalendarResource { } CalendarPropName::SyncToken => CalendarProp::SyncToken(self.0.format_synctoken()), CalendarPropName::Getctag => CalendarProp::Getctag(self.0.format_synctoken()), + CalendarPropName::Source => { + CalendarProp::Source(self.0.subscription_url.to_owned().map(HrefElement::from)) + } }) } @@ -178,6 +189,8 @@ impl Resource for CalendarResource { CalendarProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::SyncToken(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Getctag(_) => Err(rustical_dav::Error::PropReadOnly), + // Converting between a calendar subscription calendar and a normal one would be weird + CalendarProp::Source(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Invalid => Err(rustical_dav::Error::PropReadOnly), } } @@ -213,6 +226,8 @@ impl Resource for CalendarResource { CalendarPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::SyncToken => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::Getctag => Err(rustical_dav::Error::PropReadOnly), + // Converting a calendar subscription calendar into a normal one would be weird + CalendarPropName::Source => Err(rustical_dav::Error::PropReadOnly), } } @@ -226,6 +241,7 @@ impl Resource for CalendarResource { } fn get_user_privileges(&self, user: &User) -> Result { + // TODO: read-only for subscription Ok(UserPrivilegeSet::owner_only(self.0.principal == user.id)) } } diff --git a/crates/caldav/src/calendar_object/resource.rs b/crates/caldav/src/calendar_object/resource.rs index a76c93c..0d499b4 100644 --- a/crates/caldav/src/calendar_object/resource.rs +++ b/crates/caldav/src/calendar_object/resource.rs @@ -55,7 +55,7 @@ impl Resource for CalendarObjectResource { type Error = Error; type PrincipalResource = PrincipalResource; - fn get_resourcetype() -> &'static [&'static str] { + fn get_resourcetype(&self) -> &'static [&'static str] { &[] } diff --git a/crates/caldav/src/principal/mod.rs b/crates/caldav/src/principal/mod.rs index 93d146b..1926a24 100644 --- a/crates/caldav/src/principal/mod.rs +++ b/crates/caldav/src/principal/mod.rs @@ -62,7 +62,7 @@ impl Resource for PrincipalResource { type Error = Error; type PrincipalResource = PrincipalResource; - fn get_resourcetype() -> &'static [&'static str] { + fn get_resourcetype(&self) -> &'static [&'static str] { &["collection", "principal"] } diff --git a/crates/carddav/src/address_object/resource.rs b/crates/carddav/src/address_object/resource.rs index a1c0238..7db5250 100644 --- a/crates/carddav/src/address_object/resource.rs +++ b/crates/carddav/src/address_object/resource.rs @@ -56,7 +56,7 @@ impl Resource for AddressObjectResource { type Error = Error; type PrincipalResource = PrincipalResource; - fn get_resourcetype() -> &'static [&'static str] { + fn get_resourcetype(&self) -> &'static [&'static str] { &[] } diff --git a/crates/carddav/src/addressbook/resource.rs b/crates/carddav/src/addressbook/resource.rs index adf15b8..6b40a32 100644 --- a/crates/carddav/src/addressbook/resource.rs +++ b/crates/carddav/src/addressbook/resource.rs @@ -81,7 +81,7 @@ impl Resource for AddressbookResource { type Error = Error; type PrincipalResource = PrincipalResource; - fn get_resourcetype() -> &'static [&'static str] { + fn get_resourcetype(&self) -> &'static [&'static str] { &["collection", "CARD:addressbook"] } diff --git a/crates/carddav/src/principal/mod.rs b/crates/carddav/src/principal/mod.rs index 536cc8b..8489b96 100644 --- a/crates/carddav/src/principal/mod.rs +++ b/crates/carddav/src/principal/mod.rs @@ -62,7 +62,7 @@ impl Resource for PrincipalResource { type Error = Error; type PrincipalResource = PrincipalResource; - fn get_resourcetype() -> &'static [&'static str] { + fn get_resourcetype(&self) -> &'static [&'static str] { &["collection", "principal"] } diff --git a/crates/dav/src/resource/mod.rs b/crates/dav/src/resource/mod.rs index 08ac8fd..79e48ca 100644 --- a/crates/dav/src/resource/mod.rs +++ b/crates/dav/src/resource/mod.rs @@ -70,7 +70,7 @@ pub trait Resource: Clone + 'static { type Error: ResponseError + From; type PrincipalResource: Resource; - fn get_resourcetype() -> &'static [&'static str]; + fn get_resourcetype(&self) -> &'static [&'static str]; fn list_props() -> Vec<&'static str> { [Self::PropName::VARIANTS, CommonPropertiesPropName::VARIANTS].concat() @@ -84,7 +84,7 @@ pub trait Resource: Clone + 'static { ) -> Result { Ok(match prop { CommonPropertiesPropName::Resourcetype => { - CommonPropertiesProp::Resourcetype(Resourcetype(Self::get_resourcetype())) + CommonPropertiesProp::Resourcetype(Resourcetype(self.get_resourcetype())) } CommonPropertiesPropName::CurrentUserPrincipal => { CommonPropertiesProp::CurrentUserPrincipal( diff --git a/crates/dav/src/resources/root.rs b/crates/dav/src/resources/root.rs index be19e47..2881f36 100644 --- a/crates/dav/src/resources/root.rs +++ b/crates/dav/src/resources/root.rs @@ -35,7 +35,7 @@ impl Resource for RootResource { type Error = PR::Error; type PrincipalResource = PR; - fn get_resourcetype() -> &'static [&'static str] { + fn get_resourcetype(&self) -> &'static [&'static str] { &["collection"] } diff --git a/crates/dav/src/xml/multistatus.rs b/crates/dav/src/xml/multistatus.rs index 655735d..1ff9aa2 100644 --- a/crates/dav/src/xml/multistatus.rs +++ b/crates/dav/src/xml/multistatus.rs @@ -83,6 +83,8 @@ pub struct MultistatusElement { pub ns_caldav: &'static str, #[serde(rename = "@xmlns:IC")] pub ns_ical: &'static str, + #[serde(rename = "@xmlns:CS")] + pub ns_calendarserver: &'static str, #[serde(rename = "@xmlns:CARD")] pub ns_carddav: &'static str, #[serde(skip_serializing_if = "Option::is_none")] @@ -97,6 +99,7 @@ impl Default for MultistatusElement { ns_dav: Namespace::Dav.as_str(), ns_caldav: Namespace::CalDAV.as_str(), ns_ical: Namespace::ICal.as_str(), + ns_calendarserver: Namespace::CServer.as_str(), ns_carddav: Namespace::CardDAV.as_str(), sync_token: None, } diff --git a/crates/store/src/calendar/calendar.rs b/crates/store/src/calendar/calendar.rs index d17b092..1731f1b 100644 --- a/crates/store/src/calendar/calendar.rs +++ b/crates/store/src/calendar/calendar.rs @@ -13,6 +13,7 @@ pub struct Calendar { pub timezone: Option, pub deleted_at: Option, pub synctoken: i64, + pub subscription_url: Option, } impl Calendar { diff --git a/crates/store_sqlite/migrations/1_init.sql b/crates/store_sqlite/migrations/1_init.sql index e41043c..a3c412b 100644 --- a/crates/store_sqlite/migrations/1_init.sql +++ b/crates/store_sqlite/migrations/1_init.sql @@ -8,6 +8,7 @@ CREATE TABLE calendars ( color TEXT, timezone TEXT, deleted_at DATETIME, + subscription_url TEXT, PRIMARY KEY (principal, id) ); diff --git a/crates/store_sqlite/src/calendar_store.rs b/crates/store_sqlite/src/calendar_store.rs index 6ee43e8..c8814fb 100644 --- a/crates/store_sqlite/src/calendar_store.rs +++ b/crates/store_sqlite/src/calendar_store.rs @@ -62,7 +62,7 @@ impl CalendarStore for SqliteStore { async fn get_calendar(&self, principal: &str, id: &str) -> Result { let cal = sqlx::query_as!( Calendar, - r#"SELECT principal, id, synctoken, "order", displayname, description, color, timezone, deleted_at + r#"SELECT principal, id, synctoken, "order", displayname, description, color, timezone, deleted_at, subscription_url FROM calendars WHERE (principal, id) = (?, ?)"#, principal, @@ -77,7 +77,7 @@ impl CalendarStore for SqliteStore { async fn get_calendars(&self, principal: &str) -> Result, Error> { let cals = sqlx::query_as!( Calendar, - r#"SELECT principal, id, synctoken, displayname, "order", description, color, timezone, deleted_at + r#"SELECT principal, id, synctoken, displayname, "order", description, color, timezone, deleted_at, subscription_url FROM calendars WHERE principal = ? AND deleted_at IS NULL"#, principal @@ -91,7 +91,7 @@ impl CalendarStore for SqliteStore { async fn get_deleted_calendars(&self, principal: &str) -> Result, Error> { let cals = sqlx::query_as!( Calendar, - r#"SELECT principal, id, synctoken, displayname, "order", description, color, timezone, deleted_at + r#"SELECT principal, id, synctoken, displayname, "order", description, color, timezone, deleted_at, subscription_url FROM calendars WHERE principal = ? AND deleted_at IS NOT NULL"#, principal @@ -236,6 +236,7 @@ impl CalendarStore for SqliteStore { object: CalendarObject, overwrite: bool, ) -> Result<(), Error> { + // TODO: Prevent objects from being commited to a subscription calendar let mut tx = self.db.begin().await.map_err(crate::Error::from)?; let (object_id, ics) = (object.get_id(), object.get_ics()); @@ -255,7 +256,7 @@ impl CalendarStore for SqliteStore { principal, cal_id, object_id, - ics + ics, ) }) .execute(&mut *tx)