use crate::extension::BoxedExtension; use crate::methods::{route_delete, route_propfind, route_proppatch}; use crate::privileges::UserPrivilegeSet; use crate::xml::multistatus::{PropTagWrapper, PropstatElement, PropstatWrapper}; use crate::xml::{multistatus::ResponseElement, TagList}; use crate::Error; use actix_web::dev::ResourceMap; use actix_web::error::UrlGenerationError; use actix_web::http::Method; use actix_web::test::TestRequest; use actix_web::web; use actix_web::{http::StatusCode, HttpRequest, ResponseError}; use async_trait::async_trait; use itertools::Itertools; use rustical_store::auth::User; use serde::{Deserialize, Serialize}; use std::str::FromStr; use strum::VariantNames; pub trait ResourceReadProp: Serialize + 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: ResourcePropName; type Prop: ResourceProp; type Error: ResponseError + From; type ResourceType: Default + Serialize + for<'de> Deserialize<'de>; 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( &self, rmap: &ResourceMap, user: &User, prop: &Self::PropName, ) -> Result; fn set_prop(&mut self, _prop: Self::Prop) -> Result<(), crate::Error> { Err(crate::Error::PropReadOnly) } fn remove_prop(&mut self, _prop: &Self::PropName) -> Result<(), crate::Error> { Err(crate::Error::PropReadOnly) } fn resource_name() -> &'static str; fn get_url(rmap: &ResourceMap, elements: U) -> Result where U: IntoIterator, I: AsRef, { Ok(rmap .url_for( &TestRequest::default().to_http_request(), Self::resource_name(), elements, )? .path() .to_owned()) } fn get_user_privileges(&self, user: &User) -> Result; fn propfind( &self, path: &str, mut props: Vec<&str>, user: &User, rmap: &ResourceMap, ) -> Result>, Self::Error> { if props.contains(&"propname") { if props.len() != 1 { // propname MUST be the only queried prop per spec return Err( Error::BadRequest("propname MUST be the only queried prop".to_owned()).into(), ); } 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 { prop: TagList::from(props), status: format!("HTTP/1.1 {}", StatusCode::OK), })], ..Default::default() }); } if props.contains(&"allprop") { if props.len() != 1 { // allprop MUST be the only queried prop per spec return Err( Error::BadRequest("allprop MUST be the only queried prop".to_owned()).into(), ); } 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 .into_iter() .map(|prop| { if let Ok(valid_prop) = Self::PropName::from_str(prop) { (Some(valid_prop), None) } else { (None, Some(prop)) } }) .unzip(); let valid_props: Vec = valid_props.into_iter().flatten().collect(); let mut invalid_props: Vec<&str> = invalid_props.into_iter().flatten().collect(); 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 { prop: prop_responses, }, })]; if !invalid_props.is_empty() { propstats.push(PropstatWrapper::TagList(PropstatElement { status: format!("HTTP/1.1 {}", StatusCode::NOT_FOUND), prop: invalid_props .into_iter() .map(|s| s.to_owned()) .collect_vec() .into(), })); } Ok(ResponseElement { href: path.to_owned(), propstat: propstats, ..Default::default() }) } } pub trait InvalidProperty { fn invalid_property(&self) -> bool; } #[async_trait(?Send)] pub trait ResourceService: Sized + 'static { type MemberType: Resource; type PathComponents: for<'de> Deserialize<'de> + Sized + Clone + 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String) type Resource: Resource; type Error: ResponseError + From; async fn new( req: &HttpRequest, path_components: Self::PathComponents, ) -> Result; async fn get_members( &self, _rmap: &ResourceMap, ) -> Result, Self::Error> { Ok(vec![]) } async fn get_resource(&self) -> Result; async fn save_resource(&self, file: Self::Resource) -> Result<(), Self::Error>; async fn delete_resource(&self, _use_trashbin: bool) -> Result<(), Self::Error> { Err(crate::Error::Unauthorized.into()) } #[inline] fn resource_name() -> &'static str { Self::Resource::resource_name() } #[inline] fn actix_resource() -> actix_web::Resource { Self::actix_additional_routes( web::resource("") .name(Self::resource_name()) .route( web::method(Method::from_str("PROPFIND").unwrap()).to(route_propfind::), ) .route( web::method(Method::from_str("PROPPATCH").unwrap()).to(route_proppatch::), ) .delete(route_delete::), ) } /// Hook for other resources to insert their additional methods (i.e. REPORT, MKCALENDAR) #[inline] fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource { res } }