234 lines
5.3 KiB
V
234 lines
5.3 KiB
V
module webdav
|
|
|
|
import encoding.xml
|
|
import log
|
|
import freeflowuniverse.herolib.core.pathlib
|
|
import freeflowuniverse.herolib.vfs
|
|
import freeflowuniverse.herolib.vfs.vfs_db
|
|
import os
|
|
import time
|
|
import net.http
|
|
import veb
|
|
|
|
// PropfindRequest represents a parsed PROPFIND request
|
|
pub struct PropfindRequest {
|
|
pub:
|
|
typ PropfindType
|
|
props []string // Property names if typ is prop
|
|
depth Depth // Depth of the request (0, 1, or -1 for infinity)
|
|
xml_content string // Original XML content
|
|
}
|
|
|
|
pub enum Depth {
|
|
infinity = -1
|
|
zero = 0
|
|
one = 1
|
|
}
|
|
|
|
// PropfindType represents the type of PROPFIND request
|
|
pub enum PropfindType {
|
|
allprop // Request all properties
|
|
propname // Request property names only
|
|
prop // Request specific properties
|
|
invalid // Invalid request
|
|
}
|
|
|
|
// parse_propfind_xml parses the XML body of a PROPFIND request
|
|
pub fn parse_propfind_xml(req http.Request) !PropfindRequest {
|
|
data := req.data
|
|
// Parse Depth header
|
|
depth_str := req.header.get_custom('Depth') or { '0' }
|
|
depth := parse_depth(depth_str)
|
|
|
|
if data.len == 0 {
|
|
// If no body is provided, default to allprop
|
|
return PropfindRequest{
|
|
typ: .allprop
|
|
depth: depth
|
|
xml_content: ''
|
|
}
|
|
}
|
|
|
|
doc := xml.XMLDocument.from_string(data) or { return error('Failed to parse XML: ${err}') }
|
|
|
|
root := doc.root
|
|
if root.name.to_lower() != 'propfind' && !root.name.ends_with(':propfind') {
|
|
return error('Invalid PROPFIND request: root element must be propfind')
|
|
}
|
|
|
|
mut typ := PropfindType.allprop
|
|
mut props := []string{}
|
|
|
|
// Check for allprop, propname, or prop elements
|
|
for child in root.children {
|
|
if child is xml.XMLNode {
|
|
node := child as xml.XMLNode
|
|
|
|
// Check for allprop
|
|
if node.name == 'allprop' || node.name == 'D:allprop' {
|
|
typ = .allprop
|
|
break
|
|
}
|
|
|
|
// Check for propname
|
|
if node.name == 'propname' || node.name == 'D:propname' {
|
|
typ = .propname
|
|
break
|
|
}
|
|
|
|
// Check for prop
|
|
if node.name == 'prop' || node.name == 'D:prop' {
|
|
typ = .prop
|
|
|
|
// Extract property names
|
|
for prop_child in node.children {
|
|
if prop_child is xml.XMLNode {
|
|
prop_node := prop_child as xml.XMLNode
|
|
props << prop_node.name
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if typ == .invalid {
|
|
return error('Invalid PROPFIND request: missing prop, allprop, or propname element')
|
|
}
|
|
|
|
return PropfindRequest{
|
|
typ: typ
|
|
props: props
|
|
depth: depth
|
|
xml_content: data
|
|
}
|
|
}
|
|
|
|
// parse_depth parses the Depth header value
|
|
pub fn parse_depth(depth_str string) Depth {
|
|
if depth_str == 'infinity' {
|
|
return .infinity
|
|
} else if depth_str == '0' {
|
|
return .zero
|
|
} else if depth_str == '1' {
|
|
return .one
|
|
} else {
|
|
log.warn('[WebDAV] Invalid Depth header value: ${depth_str}, defaulting to infinity')
|
|
return .infinity
|
|
}
|
|
}
|
|
|
|
// Response represents a WebDAV response for a resource
|
|
pub struct PropfindResponse {
|
|
pub:
|
|
href string
|
|
found_props []Property
|
|
not_found_props []Property
|
|
}
|
|
|
|
fn (r PropfindResponse) xml() xml.XMLNodeContents {
|
|
return xml.XMLNode{
|
|
name: 'D:response'
|
|
children: [
|
|
xml.XMLNode{
|
|
name: 'D:href'
|
|
children: [xml.XMLNodeContents(r.href)]
|
|
},
|
|
xml.XMLNode{
|
|
name: 'D:propstat'
|
|
children: [xml.XMLNode{
|
|
name: 'D:prop'
|
|
children: r.found_props.map(it.xml())
|
|
}, xml.XMLNode{
|
|
name: 'D:status'
|
|
children: [xml.XMLNodeContents('HTTP/1.1 200 OK')]
|
|
}]
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
// generate_propfind_response generates a PROPFIND response XML string from Response structs
|
|
pub fn (r []PropfindResponse) xml() string {
|
|
// Create multistatus root node
|
|
multistatus_node := xml.XMLNode{
|
|
name: 'D:multistatus'
|
|
attributes: {
|
|
'xmlns:D': 'DAV:'
|
|
}
|
|
children: r.map(it.xml())
|
|
}
|
|
|
|
// Create a new XML document with the root node
|
|
doc := xml.XMLDocument{
|
|
version: '1.0'
|
|
root: multistatus_node
|
|
}
|
|
|
|
// Generate XML string
|
|
doc.validate() or { panic('this should never happen ${err}') }
|
|
return format_xml(doc.str())
|
|
}
|
|
|
|
fn get_file_content_type(path string) string {
|
|
ext := path.all_after_last('.')
|
|
content_type := if v := veb.mime_types[ext] {
|
|
v
|
|
} else {
|
|
'text/plain; charset=utf-8'
|
|
}
|
|
|
|
return content_type
|
|
}
|
|
|
|
// parse_xml takes an XML string and returns a cleaned version with whitespace removed between tags
|
|
pub fn format_xml(xml_str string) string {
|
|
mut result := ''
|
|
mut i := 0
|
|
mut in_tag := false
|
|
mut content_start := 0
|
|
|
|
// Process the string character by character
|
|
for i < xml_str.len {
|
|
ch := xml_str[i]
|
|
|
|
// Start of a tag
|
|
if ch == `<` {
|
|
// If we were collecting content between tags, process it
|
|
if !in_tag && i > content_start {
|
|
// Get the content between tags and trim whitespace
|
|
content := xml_str[content_start..i].trim_space()
|
|
result += content
|
|
}
|
|
|
|
in_tag = true
|
|
result += '<'
|
|
}
|
|
// End of a tag
|
|
else if ch == `>` {
|
|
in_tag = false
|
|
result += '>'
|
|
content_start = i + 1
|
|
}
|
|
// Inside a tag - preserve all characters including whitespace
|
|
else if in_tag {
|
|
result += ch.ascii_str()
|
|
}
|
|
// Outside a tag - only add non-whitespace or handle whitespace in content
|
|
else if !in_tag {
|
|
// We'll collect and process this content when we reach the next tag
|
|
// or at the end of the string
|
|
}
|
|
|
|
i++
|
|
}
|
|
|
|
// Handle any remaining content at the end of the string
|
|
if !in_tag && content_start < xml_str.len {
|
|
content := xml_str[content_start..].trim_space()
|
|
result += content
|
|
}
|
|
|
|
return result
|
|
}
|