Tue, 26 Apr 2022 15:19:12 +0200
add tests for propfind allprop
/* * 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 "../../util/util.h" #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\ 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\ $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\ p.xmlns,\n\ p.pname,\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\ 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 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\ p.xmlns,\n\ p.pname,\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\ 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\ 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\ p.xmlns,\n\ p.pname,\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;"; WebdavBackend* pg_webdav_create(Session *sn, Request *rq, pblock *pb) { // 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 &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, 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: property xmlns // 9: property name // 10: 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; } char *newres_href = pool_malloc(pool, pathlen+2); memcpy(newres_href, path, pathlen); if(iscollection && path[pathlen-1] != '/') { newres_href[pathlen++] = '/'; } newres_href[pathlen] = '\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); } } if(vfsprops.getlastmodified) { char *lastmodified = PQgetvalue(result, r, 5); time_t t = pg_convert_timestamp(lastmodified); 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) { char *contentlength = PQgetvalue(result, r, 7); webdav_resource_add_dav_stringproperty(resource, pool, "getcontentlength", contentlength, strlen(contentlength)); } 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 = (xmlChar*)pool_strdup(pool, xmlns); namespace->prefix = (xmlChar*)"zx1"; // TODO 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) { PgPropfind *pg = rq->userdata; pool_handle_t *pool = rq->sn->pool; PGresult *result = pg->result; PQclear(result); return 0; } int pg_dav_proppatch_do( WebdavProppatchRequest *request, WebdavResource *response, VFSFile *file, WebdavPList **out_set, WebdavPList **out_remove) { return 1; } int pg_dav_proppatch_finish( WebdavProppatchRequest *request, WebdavResource *response, VFSFile *file, WSBool commit) { return 1; }