add basic pg proppatch implementation webdav

Sat, 30 Apr 2022 20:44:38 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sat, 30 Apr 2022 20:44:38 +0200
branch
webdav
changeset 318
60870dbac94f
parent 317
09676b559091
child 319
a9b9344875aa

add basic pg proppatch implementation

doc/development/postgresql_vfs.sql file | annotate | diff | comparison | revisions
src/server/plugins/postgresql/pgtest.c file | annotate | diff | comparison | revisions
src/server/plugins/postgresql/pgtest.h file | annotate | diff | comparison | revisions
src/server/plugins/postgresql/webdav.c file | annotate | diff | comparison | revisions
src/server/public/webdav.h file | annotate | diff | comparison | revisions
src/server/util/writer.c file | annotate | diff | comparison | revisions
src/server/util/writer.h file | annotate | diff | comparison | revisions
src/server/webdav/webdav.c file | annotate | diff | comparison | revisions
src/server/webdav/xml.c file | annotate | diff | comparison | revisions
src/server/webdav/xml.h file | annotate | diff | comparison | revisions
--- a/doc/development/postgresql_vfs.sql	Tue Apr 26 18:21:25 2022 +0200
+++ b/doc/development/postgresql_vfs.sql	Sat Apr 30 20:44:38 2022 +0200
@@ -22,7 +22,9 @@
 	pname             text     not null,
 	lang              text,
 	nsdeflist         text,
-	pvalue            text          
+	pvalue            text,
+	
+	unique(resource_id, xmlns, pname)       
 );
 
 create type property_name as (
--- a/src/server/plugins/postgresql/pgtest.c	Tue Apr 26 18:21:25 2022 +0200
+++ b/src/server/plugins/postgresql/pgtest.c	Sat Apr 30 20:44:38 2022 +0200
@@ -94,6 +94,7 @@
         ucx_test_register(suite, test_pg_prepare_tests);
         ucx_test_register(suite, test_pg_webdav_propfind);
         ucx_test_register(suite, test_pg_webdav_propfind_allprop);
+        ucx_test_register(suite, test_pg_webdav_proppatch_set);
         
         PGresult *result = PQexec(test_connection, "BEGIN");
         PQclear(result);
@@ -204,6 +205,7 @@
 
 
 void test_multistatus_destroy(TestMultistatus *ms) {
+    if(!ms) return;
     xmlFreeDoc(ms->doc);
     ucx_mempool_destroy(ms->mp);
 }
@@ -481,6 +483,7 @@
     UCX_TEST_BEGIN;
     
     vfs_mkdir(vfs, "/propfind");
+    vfs_mkdir(vfs, "/proppatch");
     SYS_FILE f1;
     
     int64_t res1_id, res2_id;
@@ -506,6 +509,10 @@
     UCX_TEST_ASSERT(f1, "res4 create failed");
     vfs_close(f1);
     
+    f1 = vfs_open(vfs, "/proppatch/pp1", O_WRONLY|O_CREAT);
+    UCX_TEST_ASSERT(f1, "pp1 create failed");
+    vfs_close(f1);
+    
     // 2 properties for res1
     char idstr[32];
     snprintf(idstr, 32, "%" PRId64, res1_id);
@@ -801,3 +808,42 @@
     UCX_TEST_END;
 }
 
+UCX_TEST(test_pg_webdav_proppatch_set) {
+    Session *sn;
+    Request *rq; 
+    TestIOStream *st;
+    pblock *pb;
+    
+    UCX_TEST_BEGIN;
+    
+    // test data:
+    //
+    // /propfind/
+    // /propfind/res1     (2 properties: test, prop2)
+    // /propfind/res2     (1 property: test)
+    // /propfind/res3
+    // /propfind/sub
+    // /propfind/sub/res4
+    
+    int ret;
+    TestResponse *r1;
+    TestProperty *p;
+    // Test 1
+    init_test_webdav_method(&sn, &rq, &st, &pb, "PROPPATCH", "/proppatch/pp1", PG_TEST_PROPPATCH1);
+    rq->davCollection = create_test_pgdav(sn, rq);
+    
+    ret = webdav_proppatch(pb, sn, rq);
+    UCX_TEST_ASSERT(ret == REQ_PROCEED, "proppatch1 failed");
+    
+    printf("\n\n%.*s\n", (int)st->buf->size, st->buf->space);
+    
+    TestMultistatus *ms = test_parse_multistatus(st->buf->space, st->buf->size);
+    UCX_TEST_ASSERT(ms, "proppatch1 response is not valid xml");
+    
+    testutil_destroy_session(sn);
+    test_multistatus_destroy(ms);
+    testutil_iostream_destroy(st);
+    
+    
+    UCX_TEST_END;
+}
--- a/src/server/plugins/postgresql/pgtest.h	Tue Apr 26 18:21:25 2022 +0200
+++ b/src/server/plugins/postgresql/pgtest.h	Sat Apr 30 20:44:38 2022 +0200
@@ -59,6 +59,7 @@
 UCX_TEST(test_pg_prepare_tests);
 UCX_TEST(test_pg_webdav_propfind);
 UCX_TEST(test_pg_webdav_propfind_allprop);
+UCX_TEST(test_pg_webdav_proppatch_set);
 
 
 /* --------------------------- PROPFIND --------------------------- */
@@ -96,6 +97,12 @@
             <D:allprop/> \
         </D:propfind>"
 
+#define PG_TEST_PROPPATCH1 "<?xml version=\"1.0\" encoding=\"utf-8\" ?> \
+        <D:propertyupdate xmlns:D=\"DAV:\" xmlns:X=\"http://example.com/\"> \
+            <D:set> \
+                <D:prop><X:test>test</X:test><X:author>name</X:author></D:prop> \
+            </D:set> \
+        </D:propertyupdate>"
 
 #ifdef __cplusplus
 }
--- a/src/server/plugins/postgresql/webdav.c	Tue Apr 26 18:21:25 2022 +0200
+++ b/src/server/plugins/postgresql/webdav.c	Sat Apr 30 20:44:38 2022 +0200
@@ -30,10 +30,12 @@
 #include "vfs.h"
 
 #include "../../util/util.h"
+#include "../../util/pblock.h"
 
 #include <ucx/buffer.h>
 #include <libxml/tree.h>
 
+
 static WebdavBackend pg_webdav_backend = {
     pg_dav_propfind_init,
     pg_dav_propfind_do,
@@ -237,6 +239,28 @@
 ) p on r.resource_id = p.resource_id\n\
 order by replace(ppath, '/', chr(1)), resource_id;";
 
+// proppatch: set property
+// params: $1: resource_id
+//         $2: xmlns prefix
+//         $3: xmlns href
+//         $4: property name
+//         $5: lang attribute value
+//         $6: namespace list string
+//         $7: property value
+static const char *sql_proppatch_set = "\
+insert into Property(resource_id, prefix, xmlns, pname, lang, nsdeflist, pvalue)\n\
+values($1, $2, $3, $4, $5, $6, $7)\n\
+on conflict (resource_id, xmlns, pname) do\n\
+update set prefix=$2, lang=$5, nsdeflist=$6, pvalue=$7;";
+
+// proppatch: remove property
+// params: $1: resource_id
+//         $2: xmlns href
+//         $3: property name
+static const char *sql_proppatch_remove = "\
+delete from Property where resource_id = $1 and xmlns = $2 and pname = $3";
+
+
 WebdavBackend* pg_webdav_create(Session *sn, Request *rq, pblock *pb) {
     // resourcepool is required
     char *resource_pool = pblock_findval("resourcepool", pb);
@@ -561,7 +585,7 @@
             property->namespace = namespace;
             
             if(!lang_isnull) {
-                property->lang = (xmlChar*)pool_strdup(pool, lang);
+                property->lang = pool_strdup(pool, lang);
             }
             
             if(!pvalue_isnull) {
@@ -595,6 +619,184 @@
     return 0;
 }
 
+enum PgDavProp {
+    PG_DAV_PROPPATCH_NOT_ALLOWED = 0,
+    PG_DAV_CREATIONDATE,
+    PG_DAV_DISPLAYNAME,
+    PG_DAV_DEADPROP
+};
+/*
+ * checks if the property can be manipulated
+ */
+static enum PgDavProp proppatch_check_dav_prop(const char *name) {
+    if(!strcmp(name, "getlastmodified")) {
+        return PG_DAV_PROPPATCH_NOT_ALLOWED;
+    } else if(!strcmp(name, "getcontentlength")) {
+        return PG_DAV_PROPPATCH_NOT_ALLOWED;
+    } else if(!strcmp(name, "resourcetype")) {
+        return PG_DAV_PROPPATCH_NOT_ALLOWED;
+    } else if(!strcmp(name, "getetag")) {
+        return PG_DAV_PROPPATCH_NOT_ALLOWED;
+    } else if(!strcmp(name, "creationdate")) {
+        return PG_DAV_CREATIONDATE;
+    } else if(!strcmp(name, "displayname")) {
+        return PG_DAV_DISPLAYNAME;
+    }
+    return PG_DAV_DEADPROP;
+}
+
+typedef struct {
+    WebdavProperty *creationdate;
+    WebdavProperty *displayname;
+    int error;
+} PgProppatchOpResult;
+
+typedef int(*pg_proppatch_func)(PgWebdavBackend*, WebdavProppatchRequest*, WebdavResource*, WebdavProperty*, void*);
+
+/*
+ * This function iterates the property list 'plist',
+ * analyses if any DAV: property is in the list
+ * and calls opfunc for the each property
+ * 
+ * If the property list contains the properties creationdate or displayname,
+ * the pointers to these properties will be stored in the result structure
+ */
+static PgProppatchOpResult pg_proppatch_op(
+        PgWebdavBackend *pgdav,
+        WebdavProppatchRequest *request,
+        WebdavResource *response,
+        WebdavPList **plist,
+        enum PgDavProp forbidden_extra,
+        pg_proppatch_func opfunc,
+        void *op_userdata)
+{
+    PgProppatchOpResult result;
+    result.creationdate = NULL;
+    result.displayname = NULL;
+    result.error = 0;
+    
+    WebdavPListIterator i = webdav_plist_iterator(plist);
+    WebdavPList *cur;
+    while(webdav_plist_iterator_next(&i, &cur)) {
+        WebdavProperty *property = cur->property;
+        WSNamespace *ns = property->namespace;
+        if(!ns) {
+            continue; // maybe we should abort
+        }
+        
+        // check if the property is a DAV: property that requires special
+        // handling
+        // get* properties can't be manipulated
+        // some properties can't be removed
+        if(!strcmp((const char*)ns->href, "DAV:")) {
+            const char *name = property->name;
+            enum PgDavProp davprop = proppatch_check_dav_prop(name);
+            if(davprop != PG_DAV_DEADPROP) {
+                if(davprop == PG_DAV_PROPPATCH_NOT_ALLOWED || davprop == forbidden_extra) {
+                    response->addproperty(response, property, 409);
+                } else if(davprop == PG_DAV_CREATIONDATE) {
+                    result.creationdate = property;
+                } else if(davprop == PG_DAV_DISPLAYNAME) {
+                    result.displayname = property;
+                }
+                webdav_plist_iterator_remove_current(&i);
+                continue;
+            }
+        }
+        
+        // call op func (set, remove specific code)
+        if(opfunc(pgdav, request, response, property, op_userdata)) {
+            result.error = 1;
+            break;
+        }
+        
+        webdav_plist_iterator_remove_current(&i);
+    }
+    
+    return result;
+}
+
+static int pg_dav_set_property(
+        PgWebdavBackend *pgdav,
+        WebdavProppatchRequest *request,
+        WebdavResource *response,
+        WebdavProperty *property,
+        void *userdata)
+{
+    pool_handle_t *pool = request->sn->pool;
+    WSNamespace *ns = property->namespace;
+    char *resource_id_str = userdata;
+    int ret = 0;
+    
+    // convert the property value to WSXmlData
+    // property->vtype == WS_VALUE_XML_NODE should always be true
+    WSXmlData *property_value = property->vtype == WS_VALUE_XML_NODE ? wsxml_node2data(pool, property->value.node) : NULL;
+    char *value_str = property_value ? property_value->data : NULL;
+
+    // TODO: convert WebdavNSList to a string
+
+    // exec sql
+    const char* params[7] = { resource_id_str, (const char*)ns->prefix, (const char*)ns->href, property->name, NULL, NULL, value_str}; 
+    PGresult *result = PQexecParams(
+        pgdav->connection,
+        sql_proppatch_set,
+        7,      // number of parameters
+        NULL,
+        params, // parameter value
+        NULL,
+        NULL,
+        0);     // 0: result in text format
+
+    if(PQresultStatus(result) != PGRES_COMMAND_OK) {
+        response->addproperty(response, property, 500);
+        //printf(PQerrorMessage(pgdav->connection));
+        //fflush(stdout);
+        ret = 1;
+    } else {
+        response->addproperty(response, property, 200);
+    }
+    PQclear(result);
+    if(value_str) pool_free(pool, value_str);
+    
+    return ret;
+}
+
+
+static int pg_dav_remove_property(
+        PgWebdavBackend *pgdav,
+        WebdavProppatchRequest *request,
+        WebdavResource *response,
+        WebdavProperty *property,
+        void *userdata)
+{
+    pool_handle_t *pool = request->sn->pool;
+    WSNamespace *ns = property->namespace;
+    char *resource_id_str = userdata;
+    int ret = 0;
+
+    // exec sql
+    const char* params[3] = { resource_id_str, (const char*)ns->href, property->name }; 
+    PGresult *result = PQexecParams(
+        pgdav->connection,
+        sql_proppatch_remove,
+        3,      // number of parameters
+        NULL,
+        params, // parameter value
+        NULL,
+        NULL,
+        0);     // 0: result in text format
+
+    if(PQresultStatus(result) != PGRES_COMMAND_OK) {
+        response->addproperty(response, property, 500);
+        //printf(PQerrorMessage(pgdav->connection));
+        //fflush(stdout);
+        ret = 1;
+    }
+    PQclear(result);
+    
+    return ret;
+}
+
 int pg_dav_proppatch_do(
 	WebdavProppatchRequest *request,
 	WebdavResource *response,
@@ -602,7 +804,70 @@
 	WebdavPList **out_set,
 	WebdavPList **out_remove)
 {
-    return 1;
+    PgWebdavBackend *pgdav = request->dav->instance;
+    pool_handle_t *pool = request->sn->pool;
+    char *path = pblock_findkeyval(pb_key_path, request->rq->vars);
+    
+    // check if the resource exists, we also need the resource_id
+    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) {
+        return 1;
+    }
+    
+    // because proppatch must be atomic and we have multiple sql
+    // queries and other backends that do stuff that could fail
+    // we need the possibility to reverse all changes
+    // we use a transaction savepoint for this
+    PGresult *result = PQexec(pgdav->connection, "savepoint proppatch;");
+    ExecStatusType execStatus = PQresultStatus(result);
+    PQclear(result);
+    if(execStatus != PGRES_COMMAND_OK) {
+        return 1;
+    }
+    
+    char resource_id_str[32];
+    snprintf(resource_id_str, 32, "%" PRId64, resource_id); 
+    
+    int ret = 0;
+    PgProppatchOpResult set_res = pg_proppatch_op(
+            pgdav,
+            request,
+            response,
+            out_set,
+            PG_DAV_PROPPATCH_NOT_ALLOWED,
+            pg_dav_set_property,
+            resource_id_str);
+    if(set_res.error) {
+        return 1;
+    }
+    PgProppatchOpResult rm_res = pg_proppatch_op(
+            pgdav,
+            request,
+            response,
+            out_remove,
+            PG_DAV_CREATIONDATE, // creationdate can't be removed
+            pg_dav_remove_property,
+            resource_id_str);
+    if(rm_res.error) {
+        return 1;
+    } 
+    
+    return ret;
 }
 
 int pg_dav_proppatch_finish(
@@ -611,5 +876,15 @@
 	VFSFile *file,
 	WSBool commit)
 {
-    return 1;
+    PgWebdavBackend *pgdav = request->dav->instance;
+    int ret = 0;
+    if(!commit) {
+        PGresult *result = PQexec(pgdav->connection, "rollback to savepoint proppatch;");
+        if(PQresultStatus(result) != PGRES_COMMAND_OK) {
+            log_ereport(LOG_FAILURE, "pg_dav_proppatch_finish: rollback failed: %s", PQerrorMessage(pgdav->connection));
+            ret = 1;
+        }
+        PQclear(result);
+    }
+    return ret;
 }
--- a/src/server/public/webdav.h	Tue Apr 26 18:21:25 2022 +0200
+++ b/src/server/public/webdav.h	Sat Apr 30 20:44:38 2022 +0200
@@ -476,6 +476,10 @@
         WSXmlNode *node,
         int *error);
 
+WSXmlData* wsxml_node2data(
+        pool_handle_t *pool,
+        WSXmlNode *node);
+
 #ifdef	__cplusplus
 }
 #endif
--- a/src/server/util/writer.c	Tue Apr 26 18:21:25 2022 +0200
+++ b/src/server/util/writer.c	Sat Apr 30 20:44:38 2022 +0200
@@ -34,6 +34,16 @@
 
 void writer_init(Writer *w, SYS_NETFD fd, char *buf, size_t len) {
     w->fd = fd;
+    w->write = (wr_writefunc)net_write;
+    w->buffer = buf;
+    w->size = len;
+    w->pos = 0;
+    w->error = 0;
+}
+
+void writer_init_with_stream(Writer *w, void *stream, wr_writefunc writefunc, char *buf, size_t len) {
+    w->fd = stream;
+    w->write = writefunc;
     w->buffer = buf;
     w->size = len;
     w->pos = 0;
@@ -49,7 +59,7 @@
     size_t len = w->pos;
     
     while(len > 0) {
-        ssize_t r = net_write(w->fd, w->buffer + pos, len);
+        ssize_t r = w->write(w->fd, w->buffer + pos, len);
         if(r <= 0) {
             break;
         }
@@ -105,3 +115,8 @@
     w->buffer[w->pos++] = c;
     return 0;
 }
+
+int writer_fwrite(const void *s, size_t size, size_t nelem, Writer *out) {
+    int w = writer_put(out, s, size*nelem);
+    return w/size;
+}
--- a/src/server/util/writer.h	Tue Apr 26 18:21:25 2022 +0200
+++ b/src/server/util/writer.h	Sat Apr 30 20:44:38 2022 +0200
@@ -36,16 +36,23 @@
 extern "C" {
 #endif
 
+typedef ssize_t (*wr_writefunc)(void *, const char *, size_t);
+    
 typedef struct Writer {
-    SYS_NETFD fd;
+    void *fd;
+    wr_writefunc write;
     char *buffer;
     size_t size;
     size_t pos;
     int error;
 } Writer;
 
+
+
 void writer_init(Writer *w, SYS_NETFD fd, char *buf, size_t len);
 
+void writer_init_with_stream(Writer *w, void *stream, wr_writefunc writefunc, char *buf, size_t len);
+
 int writer_flush(Writer *w);
 
 int writer_put(Writer *w, const char *s, size_t len);
@@ -54,6 +61,8 @@
 
 int writer_putc(Writer *w, char c);
 
+int writer_fwrite(const void *s, size_t size, size_t nelem, Writer *w);
+
 #ifdef __cplusplus
 }
 #endif
--- a/src/server/webdav/webdav.c	Tue Apr 26 18:21:25 2022 +0200
+++ b/src/server/webdav/webdav.c	Sat Apr 30 20:44:38 2022 +0200
@@ -1126,7 +1126,7 @@
         WSNamespace *ns = cur->property->namespace;
         if(ns && !strcmp((const char*)ns->href, "DAV:")) {
             const char *name = cur->property->name;
-            WSBool remove_prop = TRUE;
+            WSBool remove_prop = removefromlist;
             if(!strcmp(name, "getlastmodified")) {
                 ret.getlastmodified = 1;
             } else if(!strcmp(name, "getcontentlength")) {
--- a/src/server/webdav/xml.c	Tue Apr 26 18:21:25 2022 +0200
+++ b/src/server/webdav/xml.c	Sat Apr 30 20:44:38 2022 +0200
@@ -32,6 +32,7 @@
 
 #include <ucx/string.h>
 #include <ucx/map.h>
+#include <ucx/buffer.h>
 
 #include "../util/util.h"
 
@@ -313,6 +314,49 @@
 }
 
 
+static ssize_t buf_writefunc(void *buf, const void *s, size_t len) {
+    int w = ucx_buffer_write(s, 1, len, buf);
+    return w == 0 ? IO_ERROR : w;
+}
+
+WSXmlData* wsxml_node2data(
+        pool_handle_t *pool,
+        WSXmlNode *node)
+{
+    UcxBuffer *buf = ucx_buffer_new(NULL, 1024, UCX_BUFFER_AUTOEXTEND);
+    if(!buf) {
+        return NULL;
+    }
+    
+    int error = 0;
+    WebdavNSList *nslist = wsxml_get_required_namespaces(pool, node, &error);
+    if(error) {
+        return NULL;
+    }
+    
+    Writer writer;
+    char buffer[512];
+    writer_init_with_stream(&writer, buf, buf_writefunc, buffer, 512);
+    
+    WSXmlData *data = NULL;
+    if(!wsxml_write_nodes_without_nsdef(pool, &writer, node) && !writer_flush(&writer)) {
+        data = pool_malloc(pool, sizeof(WSXmlData));
+        if(data) {
+            data->data = pool_malloc(pool, buf->size + 1);
+            if(data->data) {
+                memcpy(data->data, buf->space, buf->size);
+                data->data[buf->size] = '\0';
+                data->length = buf->size;
+                data->namespaces = nslist;
+            }
+        }
+    }
+    
+    ucx_buffer_free(buf);
+    
+    return data;
+}
+
 /*****************************************************************************
  *    Non public functions
  *****************************************************************************/
--- a/src/server/webdav/xml.h	Tue Apr 26 18:21:25 2022 +0200
+++ b/src/server/webdav/xml.h	Sat Apr 30 20:44:38 2022 +0200
@@ -33,6 +33,7 @@
 #include <libxml/tree.h>
 
 #include <ucx/map.h>
+#include <ucx/ucx.h>
 
 #include "../util/writer.h"
 

mercurial