Skip to main content

nextcloud_dav/propfind/
request_body.rs

1//! The `<d:propfind>` document a `PROPFIND` request carries.
2
3use crate::xml::XmlWriter;
4
5use super::{DavNamespace, DavProperty};
6
7/// A `PROPFIND` request body asking for a set of properties.
8///
9/// Namespace declarations are emitted here rather than by the writer, because
10/// they belong to this document shape: `oc` and `nc` are only needed when a
11/// property from that namespace is asked for.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct PropfindBody {
14    properties: Vec<DavProperty>,
15}
16
17impl PropfindBody {
18    /// Asks for exactly `properties`.
19    pub fn new(properties: impl IntoIterator<Item = DavProperty>) -> Self {
20        Self {
21            properties: properties.into_iter().collect(),
22        }
23    }
24
25    /// Asks for every property this crate decodes.
26    pub fn all() -> Self {
27        Self::new([
28            DavProperty::Etag,
29            DavProperty::ContentLength,
30            DavProperty::ContentType,
31            DavProperty::ResourceType,
32            DavProperty::LastModified,
33            DavProperty::DisplayName,
34            DavProperty::FileId,
35            DavProperty::Permissions,
36            DavProperty::HasPreview,
37        ])
38    }
39
40    /// Properties this body asks for.
41    pub fn properties(&self) -> &[DavProperty] {
42        &self.properties
43    }
44
45    /// Whether nothing is asked for, in which case the request would be
46    /// pointless.
47    pub fn is_empty(&self) -> bool {
48        self.properties.is_empty()
49    }
50
51    /// Renders the document, namespace declarations included.
52    pub fn to_xml(&self) -> String {
53        let mut writer = XmlWriter::new();
54        writer.prolog();
55        writer.open_with("d:propfind", &self.namespace_declarations());
56        writer.open("d:prop");
57        for property in &self.properties {
58            writer.empty(property.qualified_name());
59        }
60        writer.close("d:prop");
61        writer.close("d:propfind");
62        writer.into_string()
63    }
64
65    /// `DAV:` is always declared; the Nextcloud namespaces only when used, so
66    /// a request for core properties stays free of unrelated declarations.
67    fn namespace_declarations(&self) -> Vec<(&'static str, &'static str)> {
68        let mut declarations: Vec<(&'static str, &'static str)> = Vec::new();
69        for namespace in self.properties.iter().map(|p| p.namespace()) {
70            if declarations
71                .iter()
72                .any(|(key, _)| *key == declaration_key(namespace))
73            {
74                continue;
75            }
76            declarations.push((declaration_key(namespace), namespace.uri()));
77        }
78        if declarations.is_empty() {
79            let namespace = DavNamespace::Dav;
80            declarations.push((declaration_key(namespace), namespace.uri()));
81        }
82        declarations
83    }
84}
85
86/// Attribute name declaring `namespace`'s prefix.
87const fn declaration_key(namespace: DavNamespace) -> &'static str {
88    match namespace {
89        DavNamespace::Dav => "xmlns:d",
90        DavNamespace::OwnCloud => "xmlns:oc",
91        DavNamespace::Nextcloud => "xmlns:nc",
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::PropfindBody;
98    use crate::propfind::DavProperty;
99
100    #[test]
101    fn every_preset_contains_the_three_namespace_declarations() {
102        let xml = PropfindBody::all().to_xml();
103        assert!(xml.contains(r#"xmlns:d="DAV:""#), "{xml}");
104        assert!(
105            xml.contains(r#"xmlns:oc="http://owncloud.org/ns""#),
106            "{xml}"
107        );
108        assert!(
109            xml.contains(r#"xmlns:nc="http://nextcloud.org/ns""#),
110            "{xml}"
111        );
112    }
113
114    #[test]
115    fn preset_asks_for_every_property_once() {
116        let xml = PropfindBody::all().to_xml();
117        assert_eq!(xml.matches("<d:getetag/>").count(), 1);
118        assert!(xml.contains("<oc:fileid/>"));
119        assert!(xml.contains("<nc:has-preview/>"));
120        assert_eq!(xml.matches("xmlns:d=").count(), 1);
121    }
122
123    #[test]
124    fn body_is_well_formed_and_carries_the_requested_properties() {
125        let xml = PropfindBody::new([DavProperty::Etag, DavProperty::FileId]).to_xml();
126        assert!(xml.starts_with("<?xml version=\"1.0\" encoding=\"utf-8\"?>"));
127        assert!(xml.contains("<d:propfind"));
128        assert!(xml.contains("<d:prop><d:getetag/><oc:fileid/></d:prop>"));
129        assert!(xml.ends_with("</d:propfind>"));
130        assert_eq!(xml.matches("<d:prop>").count(), 1);
131        assert_eq!(xml.matches("</d:propfind>").count(), 1);
132        assert!(!xml.contains("xmlns:nc"));
133    }
134
135    #[test]
136    fn core_only_body_still_declares_the_dav_namespace() {
137        let xml = PropfindBody::new([DavProperty::Etag]).to_xml();
138        assert!(xml.contains(r#"xmlns:d="DAV:""#));
139        assert!(!xml.contains("xmlns:oc"));
140    }
141
142    #[test]
143    fn empty_body_is_reported_and_still_renders_a_document() {
144        let body = PropfindBody::new([]);
145        assert!(body.is_empty());
146        assert!(body.to_xml().contains("<d:prop></d:prop>"));
147    }
148}