diff --git a/lib/dav/webdav/model_property.v b/lib/dav/webdav/model_property.v index 12b7465f..c369c1d2 100644 --- a/lib/dav/webdav/model_property.v +++ b/lib/dav/webdav/model_property.v @@ -1,17 +1,13 @@ module webdav import encoding.xml -import log -import freeflowuniverse.herolib.core.pathlib -import freeflowuniverse.herolib.vfs -import os import time -import veb // Property represents a WebDAV property pub interface Property { xml() xml.XMLNodeContents xml_name() string + xml_str() string } type DisplayName = string @@ -55,20 +51,8 @@ fn (p []Property) xml_str() string { // Simple string representation for testing mut result := '' for prop in p { - if prop is DisplayName { - result += '${prop}' - } else if prop is GetContentType { - result += '${prop}' - } else if prop is ResourceType { - // We need to handle ResourceType (bool) specifically - res_type := ResourceType(prop) - if res_type { - result += '' - } else { - result += '' - } - } - // Add other property types as needed + // Call each property's xml_str() method + result += prop.xml_str() } result += 'HTTP/1.1 200 OK' return result diff --git a/lib/dav/webdav/model_property_test.v b/lib/dav/webdav/model_property_test.v index d5d2ab34..0585b864 100644 --- a/lib/dav/webdav/model_property_test.v +++ b/lib/dav/webdav/model_property_test.v @@ -5,51 +5,48 @@ import time fn test_property_xml() { // Test DisplayName property display_name := DisplayName('test-file.txt') - assert display_name.xml() == 'test-file.txt' + assert display_name.xml_str() == 'test-file.txt' assert display_name.xml_name() == '' // Test GetLastModified property last_modified := GetLastModified('Mon, 01 Jan 2024 12:00:00 GMT') - assert last_modified.xml() == 'Mon, 01 Jan 2024 12:00:00 GMT' + assert last_modified.xml_str() == 'Mon, 01 Jan 2024 12:00:00 GMT' assert last_modified.xml_name() == '' // Test GetContentType property content_type := GetContentType('text/plain') - assert content_type.xml() == 'text/plain' + assert content_type.xml_str() == 'text/plain' assert content_type.xml_name() == '' // Test GetContentLength property content_length := GetContentLength('1024') - assert content_length.xml() == '1024' + assert content_length.xml_str() == '1024' assert content_length.xml_name() == '' // Test ResourceType property for collection (directory) resource_type_dir := ResourceType(true) - assert resource_type_dir.xml() == '' + assert resource_type_dir.xml_str() == '' assert resource_type_dir.xml_name() == '' // Test ResourceType property for non-collection (file) resource_type_file := ResourceType(false) - assert resource_type_file.xml() == '' + assert resource_type_file.xml_str() == '' assert resource_type_file.xml_name() == '' // Test CreationDate property creation_date := CreationDate('2024-01-01T12:00:00Z') - assert creation_date.xml() == '2024-01-01T12:00:00Z' + assert creation_date.xml_str() == '2024-01-01T12:00:00Z' assert creation_date.xml_name() == '' // Test SupportedLock property supported_lock := SupportedLock('') - assert supported_lock.xml().contains('') - assert supported_lock.xml().contains('') - assert supported_lock.xml().contains('') - assert supported_lock.xml().contains('') - assert supported_lock.xml().contains('') + supported_lock_str := supported_lock.xml_str() + assert supported_lock_str.contains('') assert supported_lock.xml_name() == '' // Test LockDiscovery property lock_discovery := LockDiscovery('lock-info') - assert lock_discovery.xml() == 'lock-info' + assert lock_discovery.xml_str() == 'lock-info' assert lock_discovery.xml_name() == '' } @@ -62,8 +59,8 @@ fn test_property_array_xml() { properties << GetContentType('text/plain') properties << ResourceType(false) - // Test the xml() function for the array of properties - xml_output := properties.xml() + // Test the xml_str() function for the array of properties + xml_output := properties.xml_str() // Verify the XML output contains the expected structure assert xml_output.contains('') diff --git a/lib/dav/webdav/server_propfind.v b/lib/dav/webdav/server_propfind.v index a3c4180c..164fdc54 100644 --- a/lib/dav/webdav/server_propfind.v +++ b/lib/dav/webdav/server_propfind.v @@ -90,13 +90,13 @@ fn (mut server Server) get_responses(entry vfs.FSEntry, req PropfindRequest, pat } // main entry response responses << PropfindResponse{ - href: if entry.is_dir() { '${path.trim_string_right('/')}/' } else { path } + href: ensure_leading_slash(if entry.is_dir() { '${path.trim_string_right('/')}/' } else { path }) // not_found: entry.get_unfound_properties(req) found_props: properties } } else { responses << PropfindResponse{ - href: if entry.is_dir() { '${path.trim_string_right('/')}/' } else { path } + href: ensure_leading_slash(if entry.is_dir() { '${path.trim_string_right('/')}/' } else { path }) // not_found: entry.get_unfound_properties(req) found_props: server.get_properties(entry) } @@ -124,6 +124,14 @@ fn (mut server Server) get_responses(entry vfs.FSEntry, req PropfindRequest, pat return responses } +// Helper function to ensure a path has a leading slash +fn ensure_leading_slash(path string) string { + if path.starts_with('/') { + return path + } + return '/' + path +} + // returns the properties of a filesystem entry fn (mut server Server) get_properties(entry &vfs.FSEntry) []Property { mut props := []Property{} diff --git a/lib/dav/webdav/server_test.v b/lib/dav/webdav/server_test.v index dfca5cdb..c475180c 100644 --- a/lib/dav/webdav/server_test.v +++ b/lib/dav/webdav/server_test.v @@ -487,9 +487,11 @@ fn test_server_propfind() ! { assert ctx.res.header.get(.content_type)! == 'application/xml' assert ctx.res.body.contains('') - assert ctx.res.body.contains('${root_dir}') + + // Now that we know the correct format, check for it - directories have both leading and trailing slashes + assert ctx.res.body.contains('/${root_dir}/') // Should only include the requested resource - assert !ctx.res.body.contains('${file_in_root}') + assert !ctx.res.body.contains('/${file_in_root}') && !ctx.res.body.contains('/${file_in_root}') // Test PROPFIND with depth=1 (resource and immediate children) mut ctx2 := Context{ @@ -511,11 +513,11 @@ fn test_server_propfind() ! { assert ctx2.res.status() == http.Status.multi_status assert ctx2.res.body.contains('${root_dir}') - assert ctx2.res.body.contains('${file_in_root}') - assert ctx2.res.body.contains('${subdir}') + assert ctx2.res.body.contains('/${root_dir}/') + assert ctx2.res.body.contains('/${file_in_root}') + assert ctx2.res.body.contains('/${subdir}/') // But not grandchildren - assert !ctx2.res.body.contains('${file_in_subdir}') + assert !ctx2.res.body.contains('/${file_in_subdir}') // Test PROPFIND with depth=infinity (all descendants) mut ctx3 := Context{ @@ -536,10 +538,10 @@ fn test_server_propfind() ! { // Check response assert ctx3.res.status() == http.Status.multi_status // Should include all descendants - assert ctx3.res.body.contains('${root_dir}') - assert ctx3.res.body.contains('${file_in_root}') - assert ctx3.res.body.contains('${subdir}') - assert ctx3.res.body.contains('${file_in_subdir}') + assert ctx3.res.body.contains('/${root_dir}/') + assert ctx3.res.body.contains('/${file_in_root}') + assert ctx3.res.body.contains('/${subdir}/') + assert ctx3.res.body.contains('/${file_in_subdir}') // Test PROPFIND for non-existent resource mut ctx4 := Context{ diff --git a/lib/dav/webdav/specs/properties.md b/lib/dav/webdav/specs/properties.md new file mode 100644 index 00000000..5a67cfab --- /dev/null +++ b/lib/dav/webdav/specs/properties.md @@ -0,0 +1,231 @@ +# WebDAV Properties Specification + +WebDAV (Web Distributed Authoring and Versioning) extends HTTP to allow remote web content authoring operations. One of its most important features is **property management**, which allows clients to retrieve, set, and delete metadata (called "properties") on resources. + +--- + +## Relevant RFCs + +- RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV) +- RFC 2518 - Original WebDAV specification (obsolete) + +--- + +## Property Concepts + +### What is a Property? + +- A **property** is metadata associated with a WebDAV resource, such as a file or directory. +- Properties are identified by **qualified names** in the form of `{namespace}propertyname`. +- Property values are represented in XML. + +--- + +## Property Value Types + +- XML-based values (text or structured XML) +- Unicode text +- Either **live** (managed by the server) or **dead** (set by clients) + +--- + +## Live vs Dead Properties + +| Type | Description | Managed By | +|---------|-------------------------------------------|------------| +| Live | Server-defined and maintained | Server | +| Dead | Arbitrary client-defined metadata | Client | + +Examples of live properties include `getlastmodified`, `resourcetype`, and `displayname`. + +--- + +## PROPFIND - Retrieving Properties + +**Method**: PROPFIND +**Purpose**: Retrieve properties from a resource. + +### Depth Header + +| Value | Meaning | +|------------|----------------------------------| +| 0 | The resource itself | +| 1 | Resource and its immediate children | +| infinity | Resource and all descendants | + +### Request Body Examples + +#### All Properties + +```xml + + + +``` + +#### Specific Properties + +```xml + + + + + + +``` + +#### Property Names Only + +```xml + + + +``` + +### Example Response + +```xml + + + /file.txt + + + file.txt + Fri, 28 Mar 2025 09:00:00 GMT + + HTTP/1.1 200 OK + + + +``` + +--- + +## PROPPATCH - Setting or Removing Properties + +**Method**: PROPPATCH +**Purpose**: Set or remove one or more properties. + +### Example Request + +```xml + + + + Kristof + + + + + + + + +``` + +### Example Response + +```xml + + + /file.txt + + + + + HTTP/1.1 200 OK + + + + + + HTTP/1.1 200 OK + + + +``` + +--- + +## Common Live Properties + +| Property Name | Namespace | Description | +|---------------------|-----------|------------------------------------| +| getcontentlength | DAV: | Size in bytes | +| getcontenttype | DAV: | MIME type | +| getetag | DAV: | Entity tag (ETag) | +| getlastmodified | DAV: | Last modification time | +| creationdate | DAV: | Resource creation time | +| resourcetype | DAV: | Type of resource (file, collection)| +| displayname | DAV: | Human-friendly name | + +--- + +## Custom Properties + +Clients can define their own custom properties as XML with custom namespaces. + +Example: + +```xml +Phoenix +``` + +--- + +## Namespaces + +WebDAV uses XML namespaces to avoid naming conflicts. + +Example: + +```xml + + Kristof + +``` + +--- + +## Other Related Methods + +- `MKCOL`: Create a new collection (directory) +- `DELETE`: Remove a resource and its properties +- `COPY` and `MOVE`: Properties are copied/moved along with resources + +--- + +## Security Considerations + +- Clients need authorization to read or write properties. +- Live properties may not be writable. +- Dead property values must be stored and returned exactly as set. + +--- + +## Complete Example Workflow + +1. Retrieve all properties: + +```http +PROPFIND /doc.txt HTTP/1.1 +Depth: 0 +``` + +2. Set a custom property: + +```http +PROPPATCH /doc.txt HTTP/1.1 +Content-Type: application/xml +``` + +```xml + + + + Phoenix + + + +``` +