/* * deadprops.c: mod_dav_svn provider functions for "dead properties" * (properties implemented by Subversion or its users, * not as part of the WebDAV specification). * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== */ #include #include #include #include "svn_hash.h" #include "svn_xml.h" #include "svn_pools.h" #include "svn_dav.h" #include "svn_base64.h" #include "svn_props.h" #include "private/svn_log.h" #include "dav_svn.h" struct dav_db { const dav_resource *resource; apr_pool_t *p; /* the resource's properties that we are sequencing over */ apr_hash_t *props; apr_hash_index_t *hi; /* used for constructing repos-local names for properties */ svn_stringbuf_t *work; /* passed to svn_repos_ funcs that fetch revprops. */ svn_repos_authz_func_t authz_read_func; void *authz_read_baton; }; struct dav_deadprop_rollback { int dummy; }; /* retrieve the "right" string to use as a repos path */ static const char * get_repos_path(struct dav_resource_private *info) { return info->repos_path; } /* construct the repos-local name for the given DAV property name */ static void get_repos_propname(dav_db *db, const dav_prop_name *name, const char **repos_propname) { if (strcmp(name->ns, SVN_DAV_PROP_NS_SVN) == 0) { /* recombine the namespace ("svn:") and the name. */ svn_stringbuf_set(db->work, SVN_PROP_PREFIX); svn_stringbuf_appendcstr(db->work, name->name); *repos_propname = db->work->data; } else if (strcmp(name->ns, SVN_DAV_PROP_NS_CUSTOM) == 0) { /* the name of a custom prop is just the name -- no ns URI */ *repos_propname = name->name; } else { *repos_propname = NULL; } } static dav_error * get_value(dav_db *db, const dav_prop_name *name, svn_string_t **pvalue) { const char *propname; svn_error_t *serr; /* get the repos-local name */ get_repos_propname(db, name, &propname); if (propname == NULL) { /* we know these are not present. */ *pvalue = NULL; return NULL; } /* If db->props exists, then use it to obtain property value. */ if (db->props) { *pvalue = svn_hash_gets(db->props, propname); return NULL; } /* We've got three different types of properties (node, txn, and revision), and we've got two different protocol versions to deal with. Let's try to make some sense of this, shall we? HTTP v1: working baseline ('wbl') resource -> txn prop change non-working, baselined resource ('bln') -> rev prop change [*] working, non-baselined resource ('wrk') -> node prop change HTTP v2: transaction resource ('txn') -> txn prop change revision resource ('rev') -> rev prop change transaction root resource ('txr') -> node prop change [*] This is a violation of the DeltaV spec (### see issue #916). */ if (db->resource->baselined) { if (db->resource->type == DAV_RESOURCE_TYPE_WORKING) serr = svn_fs_txn_prop(pvalue, db->resource->info->root.txn, propname, db->p); else serr = svn_repos_fs_revision_prop(pvalue, db->resource->info->repos->repos, db->resource->info->root.rev, propname, db->authz_read_func, db->authz_read_baton, db->p); } else if (db->resource->info->restype == DAV_SVN_RESTYPE_TXN_COLLECTION) { serr = svn_fs_txn_prop(pvalue, db->resource->info->root.txn, propname, db->p); } else { serr = svn_fs_node_prop(pvalue, db->resource->info->root.root, get_repos_path(db->resource->info), propname, db->p); } if (serr != NULL) return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, "could not fetch a property", db->resource->pool); return NULL; } static svn_error_t * change_txn_prop(svn_fs_txn_t *txn, const char *propname, const svn_string_t *value, apr_pool_t *scratch_pool) { if (strcmp(propname, SVN_PROP_REVISION_AUTHOR) == 0) return svn_error_create(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, "Attempted to modify 'svn:author' property " "on a transaction"); SVN_ERR(svn_repos_fs_change_txn_prop(txn, propname, value, scratch_pool)); return SVN_NO_ERROR; } static dav_error * save_value(dav_db *db, const dav_prop_name *name, const svn_string_t *const *old_value_p, const svn_string_t *value) { const char *propname; svn_error_t *serr; const dav_resource *resource = db->resource; apr_pool_t *subpool; /* get the repos-local name */ get_repos_propname(db, name, &propname); if (propname == NULL) { if (resource->info->repos->autoversioning) /* ignore the unknown namespace of the incoming prop. */ propname = name->name; else return dav_svn__new_error(db->p, HTTP_CONFLICT, 0, 0, "Properties may only be defined in the " SVN_DAV_PROP_NS_SVN " and " SVN_DAV_PROP_NS_CUSTOM " namespaces."); } /* We've got three different types of properties (node, txn, and revision), and we've got two different protocol versions to deal with. Let's try to make some sense of this, shall we? HTTP v1: working baseline ('wbl') resource -> txn prop change non-working, baselined resource ('bln') -> rev prop change [*] working, non-baselined resource ('wrk') -> node prop change HTTP v2: transaction resource ('txn') -> txn prop change revision resource ('rev') -> rev prop change transaction root resource ('txr') -> node prop change [*] This is a violation of the DeltaV spec (### see issue #916). */ /* A subpool to cope with mod_dav making multiple calls, e.g. during PROPPATCH with multiple values. */ subpool = svn_pool_create(resource->pool); if (resource->baselined) { if (resource->working) { serr = change_txn_prop(resource->info->root.txn, propname, value, subpool); } else { serr = svn_repos_fs_change_rev_prop4(resource->info->repos->repos, resource->info->root.rev, resource->info->repos->username, propname, old_value_p, value, TRUE, TRUE, db->authz_read_func, db->authz_read_baton, subpool); /* Prepare any hook failure message to get sent over the wire */ if (serr) { svn_error_t *purged_serr = svn_error_purge_tracing(serr); if (purged_serr->apr_err == SVN_ERR_REPOS_HOOK_FAILURE) purged_serr->message = apr_xml_quote_string (purged_serr->pool, purged_serr->message, 1); /* mod_dav doesn't handle the returned error very well, it generates its own generic error that will be returned to the client. Cache the detailed error here so that it can be returned a second time when the rollback mechanism triggers. */ resource->info->revprop_error = svn_error_dup(purged_serr); } /* Tell the logging subsystem about the revprop change. */ dav_svn__operational_log(resource->info, svn_log__change_rev_prop( resource->info->root.rev, propname, subpool)); } } else if (resource->info->restype == DAV_SVN_RESTYPE_TXN_COLLECTION) { serr = change_txn_prop(resource->info->root.txn, propname, value, subpool); } else { serr = svn_repos_fs_change_node_prop(resource->info->root.root, get_repos_path(resource->info), propname, value, subpool); } svn_pool_destroy(subpool); if (serr != NULL) return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, NULL, resource->pool); /* a change to the props was made; make sure our cached copy is gone */ db->props = NULL; return NULL; } static dav_error * db_open(apr_pool_t *p, const dav_resource *resource, int ro, dav_db **pdb) { dav_db *db; dav_svn__authz_read_baton *arb; /* Some resource types do not have deadprop databases. Specifically: REGULAR, VERSION, WORKING, and our custom transaction and transaction root resources have them. (SVN does not have WORKSPACE resources, and isn't covered here.) */ if (resource->type == DAV_RESOURCE_TYPE_HISTORY || resource->type == DAV_RESOURCE_TYPE_ACTIVITY || (resource->type == DAV_RESOURCE_TYPE_PRIVATE && resource->info->restype != DAV_SVN_RESTYPE_TXN_COLLECTION && resource->info->restype != DAV_SVN_RESTYPE_TXNROOT_COLLECTION)) { *pdb = NULL; return NULL; } /* If the DB is being opened R/W, and this isn't a working resource, then we have a problem! */ if ((! ro) && resource->type != DAV_RESOURCE_TYPE_WORKING && resource->type != DAV_RESOURCE_TYPE_PRIVATE && resource->info->restype != DAV_SVN_RESTYPE_TXN_COLLECTION) { /* ### Exception: in violation of deltaV, we *are* allowing a baseline resource to receive a proppatch, as a way of changing unversioned rev props. Remove this someday: see IZ #916. */ if (! (resource->baselined && resource->type == DAV_RESOURCE_TYPE_VERSION)) return dav_svn__new_error(p, HTTP_CONFLICT, 0, 0, "Properties may only be changed on working " "resources."); } db = apr_pcalloc(p, sizeof(*db)); db->resource = resource; db->p = svn_pool_create(p); /* ### temp hack */ db->work = svn_stringbuf_create_empty(db->p); /* make our path-based authz callback available to svn_repos_* funcs. */ arb = apr_pcalloc(p, sizeof(*arb)); arb->r = resource->info->r; arb->repos = resource->info->repos; db->authz_read_baton = arb; db->authz_read_func = dav_svn__authz_read_func(arb); /* ### use RO and node's mutable status to look for an error? */ *pdb = db; return NULL; } static void db_close(dav_db *db) { svn_pool_destroy(db->p); } static dav_error * db_define_namespaces(dav_db *db, dav_xmlns_info *xi) { dav_xmlns_add(xi, "S", SVN_DAV_PROP_NS_SVN); dav_xmlns_add(xi, "C", SVN_DAV_PROP_NS_CUSTOM); dav_xmlns_add(xi, "V", SVN_DAV_PROP_NS_DAV); /* ### we don't have any other possible namespaces right now. */ return NULL; } static dav_error * db_output_value(dav_db *db, const dav_prop_name *name, dav_xmlns_info *xi, apr_text_header *phdr, int *found) { const char *prefix; const char *s; svn_string_t *propval; dav_error *err; apr_pool_t *pool = db->resource->pool; if ((err = get_value(db, name, &propval)) != NULL) return err; /* return whether the prop was found, then punt or handle it. */ *found = (propval != NULL); if (propval == NULL) return NULL; if (strcmp(name->ns, SVN_DAV_PROP_NS_CUSTOM) == 0) prefix = "C:"; else prefix = "S:"; if (propval->len == 0) { /* empty value. add an empty elem. */ s = apr_psprintf(pool, "<%s%s/>" DEBUG_CR, prefix, name->name); apr_text_append(pool, phdr, s); } else { /* add value */ const char *xml_safe; const char *encoding = ""; /* Ensure XML-safety of our property values before sending them across the wire. */ if (! svn_xml_is_xml_safe(propval->data, propval->len)) { const svn_string_t *enc_propval = svn_base64_encode_string2(propval, TRUE, pool); xml_safe = enc_propval->data; encoding = " V:encoding=\"base64\""; } else { svn_stringbuf_t *xmlval = NULL; svn_xml_escape_cdata_string(&xmlval, propval, pool); xml_safe = xmlval->data; } s = apr_psprintf(pool, "<%s%s%s>", prefix, name->name, encoding); apr_text_append(pool, phdr, s); /* the value is in our pool which means it has the right lifetime. */ /* ### at least, per the current mod_dav architecture/API */ apr_text_append(pool, phdr, xml_safe); s = apr_psprintf(pool, "" DEBUG_CR, prefix, name->name); apr_text_append(pool, phdr, s); } return NULL; } static dav_error * db_map_namespaces(dav_db *db, const apr_array_header_t *namespaces, dav_namespace_map **mapping) { /* we don't need a namespace mapping right now. nothing to do */ return NULL; } static dav_error * decode_property_value(const svn_string_t **out_propval_p, svn_boolean_t *absent, const svn_string_t *maybe_encoded_propval, const apr_xml_elem *elem, apr_pool_t *pool) { apr_xml_attr *attr = elem->attr; /* Default: no "encoding" attribute. */ *absent = FALSE; *out_propval_p = maybe_encoded_propval; /* Check for special encodings of the property value. */ while (attr) { if (strcmp(attr->name, "encoding") == 0) /* ### namespace check? */ { const char *enc_type = attr->value; /* Handle known encodings here. */ if (enc_type && (strcmp(enc_type, "base64") == 0)) *out_propval_p = svn_base64_decode_string(maybe_encoded_propval, pool); else return dav_svn__new_error(pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, "Unknown property encoding"); break; } if (strcmp(attr->name, SVN_DAV__OLD_VALUE__ABSENT) == 0) { /* ### parse attr->value */ *absent = TRUE; *out_propval_p = NULL; } /* Next attribute, please. */ attr = attr->next; } return NULL; } static dav_error * db_store(dav_db *db, const dav_prop_name *name, const apr_xml_elem *elem, dav_namespace_map *mapping) { const svn_string_t *const *old_propval_p; const svn_string_t *old_propval; const svn_string_t *propval; svn_boolean_t absent; apr_pool_t *pool = db->p; dav_error *derr; /* SVN sends property values as a big blob of bytes. Thus, there should be no child elements of the property-name element. That also means that the entire contents of the blob is located in elem->first_cdata. The dav_xml_get_cdata() will figure it all out for us, but (normally) it should be awfully fast and not need to copy any data. */ propval = svn_string_create (dav_xml_get_cdata(elem, pool, 0 /* strip_white */), pool); derr = decode_property_value(&propval, &absent, propval, elem, pool); if (derr) return derr; if (absent && ! elem->first_child) /* ### better error check */ return dav_svn__new_error(pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, apr_psprintf(pool, "'%s' cannot be specified on the " "value without specifying an " "expectation", SVN_DAV__OLD_VALUE__ABSENT)); /* ### namespace check? */ if (elem->first_child && !strcmp(elem->first_child->name, SVN_DAV__OLD_VALUE)) { /* Parse OLD_PROPVAL. */ old_propval = svn_string_create(dav_xml_get_cdata(elem->first_child, pool, 0 /* strip_white */), pool); derr = decode_property_value(&old_propval, &absent, old_propval, elem->first_child, pool); if (derr) return derr; old_propval_p = (const svn_string_t *const *) &old_propval; } else old_propval_p = NULL; return save_value(db, name, old_propval_p, propval); } static dav_error * db_remove(dav_db *db, const dav_prop_name *name) { svn_error_t *serr; const char *propname; apr_pool_t *subpool; /* get the repos-local name */ get_repos_propname(db, name, &propname); /* ### non-svn props aren't in our repos, so punt for now */ if (propname == NULL) return NULL; /* A subpool to cope with mod_dav making multiple calls, e.g. during PROPPATCH with multiple values. */ subpool = svn_pool_create(db->resource->pool); /* Working Baseline or Working (Version) Resource */ if (db->resource->baselined) if (db->resource->working) serr = change_txn_prop(db->resource->info->root.txn, propname, NULL, subpool); else /* ### VIOLATING deltaV: you can't proppatch a baseline, it's not a working resource! But this is how we currently (hackily) allow the svn client to change unversioned rev props. See issue #916. */ serr = svn_repos_fs_change_rev_prop4(db->resource->info->repos->repos, db->resource->info->root.rev, db->resource->info->repos->username, propname, NULL, NULL, TRUE, TRUE, db->authz_read_func, db->authz_read_baton, subpool); else serr = svn_repos_fs_change_node_prop(db->resource->info->root.root, get_repos_path(db->resource->info), propname, NULL, subpool); svn_pool_destroy(subpool); if (serr != NULL) return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, "could not remove a property", db->resource->pool); /* a change to the props was made; make sure our cached copy is gone */ db->props = NULL; return NULL; } static int db_exists(dav_db *db, const dav_prop_name *name) { const char *propname; svn_string_t *propval; svn_error_t *serr; int retval; /* get the repos-local name */ get_repos_propname(db, name, &propname); /* ### non-svn props aren't in our repos */ if (propname == NULL) return 0; /* Working Baseline, Baseline, or (Working) Version resource */ if (db->resource->baselined) if (db->resource->type == DAV_RESOURCE_TYPE_WORKING) serr = svn_fs_txn_prop(&propval, db->resource->info->root.txn, propname, db->p); else serr = svn_repos_fs_revision_prop(&propval, db->resource->info->repos->repos, db->resource->info->root.rev, propname, db->authz_read_func, db->authz_read_baton, db->p); else serr = svn_fs_node_prop(&propval, db->resource->info->root.root, get_repos_path(db->resource->info), propname, db->p); /* ### try and dispose of the value? */ retval = (serr == NULL && propval != NULL); svn_error_clear(serr); return retval; } static void get_name(dav_db *db, dav_prop_name *pname) { if (db->hi == NULL) { pname->ns = pname->name = NULL; } else { const char *name = apr_hash_this_key(db->hi); #define PREFIX_LEN (sizeof(SVN_PROP_PREFIX) - 1) if (strncmp(name, SVN_PROP_PREFIX, PREFIX_LEN) == 0) #undef PREFIX_LEN { pname->ns = SVN_DAV_PROP_NS_SVN; pname->name = name + 4; } else { pname->ns = SVN_DAV_PROP_NS_CUSTOM; pname->name = name; } } } static dav_error * db_first_name(dav_db *db, dav_prop_name *pname) { /* for operational logging */ const char *action = NULL; /* if we don't have a copy of the properties, then get one */ if (db->props == NULL) { svn_error_t *serr; /* Working Baseline, Baseline, or (Working) Version resource */ if (db->resource->baselined) { if (db->resource->type == DAV_RESOURCE_TYPE_WORKING) serr = svn_fs_txn_proplist(&db->props, db->resource->info->root.txn, db->p); else { action = svn_log__rev_proplist(db->resource->info->root.rev, db->resource->pool); serr = svn_repos_fs_revision_proplist (&db->props, db->resource->info->repos->repos, db->resource->info->root.rev, db->authz_read_func, db->authz_read_baton, db->p); } } else { serr = svn_fs_node_proplist(&db->props, db->resource->info->root.root, get_repos_path(db->resource->info), db->p); if (! serr) { if (db->resource->collection) action = svn_log__get_dir(db->resource->info->repos_path, db->resource->info->root.rev, FALSE, TRUE, 0, db->resource->pool); else action = svn_log__get_file(db->resource->info->repos_path, db->resource->info->root.rev, FALSE, TRUE, db->resource->pool); } } if (serr != NULL) return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, "could not begin sequencing through " "properties", db->resource->pool); } /* begin the iteration over the hash */ db->hi = apr_hash_first(db->p, db->props); /* fetch the first key */ get_name(db, pname); /* If we have a high-level action to log, do so. */ if (action != NULL) dav_svn__operational_log(db->resource->info, action); return NULL; } static dav_error * db_next_name(dav_db *db, dav_prop_name *pname) { /* skip to the next hash entry */ if (db->hi != NULL) db->hi = apr_hash_next(db->hi); /* fetch the key */ get_name(db, pname); return NULL; } static dav_error * db_get_rollback(dav_db *db, const dav_prop_name *name, dav_deadprop_rollback **prollback) { /* This gets called by mod_dav in preparation for a revprop change. mod_dav_svn doesn't need to make any changes during rollback, but we want the rollback mechanism to trigger. Making changes in response to post-revprop-change hook errors would be positively wrong. */ *prollback = apr_palloc(db->p, sizeof(dav_deadprop_rollback)); return NULL; } static dav_error * db_apply_rollback(dav_db *db, dav_deadprop_rollback *rollback) { dav_error *derr; if (! db->resource->info->revprop_error) return NULL; /* Returning the original revprop change error here will cause this detailed error to get returned to the client in preference to the more generic error created by mod_dav. */ derr = dav_svn__convert_err(db->resource->info->revprop_error, HTTP_INTERNAL_SERVER_ERROR, NULL, db->resource->pool); db->resource->info->revprop_error = NULL; return derr; } const dav_hooks_propdb dav_svn__hooks_propdb = { db_open, db_close, db_define_namespaces, db_output_value, db_map_namespaces, db_store, db_remove, db_exists, db_first_name, db_next_name, db_get_rollback, db_apply_rollback, };