From 0c8d339ced7891be855391172927409cb00fd5ec Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 3 Nov 2024 22:32:21 +0100 Subject: [PATCH] dav: Introduce resource extension for common properties --- Cargo.lock | 124 +++------------- Cargo.toml | 45 +++--- crates/caldav/src/calendar/resource.rs | 44 +++--- crates/caldav/src/calendar_object/resource.rs | 41 +++--- crates/caldav/src/principal/mod.rs | 39 +++-- crates/caldav/src/root/mod.rs | 40 +++--- crates/carddav/src/address_object/resource.rs | 35 +++-- crates/carddav/src/addressbook/resource.rs | 47 ++++--- crates/carddav/src/principal/mod.rs | 37 +++-- crates/carddav/src/root/mod.rs | 38 ++--- crates/dav/Cargo.toml | 1 + crates/dav/src/extension.rs | 133 ++++++++++++++++++ crates/dav/src/extensions/mod.rs | 77 ++++++++++ crates/dav/src/lib.rs | 2 + crates/dav/src/methods/propfind.rs | 1 + crates/dav/src/resource.rs | 50 ++++++- 16 files changed, 492 insertions(+), 262 deletions(-) create mode 100644 crates/dav/src/extension.rs create mode 100644 crates/dav/src/extensions/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 6c53225..739da89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,12 +572,6 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -792,45 +786,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console-api" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" -dependencies = [ - "futures-core", - "prost", - "prost-types", - "tonic", - "tracing-core", -] - -[[package]] -name = "console-subscriber" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" -dependencies = [ - "console-api", - "crossbeam-channel", - "crossbeam-utils", - "futures-task", - "hdrhistogram", - "humantime", - "hyper-util", - "prost", - "prost-types", - "serde", - "serde_json", - "thread_local", - "tokio", - "tokio-stream", - "tonic", - "tracing", - "tracing-core", - "tracing-subscriber", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -900,15 +855,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -1034,35 +980,22 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_filter" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "humantime", - "log", -] - [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.9" @@ -1357,19 +1290,6 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "hdrhistogram" -version = "7.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" -dependencies = [ - "base64 0.21.7", - "byteorder", - "flate2", - "nom", - "num-traits", -] - [[package]] name = "heck" version = "0.5.0" @@ -1484,12 +1404,6 @@ dependencies = [ "libm", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "1.5.0" @@ -2270,15 +2184,6 @@ dependencies = [ "syn", ] -[[package]] -name = "prost-types" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" -dependencies = [ - "prost", -] - [[package]] name = "quick-xml" version = "0.37.0" @@ -2533,8 +2438,6 @@ dependencies = [ "argon2", "async-trait", "clap", - "console-subscriber", - "env_logger", "opentelemetry", "opentelemetry-otlp", "opentelemetry-semantic-conventions", @@ -2613,6 +2516,7 @@ dependencies = [ "actix-web", "async-trait", "derive_more 1.0.0", + "erased-serde", "futures-util", "itertools", "log", @@ -3522,6 +3426,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 66cedc3..fcdc67a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,19 +39,19 @@ rstest = "0.23" rstest_reuse = "0.7" sha2 = "0.10" tokio = { version = "1", features = [ - "net", - "tracing", - "macros", - "rt-multi-thread", - "full", + "net", + "tracing", + "macros", + "rt-multi-thread", + "full", ] } url = "2.5" base64 = "0.22" thiserror = "1.0" quick-xml = { version = "0.37", features = [ - "serde", - "serde-types", - "serialize", + "serde", + "serde-types", + "serialize", ] } rust-embed = "8.5" futures-core = "0.3.31" @@ -60,17 +60,22 @@ mime_guess = "2.0.5" itertools = "0.13" log = "0.4" strum = { version = "0.26", features = ["strum_macros", "derive"] } -derive_more = { version = "1.0", features = ["from", "into", "deref"] } +derive_more = { version = "1.0", features = [ + "from", + "try_into", + "into", + "deref", +] } askama = { version = "0.12", features = ["serde", "with-actix-web"] } askama_actix = "0.14" sqlx = { version = "0.8", default-features = false, features = [ - "sqlx-sqlite", - "uuid", - "chrono", - "sqlite", - "runtime-tokio", - "macros", - "migrate", + "sqlx-sqlite", + "uuid", + "chrono", + "sqlite", + "runtime-tokio", + "macros", + "migrate", ] } ical = { version = "0.11", features = ["generator", "serde"] } toml = "0.8" @@ -97,7 +102,6 @@ toml = { workspace = true } serde = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -env_logger = "0.11" anyhow = { workspace = true } clap = { version = "4.5", features = ["derive", "env"] } sqlx = { workspace = true } @@ -111,11 +115,10 @@ opentelemetry_sdk = { version = "0.26", features = ["rt-tokio"] } opentelemetry-semantic-conventions = "0.26" tracing-opentelemetry = "0.27" tracing-subscriber = { version = "0.3", features = [ - "env-filter", - "fmt", - "registry", + "env-filter", + "fmt", + "registry", ] } -console-subscriber = "0.4" rand.workspace = true rpassword.workspace = true diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index dbfde77..5dc6737 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -12,7 +12,11 @@ use actix_web::http::Method; use actix_web::web; use actix_web::{web::Data, HttpRequest}; use async_trait::async_trait; -use derive_more::derive::{From, Into}; +use derive_more::derive::{From, Into, TryInto}; +use rustical_dav::extension::BoxedExtension; +use rustical_dav::extensions::{ + CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName, +}; use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml::HrefElement; @@ -30,11 +34,10 @@ pub struct CalendarResourceService { pub calendar_id: String, } -#[derive(EnumString, Debug, VariantNames, Clone)] +#[derive(EnumString, Debug, VariantNames, Clone, From, TryInto)] #[strum(serialize_all = "kebab-case")] pub enum CalendarPropName { Resourcetype, - CurrentUserPrincipal, Owner, Displayname, CalendarColor, @@ -44,14 +47,17 @@ pub enum CalendarPropName { SupportedCalendarComponentSet, SupportedCalendarData, Getcontenttype, - // CurrentUserPrivilegeSet, MaxResourceSize, SupportedReportSet, SyncToken, Getctag, + #[from] + #[try_into] + #[strum(disabled)] + ExtCommonProperties(CommonPropertiesPropName), } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, From, TryInto)] #[serde(rename_all = "kebab-case")] pub enum CalendarProp { // WebDAV (RFC 2518) @@ -59,12 +65,8 @@ pub enum CalendarProp { Displayname(Option), Getcontenttype(String), - // WebDAV Current Principal Extension (RFC 5397) - CurrentUserPrincipal(HrefElement), - // WebDAV Access Control (RFC 3744) Owner(HrefElement), - // CurrentUserPrivilegeSet(UserPrivilegeSet), // CalDAV (RFC 4791) #[serde(rename = "IC:calendar-color", alias = "calendar-color")] @@ -93,7 +95,13 @@ pub enum CalendarProp { // Didn't find the spec Getctag(String), - #[serde(other)] + + #[serde(skip_deserializing, untagged)] + #[from] + #[try_into] + ExtCommonProperties(CommonPropertiesProp), + + #[serde(untagged)] Invalid, } @@ -111,6 +119,12 @@ impl Resource for CalendarResource { type Prop = CalendarProp; type Error = Error; + fn list_extensions() -> Vec> { + vec![BoxedExtension::from_ext(CommonPropertiesExtension::< + PrincipalResource, + >::default())] + } + fn get_prop( &self, rmap: &ResourceMap, @@ -119,11 +133,6 @@ impl Resource for CalendarResource { ) -> Result { Ok(match prop { CalendarPropName::Resourcetype => CalendarProp::Resourcetype(Resourcetype::default()), - CalendarPropName::CurrentUserPrincipal => { - CalendarProp::CurrentUserPrincipal(HrefElement::new( - PrincipalResource::get_url(rmap, vec![&self.0.principal]).unwrap(), - )) - } CalendarPropName::Owner => CalendarProp::Owner(HrefElement::new( PrincipalResource::get_url(rmap, vec![&self.0.principal]).unwrap(), )), @@ -166,13 +175,13 @@ impl Resource for CalendarResource { } CalendarPropName::SyncToken => CalendarProp::SyncToken(self.0.format_synctoken()), CalendarPropName::Getctag => CalendarProp::Getctag(self.0.format_synctoken()), + _ => panic!("we shouldn't end up here"), }) } fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> { match prop { CalendarProp::Resourcetype(_) => Err(rustical_dav::Error::PropReadOnly), - CalendarProp::CurrentUserPrincipal(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Owner(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Displayname(displayname) => { self.0.displayname = displayname; @@ -205,13 +214,13 @@ impl Resource for CalendarResource { CalendarProp::SyncToken(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Getctag(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Invalid => Err(rustical_dav::Error::PropReadOnly), + _ => panic!("we shouldn't end up here"), } } fn remove_prop(&mut self, prop: &Self::PropName) -> Result<(), rustical_dav::Error> { match prop { CalendarPropName::Resourcetype => Err(rustical_dav::Error::PropReadOnly), - CalendarPropName::CurrentUserPrincipal => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::Owner => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::Displayname => { self.0.displayname = None; @@ -243,6 +252,7 @@ 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), + _ => panic!("we shouldn't end up here"), } } diff --git a/crates/caldav/src/calendar_object/resource.rs b/crates/caldav/src/calendar_object/resource.rs index a9772eb..670c845 100644 --- a/crates/caldav/src/calendar_object/resource.rs +++ b/crates/caldav/src/calendar_object/resource.rs @@ -2,8 +2,10 @@ use super::methods::{get_event, put_event}; use crate::{principal::PrincipalResource, Error}; use actix_web::{dev::ResourceMap, web::Data, HttpRequest}; use async_trait::async_trait; -use derive_more::derive::{From, Into}; +use derive_more::derive::{From, Into, TryInto}; use rustical_dav::{ + extension::BoxedExtension, + extensions::{CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName}, privileges::UserPrivilegeSet, resource::{InvalidProperty, Resource, ResourceService}, xml::HrefElement, @@ -21,18 +23,20 @@ pub struct CalendarObjectResourceService { pub object_id: String, } -#[derive(EnumString, Debug, VariantNames, Clone)] +#[derive(EnumString, Debug, VariantNames, Clone, From, TryInto)] #[strum(serialize_all = "kebab-case")] pub enum CalendarObjectPropName { Getetag, CalendarData, Getcontenttype, - CurrentUserPrincipal, Owner, - CurrentUserPrivilegeSet, + #[from] + #[try_into] + #[strum(disabled)] + ExtCommonProperties(CommonPropertiesPropName), } -#[derive(Deserialize, Serialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, From, TryInto)] #[serde(rename_all = "kebab-case")] pub enum CalendarObjectProp { // WebDAV (RFC 2518) @@ -43,13 +47,15 @@ pub enum CalendarObjectProp { #[serde(rename = "C:calendar-data")] CalendarData(String), - // WebDAV Current Principal Extension (RFC 5397) - CurrentUserPrincipal(HrefElement), - // WebDAV Access Control (RFC 3744) Owner(HrefElement), - CurrentUserPrivilegeSet(UserPrivilegeSet), - #[serde(other)] + + #[serde(skip_deserializing, untagged)] + #[from] + #[try_into] + ExtCommonProperties(CommonPropertiesProp), + + #[serde(untagged)] Invalid, } @@ -70,6 +76,12 @@ impl Resource for CalendarObjectResource { type Prop = CalendarObjectProp; type Error = Error; + fn list_extensions() -> Vec> { + vec![BoxedExtension::from_ext(CommonPropertiesExtension::< + PrincipalResource, + >::default())] + } + fn get_prop( &self, rmap: &ResourceMap, @@ -84,17 +96,10 @@ impl Resource for CalendarObjectResource { CalendarObjectPropName::Getcontenttype => { CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8".to_owned()) } - CalendarObjectPropName::CurrentUserPrincipal => { - CalendarObjectProp::CurrentUserPrincipal(HrefElement::new( - PrincipalResource::get_principal_url(rmap, &user.id), - )) - } CalendarObjectPropName::Owner => CalendarObjectProp::Owner( PrincipalResource::get_principal_url(rmap, &self.principal).into(), ), - CalendarObjectPropName::CurrentUserPrivilegeSet => { - CalendarObjectProp::CurrentUserPrivilegeSet(self.get_user_privileges(&user)?) - } + _ => panic!("we shouldn't end up here"), }) } diff --git a/crates/caldav/src/principal/mod.rs b/crates/caldav/src/principal/mod.rs index c689a76..6be30ab 100644 --- a/crates/caldav/src/principal/mod.rs +++ b/crates/caldav/src/principal/mod.rs @@ -4,6 +4,11 @@ use actix_web::dev::ResourceMap; use actix_web::web::Data; use actix_web::HttpRequest; use async_trait::async_trait; +use derive_more::derive::{From, TryInto}; +use rustical_dav::extension::BoxedExtension; +use rustical_dav::extensions::{ + CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName, +}; use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml::HrefElement; @@ -30,7 +35,7 @@ pub struct Resourcetype { collection: (), } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, From, TryInto)] #[serde(rename_all = "kebab-case")] pub enum PrincipalProp { // WebDAV (RFC 2518) @@ -40,12 +45,8 @@ pub enum PrincipalProp { #[serde(rename = "principal-URL")] PrincipalUrl(HrefElement), - // WebDAV Current Principal Extension (RFC 5397) - CurrentUserPrincipal(HrefElement), - // WebDAV Access Control (RFC 3744) Owner(HrefElement), - CurrentUserPrivilegeSet(UserPrivilegeSet), // CalDAV (RFC 4791) #[serde(rename = "C:calendar-home-set")] @@ -53,7 +54,12 @@ pub enum PrincipalProp { #[serde(rename = "C:calendar-user-address-set")] CalendarUserAddressSet(HrefElement), - #[serde(other)] + #[serde(skip_deserializing, untagged)] + #[from] + #[try_into] + ExtCommonProperties(CommonPropertiesProp), + + #[serde(untagged)] Invalid, } @@ -63,17 +69,19 @@ impl InvalidProperty for PrincipalProp { } } -#[derive(EnumString, Debug, VariantNames, Clone)] +#[derive(EnumString, Debug, VariantNames, Clone, From, TryInto)] #[strum(serialize_all = "kebab-case")] pub enum PrincipalPropName { Resourcetype, - CurrentUserPrincipal, Owner, - CurrentUserPrivilegeSet, #[strum(serialize = "principal-URL")] PrincipalUrl, CalendarHomeSet, CalendarUserAddressSet, + #[from] + #[try_into] + #[strum(disabled)] + ExtCommonProperties(CommonPropertiesPropName), } impl PrincipalResource { @@ -87,6 +95,12 @@ impl Resource for PrincipalResource { type Prop = PrincipalProp; type Error = Error; + fn list_extensions() -> Vec> { + vec![BoxedExtension::from_ext(CommonPropertiesExtension::< + PrincipalResource, + >::default())] + } + fn get_prop( &self, rmap: &ResourceMap, @@ -97,20 +111,15 @@ impl Resource for PrincipalResource { Ok(match prop { PrincipalPropName::Resourcetype => PrincipalProp::Resourcetype(Resourcetype::default()), - PrincipalPropName::CurrentUserPrincipal => { - PrincipalProp::CurrentUserPrincipal(principal_href) - } PrincipalPropName::Owner => PrincipalProp::Owner(HrefElement::new( PrincipalResource::get_url(rmap, vec![&self.principal]).unwrap(), )), - PrincipalPropName::CurrentUserPrivilegeSet => { - PrincipalProp::CurrentUserPrivilegeSet(self.get_user_privileges(user)?) - } PrincipalPropName::PrincipalUrl => PrincipalProp::PrincipalUrl(principal_href), PrincipalPropName::CalendarHomeSet => PrincipalProp::CalendarHomeSet(principal_href), PrincipalPropName::CalendarUserAddressSet => { PrincipalProp::CalendarUserAddressSet(principal_href) } + _ => panic!("we shouldn't end up here"), }) } diff --git a/crates/caldav/src/root/mod.rs b/crates/caldav/src/root/mod.rs index 1fbade2..7fcb9c2 100644 --- a/crates/caldav/src/root/mod.rs +++ b/crates/caldav/src/root/mod.rs @@ -3,19 +3,25 @@ use crate::Error; use actix_web::dev::ResourceMap; use actix_web::HttpRequest; use async_trait::async_trait; +use derive_more::derive::{From, TryInto}; +use rustical_dav::extension::BoxedExtension; +use rustical_dav::extensions::{ + CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName, +}; use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; -use rustical_dav::xml::HrefElement; use rustical_store::auth::User; use serde::{Deserialize, Serialize}; use strum::{EnumString, VariantNames}; -#[derive(EnumString, Debug, VariantNames, Clone)] +#[derive(EnumString, Debug, VariantNames, Clone, From, TryInto)] #[strum(serialize_all = "kebab-case")] pub enum RootPropName { Resourcetype, - CurrentUserPrincipal, - CurrentUserPrivilegeSet, + #[from] + #[try_into] + #[strum(disabled)] + ExtCommonProperties(CommonPropertiesPropName), } #[derive(Deserialize, Serialize, Default, Debug)] @@ -24,19 +30,18 @@ pub struct Resourcetype { collection: (), } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, From, TryInto)] #[serde(rename_all = "kebab-case")] pub enum RootProp { // WebDAV (RFC 2518) Resourcetype(Resourcetype), - // WebDAV Current Principal Extension (RFC 5397) - CurrentUserPrincipal(HrefElement), + #[serde(skip_deserializing, untagged)] + #[from] + #[try_into] + ExtCommonProperties(CommonPropertiesProp), - // WebDAV Access Control Protocol (RFC 3477) - CurrentUserPrivilegeSet(UserPrivilegeSet), - - #[serde(other)] + #[serde(untagged)] Invalid, } @@ -54,6 +59,12 @@ impl Resource for RootResource { type Prop = RootProp; type Error = Error; + fn list_extensions() -> Vec> { + vec![BoxedExtension::from_ext(CommonPropertiesExtension::< + PrincipalResource, + >::default())] + } + fn get_prop( &self, rmap: &ResourceMap, @@ -62,12 +73,7 @@ impl Resource for RootResource { ) -> Result { Ok(match prop { RootPropName::Resourcetype => RootProp::Resourcetype(Resourcetype::default()), - RootPropName::CurrentUserPrincipal => RootProp::CurrentUserPrincipal(HrefElement::new( - PrincipalResource::get_url(rmap, vec![&user.id]).unwrap(), - )), - RootPropName::CurrentUserPrivilegeSet => { - RootProp::CurrentUserPrivilegeSet(self.get_user_privileges(user)?) - } + _ => panic!("we shouldn't end up here"), }) } diff --git a/crates/carddav/src/address_object/resource.rs b/crates/carddav/src/address_object/resource.rs index 319a8f5..cc7b017 100644 --- a/crates/carddav/src/address_object/resource.rs +++ b/crates/carddav/src/address_object/resource.rs @@ -1,8 +1,10 @@ use crate::{principal::PrincipalResource, Error}; use actix_web::{dev::ResourceMap, web::Data, HttpRequest}; use async_trait::async_trait; -use derive_more::derive::{From, Into}; +use derive_more::derive::{From, Into, TryInto}; use rustical_dav::{ + extension::BoxedExtension, + extensions::{CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName}, privileges::UserPrivilegeSet, resource::{InvalidProperty, Resource, ResourceService}, xml::HrefElement, @@ -22,18 +24,20 @@ pub struct AddressObjectResourceService { pub object_id: String, } -#[derive(EnumString, Debug, VariantNames, Clone)] +#[derive(EnumString, Debug, VariantNames, Clone, From, TryInto)] #[strum(serialize_all = "kebab-case")] pub enum AddressObjectPropName { Getetag, AddressData, Getcontenttype, - CurrentUserPrincipal, Owner, - CurrentUserPrivilegeSet, + #[from] + #[try_into] + #[strum(disabled)] + ExtCommonProperties(CommonPropertiesPropName), } -#[derive(Deserialize, Serialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, From, TryInto)] #[serde(rename_all = "kebab-case")] pub enum AddressObjectProp { // WebDAV (RFC 2518) @@ -50,7 +54,13 @@ pub enum AddressObjectProp { // CalDAV (RFC 4791) #[serde(rename = "CARD:address-data")] AddressData(String), - #[serde(other)] + + #[serde(skip_deserializing, untagged)] + #[from] + #[try_into] + ExtCommonProperties(CommonPropertiesProp), + + #[serde(untagged)] Invalid, } @@ -71,6 +81,12 @@ impl Resource for AddressObjectResource { type Prop = AddressObjectProp; type Error = Error; + fn list_extensions() -> Vec> { + vec![BoxedExtension::from_ext(CommonPropertiesExtension::< + PrincipalResource, + >::default())] + } + fn get_prop( &self, rmap: &ResourceMap, @@ -85,15 +101,10 @@ impl Resource for AddressObjectResource { AddressObjectPropName::Getcontenttype => { AddressObjectProp::Getcontenttype("text/vcard;charset=utf-8".to_owned()) } - AddressObjectPropName::CurrentUserPrincipal => AddressObjectProp::CurrentUserPrincipal( - HrefElement::new(PrincipalResource::get_principal_url(rmap, &user.id)), - ), AddressObjectPropName::Owner => AddressObjectProp::Owner( PrincipalResource::get_principal_url(rmap, &self.principal).into(), ), - AddressObjectPropName::CurrentUserPrivilegeSet => { - AddressObjectProp::CurrentUserPrivilegeSet(UserPrivilegeSet::all()) - } + _ => panic!("we shouldn't end up here"), }) } diff --git a/crates/carddav/src/addressbook/resource.rs b/crates/carddav/src/addressbook/resource.rs index a6fce2e..f5f5931 100644 --- a/crates/carddav/src/addressbook/resource.rs +++ b/crates/carddav/src/addressbook/resource.rs @@ -9,7 +9,11 @@ use actix_web::http::Method; use actix_web::web; use actix_web::{web::Data, HttpRequest}; use async_trait::async_trait; -use derive_more::derive::{From, Into}; +use derive_more::derive::{From, Into, TryInto}; +use rustical_dav::extension::BoxedExtension; +use rustical_dav::extensions::{ + CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName, +}; use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml::HrefElement; @@ -27,24 +31,25 @@ pub struct AddressbookResourceService { pub addressbook_id: String, } -#[derive(EnumString, Debug, VariantNames, Clone)] +#[derive(EnumString, Debug, VariantNames, Clone, From, TryInto)] #[strum(serialize_all = "kebab-case")] pub enum AddressbookPropName { Resourcetype, Displayname, Getcontenttype, - CurrentUserPrincipal, Owner, - CurrentUserPrivilegeSet, AddressbookDescription, SupportedAddressData, SupportedReportSet, MaxResourceSize, SyncToken, Getctag, + #[try_into] + #[strum(disabled)] + ExtCommonProperties(CommonPropertiesPropName), } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, From, TryInto)] #[serde(rename_all = "kebab-case")] pub enum AddressbookProp { // WebDAV (RFC 2518) @@ -52,12 +57,8 @@ pub enum AddressbookProp { Displayname(Option), Getcontenttype(String), - // WebDAV Current Principal Extension (RFC 5397) - CurrentUserPrincipal(HrefElement), - // WebDAV Access Control (RFC 3744) Owner(HrefElement), - CurrentUserPrivilegeSet(UserPrivilegeSet), // CardDAV (RFC 6352) #[serde( @@ -79,7 +80,12 @@ pub enum AddressbookProp { // Didn't find the spec Getctag(String), - #[serde(other)] + #[serde(skip_deserializing, untagged)] + #[from] + #[try_into] + ExtCommonProperties(CommonPropertiesProp), + + #[serde(untagged)] Invalid, } @@ -97,6 +103,12 @@ impl Resource for AddressbookResource { type Prop = AddressbookProp; type Error = Error; + fn list_extensions() -> Vec> { + vec![BoxedExtension::from_ext(CommonPropertiesExtension::< + PrincipalResource, + >::default())] + } + fn get_prop( &self, rmap: &ResourceMap, @@ -107,17 +119,9 @@ impl Resource for AddressbookResource { AddressbookPropName::Resourcetype => { AddressbookProp::Resourcetype(Resourcetype::default()) } - AddressbookPropName::CurrentUserPrincipal => { - AddressbookProp::CurrentUserPrincipal(HrefElement::new( - PrincipalResource::get_principal_url(rmap, &self.0.principal), - )) - } AddressbookPropName::Owner => AddressbookProp::Owner( PrincipalResource::get_principal_url(rmap, &self.0.principal).into(), ), - AddressbookPropName::CurrentUserPrivilegeSet => { - AddressbookProp::CurrentUserPrivilegeSet(UserPrivilegeSet::all()) - } AddressbookPropName::Displayname => { AddressbookProp::Displayname(self.0.displayname.clone()) } @@ -136,13 +140,13 @@ impl Resource for AddressbookResource { } AddressbookPropName::SyncToken => AddressbookProp::SyncToken(self.0.format_synctoken()), AddressbookPropName::Getctag => AddressbookProp::Getctag(self.0.format_synctoken()), + _ => panic!("we shouldn't end up here"), }) } fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> { match prop { AddressbookProp::Resourcetype(_) => Err(rustical_dav::Error::PropReadOnly), - AddressbookProp::CurrentUserPrincipal(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::Owner(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::Displayname(displayname) => { self.0.displayname = displayname; @@ -154,19 +158,18 @@ impl Resource for AddressbookResource { } AddressbookProp::Getcontenttype(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::MaxResourceSize(_) => Err(rustical_dav::Error::PropReadOnly), - AddressbookProp::CurrentUserPrivilegeSet(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::SupportedAddressData(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::SyncToken(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::Getctag(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::Invalid => Err(rustical_dav::Error::PropReadOnly), + _ => panic!("we shouldn't end up here"), } } fn remove_prop(&mut self, prop: &Self::PropName) -> Result<(), rustical_dav::Error> { match prop { AddressbookPropName::Resourcetype => Err(rustical_dav::Error::PropReadOnly), - AddressbookPropName::CurrentUserPrincipal => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::Owner => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::Displayname => { self.0.displayname = None; @@ -178,11 +181,11 @@ impl Resource for AddressbookResource { } AddressbookPropName::Getcontenttype => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly), - AddressbookPropName::CurrentUserPrivilegeSet => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::SupportedAddressData => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::SyncToken => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::Getctag => Err(rustical_dav::Error::PropReadOnly), + _ => panic!("we shouldn't end up here"), } } diff --git a/crates/carddav/src/principal/mod.rs b/crates/carddav/src/principal/mod.rs index de81bfe..d4a91eb 100644 --- a/crates/carddav/src/principal/mod.rs +++ b/crates/carddav/src/principal/mod.rs @@ -4,6 +4,11 @@ use actix_web::dev::ResourceMap; use actix_web::web::Data; use actix_web::HttpRequest; use async_trait::async_trait; +use derive_more::derive::{From, TryInto}; +use rustical_dav::extension::BoxedExtension; +use rustical_dav::extensions::{ + CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName, +}; use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; use rustical_dav::xml::HrefElement; @@ -18,7 +23,7 @@ pub struct PrincipalResourceService { addr_store: Arc, } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct PrincipalResource { principal: String, } @@ -30,7 +35,7 @@ pub struct Resourcetype { collection: (), } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, From, TryInto)] #[serde(rename_all = "kebab-case")] pub enum PrincipalProp { // WebDAV (RFC 2518) @@ -40,15 +45,18 @@ pub enum PrincipalProp { #[serde(rename = "principal-URL")] PrincipalUrl(HrefElement), - // WebDAV Current Principal Extension (RFC 5397) - CurrentUserPrincipal(HrefElement), - // CardDAV (RFC 6352) #[serde(rename = "CARD:addressbook-home-set")] AddressbookHomeSet(HrefElement), #[serde(rename = "CARD:principal-address")] PrincipalAddress(Option), - #[serde(other)] + + #[serde(skip_deserializing, untagged)] + #[from] + #[try_into] + ExtRFC5397RFC3477(CommonPropertiesProp), + + #[serde(untagged)] Invalid, } @@ -58,15 +66,18 @@ impl InvalidProperty for PrincipalProp { } } -#[derive(EnumString, Debug, VariantNames, Clone)] +#[derive(EnumString, Debug, VariantNames, Clone, From, TryInto)] #[strum(serialize_all = "kebab-case")] pub enum PrincipalPropName { Resourcetype, - CurrentUserPrincipal, #[strum(serialize = "principal-URL")] PrincipalUrl, AddressbookHomeSet, PrincipalAddress, + #[from] + #[try_into] + #[strum(disabled)] + ExtRFC5397(CommonPropertiesPropName), } impl PrincipalResource { @@ -80,6 +91,12 @@ impl Resource for PrincipalResource { type Prop = PrincipalProp; type Error = Error; + fn list_extensions() -> Vec> { + vec![BoxedExtension::from_ext(CommonPropertiesExtension::< + PrincipalResource, + >::default())] + } + fn get_prop( &self, rmap: &ResourceMap, @@ -90,14 +107,12 @@ impl Resource for PrincipalResource { Ok(match prop { PrincipalPropName::Resourcetype => PrincipalProp::Resourcetype(Resourcetype::default()), - PrincipalPropName::CurrentUserPrincipal => { - PrincipalProp::CurrentUserPrincipal(principal_href) - } PrincipalPropName::PrincipalUrl => PrincipalProp::PrincipalUrl(principal_href), PrincipalPropName::AddressbookHomeSet => { PrincipalProp::AddressbookHomeSet(principal_href) } PrincipalPropName::PrincipalAddress => PrincipalProp::PrincipalAddress(None), + _ => panic!("we shouldn't end up here"), }) } diff --git a/crates/carddav/src/root/mod.rs b/crates/carddav/src/root/mod.rs index 8246f43..bf8a394 100644 --- a/crates/carddav/src/root/mod.rs +++ b/crates/carddav/src/root/mod.rs @@ -3,19 +3,25 @@ use crate::Error; use actix_web::dev::ResourceMap; use actix_web::HttpRequest; use async_trait::async_trait; +use derive_more::{From, TryInto}; +use rustical_dav::extension::BoxedExtension; +use rustical_dav::extensions::{ + CommonPropertiesExtension, CommonPropertiesProp, CommonPropertiesPropName, +}; use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::resource::{InvalidProperty, Resource, ResourceService}; -use rustical_dav::xml::HrefElement; use rustical_store::auth::User; use serde::{Deserialize, Serialize}; use strum::{EnumString, VariantNames}; -#[derive(EnumString, Debug, VariantNames, Clone)] +#[derive(EnumString, Debug, VariantNames, Clone, From, TryInto)] #[strum(serialize_all = "kebab-case")] pub enum RootPropName { Resourcetype, - CurrentUserPrincipal, - CurrentUserPrivilegeSet, + #[from] + #[try_into] + #[strum(disabled)] + ExtCommonProperties(CommonPropertiesPropName), } #[derive(Deserialize, Serialize, Default, Debug)] @@ -24,17 +30,16 @@ pub struct Resourcetype { collection: (), } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, From, TryInto)] #[serde(rename_all = "kebab-case")] pub enum RootProp { // WebDAV (RFC 2518) Resourcetype(Resourcetype), - // WebDAV Current Principal Extension (RFC 5397) - CurrentUserPrincipal(HrefElement), - - // WebDAV Access Control Protocol (RFC 3477) - CurrentUserPrivilegeSet(UserPrivilegeSet), + #[serde(skip_deserializing, untagged)] + #[from] + #[try_into] + ExtCommonProperties(CommonPropertiesProp), #[serde(untagged)] Invalid, @@ -54,6 +59,12 @@ impl Resource for RootResource { type Prop = RootProp; type Error = Error; + fn list_extensions() -> Vec> { + vec![BoxedExtension::from_ext(CommonPropertiesExtension::< + PrincipalResource, + >::default())] + } + fn get_prop( &self, rmap: &ResourceMap, @@ -62,12 +73,7 @@ impl Resource for RootResource { ) -> Result { Ok(match prop { RootPropName::Resourcetype => RootProp::Resourcetype(Resourcetype::default()), - RootPropName::CurrentUserPrincipal => RootProp::CurrentUserPrincipal( - PrincipalResource::get_principal_url(rmap, &user.id).into(), - ), - RootPropName::CurrentUserPrivilegeSet => { - RootProp::CurrentUserPrivilegeSet(self.get_user_privileges(user)?) - } + _ => panic!("we shouldn't end up here"), }) } diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 5881329..8917d4b 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -20,3 +20,4 @@ log = { workspace = true } derive_more = { workspace = true } tracing = { workspace = true } tracing-actix-web = { workspace = true } +erased-serde = "0.4.5" diff --git a/crates/dav/src/extension.rs b/crates/dav/src/extension.rs new file mode 100644 index 0000000..656b30c --- /dev/null +++ b/crates/dav/src/extension.rs @@ -0,0 +1,133 @@ +use crate::resource::{Resource, ResourceProp, ResourcePropName}; +use actix_web::dev::ResourceMap; +use derive_more::derive::Deref; +use rustical_store::auth::User; +use std::str::FromStr; +use strum::VariantNames; + +pub trait ResourceExtension: Clone { + type PropName: ResourcePropName; + type Prop: ResourceProp; + type Error: Into + From; + + fn list_props() -> &'static [&'static str] { + Self::PropName::VARIANTS + } + + fn get_prop( + &self, + resource: &R, + rmap: &ResourceMap, + user: &User, + prop: Self::PropName, + ) -> Result; + + fn remove_prop(&mut self, _prop: &Self::PropName) -> Result<(), Self::Error> { + Err(crate::Error::PropReadOnly.into()) + } +} + +pub struct ResourceExtensionWrapper; + +pub trait BoxableExtension { + fn get_prop( + &self, + resource: &R, + rmap: &ResourceMap, + user: &User, + prop: R::PropName, + ) -> Result, R::Error>; + + fn propfind<'a>( + &self, + resource: &R, + props: Vec<&'a str>, + user: &User, + rmap: &ResourceMap, + ) -> Result<(Vec<&'a str>, Vec), R::Error>; + + fn list_props(&self) -> &'static [&'static str]; +} + +impl< + R: Resource, + RE: ResourceExtension< + R, + PropName: Into + TryFrom, + Prop: Into + TryFrom, + Error: Into, + >, + > BoxableExtension for RE +{ + fn get_prop( + &self, + resource: &R, + rmap: &ResourceMap, + user: &User, + prop: ::PropName, + ) -> Result, R::Error> { + let prop: RE::PropName = if let Ok(prop) = prop.try_into() { + prop + } else { + return Ok(None); + }; + + let prop = ResourceExtension::::get_prop(self, resource, rmap, user, prop) + .map_err(RE::Error::into)?; + Ok(Some(prop.into())) + } + + fn propfind<'a>( + &self, + resource: &R, + props: Vec<&'a str>, + user: &User, + rmap: &ResourceMap, + ) -> Result<(Vec<&'a str>, Vec), R::Error> { + let (valid_props, invalid_props): (Vec>, Vec>) = props + .into_iter() + .map(|prop| { + if let Ok(valid_prop) = RE::PropName::from_str(prop) { + (Some(valid_prop), None) + } else { + (None, Some(prop)) + } + }) + .unzip(); + let valid_props: Vec = valid_props.into_iter().flatten().collect(); + let invalid_props: Vec<&str> = invalid_props.into_iter().flatten().collect(); + + let prop_responses = valid_props + .into_iter() + .map(|prop| self.get_prop(resource, rmap, user, prop)) + .collect::, RE::Error>>() + .map_err(RE::Error::into)? + .into_iter() + .map(|prop| prop.into()) + .collect::>(); + + Ok((invalid_props, prop_responses)) + } + + fn list_props(&self) -> &'static [&'static str] { + Self::list_props() + } +} + +#[derive(Deref)] +pub struct BoxedExtension(Box>); + +impl BoxedExtension { + pub fn from_ext< + RE: ResourceExtension< + R, + PropName: Into + TryFrom, + Prop: Into + TryFrom, + > + 'static, + >( + ext: RE, + ) -> Self { + let boxed_ext: Box> = Box::new(ext); + BoxedExtension(boxed_ext) + } +} diff --git a/crates/dav/src/extensions/mod.rs b/crates/dav/src/extensions/mod.rs new file mode 100644 index 0000000..da2d5ee --- /dev/null +++ b/crates/dav/src/extensions/mod.rs @@ -0,0 +1,77 @@ +use std::marker::PhantomData; + +use actix_web::dev::ResourceMap; +use rustical_store::auth::User; +use serde::{Deserialize, Serialize}; +use strum::{EnumString, VariantNames}; + +use crate::{ + extension::ResourceExtension, + privileges::UserPrivilegeSet, + resource::{InvalidProperty, Resource}, + xml::HrefElement, +}; + +#[derive(Debug, Clone)] +pub struct CommonPropertiesExtension(PhantomData); + +impl Default for CommonPropertiesExtension { + fn default() -> Self { + Self(PhantomData) + } +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum CommonPropertiesProp { + // WebDAV Current Principal Extension (RFC 5397) + CurrentUserPrincipal(HrefElement), + + // WebDAV Access Control Protocol (RFC 3477) + CurrentUserPrivilegeSet(UserPrivilegeSet), + + #[serde(untagged)] + Invalid, +} + +impl InvalidProperty for CommonPropertiesProp { + fn invalid_property(&self) -> bool { + matches!(self, Self::Invalid) + } +} + +#[derive(EnumString, Debug, VariantNames, Clone)] +#[strum(serialize_all = "kebab-case")] +pub enum CommonPropertiesPropName { + CurrentUserPrincipal, + CurrentUserPrivilegeSet, +} + +impl ResourceExtension for CommonPropertiesExtension +where + R::PropName: TryInto, + R::Prop: From, +{ + type Prop = CommonPropertiesProp; + type PropName = CommonPropertiesPropName; + type Error = R::Error; + + fn get_prop( + &self, + resource: &R, + rmap: &ResourceMap, + user: &User, + prop: Self::PropName, + ) -> Result { + Ok(match prop { + CommonPropertiesPropName::CurrentUserPrincipal => { + CommonPropertiesProp::CurrentUserPrincipal( + PR::get_url(rmap, &[&user.id]).unwrap().into(), + ) + } + CommonPropertiesPropName::CurrentUserPrivilegeSet => { + CommonPropertiesProp::CurrentUserPrivilegeSet(resource.get_user_privileges(user)?) + } + }) + } +} diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index 753b78f..8b2b771 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -1,5 +1,7 @@ pub mod depth_header; pub mod error; +pub mod extension; +pub mod extensions; pub mod methods; pub mod namespace; pub mod privileges; diff --git a/crates/dav/src/methods/propfind.rs b/crates/dav/src/methods/propfind.rs index 17334a3..3692b18 100644 --- a/crates/dav/src/methods/propfind.rs +++ b/crates/dav/src/methods/propfind.rs @@ -1,5 +1,6 @@ use crate::depth_header::Depth; use crate::privileges::UserPrivilege; +use crate::resource::InvalidProperty; use crate::resource::Resource; use crate::resource::ResourceService; use crate::xml::multistatus::PropstatWrapper; diff --git a/crates/dav/src/resource.rs b/crates/dav/src/resource.rs index 0a2931b..b24c64b 100644 --- a/crates/dav/src/resource.rs +++ b/crates/dav/src/resource.rs @@ -1,3 +1,4 @@ +use crate::extension::BoxedExtension; use crate::methods::{route_delete, route_propfind, route_proppatch}; use crate::privileges::UserPrivilegeSet; use crate::xml::multistatus::{PropTagWrapper, PropstatElement, PropstatWrapper}; @@ -17,13 +18,31 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; use strum::VariantNames; +pub trait ResourceReadProp: Serialize + fmt::Debug + InvalidProperty {} +impl ResourceReadProp for T {} + +pub trait ResourceProp: ResourceReadProp + for<'de> Deserialize<'de> {} +impl Deserialize<'de>> ResourceProp for T {} + +pub trait ResourcePropName: FromStr + VariantNames {} +impl ResourcePropName for T {} + pub trait Resource: Clone { - type PropName: FromStr + VariantNames; - type Prop: Serialize + for<'de> Deserialize<'de> + fmt::Debug + InvalidProperty; + type PropName: ResourcePropName; + type Prop: ResourceProp; type Error: ResponseError + From; - fn list_props() -> &'static [&'static str] { + fn list_extensions() -> Vec> { + vec![] + } + + fn list_props() -> Vec<&'static str> { Self::PropName::VARIANTS + .iter() + .map(|&prop| prop) + // Bodge, since VariantNames somehow includes Ext... props despite the strum(disabled) flag + .filter(|prop| !prop.starts_with("ext-")) + .collect() } fn get_prop( @@ -74,10 +93,18 @@ pub trait Resource: Clone { Error::BadRequest("propname MUST be the only queried prop".to_owned()).into(), ); } - let props: Vec = Self::list_props() + let mut props: Vec = Self::list_props() .iter() .map(|&prop| prop.to_string()) .collect(); + for extension in Self::list_extensions() { + let ext_props: Vec = extension + .list_props() + .iter() + .map(|&prop| prop.to_string()) + .collect(); + props.extend(ext_props); + } return Ok(ResponseElement { href: path.to_owned(), propstat: vec![PropstatWrapper::TagList(PropstatElement { @@ -95,6 +122,10 @@ pub trait Resource: Clone { ); } props = Self::list_props().into(); + for extension in Self::list_extensions() { + let ext_props: Vec<&str> = extension.list_props().into(); + props.extend(ext_props); + } } let (valid_props, invalid_props): (Vec>, Vec>) = props @@ -108,13 +139,20 @@ pub trait Resource: Clone { }) .unzip(); let valid_props: Vec = valid_props.into_iter().flatten().collect(); - let invalid_props: Vec<&str> = invalid_props.into_iter().flatten().collect(); + let mut invalid_props: Vec<&str> = invalid_props.into_iter().flatten().collect(); - let prop_responses = valid_props + let mut prop_responses = valid_props .into_iter() .map(|prop| self.get_prop(rmap, user, &prop)) .collect::, Self::Error>>()?; + for extension in Self::list_extensions() { + let (ext_invalid_props, ext_responses) = + extension.propfind(self, invalid_props, user, rmap)?; + invalid_props = ext_invalid_props; + prop_responses.extend(ext_responses); + } + let mut propstats = vec![PropstatWrapper::Normal(PropstatElement { status: format!("HTTP/1.1 {}", StatusCode::OK), prop: PropTagWrapper {