src/server/plugins/postgresql/webdav.c

Wed, 10 Aug 2022 20:32:49 +0200

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Wed, 10 Aug 2022 20:32:49 +0200
branch
webdav
changeset 368
69dbcc7e7f93
parent 366
47bc686fafe4
child 370
d6d8c86c95c3
permissions
-rw-r--r--

implement named dav repositories

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2022 Olaf Wintermann. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "webdav.h"
#include "vfs.h"
#include "config.h"

#include "../../util/util.h"
#include "../../util/pblock.h"

#include "../../daemon/http.h" // etag

#include <ucx/buffer.h>
#include <libxml/tree.h>


static WebdavBackend pg_webdav_backend = {
    pg_dav_propfind_init,
    pg_dav_propfind_do,
    pg_dav_propfind_finish,
    pg_dav_proppatch_do,
    pg_dav_proppatch_finish,
    NULL, // opt_mkcol
    NULL, // opt_mkcol_finish
    NULL, // opt_delete
    NULL, // opt_delete_finish
    0,
    NULL,
    NULL
};


/*
 * SQL Queries
 */

// propfind with depth = 0
// params: $1: resource_id
static const char *sql_propfind_allprop_depth0 = "\
select\n\
    $2::text 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\
    r.etag,\n\
    p.prefix,\n\
    p.xmlns,\n\
    p.pname,\n\
    p.lang,\n\
    p.nsdeflist,\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\
    $2::text 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\
    r.etag,\n\
    p.prefix,\n\
    p.xmlns,\n\
    p.pname,\n\
    p.lang,\n\
    p.nsdeflist,\n\
    p.pvalue\n\
from Resource r\n\
left join (\n\
    select p.* from Property p\
    inner join (select unnest($3::text[]) as xmlns, unnest($4::text[]) as pname) n\n\
       on p.xmlns = n.xmlns and p.pname = n.pname\n\
) p on r.resource_id = p.resource_id\n\
where r.resource_id = $1;";

// propfind with depth = 1
// params: $1: resource_id
static const char *sql_propfind_allprop_depth1 = "\
select\n\
    case when r.resource_id = $1 then $2\n\
         else $2 || '/' || r.nodename\n\
    end 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\
    r.etag,\n\
    p.prefix,\n\
    p.xmlns,\n\
    p.pname,\n\
    p.lang,\n\
    p.nsdeflist,\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 case when r.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\
    case when r.resource_id = $1 then $2\n\
         else $2 || '/' || r.nodename\n\
    end 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\
    r.etag,\n\
    p.prefix,\n\
    p.xmlns,\n\
    p.pname,\n\
    p.lang,\n\
    p.nsdeflist,\n\
    p.pvalue\n\
from Resource r\n\
left join (\n\
    select p.* from Property p\
    inner join (select unnest($3::text[]) as xmlns, unnest($4::text[]) as pname) n\n\
       on p.xmlns = n.xmlns and p.pname = n.pname\n\
) p on r.resource_id = p.resource_id\n\
where r.resource_id = $1 or r.parent_id = $1\n\
order by case when r.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\
    case when r.resource_id = $1 then $2\n\
         else $2 || r.ppath\n\
    end 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\
    r.etag,\n\
    p.prefix,\n\
    p.xmlns,\n\
    p.pname,\n\
    p.lang,\n\
    p.nsdeflist,\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\
    case when r.resource_id = $1 then $2\n\
         else $2 || r.ppath\n\
    end 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\
    r.etag,\n\
    p.prefix,\n\
    p.xmlns,\n\
    p.pname,\n\
    p.lang,\n\
    p.nsdeflist,\n\
    p.pvalue\n\
from resolvepath r\n\
left join (\n\
    select p.* from Property p\
    inner join (select unnest($3::text[]) as xmlns, unnest($4::text[]) as pname) n\n\
       on p.xmlns = n.xmlns and p.pname = n.pname\n\
) 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";


void* pg_webdav_init(ServerConfiguration *cfg, pool_handle_t *pool, WSConfigNode *config) {
    return pg_init_repo(pool, config);
}

WebdavBackend* pg_webdav_create(Session *sn, Request *rq, pblock *pb, void *initData) {
    // resourcepool is required
    char *resource_pool = pblock_findval("resourcepool", pb);
    if(!resource_pool) {
        log_ereport(LOG_MISCONFIG, "pg_webdav_create: missing resourcepool parameter");
        return NULL;
    }
    
    // get the resource first (should only fail in case of misconfig)
    ResourceData *resdata = resourcepool_lookup(sn, rq, resource_pool, 0);
    if(!resdata) {
        log_ereport(LOG_MISCONFIG, "postgresql webdav: resource pool %s not found", resource_pool);
        return NULL;
    }
    
    return pg_webdav_create_from_resdata(sn, rq, resdata);
}

WebdavBackend* pg_webdav_create_from_resdata(Session *sn, Request *rq, ResourceData *resdata) {
    WebdavBackend *webdav = pool_malloc(sn->pool, sizeof(WebdavBackend));
    if(!webdav) {
        return NULL;
    }
    *webdav = pg_webdav_backend;
    
    PgWebdavBackend *instance = pool_malloc(sn->pool, sizeof(PgWebdavBackend));
    if(!instance) {
        pool_free(sn->pool, webdav);
        return NULL;
    }
    webdav->instance = instance;
    
    instance->pg_resource = resdata;
    instance->connection = resdata->data;
    
    return webdav;
}

WebdavBackend* pg_webdav_prop_create(Session *sn, Request *rq, pblock *pb) {
    return NULL;
}

/*
 * adds str to the buffer
 * some characters will be escaped: \,{}
 */
static void buf_addstr_escaped(UcxBuffer *buf, const char *str) {
    size_t len = strlen(str);
    for(size_t i=0;i<len;i++) {
        char c = str[i];
        if(c == '{' || c == '}' || c == ',' || c == '\\') {
            ucx_buffer_putc(buf, '\\');
        }
        ucx_buffer_putc(buf, c);
    }
}

/*
 * convert a property list to two pg array parameter strings
 * array format: {elm1,elm2,elm3}
 * xmlns: buffer for the xmlns array
 * pname: buffer for the property name array
 * 
 * returns 0 on success, 1 otherwise
 */
int pg_create_property_param_arrays(WebdavPList *plist, UcxBuffer *xmlns, UcxBuffer *pname) {
    ucx_buffer_putc(xmlns, '{');
    ucx_buffer_putc(pname, '{');
    while(plist) {
        WebdavProperty *property = plist->property;
        if(property && property->namespace && property->namespace->href && property->name) {
            buf_addstr_escaped(xmlns, (const char*)property->namespace->href);
            buf_addstr_escaped(pname, (const char*)property->name);
            if(plist->next) {
                ucx_buffer_putc(xmlns, ',');
                ucx_buffer_putc(pname, ',');
            }
        }
        plist = plist->next;
    }
    int r1 = ucx_buffer_write("}\0", 2, 1, xmlns) == 0;
    int r2 = ucx_buffer_write("}\0", 2, 1, pname) == 0;
    return r1+r2 != 0;
}


int pg_dav_propfind_init(
        WebdavPropfindRequest *rq,
        const char *path,
        const char *href,
        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
            NULL, // etag
            &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);
    
    size_t href_len = strlen(href);
    char *href_param = pool_malloc(rq->sn->pool, href_len + 1);
    memcpy(href_param, href, href_len);
    if(href_param[href_len-1] == '/') {
        href_len--;
    }
    href_param[href_len] = '\0';
    
    // if allprop is false, create array pair for xmlns/property names
    UcxBuffer *xmlns_buf = NULL;
    UcxBuffer *pname_buf = NULL;
    char *xmlns_param = NULL;
    char *pname_param = NULL;
    int nparam = 2;
    if(!rq->allprop) {
        size_t bufsize = rq->propcount < 200 ? 8 + rq->propcount * 32 : 4096;
        xmlns_buf = ucx_buffer_new(NULL, bufsize, UCX_BUFFER_AUTOEXTEND);
        if(!xmlns_buf) {
            return 1;
        }
        pname_buf = ucx_buffer_new(NULL, bufsize, UCX_BUFFER_AUTOEXTEND);
        if(!pname_buf) {
            ucx_buffer_free(xmlns_buf);
            return 1;
        }
        if(pg_create_property_param_arrays(rq->properties, xmlns_buf, pname_buf)) {
            ucx_buffer_free(xmlns_buf);
            ucx_buffer_free(pname_buf);
            return 1;
        }
        xmlns_param = xmlns_buf->space;
        pname_param = pname_buf->space;
        nparam = 4;
    }
    
    const char* params[4] = { resource_id_str, href_param, xmlns_param, pname_param };
    PGresult *result = PQexecParams(
            pgdav->connection,
            query,
            nparam,     // number of parameters
            NULL,
            params, // parameter value
            NULL,
            NULL,
            0);    // 0: result in text format
    int nrows = PQntuples(result);
    pool_free(rq->sn->pool, href_param);
    if(xmlns_buf) {
        ucx_buffer_free(xmlns_buf);
        ucx_buffer_free(pname_buf);
    }
    if(nrows < 1) {
        PQclear(result);
        return 1;
    }
    
    PgPropfind *pg = pool_malloc(rq->sn->pool, sizeof(PgPropfind));
    rq->userdata = pg;
    
    pg->path = path;
    pg->resource_id = resource_id;
    pg->vfsproperties = webdav_vfs_properties(outplist, TRUE, rq->allprop, 0);
    pg->result = result;
    pg->nrows = nrows;
    
    return 0;
}

int pg_dav_propfind_do(
	WebdavPropfindRequest *rq,
	WebdavResponse *response,
	VFS_DIR parent,
	WebdavResource *resource,
	struct stat *s)
{
    PgPropfind *pg = rq->userdata;
    pool_handle_t *pool = rq->sn->pool;
    PGresult *result = pg->result;
    WebdavVFSProperties vfsprops = pg->vfsproperties;
    
    WSBool vfsprops_set = 0; // are live properties added to the response?
    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: etag
        //  9: property prefix
        // 10: property xmlns
        // 11: property name
        // 12: property lang
        // 13: property nsdeflist
        // 14: property value
        
        char *path = PQgetvalue(result, r, 0);
        char *res_id = PQgetvalue(result, r, 1);
        char *iscollection_str = PQgetvalue(result, r, 4);
        WSBool iscollection = iscollection_str && iscollection_str[0] == 't';
        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;
        }
        
        if(resource_id != current_resource_id) {
            // create a href string for the new resource
            // if the resource is a collection, it should have a trailing '/'
            size_t pathlen = strlen(path);
            if(pathlen == 0) {
                log_ereport(LOG_FAILURE, "pg_dav_propfind_do: query returned invalid path");
                return 1;
            }
            if(pathlen > PG_MAX_PATH_LEN) {
                log_ereport(LOG_FAILURE, "pg_dav_propfind_do: path too long: resource_id: %s", res_id);
                return 1;
            }
            char *newres_href = pool_malloc(pool, (pathlen*3)+2);
            util_uri_escape(newres_href, path);
            if(iscollection && path[pathlen-1] != '/') {
                size_t newres_href_len = strlen(newres_href);
                newres_href[newres_href_len] = '/';
                newres_href[newres_href_len+1] = '\0';
            }
            
            // new resource
            resource = response->addresource(response, newres_href);
            vfsprops_set = FALSE;
            current_resource_id = resource_id;
        }
        
        // standard webdav live properties
        if(!vfsprops_set) {
            if(vfsprops.getresourcetype) {
                if(iscollection) {
                    resource->addproperty(resource, webdav_resourcetype_collection(), 200);
                } else {
                    resource->addproperty(resource, webdav_resourcetype_empty(), 200);
                }
            }
            
            char *lastmodified = PQgetvalue(result, r, 5);
            char *contentlength = PQgetvalue(result, r, 7);
            time_t t = pg_convert_timestamp(lastmodified);
            
            if(vfsprops.getlastmodified) {
                struct tm tm;
                gmtime_r(&t, &tm);
                
                char buf[HTTP_DATE_LEN+1];
                strftime(buf, HTTP_DATE_LEN, HTTP_DATE_FMT, &tm);
                webdav_resource_add_dav_stringproperty(resource, pool, "getlastmodified", buf, strlen(buf));
            }
            if(vfsprops.creationdate) {
                char *creationdate = PQgetvalue(result, r, 6);
                webdav_resource_add_dav_stringproperty(resource, pool, "creationdate", creationdate, strlen(creationdate));
            }
            if(vfsprops.getcontentlength && !iscollection) {
                webdav_resource_add_dav_stringproperty(resource, pool, "getcontentlength", contentlength, strlen(contentlength));
            }
            if(vfsprops.getetag) {
                char *etag = PQgetvalue(result, r, 8);
                if(!PQgetisnull(result, r, 8)) {
                    webdav_resource_add_dav_stringproperty(resource, pool, "getetag", etag, strlen(etag));
                } else {
                    int64_t ctlen;
                    if(util_strtoint(contentlength, &ctlen)) {
                        char etag[MAX_ETAG];
                        http_format_etag(rq->sn, rq->rq, etag, MAX_ETAG, ctlen, t);
                        webdav_resource_add_dav_stringproperty(resource, pool, "getetag", etag, strlen(etag));
                    }
                }
            }
            
            vfsprops_set = TRUE;
        }
        
        // dead properties
        if(!PQgetisnull(result, r, 9)) {
            char *prefix = PQgetvalue(result, r, 9);
            char *xmlns = PQgetvalue(result, r, 10);
            char *pname = PQgetvalue(result, r, 11);
            char *lang = PQgetvalue(result, r, 12);
            char *nsdef = PQgetvalue(result, r, 13);
            char *pvalue = PQgetvalue(result, r, 14);
            
            int pvalue_len = PQgetlength(result, r, 14);
            WSBool lang_isnull = PQgetisnull(result, r, 12);
            WSBool nsdef_isnull = PQgetisnull(result, r, 13);
            WSBool pvalue_isnull = PQgetisnull(result, r, 14);
            
            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 = (xmlChar*)pool_strdup(pool, xmlns);
            namespace->prefix = (xmlChar*)pool_strdup(pool, prefix);
            property->namespace = namespace;
            
            if(!lang_isnull) {
                property->lang = pool_strdup(pool, lang);
            }
            
            if(!pvalue_isnull) {
                char *content = pool_malloc(pool, pvalue_len+1);
                memcpy(content, pvalue, pvalue_len);
                content[pvalue_len] = '\0';
                
                if(nsdef_isnull) {
                    property->vtype = WS_VALUE_TEXT;
                    property->value.text.str = content;
                    property->value.text.length = pvalue_len;
                } else {
                    WebdavNSList *nslist = wsxml_string2nslist(pool, nsdef); 
                    property->vtype = WS_VALUE_XML_DATA;
                    property->value.data->data = content;
                    property->value.data->length = pvalue_len;
                    property->value.data->namespaces = nslist;
                    
                }
            }
            
            resource->addproperty(resource, property, 200);
        }
    }
        
    return 0;
}

int pg_dav_propfind_finish(WebdavPropfindRequest *rq) {
    PgPropfind *pg = rq->userdata;
    pool_handle_t *pool = rq->sn->pool;
    PGresult *result = pg->result;
    
    PQclear(result);
    
    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 = NULL;
    char *nsdef_str = NULL;
    if(property_value) {
        value_str = property_value->data;
        if(property_value->namespaces) {
            nsdef_str = wsxml_nslist2string(pool, property_value->namespaces);
            if(!nsdef_str) {
                return 1; // OOM
            }
        }
    }

    // exec sql
    const char* params[7] = { resource_id_str, (const char*)ns->prefix, (const char*)ns->href, property->name, NULL, nsdef_str, 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,
	VFSFile *file,
	WebdavPList **out_set,
	WebdavPList **out_remove)
{
    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
            NULL, // etag
            &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(
	WebdavProppatchRequest *request,
	WebdavResource *response,
	VFSFile *file,
	WSBool commit)
{
    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;
}

mercurial