Sun, 24 Apr 2022 18:35:44 +0200
add semi functional pg propfind handler
--- a/doc/development/postgresql_vfs.sql Thu Apr 21 17:16:49 2022 +0200 +++ b/doc/development/postgresql_vfs.sql Sun Apr 24 18:35:44 2022 +0200 @@ -14,3 +14,16 @@ unique(parent_id, nodename) ); +create table Property ( + property_id serial primary key, + resource_id int references Resource(resource_id) on delete cascade, + xmlns text not null, + pname text not null, + pvalue text +); + +create type property_name as ( + xmlns text, + name text +); +
--- a/src/server/plugins/postgresql/pgtest.c Thu Apr 21 17:16:49 2022 +0200 +++ b/src/server/plugins/postgresql/pgtest.c Sun Apr 24 18:35:44 2022 +0200 @@ -27,19 +27,27 @@ */ #include <stdio.h> +#include <inttypes.h> #include "../../util/util.h" #include "../../test/testutils.h" +#include "../../test/webdav.h" #include "../../public/nsapi.h" +#include "../../public/webdav.h" +#include "../../webdav/webdav.h" #include <ucx/string.h> #include <ucx/utils.h> +#include <ucx/buffer.h> #include "pgtest.h" #include "vfs.h" +#include "webdav.h" #include <libpq-fe.h> +#include <libxml/tree.h> + static char *pg_connstr = "postgresql://localhost/test1"; static int abort_pg_tests = 0; @@ -83,6 +91,10 @@ ucx_test_register(suite, test_pg_vfs_unlink); ucx_test_register(suite, test_pg_vfs_rmdir); + ucx_test_register(suite, test_pg_webdav_create_from_resdata); + ucx_test_register(suite, test_pg_prepare_tests); + ucx_test_register(suite, test_pg_webdav_propfind); + PGresult *result = PQexec(test_connection, "BEGIN"); PQclear(result); } @@ -331,3 +343,137 @@ testutil_destroy_session(sn); } + +/* ----------------------------- WebDAV tests ----------------------------- */ + + +static WebdavBackend* create_test_pgdav(Session *sn, Request *rq) { + return pg_webdav_create_from_resdata(sn, rq, &resdata); +} + +UCX_TEST(test_pg_webdav_create_from_resdata) { + Session *sn = testutil_session(); + Request *rq = testutil_request(sn->pool, "PROPFIND", "/"); + + UCX_TEST_BEGIN; + + WebdavBackend *dav = create_test_pgdav(sn, rq); + UCX_TEST_ASSERT(dav, "cannot create pg dav backend"); + + UCX_TEST_END; +} + +UCX_TEST(test_pg_prepare_tests) { + Session *sn = testutil_session(); + Request *rq = testutil_request(sn->pool, "PUT", "/"); + rq->vfs = create_test_pgvfs(sn, rq); + VFSContext *vfs = vfs_request_context(sn, rq); + + UCX_TEST_BEGIN; + + vfs_mkdir(vfs, "/propfind"); + SYS_FILE f1; + + int64_t res1_id, res2_id; + + f1 = vfs_open(vfs, "/propfind/res1", O_WRONLY|O_CREAT); + UCX_TEST_ASSERT(f1, "res1 create failed"); + res1_id = ((PgFile*)f1->data)->resource_id; + vfs_close(f1); + + f1 = vfs_open(vfs, "/propfind/res2", O_WRONLY|O_CREAT); + UCX_TEST_ASSERT(f1, "res2 create failed"); + res2_id = ((PgFile*)f1->data)->resource_id; + vfs_close(f1); + + f1 = vfs_open(vfs, "/propfind/res3", O_WRONLY|O_CREAT); + UCX_TEST_ASSERT(f1, "res3 create failed"); + vfs_close(f1); + + // 2 properties for res1 + char idstr[32]; + snprintf(idstr, 32, "%" PRId64, res1_id); + const char* params[1] = { idstr }; + PGresult *result = PQexecParams( + test_connection, + "insert into Property(resource_id, xmlns, pname, pvalue) values ($1, 'http://example.com/', 'test', 'testvalue');", + 1, // number of parameters + NULL, + params, // parameter value + NULL, + NULL, + 0); // 0: result in text format + + UCX_TEST_ASSERT(PQresultStatus(result) == PGRES_COMMAND_OK, "cannot create property 1"); + PQclear(result); + + result = PQexecParams( + test_connection, + "insert into Property(resource_id, xmlns, pname, pvalue) values ($1, 'http://example.com/', 'prop2', 'value2');", + 1, // number of parameters + NULL, + params, // parameter value + NULL, + NULL, + 0); // 0: result in text format + + UCX_TEST_ASSERT(PQresultStatus(result) == PGRES_COMMAND_OK, "cannot create property 1"); + PQclear(result); + + // 1 property for res2 + snprintf(idstr, 32, "%" PRId64, res2_id); + result = PQexecParams( + test_connection, + "insert into Property(resource_id, xmlns, pname, pvalue) values ($1, 'http://example.com/', 'test', 'res2test');", + 1, // number of parameters + NULL, + params, // parameter value + NULL, + NULL, + 0); // 0: result in text format + + UCX_TEST_ASSERT(PQresultStatus(result) == PGRES_COMMAND_OK, "cannot create property 1"); + PQclear(result); + + UCX_TEST_END; + + testutil_destroy_session(sn); +} + +UCX_TEST(test_pg_webdav_propfind) { + Session *sn; + Request *rq; + TestIOStream *st; + pblock *pb; + + UCX_TEST_BEGIN; + + // test data: + // + // /propfind/ + // /propfind/res1 (2 properties) + // /propfind/res2 (1 property) + // /propfind/res3 + + int ret; + // Test 1 + init_test_webdav_method(&sn, &rq, &st, &pb, "PROPFIND", "/propfind", PG_TEST_PROPFIND1); + rq->davCollection = create_test_pgdav(sn, rq); + + ret = webdav_propfind(pb, sn, rq); + + UCX_TEST_ASSERT(ret == REQ_PROCEED, "webdav_propfind (1) failed"); + + xmlDoc *doc = xmlReadMemory( + st->buf->space, st->buf->size, NULL, NULL, 0); + UCX_TEST_ASSERT(doc, "propfind1: response is not valid xml"); + + printf("\n\n%.*s\n", (int)st->buf->size, st->buf->space); + + + testutil_destroy_session(sn); + xmlFreeDoc(doc); + testutil_iostream_destroy(st); + + UCX_TEST_END; +}
--- a/src/server/plugins/postgresql/pgtest.h Thu Apr 21 17:16:49 2022 +0200 +++ b/src/server/plugins/postgresql/pgtest.h Sun Apr 24 18:35:44 2022 +0200 @@ -29,6 +29,26 @@ UCX_TEST(test_pg_vfs_unlink); UCX_TEST(test_pg_vfs_rmdir); +UCX_TEST(test_pg_webdav_create_from_resdata); +UCX_TEST(test_pg_prepare_tests); +UCX_TEST(test_pg_webdav_propfind); + + +/* --------------------------- PROPFIND --------------------------- */ + +#define PG_TEST_PROPFIND1 "<?xml version=\"1.0\" encoding=\"utf-8\" ?> \ + <D:propfind xmlns:D=\"DAV:\"> \ + <D:prop> \ + <D:displayname/> \ + <D:getcontentlength/> \ + <D:getcontenttype/> \ + <D:getlastmodified/> \ + <D:resourcetype/> \ + <D:getetag/> \ + </D:prop> \ + </D:propfind>" + + #ifdef __cplusplus } #endif
--- a/src/server/plugins/postgresql/webdav.c Thu Apr 21 17:16:49 2022 +0200 +++ b/src/server/plugins/postgresql/webdav.c Sun Apr 24 18:35:44 2022 +0200 @@ -27,6 +27,11 @@ */ #include "webdav.h" +#include "vfs.h" + +#include "../../util/util.h" + +#include <libxml/tree.h> static WebdavBackend pg_webdav_backend = { pg_dav_propfind_init, @@ -43,6 +48,162 @@ NULL }; + +/* + * SQL Queries + */ + +// propfind with depth = 0 +// params: $1: resource_id +static const char *sql_propfind_allprop_depth0 = "\ +select\n\ + NULL as ppath,\n\ + r.resource_id,\n\ + r.parent_id,\n\ + r.nodename,\n\ + r.iscollection,\n\ + r.lastmodified,\n\ + r.creationdate,\n\ + r.contentlength,\n\ + p.xmlns,\n\ + p.pname,\n\ + p.pvalue\n\ +from Resource r\n\ +left join Property p on r.resource_id = p.resource_id\n\ +where r.resource_id = $1;"; + +// propfind with depth = 0 for specific properties +// params: $1: resource_id +static const char *sql_propfind_depth0 = "\ +select\n\ + NULL as ppath,\n\ + r.resource_id,\n\ + r.parent_id,\n\ + r.nodename,\n\ + r.iscollection,\n\ + r.lastmodified,\n\ + r.creationdate,\n\ + r.contentlength,\n\ + p.xmlns,\n\ + p.pname,\n\ + p.pvalue\n\ +from Resource r\n\ +left join Property p on r.resource_id = p.resource_id\n\ +inner join (select unnest($2::text[]) as xmlns, unnest($3::text[]) as pname) n\n\ + on ( p.xmlns = n.xmlns and p.pname = n.pname ) or p.property_id is null\n\ +where r.resource_id = $1;"; + +// propfind with depth = 1 +// params: $1: resource_id +static const char *sql_propfind_allprop_depth1 = "\ +select\n\ + NULL ass ppath,\n\ + r.resource_id,\n\ + r.parent_id,\n\ + r.nodename,\n\ + r.iscollection,\n\ + r.lastmodified,\n\ + r.creationdate,\n\ + r.contentlength,\n\ + p.xmlns,\n\ + p.pname,\n\ + p.pvalue\n\ +from Resource r\n\ +left join Property p on r.resource_id = p.resource_id\n\ +where r.resource_id = $1 or r.parent_id = $1\n\ +order by order by case when resource_id = $1 then 0 else 1 end, nodename, resource_id;"; + + +// propfind with depth = 1 for specific properties +// params: $1: resource_id +static const char *sql_propfind_depth1 = "\ +select\n\ + NULL as ppath,\n\ + r.resource_id,\n\ + r.parent_id,\n\ + r.nodename,\n\ + r.iscollection,\n\ + r.lastmodified,\n\ + r.creationdate,\n\ + r.contentlength,\n\ + p.xmlns,\n\ + p.pname,\n\ + p.pvalue\n\ +from Resource r\n\ +left join Property p on r.resource_id = p.resource_id\n\ +inner join (select unnest($2::text[]) as xmlns, unnest($3::text[]) as pname) n\n\ + on ( p.xmlns = n.xmlns and p.pname = n.pname ) or p.property_id is null\n\ +where r.resource_id = $1 or r.parent_id = $1\n\ +order by order by case when resource_id = $1 then 0 else 1 end, nodename, resource_id;"; + +// recursive propfind +// params: $1: resource_id +static const char *sql_propfind_allprop_recursive = "\ +with recursive resolvepath as (\n\ + select\n\ + '' as ppath,\n\ + *\n\ + from Resource\n\ + where resource_id = $1 \n\ + union\n\ + select\n\ + p.ppath || '/' || r.nodename,\n\ + r.*\n\ + from Resource r\n\ + inner join resolvepath p on r.parent_id = p.resource_id\n\ + )\n\ +select\n\ + r.ppath,\n\ + r.resource_id,\n\ + r.parent_id,\n\ + r.nodename,\n\ + r.iscollection,\n\ + r.lastmodified,\n\ + r.creationdate,\n\ + r.contentlength,\n\ + p.xmlns,\n\ + p.pname,\n\ + p.pvalue\n\ + from resolvepath r\n\ +left join Property p on r.resource_id = p.resource_id\n\ +order by replace(ppath, '/', chr(1)), resource_id;"; + +// recursive propfind for specific properties +// params: $1: resource_id +// $2: array of property xmlns +// $3: array of property names +static const char *sql_propfind_recursive = "\ +with recursive resolvepath as (\n\ + select\n\ + '' as ppath,\n\ + *\n\ + from Resource\n\ + where resource_id = $1 \n\ + union\n\ + select\n\ + p.ppath || '/' || r.nodename,\n\ + r.*\n\ + from Resource r\n\ + inner join resolvepath p on r.parent_id = p.resource_id\n\ + )\n\ +select\n\ + r.ppath,\n\ + r.resource_id,\n\ + r.parent_id,\n\ + r.nodename,\n\ + r.iscollection,\n\ + r.lastmodified,\n\ + r.creationdate,\n\ + r.contentlength,\n\ + p.xmlns,\n\ + p.pname,\n\ + p.pvalue\n\ +from resolvepath r\n\ +left join Property p on r.resource_id = p.resource_id\n\ +inner join (select unnest($2::text[]) as xmlns, unnest($3::text[]) as pname) n\n\ + on ( p.xmlns = n.xmlns and p.pname = n.pname ) or p.property_id is null\n\ +order by replace(ppath, '/', chr(1)), resource_id;"; + WebdavBackend* pg_webdav_create(Session *sn, Request *rq, pblock *pb) { // resourcepool is required char *resource_pool = pblock_findval("resourcepool", pb); @@ -69,7 +230,7 @@ *webdav = pg_webdav_backend; PgWebdavBackend *instance = pool_malloc(sn->pool, sizeof(PgWebdavBackend)); - if(instance) { + if(!instance) { pool_free(sn->pool, webdav); return NULL; } @@ -91,10 +252,75 @@ const char *path, WebdavPList **outplist) { + PgWebdavBackend *pgdav = rq->dav->instance; + + // check if the resource exists + int64_t parent_id; + int64_t resource_id; + const char *resourcename; + WSBool iscollection; + int res_errno = 0; + int err = pg_resolve_path( + pgdav->connection, + path, + &parent_id, + &resource_id, + NULL, // OID + &resourcename, + &iscollection, + NULL, // stat + &res_errno); + + if(err) { + if(res_errno == ENOENT) { + protocol_status(rq->sn, rq->rq, PROTOCOL_NOT_FOUND, NULL); + } + return 1; + } + + // choose sql query + const char *query = NULL; + if(!iscollection || rq->depth == 0) { + query = rq->allprop ? sql_propfind_allprop_depth0 : sql_propfind_depth0; + } else if(rq->depth == 1) { + query = rq->allprop ? sql_propfind_allprop_depth1 : sql_propfind_depth1; + } else if(rq->depth == -1) { + query = rq->allprop ? sql_propfind_allprop_recursive : sql_propfind_recursive; + } else { + log_ereport(LOG_FAILURE, "%s", "pg_dav_propfind_init: invalid depth"); + return 1; + } + + // get all resources and properties + char resource_id_str[32]; + snprintf(resource_id_str, 32, "%" PRId64, resource_id); + + const char* params[1] = { resource_id_str }; + PGresult *result = PQexecParams( + pgdav->connection, + sql_propfind_allprop_recursive, + 1, // number of parameters + NULL, + params, // parameter value + NULL, + NULL, + 0); // 0: result in text format + int nrows = PQntuples(result); + if(nrows < 1) { + PQclear(result); + return 1; + } + PgPropfind *pg = pool_malloc(rq->sn->pool, sizeof(PgPropfind)); rq->userdata = pg; - return 1; + pg->path = path; + pg->resource_id = resource_id; + pg->vfsproperties = webdav_vfs_properties(outplist, TRUE, 0); + pg->result = result; + pg->nrows = nrows; + + return 0; } int pg_dav_propfind_do( @@ -104,11 +330,103 @@ WebdavResource *resource, struct stat *s) { - return 1; + PgPropfind *pg = rq->userdata; + pool_handle_t *pool = rq->sn->pool; + PGresult *result = pg->result; + WebdavVFSProperties vfsprops = pg->vfsproperties; + + WSBool vfsprops_set = 0; + int64_t current_resource_id = pg->resource_id; + for(int r=0;r<pg->nrows;r++) { + // columns: + // 0: path + // 1: resource_id + // 2: parent_id + // 3: nodename + // 4: iscollection + // 5: lastmodified + // 6: creationdate + // 7: contentlength + // 8: property xmlns + // 9: property name + // 10: property value + + char *path = PQgetvalue(result, r, 0); + char *res_id = PQgetvalue(result, r, 1); + int64_t resource_id; + if(!util_strtoint(res_id, &resource_id)) { + log_ereport(LOG_FAILURE, "pg_dav_propfind_do: cannot convert resource_id '%s' to int", res_id); + return 1; + } + + char *nodename = PQgetvalue(result, r, 3); + if(resource_id != current_resource_id) { + // new resource + resource = response->addresource(response, nodename); + vfsprops_set = FALSE; + current_resource_id = resource_id; + } + + // standard webdav properties + if(!vfsprops_set) { + if(vfsprops.getresourcetype) { + char *iscollection = PQgetvalue(result, r, 4); + if(iscollection && iscollection[0] == 't') { + resource->addproperty(resource, webdav_resourcetype_collection(), 200); + } else { + resource->addproperty(resource, webdav_resourcetype_empty(), 200); + } + } + if(vfsprops.getlastmodified) { + char *lastmodified = PQgetvalue(result, r, 5); + } + if(vfsprops.creationdate) { + char *creationdate = PQgetvalue(result, r, 6); + } + if(vfsprops.getcontentlength) { + char *contentlength = PQgetvalue(result, r, 7); + } + if(vfsprops.getetag) { + + } + + + vfsprops_set = TRUE; + } + + // dead properties + if(!PQgetisnull(result, r, 9)) { + char *xmlns = PQgetvalue(result, r, 8); + char *pname = PQgetvalue(result, r, 9); + char *pvalue = PQgetvalue(result, r, 10); + + WebdavProperty *property = pool_malloc(pool, sizeof(WebdavProperty)); + property->lang = NULL; + property->name = pool_strdup(pool, pname); + + xmlNs *namespace = pool_malloc(pool, sizeof(xmlNs)); + memset(namespace, 0, sizeof(struct _xmlNs)); + namespace->href = pool_strdup(pool, xmlns); + namespace->prefix = "zx1"; + property->namespace = namespace; + + property->vtype = WS_VALUE_TEXT; + property->value.text.str = pool_strdup(pool, pvalue); + property->value.text.length = strlen(pvalue); + + resource->addproperty(resource, property, 200); + } + + + } + + + + return 0; } int pg_dav_propfind_finish(WebdavPropfindRequest *rq) { - return 1; + return 0; } int pg_dav_proppatch_do(
--- a/src/server/plugins/postgresql/webdav.h Thu Apr 21 17:16:49 2022 +0200 +++ b/src/server/plugins/postgresql/webdav.h Sun Apr 24 18:35:44 2022 +0200 @@ -44,8 +44,11 @@ } PgWebdavBackend; typedef struct PgPropfind { - ResourceData *pg_resource; - PGconn *connection; + const char *path; + int64_t resource_id; + WebdavVFSProperties vfsproperties; + PGresult *result; + int nrows; } PgPropfind; WebdavBackend* pg_webdav_create(Session *sn, Request *rq, pblock *pb);
--- a/src/server/public/webdav.h Thu Apr 21 17:16:49 2022 +0200 +++ b/src/server/public/webdav.h Sun Apr 24 18:35:44 2022 +0200 @@ -160,6 +160,8 @@ struct WebdavPropfindRequest { Session *sn; Request *rq; + + WebdavBackend *dav; void *doc; @@ -189,6 +191,8 @@ Session *sn; Request *rq; + WebdavBackend *dav; + void *doc; WebdavPList *set; @@ -422,6 +426,8 @@ void webdav_plist_iterator_remove_current(WebdavPListIterator *i); WSNamespace* webdav_dav_namespace(void); +WebdavProperty* webdav_resourcetype_collection(void); +WebdavProperty* webdav_resourcetype_empty(void); WebdavProperty* webdav_dav_property( pool_handle_t *pool, const char *name);
--- a/src/server/test/webdav.c Thu Apr 21 17:16:49 2022 +0200 +++ b/src/server/test/webdav.c Sun Apr 24 18:35:44 2022 +0200 @@ -1086,12 +1086,13 @@ testutil_destroy_session(sn); } -static void init_test_webdav_method( +void init_test_webdav_method( Session **out_sn, Request **out_rq, TestIOStream **out_st, pblock **out_pb, const char *method, + const char *path, const char *request_body) { Session *sn; @@ -1102,8 +1103,8 @@ sn = testutil_session(); rq = testutil_request(sn->pool, method, "/"); - pblock_nvinsert("path", "/", rq->vars); - pblock_nvinsert("uri", "/", rq->reqpb); + pblock_nvinsert("path", path, rq->vars); + pblock_nvinsert("uri", path, rq->reqpb); st = testutil_iostream(2048, TRUE); sn->csd = (IOStream*)st; @@ -1130,7 +1131,7 @@ int ret; // Test 1 - init_test_webdav_method(&sn, &rq, &st, &pb, "PROPFIND", TEST_PROPFIND1); + init_test_webdav_method(&sn, &rq, &st, &pb, "PROPFIND", "/", TEST_PROPFIND1); ret = webdav_propfind(pb, sn, rq); @@ -1147,7 +1148,7 @@ testutil_iostream_destroy(st); // Test2 - init_test_webdav_method(&sn, &rq, &st, &pb, "PROPFIND", TEST_PROPFIND2); + init_test_webdav_method(&sn, &rq, &st, &pb, "PROPFIND", "/", TEST_PROPFIND2); ret = webdav_propfind(pb, sn, rq); @@ -1420,7 +1421,7 @@ int ret; // Test 1 - init_test_webdav_method(&sn, &rq, &st, &pb, "PROPPATCH", TEST_PROPPATCH2); + init_test_webdav_method(&sn, &rq, &st, &pb, "PROPPATCH", "/", TEST_PROPPATCH2); rq->davCollection = &backend1; ret = webdav_proppatch(pb, sn, rq); @@ -1584,7 +1585,7 @@ // behaves the same for both operations // the only difference are the callbacks - init_test_webdav_method(&sn, &rq, &st, &pb, "MKCOL", NULL); + init_test_webdav_method(&sn, &rq, &st, &pb, "MKCOL", "/", NULL); VFS *testvfs = testvfs_create(sn); rq->vfs = testvfs; @@ -1667,7 +1668,7 @@ TestIOStream *st; pblock *pb; - init_test_webdav_method(&sn, &rq, &st, &pb, "DELETE", NULL); + init_test_webdav_method(&sn, &rq, &st, &pb, "DELETE", "/", NULL); rq->vfs = testvfs_create(sn); WebdavBackend dav1; @@ -1735,7 +1736,7 @@ const char *content_const = "Hello World"; - init_test_webdav_method(&sn, &rq, &st, &pb, "PUT", content_const); + init_test_webdav_method(&sn, &rq, &st, &pb, "PUT", "/", content_const); rq->vfs = testvfs_create(sn); UCX_TEST_BEGIN;
--- a/src/server/test/webdav.h Thu Apr 21 17:16:49 2022 +0200 +++ b/src/server/test/webdav.h Sun Apr 24 18:35:44 2022 +0200 @@ -38,6 +38,15 @@ extern "C" { #endif +void init_test_webdav_method( + Session **out_sn, + Request **out_rq, + TestIOStream **out_st, + pblock **out_pb, + const char *method, + const char *path, + const char *request_body); + UCX_TEST(test_webdav_plist_add); UCX_TEST(test_webdav_plist_size);
--- a/src/server/webdav/operation.c Thu Apr 21 17:16:49 2022 +0200 +++ b/src/server/webdav/operation.c Sun Apr 24 18:35:44 2022 +0200 @@ -324,6 +324,7 @@ op->sn->pool, sizeof(WebdavProppatchRequest)); memcpy(req, orig_request, sizeof(WebdavProppatchRequest)); + req->dav = dav; req->set = set; req->setcount = set_count; req->remove = remove;
--- a/src/server/webdav/webdav.c Thu Apr 21 17:16:49 2022 +0200 +++ b/src/server/webdav/webdav.c Sun Apr 24 18:35:44 2022 +0200 @@ -89,7 +89,14 @@ return ucx_map_cstr_put(webdav_type_map, name, webdavCreate); } +static WSBool webdav_is_initialized = FALSE; + int webdav_init(pblock *pb, Session *sn, Request *rq) { + if(webdav_is_initialized) { + return REQ_NOACTION; + } + webdav_is_initialized = TRUE; + webdav_type_map = ucx_map_new(8); if(!webdav_type_map) { return REQ_ABORTED; @@ -297,6 +304,7 @@ // use new plist after previous init (or orig. plist in the first run) pReq->properties = newProp; pReq->propcount = newPropCount; + pReq->dav = davList; // add new WebdavPropfindRequest object to list for later use requestObjects = ucx_list_append_a(a, requestObjects, pReq); @@ -979,6 +987,14 @@ return &dav_namespace; } +WebdavProperty* webdav_resourcetype_collection(void) { + return &dav_resourcetype_collection; +} + +WebdavProperty* webdav_resourcetype_empty(void) { + return &dav_resourcetype_empty; +} + WebdavProperty* webdav_dav_property( pool_handle_t *pool, const char *name)