diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 06:33:50 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 06:33:50 +0000 |
commit | fe39ffb8b90ae4e002ed73fe98617cd590abb467 (patch) | |
tree | b80e5956907d8aeaaffe4e4f0c068c0e6157ce8b /modules/dav/main/mod_dav.c | |
parent | Initial commit. (diff) | |
download | apache2-fe39ffb8b90ae4e002ed73fe98617cd590abb467.tar.xz apache2-fe39ffb8b90ae4e002ed73fe98617cd590abb467.zip |
Adding upstream version 2.4.56.upstream/2.4.56upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'modules/dav/main/mod_dav.c')
-rw-r--r-- | modules/dav/main/mod_dav.c | 5237 |
1 files changed, 5237 insertions, 0 deletions
diff --git a/modules/dav/main/mod_dav.c b/modules/dav/main/mod_dav.c new file mode 100644 index 0000000..d16ab4a --- /dev/null +++ b/modules/dav/main/mod_dav.c @@ -0,0 +1,5237 @@ +/* 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. + */ + +/* + * DAV extension module for Apache 2.0.* + * + * This module is repository-independent. It depends on hooks provided by a + * repository implementation. + * + * APACHE ISSUES: + * - within a DAV hierarchy, if an unknown method is used and we default + * to Apache's implementation, it sends back an OPTIONS with the wrong + * set of methods -- there is NO HOOK for us. + * therefore: we need to manually handle the HTTP_METHOD_NOT_ALLOWED + * and HTTP_NOT_IMPLEMENTED responses (not ap_send_error_response). + * - process_mkcol_body() had to dup code from ap_setup_client_block(). + * - it would be nice to get status lines from Apache for arbitrary + * status codes + * - it would be nice to be able to extend Apache's set of response + * codes so that it doesn't return 500 when an unknown code is placed + * into r->status. + * - http_vhost functions should apply "const" to their params + * + * DESIGN NOTES: + * - For PROPFIND, we batch up the entire response in memory before + * sending it. We may want to reorganize around sending the information + * as we suck it in from the propdb. Alternatively, we should at least + * generate a total Content-Length if we're going to buffer in memory + * so that we can keep the connection open. + */ + +#include "apr_strings.h" +#include "apr_lib.h" /* for apr_is* */ + +#define APR_WANT_STRFUNC +#include "apr_want.h" + +#include "httpd.h" +#include "http_config.h" +#include "http_core.h" +#include "http_log.h" +#include "http_main.h" +#include "http_protocol.h" +#include "http_request.h" +#include "util_script.h" + +#include "mod_dav.h" + +#include "ap_provider.h" + + +/* ### what is the best way to set this? */ +#define DAV_DEFAULT_PROVIDER "filesystem" + +/* used to denote that mod_dav will be handling this request */ +#define DAV_HANDLER_NAME "dav-handler" + +APLOG_USE_MODULE(dav); + +enum { + DAV_ENABLED_UNSET = 0, + DAV_ENABLED_OFF, + DAV_ENABLED_ON +}; + +/* per-dir configuration */ +typedef struct { + const char *provider_name; + const dav_provider *provider; + const char *dir; + int locktimeout; + int allow_depthinfinity; + int allow_lockdiscovery; + +} dav_dir_conf; + +/* per-server configuration */ +typedef struct { + int unused; + +} dav_server_conf; + +#define DAV_INHERIT_VALUE(parent, child, field) \ + ((child)->field ? (child)->field : (parent)->field) + + +/* forward-declare for use in configuration lookup */ +extern module DAV_DECLARE_DATA dav_module; + +/* DAV methods */ +enum { + DAV_M_BIND = 0, + DAV_M_SEARCH, + DAV_M_LAST +}; +static int dav_methods[DAV_M_LAST]; + + +static int dav_init_handler(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, + server_rec *s) +{ + /* DBG0("dav_init_handler"); */ + + /* Register DAV methods */ + dav_methods[DAV_M_BIND] = ap_method_register(p, "BIND"); + dav_methods[DAV_M_SEARCH] = ap_method_register(p, "SEARCH"); + + return OK; +} + +static void *dav_create_server_config(apr_pool_t *p, server_rec *s) +{ + dav_server_conf *newconf; + + newconf = (dav_server_conf *)apr_pcalloc(p, sizeof(*newconf)); + + /* ### this isn't used at the moment... */ + + return newconf; +} + +static void *dav_merge_server_config(apr_pool_t *p, void *base, void *overrides) +{ +#if 0 + dav_server_conf *child = overrides; +#endif + dav_server_conf *newconf; + + newconf = (dav_server_conf *)apr_pcalloc(p, sizeof(*newconf)); + + /* ### nothing to merge right now... */ + + return newconf; +} + +static void *dav_create_dir_config(apr_pool_t *p, char *dir) +{ + /* NOTE: dir==NULL creates the default per-dir config */ + + dav_dir_conf *conf; + + conf = (dav_dir_conf *)apr_pcalloc(p, sizeof(*conf)); + + /* clean up the directory to remove any trailing slash */ + if (dir != NULL) { + char *d; + apr_size_t l; + + l = strlen(dir); + d = apr_pstrmemdup(p, dir, l); + if (l > 1 && d[l - 1] == '/') + d[l - 1] = '\0'; + conf->dir = d; + } + + return conf; +} + +static void *dav_merge_dir_config(apr_pool_t *p, void *base, void *overrides) +{ + dav_dir_conf *parent = base; + dav_dir_conf *child = overrides; + dav_dir_conf *newconf = (dav_dir_conf *)apr_pcalloc(p, sizeof(*newconf)); + + /* DBG3("dav_merge_dir_config: new=%08lx base=%08lx overrides=%08lx", + (long)newconf, (long)base, (long)overrides); */ + + newconf->provider_name = DAV_INHERIT_VALUE(parent, child, provider_name); + newconf->provider = DAV_INHERIT_VALUE(parent, child, provider); + if (parent->provider_name != NULL) { + if (child->provider_name == NULL) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, APLOGNO(00578) + "\"DAV Off\" cannot be used to turn off a subtree " + "of a DAV-enabled location."); + } + else if (strcasecmp(child->provider_name, + parent->provider_name) != 0) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, APLOGNO(00579) + "A subtree cannot specify a different DAV provider " + "than its parent."); + } + } + + newconf->locktimeout = DAV_INHERIT_VALUE(parent, child, locktimeout); + newconf->dir = DAV_INHERIT_VALUE(parent, child, dir); + newconf->allow_depthinfinity = DAV_INHERIT_VALUE(parent, child, + allow_depthinfinity); + newconf->allow_lockdiscovery = DAV_INHERIT_VALUE(parent, child, + allow_lockdiscovery); + + return newconf; +} + +DAV_DECLARE(const char *) dav_get_provider_name(request_rec *r) +{ + dav_dir_conf *conf = ap_get_module_config(r->per_dir_config, &dav_module); + return conf ? conf->provider_name : NULL; +} + +DAV_DECLARE(const dav_provider *) dav_get_provider(request_rec *r) +{ + dav_dir_conf *conf; + + conf = ap_get_module_config(r->per_dir_config, &dav_module); + /* assert: conf->provider_name != NULL + (otherwise, DAV is disabled, and we wouldn't be here) */ + + /* assert: conf->provider != NULL + (checked when conf->provider_name is set) */ + return conf->provider; +} + +DAV_DECLARE(const dav_hooks_locks *) dav_get_lock_hooks(request_rec *r) +{ + return dav_get_provider(r)->locks; +} + +DAV_DECLARE(const dav_hooks_propdb *) dav_get_propdb_hooks(request_rec *r) +{ + return dav_get_provider(r)->propdb; +} + +DAV_DECLARE(const dav_hooks_vsn *) dav_get_vsn_hooks(request_rec *r) +{ + return dav_get_provider(r)->vsn; +} + +DAV_DECLARE(const dav_hooks_binding *) dav_get_binding_hooks(request_rec *r) +{ + return dav_get_provider(r)->binding; +} + +DAV_DECLARE(const dav_hooks_search *) dav_get_search_hooks(request_rec *r) +{ + return dav_get_provider(r)->search; +} + +/* + * Command handler for the DAV directive, which is TAKE1. + */ +static const char *dav_cmd_dav(cmd_parms *cmd, void *config, const char *arg1) +{ + dav_dir_conf *conf = (dav_dir_conf *)config; + + if (strcasecmp(arg1, "on") == 0) { + conf->provider_name = DAV_DEFAULT_PROVIDER; + } + else if (strcasecmp(arg1, "off") == 0) { + conf->provider_name = NULL; + conf->provider = NULL; + } + else { + conf->provider_name = arg1; + } + + if (conf->provider_name != NULL) { + /* lookup and cache the actual provider now */ + conf->provider = dav_lookup_provider(conf->provider_name); + + if (conf->provider == NULL) { + /* by the time they use it, the provider should be loaded and + registered with us. */ + return apr_psprintf(cmd->pool, + "Unknown DAV provider: %s", + conf->provider_name); + } + } + + return NULL; +} + +/* + * Command handler for the DAVDepthInfinity directive, which is FLAG. + */ +static const char *dav_cmd_davdepthinfinity(cmd_parms *cmd, void *config, + int arg) +{ + dav_dir_conf *conf = (dav_dir_conf *)config; + + if (arg) + conf->allow_depthinfinity = DAV_ENABLED_ON; + else + conf->allow_depthinfinity = DAV_ENABLED_OFF; + return NULL; +} + +/* + * Command handler for the DAVLockDiscovery directive, which is FLAG. + */ +static const char *dav_cmd_davlockdiscovery(cmd_parms *cmd, void *config, + int arg) +{ + dav_dir_conf *conf = (dav_dir_conf *)config; + + if (arg) + conf->allow_lockdiscovery = DAV_ENABLED_ON; + else + conf->allow_lockdiscovery = DAV_ENABLED_OFF; + return NULL; +} + +/* + * Command handler for DAVMinTimeout directive, which is TAKE1 + */ +static const char *dav_cmd_davmintimeout(cmd_parms *cmd, void *config, + const char *arg1) +{ + dav_dir_conf *conf = (dav_dir_conf *)config; + + conf->locktimeout = atoi(arg1); + if (conf->locktimeout < 0) + return "DAVMinTimeout requires a non-negative integer."; + + return NULL; +} + +/* +** dav_error_response() +** +** Send a nice response back to the user. In most cases, Apache doesn't +** allow us to provide details in the body about what happened. This +** function allows us to completely specify the response body. +** +** ### this function is not logging any errors! (e.g. the body) +*/ +static int dav_error_response(request_rec *r, int status, const char *body) +{ + r->status = status; + r->status_line = ap_get_status_line(status); + + ap_set_content_type(r, "text/html; charset=ISO-8859-1"); + + /* begin the response now... */ + ap_rvputs(r, + DAV_RESPONSE_BODY_1, + r->status_line, + DAV_RESPONSE_BODY_2, + &r->status_line[4], + DAV_RESPONSE_BODY_3, + body, + DAV_RESPONSE_BODY_4, + ap_psignature("<hr />\n", r), + DAV_RESPONSE_BODY_5, + NULL); + + /* the response has been sent. */ + /* + * ### Use of DONE obviates logging..! + */ + return DONE; +} + + +/* + * Send a "standardized" error response based on the error's namespace & tag + */ +static int dav_error_response_tag(request_rec *r, + dav_error *err) +{ + r->status = err->status; + + ap_set_content_type(r, DAV_XML_CONTENT_TYPE); + + ap_rputs(DAV_XML_HEADER DEBUG_CR + "<D:error xmlns:D=\"DAV:\"", r); + + if (err->desc != NULL) { + /* ### should move this namespace somewhere (with the others!) */ + ap_rputs(" xmlns:m=\"http://apache.org/dav/xmlns\"", r); + } + + if (err->childtags) { + if (err->namespace != NULL) { + ap_rprintf(r, + " xmlns:C=\"%s\">" DEBUG_CR + "<C:%s>%s</C:%s>" DEBUG_CR, + err->namespace, + err->tagname, err->childtags, err->tagname); + } + else { + ap_rprintf(r, + ">" DEBUG_CR + "<D:%s>%s</D:%s>" DEBUG_CR, + err->tagname, err->childtags, err->tagname); + } + } + else { + if (err->namespace != NULL) { + ap_rprintf(r, + " xmlns:C=\"%s\">" DEBUG_CR + "<C:%s/>" DEBUG_CR, + err->namespace, err->tagname); + } + else { + ap_rprintf(r, + ">" DEBUG_CR + "<D:%s/>" DEBUG_CR, err->tagname); + } + } + + /* here's our mod_dav specific tag: */ + if (err->desc != NULL) { + ap_rprintf(r, + "<m:human-readable errcode=\"%d\">" DEBUG_CR + "%s" DEBUG_CR + "</m:human-readable>" DEBUG_CR, + err->error_id, + apr_xml_quote_string(r->pool, err->desc, 0)); + } + + ap_rputs("</D:error>" DEBUG_CR, r); + + /* the response has been sent. */ + /* + * ### Use of DONE obviates logging..! + */ + return DONE; +} + + +/* + * Apache's URI escaping does not replace '&' since that is a valid character + * in a URI (to form a query section). We must explicitly handle it so that + * we can embed the URI into an XML document. + */ +static const char *dav_xml_escape_uri(apr_pool_t *p, const char *uri) +{ + const char *e_uri = ap_escape_uri(p, uri); + + /* check the easy case... */ + if (ap_strchr_c(e_uri, '&') == NULL) + return e_uri; + + /* there was a '&', so more work is needed... sigh. */ + + /* + * Note: this is a teeny bit of overkill since we know there are no + * '<' or '>' characters, but who cares. + */ + return apr_xml_quote_string(p, e_uri, 0); +} + + +/* Write a complete RESPONSE object out as a <DAV:response> xml + element. Data is sent into brigade BB, which is auto-flushed into + the output filter stack for request R. Use POOL for any temporary + allocations. + + [Presumably the <multistatus> tag has already been written; this + routine is shared by dav_send_multistatus and dav_stream_response.] +*/ +DAV_DECLARE(void) dav_send_one_response(dav_response *response, + apr_bucket_brigade *bb, + request_rec *r, + apr_pool_t *pool) +{ + apr_text *t = NULL; + + if (response->propresult.xmlns == NULL) { + ap_fputs(r->output_filters, bb, "<D:response>"); + } + else { + ap_fputs(r->output_filters, bb, "<D:response"); + for (t = response->propresult.xmlns; t; t = t->next) { + ap_fputs(r->output_filters, bb, t->text); + } + ap_fputc(r->output_filters, bb, '>'); + } + + ap_fputstrs(r->output_filters, bb, + DEBUG_CR "<D:href>", + dav_xml_escape_uri(pool, response->href), + "</D:href>" DEBUG_CR, + NULL); + + if (response->propresult.propstats == NULL) { + /* use the Status-Line text from Apache. Note, this will + * default to 500 Internal Server Error if first->status + * is not a known (or valid) status code. + */ + ap_fputstrs(r->output_filters, bb, + "<D:status>HTTP/1.1 ", + ap_get_status_line(response->status), + "</D:status>" DEBUG_CR, + NULL); + } + else { + /* assume this includes <propstat> and is quoted properly */ + for (t = response->propresult.propstats; t; t = t->next) { + ap_fputs(r->output_filters, bb, t->text); + } + } + + if (response->desc != NULL) { + /* + * We supply the description, so we know it doesn't have to + * have any escaping/encoding applied to it. + */ + ap_fputstrs(r->output_filters, bb, + "<D:responsedescription>", + response->desc, + "</D:responsedescription>" DEBUG_CR, + NULL); + } + + ap_fputs(r->output_filters, bb, "</D:response>" DEBUG_CR); +} + + +/* Factorized helper function: prep request_rec R for a multistatus + response and write <multistatus> tag into BB, destined for + R->output_filters. Use xml NAMESPACES in initial tag, if + non-NULL. */ +DAV_DECLARE(void) dav_begin_multistatus(apr_bucket_brigade *bb, + request_rec *r, int status, + apr_array_header_t *namespaces) +{ + /* Set the correct status and Content-Type */ + r->status = status; + ap_set_content_type(r, DAV_XML_CONTENT_TYPE); + + /* Send the headers and actual multistatus response now... */ + ap_fputs(r->output_filters, bb, DAV_XML_HEADER DEBUG_CR + "<D:multistatus xmlns:D=\"DAV:\""); + + if (namespaces != NULL) { + int i; + + for (i = namespaces->nelts; i--; ) { + ap_fprintf(r->output_filters, bb, " xmlns:ns%d=\"%s\"", i, + APR_XML_GET_URI_ITEM(namespaces, i)); + } + } + + ap_fputs(r->output_filters, bb, ">" DEBUG_CR); +} + +/* Finish a multistatus response started by dav_begin_multistatus: */ +DAV_DECLARE(apr_status_t) dav_finish_multistatus(request_rec *r, + apr_bucket_brigade *bb) +{ + apr_bucket *b; + + ap_fputs(r->output_filters, bb, "</D:multistatus>" DEBUG_CR); + + /* indicate the end of the response body */ + b = apr_bucket_eos_create(r->connection->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + + /* deliver whatever might be remaining in the brigade */ + return ap_pass_brigade(r->output_filters, bb); +} + +DAV_DECLARE(void) dav_send_multistatus(request_rec *r, int status, + dav_response *first, + apr_array_header_t *namespaces) +{ + apr_pool_t *subpool; + apr_bucket_brigade *bb = apr_brigade_create(r->pool, + r->connection->bucket_alloc); + + dav_begin_multistatus(bb, r, status, namespaces); + + apr_pool_create(&subpool, r->pool); + apr_pool_tag(subpool, "mod_dav-multistatus"); + + for (; first != NULL; first = first->next) { + apr_pool_clear(subpool); + dav_send_one_response(first, bb, r, subpool); + } + apr_pool_destroy(subpool); + + dav_finish_multistatus(r, bb); +} + +/* + * dav_log_err() + * + * Write error information to the log. + */ +static void dav_log_err(request_rec *r, dav_error *err, int level) +{ + dav_error *errscan; + + /* Log the errors */ + /* ### should have a directive to log the first or all */ + for (errscan = err; errscan != NULL; errscan = errscan->prev) { + if (errscan->desc == NULL) + continue; + + /* Intentional no APLOGNO */ + ap_log_rerror(APLOG_MARK, level, errscan->aprerr, r, "%s [%d, #%d]", + errscan->desc, errscan->status, errscan->error_id); + } +} + +/* + * dav_handle_err() + * + * Handle the standard error processing. <err> must be non-NULL. + * + * <response> is set by the following: + * - dav_validate_request() + * - dav_add_lock() + * - repos_hooks->remove_resource + * - repos_hooks->move_resource + * - repos_hooks->copy_resource + * - vsn_hooks->update + */ +DAV_DECLARE(int) dav_handle_err(request_rec *r, dav_error *err, + dav_response *response) +{ + /* log the errors */ + dav_log_err(r, err, APLOG_ERR); + + if (!ap_is_HTTP_VALID_RESPONSE(err->status)) { + /* we have responded already */ + return AP_FILTER_ERROR; + } + + if (response == NULL) { + dav_error *stackerr = err; + + /* our error messages are safe; tell Apache this */ + apr_table_setn(r->notes, "verbose-error-to", "*"); + + /* Didn't get a multistatus response passed in, but we still + might be able to generate a standard <D:error> response. + Search the error stack for an errortag. */ + while (stackerr != NULL && stackerr->tagname == NULL) + stackerr = stackerr->prev; + + if (stackerr != NULL && stackerr->tagname != NULL) + return dav_error_response_tag(r, stackerr); + + return err->status; + } + + /* send the multistatus and tell Apache the request/response is DONE. */ + dav_send_multistatus(r, err->status, response, NULL); + return DONE; +} + +/* handy function for return values of methods that (may) create things. + * locn if provided is assumed to be escaped. */ +static int dav_created(request_rec *r, const char *locn, const char *what, + int replaced) +{ + const char *body; + + if (locn == NULL) { + locn = ap_escape_uri(r->pool, r->uri); + } + + /* did the target resource already exist? */ + if (replaced) { + /* Apache will supply a default message */ + return HTTP_NO_CONTENT; + } + + /* Per HTTP/1.1, S10.2.2: add a Location header to contain the + * URI that was created. */ + + /* Convert locn to an absolute URI, and return in Location header */ + apr_table_setn(r->headers_out, "Location", ap_construct_url(r->pool, locn, r)); + + /* ### insert an ETag header? see HTTP/1.1 S10.2.2 */ + + /* Apache doesn't allow us to set a variable body for HTTP_CREATED, so + * we must manufacture the entire response. */ + body = apr_pstrcat(r->pool, what, " ", ap_escape_html(r->pool, locn), + " has been created.", NULL); + return dav_error_response(r, HTTP_CREATED, body); +} + +/* ### move to dav_util? */ +DAV_DECLARE(int) dav_get_depth(request_rec *r, int def_depth) +{ + const char *depth = apr_table_get(r->headers_in, "Depth"); + + if (depth == NULL) { + return def_depth; + } + + if (ap_cstr_casecmp(depth, "infinity") == 0) { + return DAV_INFINITY; + } + else if (strcmp(depth, "0") == 0) { + return 0; + } + else if (strcmp(depth, "1") == 0) { + return 1; + } + + /* The caller will return an HTTP_BAD_REQUEST. This will augment the + * default message that Apache provides. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00580) + "An invalid Depth header was specified."); + return -1; +} + +static int dav_get_overwrite(request_rec *r) +{ + const char *overwrite = apr_table_get(r->headers_in, "Overwrite"); + + if (overwrite == NULL) { + return 1; /* default is "T" */ + } + + if ((*overwrite == 'F' || *overwrite == 'f') && overwrite[1] == '\0') { + return 0; + } + + if ((*overwrite == 'T' || *overwrite == 't') && overwrite[1] == '\0') { + return 1; + } + + /* The caller will return an HTTP_BAD_REQUEST. This will augment the + * default message that Apache provides. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00581) + "An invalid Overwrite header was specified."); + return -1; +} + +/* resolve a request URI to a resource descriptor. + * + * If label_allowed != 0, then allow the request target to be altered by + * a Label: header. + * + * If use_checked_in is true, then the repository provider should return + * the resource identified by the DAV:checked-in property of the resource + * identified by the Request-URI. + */ +DAV_DECLARE(dav_error *) dav_get_resource(request_rec *r, int label_allowed, + int use_checked_in, dav_resource **res_p) +{ + dav_dir_conf *conf; + const char *label = NULL; + dav_error *err; + + /* if the request target can be overridden, get any target selector */ + if (label_allowed) { + label = apr_table_get(r->headers_in, "label"); + } + + conf = ap_get_module_config(r->per_dir_config, &dav_module); + /* assert: conf->provider != NULL */ + if (conf->provider == NULL) { + return dav_new_error(r->pool, HTTP_METHOD_NOT_ALLOWED, 0, 0, + apr_psprintf(r->pool, + "DAV not enabled for %s", + ap_escape_html(r->pool, r->uri))); + } + + /* resolve the resource */ + err = (*conf->provider->repos->get_resource)(r, conf->dir, + label, use_checked_in, + res_p); + if (err != NULL) { + err = dav_push_error(r->pool, err->status, 0, + "Could not fetch resource information.", err); + return err; + } + + /* Note: this shouldn't happen, but just be sure... */ + if (*res_p == NULL) { + /* ### maybe use HTTP_INTERNAL_SERVER_ERROR */ + return dav_new_error(r->pool, HTTP_NOT_FOUND, 0, 0, + apr_psprintf(r->pool, + "The provider did not define a " + "resource for %s.", + ap_escape_html(r->pool, r->uri))); + } + + /* ### hmm. this doesn't feel like the right place or thing to do */ + /* if there were any input headers requiring a Vary header in the response, + * add it now */ + dav_add_vary_header(r, r, *res_p); + + return NULL; +} + +DAV_DECLARE(dav_error *) dav_open_lockdb(request_rec *r, + int ro, + dav_lockdb **lockdb) +{ + const dav_hooks_locks *hooks = DAV_GET_HOOKS_LOCKS(r); + + if (hooks == NULL) { + *lockdb = NULL; + return NULL; + } + + /* open the thing lazily */ + return (*hooks->open_lockdb)(r, ro, 0, lockdb); +} + +DAV_DECLARE(void) dav_close_lockdb(dav_lockdb *lockdb) +{ + (lockdb->hooks->close_lockdb)(lockdb); +} + +/** + * @return 1 if valid content-range, + * 0 if no content-range, + * -1 if malformed content-range + */ +static int dav_parse_range(request_rec *r, + apr_off_t *range_start, apr_off_t *range_end) +{ + const char *range_c; + char *range; + char *dash; + char *slash; + + range_c = apr_table_get(r->headers_in, "content-range"); + if (range_c == NULL) + return 0; + + range = apr_pstrdup(r->pool, range_c); + if (ap_cstr_casecmpn(range, "bytes ", 6) != 0 + || (dash = ap_strchr(range + 6, '-')) == NULL + || (slash = ap_strchr(range + 6, '/')) == NULL) { + /* malformed header */ + return -1; + } + + *dash++ = *slash++ = '\0'; + + /* detect invalid ranges */ + if (!ap_parse_strict_length(range_start, range + 6)) { + return -1; + } + if (!ap_parse_strict_length(range_end, dash) + || *range_end < *range_start) { + return -1; + } + + if (*slash != '*') { + apr_off_t dummy; + + if (!ap_parse_strict_length(&dummy, slash) + || dummy <= *range_end) { + return -1; + } + } + + /* we now have a valid range */ + return 1; +} + +/* handle the GET method */ +static int dav_method_get(request_rec *r) +{ + dav_resource *resource; + dav_error *err; + int status; + + /* This method should only be called when the resource is not + * visible to Apache. We will fetch the resource from the repository, + * then create a subrequest for Apache to handle. + */ + err = dav_get_resource(r, 1 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* set up the HTTP headers for the response */ + if ((err = (*resource->hooks->set_headers)(r, resource)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + "Unable to set up HTTP headers.", + err); + return dav_handle_err(r, err, NULL); + } + + /* Handle conditional requests */ + status = ap_meets_conditions(r); + if (status) { + return status; + } + + if (r->header_only) { + return DONE; + } + + /* okay... time to deliver the content */ + if ((err = (*resource->hooks->deliver)(resource, + r->output_filters)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + "Unable to deliver content.", + err); + return dav_handle_err(r, err, NULL); + } + + return DONE; +} + +/* validate resource/locks on POST, then pass to the default handler */ +static int dav_method_post(request_rec *r) +{ + dav_resource *resource; + dav_error *err; + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + /* Note: depth == 0. Implies no need for a multistatus response. */ + if ((err = dav_validate_request(r, resource, 0, NULL, NULL, + DAV_VALIDATE_RESOURCE, NULL)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + + return DECLINED; +} + +/* handle the PUT method */ +static int dav_method_put(request_rec *r) +{ + dav_resource *resource; + int resource_state; + dav_auto_version_info av_info; + const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r); + const char *body; + dav_error *err; + dav_error *err2; + dav_stream_mode mode; + dav_stream *stream; + dav_response *multi_response; + int has_range; + apr_off_t range_start; + apr_off_t range_end; + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + /* If not a file or collection resource, PUT not allowed */ + if (resource->type != DAV_RESOURCE_TYPE_REGULAR + && resource->type != DAV_RESOURCE_TYPE_WORKING) { + body = apr_psprintf(r->pool, + "Cannot create resource %s with PUT.", + ap_escape_html(r->pool, r->uri)); + return dav_error_response(r, HTTP_CONFLICT, body); + } + + /* Cannot PUT a collection */ + if (resource->collection) { + return dav_error_response(r, HTTP_CONFLICT, + "Cannot PUT to a collection."); + + } + + resource_state = dav_get_resource_state(r, resource); + + /* + * Note: depth == 0 normally requires no multistatus response. However, + * if we pass DAV_VALIDATE_PARENT, then we could get an error on a URI + * other than the Request-URI, thereby requiring a multistatus. + * + * If the resource does not exist (DAV_RESOURCE_NULL), then we must + * check the resource *and* its parent. If the resource exists or is + * a locknull resource, then we check only the resource. + */ + if ((err = dav_validate_request(r, resource, 0, NULL, &multi_response, + resource_state == DAV_RESOURCE_NULL ? + DAV_VALIDATE_PARENT : + DAV_VALIDATE_RESOURCE, NULL)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, multi_response); + } + + has_range = dav_parse_range(r, &range_start, &range_end); + if (has_range < 0) { + /* RFC 2616 14.16: If we receive an invalid Content-Range we must + * not use the content. + */ + body = apr_psprintf(r->pool, + "Malformed Content-Range header for PUT %s.", + ap_escape_html(r->pool, r->uri)); + return dav_error_response(r, HTTP_BAD_REQUEST, body); + } else if (has_range) { + mode = DAV_MODE_WRITE_SEEKABLE; + } + else { + mode = DAV_MODE_WRITE_TRUNC; + } + + /* make sure the resource can be modified (if versioning repository) */ + if ((err = dav_auto_checkout(r, resource, + 0 /* not parent_only */, + &av_info)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + + /* Create the new file in the repository */ + if ((err = (*resource->hooks->open_stream)(resource, mode, + &stream)) != NULL) { + int status = err->status ? err->status : HTTP_FORBIDDEN; + if (status > 299) { + err = dav_push_error(r->pool, status, 0, + apr_psprintf(r->pool, + "Unable to PUT new contents for %s.", + ap_escape_html(r->pool, r->uri)), + err); + } + else { + err = NULL; + } + } + + if (err == NULL && has_range) { + /* a range was provided. seek to the start */ + err = (*resource->hooks->seek_stream)(stream, range_start); + } + + if (err == NULL) { + apr_bucket_brigade *bb; + apr_bucket *b; + int seen_eos = 0; + + bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + + do { + apr_status_t rc; + + rc = ap_get_brigade(r->input_filters, bb, AP_MODE_READBYTES, + APR_BLOCK_READ, DAV_READ_BLOCKSIZE); + + if (rc != APR_SUCCESS) { + int http_err; + char *msg = ap_escape_html(r->pool, r->uri); + http_err = ap_map_http_request_error(rc, HTTP_BAD_REQUEST); + msg = apr_psprintf(r->pool, "An error occurred while reading " + "the request body (URI: %s)", + msg); + err = dav_new_error(r->pool, http_err, 0, rc, msg); + break; + } + + for (b = APR_BRIGADE_FIRST(bb); + b != APR_BRIGADE_SENTINEL(bb); + b = APR_BUCKET_NEXT(b)) + { + const char *data; + apr_size_t len; + + if (APR_BUCKET_IS_EOS(b)) { + seen_eos = 1; + break; + } + + if (APR_BUCKET_IS_METADATA(b)) { + continue; + } + + if (err == NULL) { + /* write whatever we read, until we see an error */ + rc = apr_bucket_read(b, &data, &len, APR_BLOCK_READ); + if (rc != APR_SUCCESS) { + err = dav_new_error(r->pool, HTTP_BAD_REQUEST, 0, rc, + apr_psprintf(r->pool, + "An error occurred while" + " reading the request body" + " from the bucket (URI: %s)", + ap_escape_html(r->pool, r->uri))); + break; + } + + err = (*resource->hooks->write_stream)(stream, data, len); + } + } + + apr_brigade_cleanup(bb); + } while (!seen_eos); + + apr_brigade_destroy(bb); + + err2 = (*resource->hooks->close_stream)(stream, + err == NULL /* commit */); + err = dav_join_error(err, err2); + } + + /* + * Ensure that we think the resource exists now. + * ### eek. if an error occurred during the write and we did not commit, + * ### then the resource might NOT exist (e.g. dav_fs_repos.c) + */ + if (err == NULL) { + resource->exists = 1; + } + + /* restore modifiability of resources back to what they were */ + err2 = dav_auto_checkin(r, resource, err != NULL /* undo if error */, + 0 /*unlock*/, &av_info); + + /* check for errors now */ + if (err != NULL) { + err = dav_join_error(err, err2); /* don't forget err2 */ + return dav_handle_err(r, err, NULL); + } + + if (err2 != NULL) { + /* just log a warning */ + err2 = dav_push_error(r->pool, err2->status, 0, + "The PUT was successful, but there " + "was a problem automatically checking in " + "the resource or its parent collection.", + err2); + dav_log_err(r, err2, APLOG_WARNING); + } + + /* ### place the Content-Type and Content-Language into the propdb */ + + if (locks_hooks != NULL) { + dav_lockdb *lockdb; + + if ((err = (*locks_hooks->open_lockdb)(r, 0, 0, &lockdb)) != NULL) { + /* The file creation was successful, but the locking failed. */ + err = dav_push_error(r->pool, err->status, 0, + "The file was PUT successfully, but there " + "was a problem opening the lock database " + "which prevents inheriting locks from the " + "parent resources.", + err); + return dav_handle_err(r, err, NULL); + } + + /* notify lock system that we have created/replaced a resource */ + err = dav_notify_created(r, lockdb, resource, resource_state, 0); + + (*locks_hooks->close_lockdb)(lockdb); + + if (err != NULL) { + /* The file creation was successful, but the locking failed. */ + err = dav_push_error(r->pool, err->status, 0, + "The file was PUT successfully, but there " + "was a problem updating its lock " + "information.", + err); + return dav_handle_err(r, err, NULL); + } + } + + /* NOTE: WebDAV spec, S8.7.1 states properties should be unaffected */ + + /* return an appropriate response (HTTP_CREATED or HTTP_NO_CONTENT) */ + return dav_created(r, NULL, "Resource", resource_state == DAV_RESOURCE_EXISTS); +} + + +/* Use POOL to temporarily construct a dav_response object (from WRES + STATUS, and PROPSTATS) and stream it via WRES's ctx->brigade. */ +static void dav_stream_response(dav_walk_resource *wres, + int status, + dav_get_props_result *propstats, + apr_pool_t *pool) +{ + dav_response resp = { 0 }; + dav_walker_ctx *ctx = wres->walk_ctx; + + resp.href = wres->resource->uri; + resp.status = status; + if (propstats) { + resp.propresult = *propstats; + } + + dav_send_one_response(&resp, ctx->bb, ctx->r, pool); +} + + +/* ### move this to dav_util? */ +DAV_DECLARE(void) dav_add_response(dav_walk_resource *wres, + int status, dav_get_props_result *propstats) +{ + dav_response *resp; + + /* just drop some data into an dav_response */ + resp = apr_pcalloc(wres->pool, sizeof(*resp)); + resp->href = apr_pstrdup(wres->pool, wres->resource->uri); + resp->status = status; + if (propstats) { + resp->propresult = *propstats; + } + + resp->next = wres->response; + wres->response = resp; +} + + +/* handle the DELETE method */ +static int dav_method_delete(request_rec *r) +{ + dav_resource *resource; + dav_auto_version_info av_info; + dav_error *err; + dav_error *err2; + dav_response *multi_response; + int result; + int depth; + + /* We don't use the request body right now, so torch it. */ + if ((result = ap_discard_request_body(r)) != OK) { + return result; + } + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* 2518 says that depth must be infinity only for collections. + * For non-collections, depth is ignored, unless it is an illegal value (1). + */ + depth = dav_get_depth(r, DAV_INFINITY); + + if (resource->collection && depth != DAV_INFINITY) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00582) + "Depth must be \"infinity\" for DELETE of a collection."); + return HTTP_BAD_REQUEST; + } + + if (!resource->collection && depth == 1) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00583) + "Depth of \"1\" is not allowed for DELETE."); + return HTTP_BAD_REQUEST; + } + + /* + ** If any resources fail the lock/If: conditions, then we must fail + ** the delete. Each of the failing resources will be listed within + ** a DAV:multistatus body, wrapped into a 424 response. + ** + ** Note that a failure on the resource itself does not generate a + ** multistatus response -- only internal members/collections. + */ + if ((err = dav_validate_request(r, resource, depth, NULL, + &multi_response, + DAV_VALIDATE_PARENT + | DAV_VALIDATE_USE_424, NULL)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not DELETE %s due to a failed " + "precondition (e.g. locks).", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, multi_response); + } + + /* ### RFC 2518 s. 8.10.5 says to remove _all_ locks, not just those + * locked by the token(s) in the if_header. + */ + if ((result = dav_unlock(r, resource, NULL)) != OK) { + return result; + } + + /* if versioned resource, make sure parent is checked out */ + if ((err = dav_auto_checkout(r, resource, 1 /* parent_only */, + &av_info)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + + /* try to remove the resource */ + err = (*resource->hooks->remove_resource)(resource, &multi_response); + + /* restore writability of parent back to what it was */ + err2 = dav_auto_checkin(r, NULL, err != NULL /* undo if error */, + 0 /*unlock*/, &av_info); + + /* check for errors now */ + if (err != NULL) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not DELETE %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, multi_response); + } + if (err2 != NULL) { + /* just log a warning */ + err = dav_push_error(r->pool, err2->status, 0, + "The DELETE was successful, but there " + "was a problem automatically checking in " + "the parent collection.", + err2); + dav_log_err(r, err, APLOG_WARNING); + } + + /* ### HTTP_NO_CONTENT if no body, HTTP_OK if there is a body (some day) */ + + /* Apache will supply a default error for this. */ + return HTTP_NO_CONTENT; +} + +/* generate DAV:supported-method-set OPTIONS response */ +static dav_error *dav_gen_supported_methods(request_rec *r, + const apr_xml_elem *elem, + const apr_table_t *methods, + apr_text_header *body) +{ + const apr_array_header_t *arr; + const apr_table_entry_t *elts; + apr_xml_elem *child; + apr_xml_attr *attr; + char *s; + int i; + + apr_text_append(r->pool, body, "<D:supported-method-set>" DEBUG_CR); + + if (elem->first_child == NULL) { + /* show all supported methods */ + arr = apr_table_elts(methods); + elts = (const apr_table_entry_t *)arr->elts; + + for (i = 0; i < arr->nelts; ++i) { + if (elts[i].key == NULL) + continue; + + s = apr_pstrcat(r->pool, + "<D:supported-method D:name=\"", + elts[i].key, + "\"/>" DEBUG_CR, NULL); + apr_text_append(r->pool, body, s); + } + } + else { + /* check for support of specific methods */ + for (child = elem->first_child; child != NULL; child = child->next) { + if (child->ns == APR_XML_NS_DAV_ID + && strcmp(child->name, "supported-method") == 0) { + const char *name = NULL; + + /* go through attributes to find method name */ + for (attr = child->attr; attr != NULL; attr = attr->next) { + if (attr->ns == APR_XML_NS_DAV_ID + && strcmp(attr->name, "name") == 0) + name = attr->value; + } + + if (name == NULL) { + return dav_new_error(r->pool, HTTP_BAD_REQUEST, 0, 0, + "A DAV:supported-method element " + "does not have a \"name\" attribute"); + } + + /* see if method is supported */ + if (apr_table_get(methods, name) != NULL) { + s = apr_pstrcat(r->pool, + "<D:supported-method D:name=\"", + name, "\"/>" DEBUG_CR, NULL); + apr_text_append(r->pool, body, s); + } + } + } + } + + apr_text_append(r->pool, body, "</D:supported-method-set>" DEBUG_CR); + return NULL; +} + +/* generate DAV:supported-live-property-set OPTIONS response */ +static dav_error *dav_gen_supported_live_props(request_rec *r, + const dav_resource *resource, + const apr_xml_elem *elem, + apr_text_header *body) +{ + dav_lockdb *lockdb; + dav_propdb *propdb; + apr_xml_elem *child; + apr_xml_attr *attr; + dav_error *err; + + /* open lock database, to report on supported lock properties */ + if ((err = dav_open_lockdb(r, 1, &lockdb)) != NULL) { + return dav_push_error(r->pool, err->status, 0, + "The lock database could not be opened, " + "preventing the reporting of supported lock " + "properties.", + err); + } + + /* open the property database (readonly) for the resource */ + if ((err = dav_open_propdb(r, lockdb, resource, DAV_PROPDB_RO, NULL, + &propdb)) != NULL) { + if (lockdb != NULL) + (*lockdb->hooks->close_lockdb)(lockdb); + + return dav_push_error(r->pool, err->status, 0, + "The property database could not be opened, " + "preventing report of supported properties.", + err); + } + + apr_text_append(r->pool, body, "<D:supported-live-property-set>" DEBUG_CR); + + if (elem->first_child == NULL) { + /* show all supported live properties */ + dav_get_props_result props = dav_get_allprops(propdb, DAV_PROP_INSERT_SUPPORTED); + body->last->next = props.propstats; + while (body->last->next != NULL) + body->last = body->last->next; + } + else { + /* check for support of specific live property */ + for (child = elem->first_child; child != NULL; child = child->next) { + if (child->ns == APR_XML_NS_DAV_ID + && strcmp(child->name, "supported-live-property") == 0) { + const char *name = NULL; + const char *nmspace = NULL; + + /* go through attributes to find name and namespace */ + for (attr = child->attr; attr != NULL; attr = attr->next) { + if (attr->ns == APR_XML_NS_DAV_ID) { + if (strcmp(attr->name, "name") == 0) + name = attr->value; + else if (strcmp(attr->name, "namespace") == 0) + nmspace = attr->value; + } + } + + if (name == NULL) { + err = dav_new_error(r->pool, HTTP_BAD_REQUEST, 0, 0, + "A DAV:supported-live-property " + "element does not have a \"name\" " + "attribute"); + break; + } + + /* default namespace to DAV: */ + if (nmspace == NULL) + nmspace = "DAV:"; + + /* check for support of property */ + dav_get_liveprop_supported(propdb, nmspace, name, body); + } + } + } + + apr_text_append(r->pool, body, "</D:supported-live-property-set>" DEBUG_CR); + + dav_close_propdb(propdb); + + if (lockdb != NULL) + (*lockdb->hooks->close_lockdb)(lockdb); + + return err; +} + + +/* generate DAV:supported-report-set OPTIONS response */ +static dav_error *dav_gen_supported_reports(request_rec *r, + const dav_resource *resource, + const apr_xml_elem *elem, + apr_text_header *body) +{ + apr_xml_elem *child; + apr_xml_attr *attr; + dav_error *err = NULL; + char *s; + apr_array_header_t *reports; + const dav_report_elem *rp; + + apr_text_append(r->pool, body, "<D:supported-report-set>" DEBUG_CR); + + reports = apr_array_make(r->pool, 5, sizeof(const char *)); + dav_run_gather_reports(r, resource, reports, &err); + if (err != NULL) { + return dav_push_error(r->pool, err->status, 0, + "DAV:supported-report-set could not be " + "determined due to a problem fetching the " + "available reports for this resource.", + err); + } + + if (elem->first_child == NULL) { + int i; + + /* show all supported reports */ + rp = (const dav_report_elem *)reports->elts; + for (i = 0; i < reports->nelts; i++, rp++) { + /* Note: we presume reports->namespace is + * properly XML/URL quoted */ + s = apr_pstrcat(r->pool, + "<D:supported-report D:name=\"", + rp->name, + "\" D:namespace=\"", + rp->nmspace, + "\"/>" DEBUG_CR, NULL); + apr_text_append(r->pool, body, s); + } + } + else { + /* check for support of specific report */ + for (child = elem->first_child; child != NULL; child = child->next) { + if (child->ns == APR_XML_NS_DAV_ID + && strcmp(child->name, "supported-report") == 0) { + const char *name = NULL; + const char *nmspace = NULL; + int i; + + /* go through attributes to find name and namespace */ + for (attr = child->attr; attr != NULL; attr = attr->next) { + if (attr->ns == APR_XML_NS_DAV_ID) { + if (strcmp(attr->name, "name") == 0) + name = attr->value; + else if (strcmp(attr->name, "namespace") == 0) + nmspace = attr->value; + } + } + + if (name == NULL) { + return dav_new_error(r->pool, HTTP_BAD_REQUEST, 0, 0, + "A DAV:supported-report element " + "does not have a \"name\" attribute"); + } + + /* default namespace to DAV: */ + if (nmspace == NULL) { + nmspace = "DAV:"; + } + + rp = (const dav_report_elem *)reports->elts; + for (i = 0; i < reports->nelts; i++, rp++) { + if (strcmp(name, rp->name) == 0 + && strcmp(nmspace, rp->nmspace) == 0) { + /* Note: we presume reports->nmspace is + * properly XML/URL quoted + */ + s = apr_pstrcat(r->pool, + "<D:supported-report " + "D:name=\"", + rp->name, + "\" D:namespace=\"", + rp->nmspace, + "\"/>" DEBUG_CR, NULL); + apr_text_append(r->pool, body, s); + break; + } + } + } + } + } + + apr_text_append(r->pool, body, "</D:supported-report-set>" DEBUG_CR); + return NULL; +} + + +/* handle the SEARCH method */ +static int dav_method_search(request_rec *r) +{ + const dav_hooks_search *search_hooks = DAV_GET_HOOKS_SEARCH(r); + dav_resource *resource; + dav_error *err; + dav_response *multi_status; + + /* If no search provider, decline the request */ + if (search_hooks == NULL) + return DECLINED; + + /* This method should only be called when the resource is not + * visible to Apache. We will fetch the resource from the repository, + * then create a subrequest for Apache to handle. + */ + err = dav_get_resource(r, 1 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* set up the HTTP headers for the response */ + if ((err = (*resource->hooks->set_headers)(r, resource)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + "Unable to set up HTTP headers.", + err); + return dav_handle_err(r, err, NULL); + } + + if (r->header_only) { + return DONE; + } + + /* okay... time to search the content */ + /* Let's validate XML and process walk function + * in the hook function + */ + if ((err = (*search_hooks->search_resource)(r, &multi_status)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + + /* We have results in multi_status */ + /* Should I pass namespace?? */ + dav_send_multistatus(r, HTTP_MULTI_STATUS, multi_status, NULL); + + return DONE; +} + + +/* handle the OPTIONS method */ +static int dav_method_options(request_rec *r) +{ + const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r); + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + const dav_hooks_binding *binding_hooks = DAV_GET_HOOKS_BINDING(r); + const dav_hooks_search *search_hooks = DAV_GET_HOOKS_SEARCH(r); + dav_resource *resource; + const char *dav_level; + char *allow; + char *s; + const apr_array_header_t *arr; + const apr_table_entry_t *elts; + apr_table_t *methods = apr_table_make(r->pool, 12); + apr_text_header vsn_options = { 0 }; + apr_text_header body = { 0 }; + apr_text *t; + int text_size; + int result; + int i; + apr_array_header_t *uri_ary; + apr_xml_doc *doc; + const apr_xml_elem *elem; + dav_error *err; + + apr_array_header_t *extensions; + ap_list_provider_names_t *entry; + + /* resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* parse any request body */ + if ((result = ap_xml_parse_input(r, &doc)) != OK) { + return result; + } + /* note: doc == NULL if no request body */ + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (doc && !dav_validate_root(doc, "options")) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00584) + "The \"options\" element was not found."); + return HTTP_BAD_REQUEST; + } + + /* determine which providers are available */ + dav_level = "1"; + + if (locks_hooks != NULL) { + dav_level = "1,2"; + } + + if (binding_hooks != NULL) + dav_level = apr_pstrcat(r->pool, dav_level, ",bindings", NULL); + + /* DAV header additions registered by external modules */ + extensions = ap_list_provider_names(r->pool, DAV_OPTIONS_EXTENSION_GROUP, "0"); + entry = (ap_list_provider_names_t *)extensions->elts; + + for (i = 0; i < extensions->nelts; i++, entry++) { + const dav_options_provider *options = + dav_get_options_providers(entry->provider_name); + + if (options && options->dav_header) { + apr_text_header hoptions = { 0 }; + + options->dav_header(r, resource, &hoptions); + for (t = hoptions.first; t && t->text; t = t->next) + dav_level = apr_pstrcat(r->pool, dav_level, ",", t->text, NULL); + } + } + + /* ### + * MSFT Web Folders chokes if length of DAV header value > 63 characters! + * To workaround that, we use separate DAV headers for versioning and + * live prop provider namespace URIs. + * ### + */ + apr_table_setn(r->headers_out, "DAV", dav_level); + + /* + * If there is a versioning provider, generate DAV headers + * for versioning options. + */ + if (vsn_hooks != NULL) { + (*vsn_hooks->get_vsn_options)(r->pool, &vsn_options); + + for (t = vsn_options.first; t != NULL; t = t->next) + apr_table_addn(r->headers_out, "DAV", t->text); + } + + /* + * Gather property set URIs from all the liveprop providers, + * and generate a separate DAV header for each URI, to avoid + * problems with long header lengths. + */ + uri_ary = apr_array_make(r->pool, 5, sizeof(const char *)); + dav_run_gather_propsets(uri_ary); + for (i = 0; i < uri_ary->nelts; ++i) { + if (((char **)uri_ary->elts)[i] != NULL) + apr_table_addn(r->headers_out, "DAV", ((char **)uri_ary->elts)[i]); + } + + /* this tells MSFT products to skip looking for FrontPage extensions */ + apr_table_setn(r->headers_out, "MS-Author-Via", "DAV"); + + /* + * Determine which methods are allowed on the resource. + * Three cases: resource is null (3), is lock-null (7.4), or exists. + * + * All cases support OPTIONS, and if there is a lock provider, LOCK. + * (Lock-) null resources also support MKCOL and PUT. + * Lock-null supports PROPFIND and UNLOCK. + * Existing resources support lots of stuff. + */ + + apr_table_addn(methods, "OPTIONS", ""); + + /* ### take into account resource type */ + switch (dav_get_resource_state(r, resource)) + { + case DAV_RESOURCE_EXISTS: + /* resource exists */ + apr_table_addn(methods, "GET", ""); + apr_table_addn(methods, "HEAD", ""); + apr_table_addn(methods, "POST", ""); + apr_table_addn(methods, "DELETE", ""); + apr_table_addn(methods, "TRACE", ""); + apr_table_addn(methods, "PROPFIND", ""); + apr_table_addn(methods, "PROPPATCH", ""); + apr_table_addn(methods, "COPY", ""); + apr_table_addn(methods, "MOVE", ""); + + if (!resource->collection) + apr_table_addn(methods, "PUT", ""); + + if (locks_hooks != NULL) { + apr_table_addn(methods, "LOCK", ""); + apr_table_addn(methods, "UNLOCK", ""); + } + + break; + + case DAV_RESOURCE_LOCK_NULL: + /* resource is lock-null. */ + apr_table_addn(methods, "MKCOL", ""); + apr_table_addn(methods, "PROPFIND", ""); + apr_table_addn(methods, "PUT", ""); + + if (locks_hooks != NULL) { + apr_table_addn(methods, "LOCK", ""); + apr_table_addn(methods, "UNLOCK", ""); + } + + break; + + case DAV_RESOURCE_NULL: + /* resource is null. */ + apr_table_addn(methods, "MKCOL", ""); + apr_table_addn(methods, "PUT", ""); + + if (locks_hooks != NULL) + apr_table_addn(methods, "LOCK", ""); + + break; + + default: + /* ### internal error! */ + break; + } + + /* If there is a versioning provider, add versioning methods */ + if (vsn_hooks != NULL) { + if (!resource->exists) { + if ((*vsn_hooks->versionable)(resource)) + apr_table_addn(methods, "VERSION-CONTROL", ""); + + if (vsn_hooks->can_be_workspace != NULL + && (*vsn_hooks->can_be_workspace)(resource)) + apr_table_addn(methods, "MKWORKSPACE", ""); + + if (vsn_hooks->can_be_activity != NULL + && (*vsn_hooks->can_be_activity)(resource)) + apr_table_addn(methods, "MKACTIVITY", ""); + } + else if (!resource->versioned) { + if ((*vsn_hooks->versionable)(resource)) + apr_table_addn(methods, "VERSION-CONTROL", ""); + } + else if (resource->working) { + apr_table_addn(methods, "CHECKIN", ""); + + /* ### we might not support this DeltaV option */ + apr_table_addn(methods, "UNCHECKOUT", ""); + } + else if (vsn_hooks->add_label != NULL) { + apr_table_addn(methods, "CHECKOUT", ""); + apr_table_addn(methods, "LABEL", ""); + } + else { + apr_table_addn(methods, "CHECKOUT", ""); + } + } + + /* If there is a bindings provider, see if resource is bindable */ + if (binding_hooks != NULL + && (*binding_hooks->is_bindable)(resource)) { + apr_table_addn(methods, "BIND", ""); + } + + /* If there is a search provider, set SEARCH in option */ + if (search_hooks != NULL) { + apr_table_addn(methods, "SEARCH", ""); + } + + /* additional methods registered by external modules */ + extensions = ap_list_provider_names(r->pool, DAV_OPTIONS_EXTENSION_GROUP, "0"); + entry = (ap_list_provider_names_t *)extensions->elts; + + for (i = 0; i < extensions->nelts; i++, entry++) { + const dav_options_provider *options = + dav_get_options_providers(entry->provider_name); + + if (options && options->dav_method) { + apr_text_header hoptions = { 0 }; + + options->dav_method(r, resource, &hoptions); + for (t = hoptions.first; t && t->text; t = t->next) + apr_table_addn(methods, t->text, ""); + } + } + + /* Generate the Allow header */ + arr = apr_table_elts(methods); + elts = (const apr_table_entry_t *)arr->elts; + text_size = 0; + + /* first, compute total length */ + for (i = 0; i < arr->nelts; ++i) { + if (elts[i].key == NULL) + continue; + + /* add 1 for comma or null */ + text_size += strlen(elts[i].key) + 1; + } + + s = allow = apr_palloc(r->pool, text_size); + + for (i = 0; i < arr->nelts; ++i) { + if (elts[i].key == NULL) + continue; + + if (s != allow) + *s++ = ','; + + strcpy(s, elts[i].key); + s += strlen(s); + } + + apr_table_setn(r->headers_out, "Allow", allow); + + + /* If there is search set_option_head function, set head */ + /* DASL: <DAV:basicsearch> + * DASL: <http://foo.bar.com/syntax1> + * DASL: <http://akuma.com/syntax2> + */ + if (search_hooks != NULL + && *search_hooks->set_option_head != NULL) { + if ((err = (*search_hooks->set_option_head)(r)) != NULL) { + return dav_handle_err(r, err, NULL); + } + } + + /* if there was no request body, then there is no response body */ + if (doc == NULL) { + ap_set_content_length(r, 0); + + /* ### this sends a Content-Type. the default OPTIONS does not. */ + + /* ### the default (ap_send_http_options) returns OK, but I believe + * ### that is because it is the default handler and nothing else + * ### will run after the thing. */ + return DONE; + } + + /* handle each options request */ + for (elem = doc->root->first_child; elem != NULL; elem = elem->next) { + /* check for something we recognize first */ + int core_option = 0; + dav_error *err = NULL; + + if (elem->ns == APR_XML_NS_DAV_ID) { + if (strcmp(elem->name, "supported-method-set") == 0) { + err = dav_gen_supported_methods(r, elem, methods, &body); + core_option = 1; + } + else if (strcmp(elem->name, "supported-live-property-set") == 0) { + err = dav_gen_supported_live_props(r, resource, elem, &body); + core_option = 1; + } + else if (strcmp(elem->name, "supported-report-set") == 0) { + err = dav_gen_supported_reports(r, resource, elem, &body); + core_option = 1; + } + } + + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* if unrecognized option, pass to versioning provider */ + if (!core_option && vsn_hooks != NULL) { + if ((err = (*vsn_hooks->get_option)(resource, elem, &body)) + != NULL) { + return dav_handle_err(r, err, NULL); + } + } + } + + /* send the options response */ + r->status = HTTP_OK; + ap_set_content_type(r, DAV_XML_CONTENT_TYPE); + + /* send the headers and response body */ + ap_rputs(DAV_XML_HEADER DEBUG_CR + "<D:options-response xmlns:D=\"DAV:\">" DEBUG_CR, r); + + for (t = body.first; t != NULL; t = t->next) + ap_rputs(t->text, r); + + ap_rputs("</D:options-response>" DEBUG_CR, r); + + /* we've sent everything necessary to the client. */ + return DONE; +} + +static void dav_cache_badprops(dav_walker_ctx *ctx) +{ + const apr_xml_elem *elem; + apr_text_header hdr = { 0 }; + + /* just return if we built the thing already */ + if (ctx->propstat_404 != NULL) { + return; + } + + apr_text_append(ctx->w.pool, &hdr, + "<D:propstat>" DEBUG_CR + "<D:prop>" DEBUG_CR); + + elem = dav_find_child(ctx->doc->root, "prop"); + for (elem = elem->first_child; elem; elem = elem->next) { + apr_text_append(ctx->w.pool, &hdr, + apr_xml_empty_elem(ctx->w.pool, elem)); + } + + apr_text_append(ctx->w.pool, &hdr, + "</D:prop>" DEBUG_CR + "<D:status>HTTP/1.1 404 Not Found</D:status>" DEBUG_CR + "</D:propstat>" DEBUG_CR); + + ctx->propstat_404 = hdr.first; +} + +static dav_error * dav_propfind_walker(dav_walk_resource *wres, int calltype) +{ + dav_walker_ctx *ctx = wres->walk_ctx; + dav_dir_conf *conf; + int flags = DAV_PROPDB_RO; + dav_error *err; + dav_propdb *propdb; + dav_get_props_result propstats = { 0 }; + + /* check for any method preconditions */ + if (dav_run_method_precondition(ctx->r, NULL, wres->resource, ctx->doc, &err) != DECLINED + && err) { + apr_pool_clear(ctx->scratchpool); + return NULL; + } + + conf = ap_get_module_config(ctx->r->per_dir_config, &dav_module); + if (conf && conf->allow_lockdiscovery == DAV_ENABLED_OFF) + flags |= DAV_PROPDB_DISABLE_LOCKDISCOVERY; + + /* + ** Note: ctx->doc can only be NULL for DAV_PROPFIND_IS_ALLPROP. Since + ** dav_get_allprops() does not need to do namespace translation, + ** we're okay. + ** + ** Note: we cast to lose the "const". The propdb won't try to change + ** the resource, however, since we are opening readonly. + */ + err = dav_popen_propdb(ctx->scratchpool, + ctx->r, ctx->w.lockdb, wres->resource, flags, + ctx->doc ? ctx->doc->namespaces : NULL, &propdb); + if (err != NULL) { + /* ### do something with err! */ + + if (ctx->propfind_type == DAV_PROPFIND_IS_PROP) { + dav_get_props_result badprops = { 0 }; + + /* some props were expected on this collection/resource */ + dav_cache_badprops(ctx); + badprops.propstats = ctx->propstat_404; + dav_stream_response(wres, 0, &badprops, ctx->scratchpool); + } + else { + /* no props on this collection/resource */ + dav_stream_response(wres, HTTP_OK, NULL, ctx->scratchpool); + } + + apr_pool_clear(ctx->scratchpool); + return NULL; + } + /* ### what to do about closing the propdb on server failure? */ + + if (ctx->propfind_type == DAV_PROPFIND_IS_PROP) { + propstats = dav_get_props(propdb, ctx->doc); + } + else { + dav_prop_insert what = ctx->propfind_type == DAV_PROPFIND_IS_ALLPROP + ? DAV_PROP_INSERT_VALUE + : DAV_PROP_INSERT_NAME; + propstats = dav_get_allprops(propdb, what); + } + dav_stream_response(wres, 0, &propstats, ctx->scratchpool); + + dav_close_propdb(propdb); + + /* at this point, ctx->scratchpool has been used to stream a + single response. this function fully controls the pool, and + thus has the right to clear it for the next iteration of this + callback. */ + apr_pool_clear(ctx->scratchpool); + + return NULL; +} + +/* handle the PROPFIND method */ +static int dav_method_propfind(request_rec *r) +{ + dav_resource *resource; + int depth; + dav_error *err; + int result; + apr_xml_doc *doc; + dav_walker_ctx ctx = { { 0 } }; + dav_response *multi_status; + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 1 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + if ((result = ap_xml_parse_input(r, &doc)) != OK) { + return result; + } + /* note: doc == NULL if no request body */ + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (dav_get_resource_state(r, resource) == DAV_RESOURCE_NULL) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + if ((depth = dav_get_depth(r, DAV_INFINITY)) < 0) { + /* dav_get_depth() supplies additional information for the + * default message. */ + return HTTP_BAD_REQUEST; + } + + if (depth == DAV_INFINITY && resource->collection) { + dav_dir_conf *conf; + conf = (dav_dir_conf *)ap_get_module_config(r->per_dir_config, + &dav_module); + /* default is to DISALLOW these requests */ + if (conf->allow_depthinfinity != DAV_ENABLED_ON) { + return dav_error_response(r, HTTP_FORBIDDEN, + apr_psprintf(r->pool, + "PROPFIND requests with a " + "Depth of \"infinity\" are " + "not allowed for %s.", + ap_escape_html(r->pool, + r->uri))); + } + } + + if (doc && !dav_validate_root(doc, "propfind")) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00585) + "The \"propfind\" element was not found."); + return HTTP_BAD_REQUEST; + } + + /* ### validate that only one of these three elements is present */ + + if (doc == NULL || dav_find_child(doc->root, "allprop") != NULL) { + /* note: no request body implies allprop */ + ctx.propfind_type = DAV_PROPFIND_IS_ALLPROP; + } + else if (dav_find_child(doc->root, "propname") != NULL) { + ctx.propfind_type = DAV_PROPFIND_IS_PROPNAME; + } + else if (dav_find_child(doc->root, "prop") != NULL) { + ctx.propfind_type = DAV_PROPFIND_IS_PROP; + } + else { + /* "propfind" element must have one of the above three children */ + + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00586) + "The \"propfind\" element does not contain one of " + "the required child elements (the specific command)."); + return HTTP_BAD_REQUEST; + } + + ctx.w.walk_type = DAV_WALKTYPE_NORMAL | DAV_WALKTYPE_AUTH; + ctx.w.func = dav_propfind_walker; + ctx.w.walk_ctx = &ctx; + ctx.w.pool = r->pool; + ctx.w.root = resource; + + ctx.doc = doc; + ctx.r = r; + ctx.bb = apr_brigade_create(r->pool, r->connection->bucket_alloc); + apr_pool_create(&ctx.scratchpool, r->pool); + apr_pool_tag(ctx.scratchpool, "mod_dav-scratch"); + + if ((err = dav_open_lockdb(r, 1, &ctx.w.lockdb)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + "The lock database could not be opened, " + "preventing access to the various lock " + "properties for the PROPFIND.", + err); + return dav_handle_err(r, err, NULL); + } + if (ctx.w.lockdb != NULL) { + /* if we have a lock database, then we can walk locknull resources */ + ctx.w.walk_type |= DAV_WALKTYPE_LOCKNULL; + } + + /* send <multistatus> tag, with all doc->namespaces attached. */ + + /* NOTE: we *cannot* leave out the doc's namespaces from the + initial <multistatus> tag. if a 404 was generated for an HREF, + then we need to spit out the doc's namespaces for use by the + 404. Note that <response> elements will override these ns0, + ns1, etc, but NOT within the <response> scope for the + badprops. */ + dav_begin_multistatus(ctx.bb, r, HTTP_MULTI_STATUS, + doc ? doc->namespaces : NULL); + + /* Have the provider walk the resource. */ + err = (*resource->hooks->walk)(&ctx.w, depth, &multi_status); + + if (ctx.w.lockdb != NULL) { + (*ctx.w.lockdb->hooks->close_lockdb)(ctx.w.lockdb); + } + + if (err != NULL) { + /* If an error occurred during the resource walk, there's + basically nothing we can do but abort the connection and + log an error. This is one of the limitations of HTTP; it + needs to "know" the entire status of the response before + generating it, which is just impossible in these streamy + response situations. */ + err = dav_push_error(r->pool, err->status, 0, + "Provider encountered an error while streaming" + " a multistatus PROPFIND response.", err); + dav_log_err(r, err, APLOG_ERR); + r->connection->aborted = 1; + return DONE; + } + + dav_finish_multistatus(r, ctx.bb); + + /* the response has been sent. */ + return DONE; +} + +DAV_DECLARE(apr_text *) dav_failed_proppatch(apr_pool_t *p, + apr_array_header_t *prop_ctx) +{ + apr_text_header hdr = { 0 }; + int i = prop_ctx->nelts; + dav_prop_ctx *ctx = (dav_prop_ctx *)prop_ctx->elts; + dav_error *err424_set = NULL; + dav_error *err424_delete = NULL; + const char *s; + + /* ### might be nice to sort by status code and description */ + + for ( ; i-- > 0; ++ctx ) { + apr_text_append(p, &hdr, + "<D:propstat>" DEBUG_CR + "<D:prop>"); + apr_text_append(p, &hdr, apr_xml_empty_elem(p, ctx->prop)); + apr_text_append(p, &hdr, "</D:prop>" DEBUG_CR); + + if (ctx->err == NULL) { + /* nothing was assigned here yet, so make it a 424 */ + + if (ctx->operation == DAV_PROP_OP_SET) { + if (err424_set == NULL) + err424_set = dav_new_error(p, HTTP_FAILED_DEPENDENCY, 0, 0, + "Attempted DAV:set operation " + "could not be completed due " + "to other errors."); + ctx->err = err424_set; + } + else if (ctx->operation == DAV_PROP_OP_DELETE) { + if (err424_delete == NULL) + err424_delete = dav_new_error(p, HTTP_FAILED_DEPENDENCY, 0, 0, + "Attempted DAV:remove " + "operation could not be " + "completed due to other " + "errors."); + ctx->err = err424_delete; + } + } + + s = apr_psprintf(p, + "<D:status>" + "HTTP/1.1 %d (status)" + "</D:status>" DEBUG_CR, + ctx->err->status); + apr_text_append(p, &hdr, s); + + /* ### we should use compute_desc if necessary... */ + if (ctx->err->desc != NULL) { + apr_text_append(p, &hdr, "<D:responsedescription>" DEBUG_CR); + apr_text_append(p, &hdr, ctx->err->desc); + apr_text_append(p, &hdr, "</D:responsedescription>" DEBUG_CR); + } + + apr_text_append(p, &hdr, "</D:propstat>" DEBUG_CR); + } + + return hdr.first; +} + +DAV_DECLARE(apr_text *) dav_success_proppatch(apr_pool_t *p, + apr_array_header_t *prop_ctx) +{ + apr_text_header hdr = { 0 }; + int i = prop_ctx->nelts; + dav_prop_ctx *ctx = (dav_prop_ctx *)prop_ctx->elts; + + /* + * ### we probably need to revise the way we assemble the response... + * ### this code assumes everything will return status==200. + */ + + apr_text_append(p, &hdr, + "<D:propstat>" DEBUG_CR + "<D:prop>" DEBUG_CR); + + for ( ; i-- > 0; ++ctx ) { + apr_text_append(p, &hdr, apr_xml_empty_elem(p, ctx->prop)); + } + + apr_text_append(p, &hdr, + "</D:prop>" DEBUG_CR + "<D:status>HTTP/1.1 200 OK</D:status>" DEBUG_CR + "</D:propstat>" DEBUG_CR); + + return hdr.first; +} + +static void dav_prop_log_errors(dav_prop_ctx *ctx) +{ + dav_log_err(ctx->r, ctx->err, APLOG_ERR); +} + +/* + * Call <func> for each context. This can stop when an error occurs, or + * simply iterate through the whole list. + * + * Returns 1 if an error occurs (and the iteration is aborted). Returns 0 + * if all elements are processed. + * + * If <reverse> is true (non-zero), then the list is traversed in + * reverse order. + */ +static int dav_process_ctx_list(void (*func)(dav_prop_ctx *ctx), + apr_array_header_t *ctx_list, int stop_on_error, + int reverse) +{ + int i = ctx_list->nelts; + dav_prop_ctx *ctx = (dav_prop_ctx *)ctx_list->elts; + + if (reverse) + ctx += i; + + while (i--) { + if (reverse) + --ctx; + + (*func)(ctx); + if (stop_on_error && DAV_PROP_CTX_HAS_ERR(*ctx)) { + return 1; + } + + if (!reverse) + ++ctx; + } + + return 0; +} + +/* handle the PROPPATCH method */ +static int dav_method_proppatch(request_rec *r) +{ + dav_error *err; + dav_resource *resource; + int result; + apr_xml_doc *doc; + apr_xml_elem *child; + dav_propdb *propdb; + int failure = 0; + dav_response resp = { 0 }; + apr_text *propstat_text; + apr_array_header_t *ctx_list; + dav_prop_ctx *ctx; + dav_auto_version_info av_info; + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + if ((result = ap_xml_parse_input(r, &doc)) != OK) { + return result; + } + /* note: doc == NULL if no request body */ + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + if (doc == NULL || !dav_validate_root(doc, "propertyupdate")) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00587) + "The request body does not contain " + "a \"propertyupdate\" element."); + return HTTP_BAD_REQUEST; + } + + /* Check If-Headers and existing locks */ + /* Note: depth == 0. Implies no need for a multistatus response. */ + if ((err = dav_validate_request(r, resource, 0, NULL, NULL, + DAV_VALIDATE_RESOURCE, NULL)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + + /* make sure the resource can be modified (if versioning repository) */ + if ((err = dav_auto_checkout(r, resource, + 0 /* not parent_only */, + &av_info)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + + if ((err = dav_open_propdb(r, NULL, resource, + DAV_PROPDB_NONE, doc->namespaces, + &propdb)) != NULL) { + /* undo any auto-checkout */ + dav_auto_checkin(r, resource, 1 /*undo*/, 0 /*unlock*/, &av_info); + + err = dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0, + apr_psprintf(r->pool, + "Could not open the property " + "database for %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, NULL); + } + /* ### what to do about closing the propdb on server failure? */ + + /* ### validate "live" properties */ + + /* set up an array to hold property operation contexts */ + ctx_list = apr_array_make(r->pool, 10, sizeof(dav_prop_ctx)); + + /* do a first pass to ensure that all "remove" properties exist */ + for (child = doc->root->first_child; child; child = child->next) { + int is_remove; + apr_xml_elem *prop_group; + apr_xml_elem *one_prop; + + /* Ignore children that are not set/remove */ + if (child->ns != APR_XML_NS_DAV_ID + || (!(is_remove = (strcmp(child->name, "remove") == 0)) + && strcmp(child->name, "set") != 0)) { + continue; + } + + /* make sure that a "prop" child exists for set/remove */ + if ((prop_group = dav_find_child(child, "prop")) == NULL) { + dav_close_propdb(propdb); + + /* undo any auto-checkout */ + dav_auto_checkin(r, resource, 1 /*undo*/, 0 /*unlock*/, &av_info); + + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00588) + "A \"prop\" element is missing inside " + "the propertyupdate command."); + return HTTP_BAD_REQUEST; + } + + for (one_prop = prop_group->first_child; one_prop; + one_prop = one_prop->next) { + + ctx = (dav_prop_ctx *)apr_array_push(ctx_list); + ctx->propdb = propdb; + ctx->operation = is_remove ? DAV_PROP_OP_DELETE : DAV_PROP_OP_SET; + ctx->prop = one_prop; + + ctx->r = r; /* for later use by dav_prop_log_errors() */ + + dav_prop_validate(ctx); + + if ( DAV_PROP_CTX_HAS_ERR(*ctx) ) { + failure = 1; + } + } + } + + /* ### should test that we found at least one set/remove */ + + /* execute all of the operations */ + if (!failure && dav_process_ctx_list(dav_prop_exec, ctx_list, 1, 0)) { + failure = 1; + } + + /* generate a failure/success response */ + if (failure) { + (void)dav_process_ctx_list(dav_prop_rollback, ctx_list, 0, 1); + propstat_text = dav_failed_proppatch(r->pool, ctx_list); + } + else { + (void)dav_process_ctx_list(dav_prop_commit, ctx_list, 0, 0); + propstat_text = dav_success_proppatch(r->pool, ctx_list); + } + + /* make sure this gets closed! */ + dav_close_propdb(propdb); + + /* complete any auto-versioning */ + dav_auto_checkin(r, resource, failure, 0 /*unlock*/, &av_info); + + /* log any errors that occurred */ + (void)dav_process_ctx_list(dav_prop_log_errors, ctx_list, 0, 0); + + resp.href = resource->uri; + + /* ### should probably use something new to pass along this text... */ + resp.propresult.propstats = propstat_text; + + dav_send_multistatus(r, HTTP_MULTI_STATUS, &resp, doc->namespaces); + + /* the response has been sent. */ + return DONE; +} + +static int process_mkcol_body(request_rec *r) +{ + /* This is snarfed from ap_setup_client_block(). We could get pretty + * close to this behavior by passing REQUEST_NO_BODY, but we need to + * return HTTP_UNSUPPORTED_MEDIA_TYPE (while ap_setup_client_block + * returns HTTP_REQUEST_ENTITY_TOO_LARGE). */ + + const char *tenc = apr_table_get(r->headers_in, "Transfer-Encoding"); + const char *lenp = apr_table_get(r->headers_in, "Content-Length"); + + /* make sure to set the Apache request fields properly. */ + r->read_body = REQUEST_NO_BODY; + r->read_chunked = 0; + r->remaining = 0; + + if (tenc) { + if (ap_cstr_casecmp(tenc, "chunked")) { + /* Use this instead of Apache's default error string */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00589) + "Unknown Transfer-Encoding %s", tenc); + return HTTP_NOT_IMPLEMENTED; + } + + r->read_chunked = 1; + } + else if (lenp) { + if (!ap_parse_strict_length(&r->remaining, lenp)) { + r->remaining = 0; + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00590) + "Invalid Content-Length %s", lenp); + return HTTP_BAD_REQUEST; + } + } + + if (r->read_chunked || r->remaining > 0) { + /* ### log something? */ + + /* Apache will supply a default error for this. */ + return HTTP_UNSUPPORTED_MEDIA_TYPE; + } + + /* + * Get rid of the body. this will call ap_setup_client_block(), but + * our copy above has already verified its work. + */ + return ap_discard_request_body(r); +} + +/* handle the MKCOL method */ +static int dav_method_mkcol(request_rec *r) +{ + dav_resource *resource; + int resource_state; + dav_auto_version_info av_info; + const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r); + dav_error *err; + dav_error *err2; + int result; + dav_response *multi_status; + + /* handle the request body */ + /* ### this may move lower once we start processing bodies */ + if ((result = process_mkcol_body(r)) != OK) { + return result; + } + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (resource->exists) { + /* oops. something was already there! */ + + /* Apache will supply a default error for this. */ + /* ### we should provide a specific error message! */ + return HTTP_METHOD_NOT_ALLOWED; + } + + resource_state = dav_get_resource_state(r, resource); + + /* + * Check If-Headers and existing locks. + * + * Note: depth == 0 normally requires no multistatus response. However, + * if we pass DAV_VALIDATE_PARENT, then we could get an error on a URI + * other than the Request-URI, thereby requiring a multistatus. + * + * If the resource does not exist (DAV_RESOURCE_NULL), then we must + * check the resource *and* its parent. If the resource exists or is + * a locknull resource, then we check only the resource. + */ + if ((err = dav_validate_request(r, resource, 0, NULL, &multi_status, + resource_state == DAV_RESOURCE_NULL ? + DAV_VALIDATE_PARENT : + DAV_VALIDATE_RESOURCE, NULL)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, multi_status); + } + + /* if versioned resource, make sure parent is checked out */ + if ((err = dav_auto_checkout(r, resource, 1 /* parent_only */, + &av_info)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + + /* try to create the collection */ + resource->collection = 1; + err = (*resource->hooks->create_collection)(resource); + + /* restore modifiability of parent back to what it was */ + err2 = dav_auto_checkin(r, NULL, err != NULL /* undo if error */, + 0 /*unlock*/, &av_info); + + /* check for errors now */ + if (err != NULL) { + return dav_handle_err(r, err, NULL); + } + if (err2 != NULL) { + /* just log a warning */ + err = dav_push_error(r->pool, err2->status, 0, + "The MKCOL was successful, but there " + "was a problem automatically checking in " + "the parent collection.", + err2); + dav_log_err(r, err, APLOG_WARNING); + } + + if (locks_hooks != NULL) { + dav_lockdb *lockdb; + + if ((err = (*locks_hooks->open_lockdb)(r, 0, 0, &lockdb)) != NULL) { + /* The directory creation was successful, but the locking failed. */ + err = dav_push_error(r->pool, err->status, 0, + "The MKCOL was successful, but there " + "was a problem opening the lock database " + "which prevents inheriting locks from the " + "parent resources.", + err); + return dav_handle_err(r, err, NULL); + } + + /* notify lock system that we have created/replaced a resource */ + err = dav_notify_created(r, lockdb, resource, resource_state, 0); + + (*locks_hooks->close_lockdb)(lockdb); + + if (err != NULL) { + /* The dir creation was successful, but the locking failed. */ + err = dav_push_error(r->pool, err->status, 0, + "The MKCOL was successful, but there " + "was a problem updating its lock " + "information.", + err); + return dav_handle_err(r, err, NULL); + } + } + + /* return an appropriate response (HTTP_CREATED) */ + return dav_created(r, NULL, "Collection", 0); +} + +/* handle the COPY and MOVE methods */ +static int dav_method_copymove(request_rec *r, int is_move) +{ + dav_resource *resource; + dav_resource *resnew; + dav_auto_version_info src_av_info = { 0 }; + dav_auto_version_info dst_av_info = { 0 }; + const char *body; + const char *dest; + dav_error *err; + dav_error *err2; + dav_error *err3; + dav_response *multi_response; + dav_lookup_result lookup; + int is_dir; + int overwrite; + int depth; + int result; + dav_lockdb *lockdb; + int replace_dest; + int resnew_state; + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, !is_move /* label_allowed */, + 0 /* use_checked_in */, &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* If not a file or collection resource, COPY/MOVE not allowed */ + /* ### allow COPY/MOVE of DeltaV resource types */ + if (resource->type != DAV_RESOURCE_TYPE_REGULAR) { + body = apr_psprintf(r->pool, + "Cannot COPY/MOVE resource %s.", + ap_escape_html(r->pool, r->uri)); + return dav_error_response(r, HTTP_METHOD_NOT_ALLOWED, body); + } + + /* get the destination URI */ + dest = apr_table_get(r->headers_in, "Destination"); + if (dest == NULL) { + /* Look in headers provided by Netscape's Roaming Profiles */ + const char *nscp_host = apr_table_get(r->headers_in, "Host"); + const char *nscp_path = apr_table_get(r->headers_in, "New-uri"); + + if (nscp_host != NULL && nscp_path != NULL) + dest = apr_pstrcat(r->pool, "http://", nscp_host, nscp_path, NULL); + } + if (dest == NULL) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00591) + "The request is missing a Destination header."); + return HTTP_BAD_REQUEST; + } + + lookup = dav_lookup_uri(dest, r, 1 /* must_be_absolute */); + if (lookup.rnew == NULL) { + if (lookup.err.status == HTTP_BAD_REQUEST) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00592) + "%s", lookup.err.desc); + return HTTP_BAD_REQUEST; + } + + /* ### this assumes that dav_lookup_uri() only generates a status + * ### that Apache can provide a status line for!! */ + + return dav_error_response(r, lookup.err.status, lookup.err.desc); + } + if (lookup.rnew->status != HTTP_OK) { + const char *auth = apr_table_get(lookup.rnew->err_headers_out, + "WWW-Authenticate"); + if (lookup.rnew->status == HTTP_UNAUTHORIZED && auth != NULL) { + /* propagate the WWW-Authorization header up from the + * subreq so the client sees it. */ + apr_table_setn(r->err_headers_out, "WWW-Authenticate", + apr_pstrdup(r->pool, auth)); + } + + /* ### how best to report this... */ + return dav_error_response(r, lookup.rnew->status, + "Destination URI had an error."); + } + + /* Resolve destination resource */ + err = dav_get_resource(lookup.rnew, 0 /* label_allowed */, + 0 /* use_checked_in */, &resnew); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, resnew, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + /* are the two resources handled by the same repository? */ + if (resource->hooks != resnew->hooks) { + /* ### this message exposes some backend config, but screw it... */ + return dav_error_response(r, HTTP_BAD_GATEWAY, + "Destination URI is handled by a " + "different repository than the source URI. " + "MOVE or COPY between repositories is " + "not possible."); + } + + /* get and parse the overwrite header value */ + if ((overwrite = dav_get_overwrite(r)) < 0) { + /* dav_get_overwrite() supplies additional information for the + * default message. */ + return HTTP_BAD_REQUEST; + } + + /* quick failure test: if dest exists and overwrite is false. */ + if (resnew->exists && !overwrite) { + /* Supply some text for the error response body. */ + return dav_error_response(r, HTTP_PRECONDITION_FAILED, + "Destination is not empty and " + "Overwrite is not \"T\""); + } + + /* are the source and destination the same? */ + if ((*resource->hooks->is_same_resource)(resource, resnew)) { + /* Supply some text for the error response body. */ + return dav_error_response(r, HTTP_FORBIDDEN, + "Source and Destination URIs are the same."); + + } + + is_dir = resource->collection; + + /* get and parse the Depth header value. "0" and "infinity" are legal. */ + if ((depth = dav_get_depth(r, DAV_INFINITY)) < 0) { + /* dav_get_depth() supplies additional information for the + * default message. */ + return HTTP_BAD_REQUEST; + } + if (depth == 1) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00593) + "Depth must be \"0\" or \"infinity\" for COPY or MOVE."); + return HTTP_BAD_REQUEST; + } + if (is_move && is_dir && depth != DAV_INFINITY) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00594) + "Depth must be \"infinity\" when moving a collection."); + return HTTP_BAD_REQUEST; + } + + /* + * Check If-Headers and existing locks for each resource in the source. + * We will return a 424 response with a DAV:multistatus body. + * The multistatus responses will contain the information about any + * resource that fails the validation. + * + * We check the parent resource, too, if this is a MOVE. Moving the + * resource effectively removes it from the parent collection, so we + * must ensure that we have met the appropriate conditions. + * + * If a problem occurs with the Request-URI itself, then a plain error + * (rather than a multistatus) will be returned. + */ + if ((err = dav_validate_request(r, resource, depth, NULL, + &multi_response, + (is_move ? DAV_VALIDATE_PARENT + : DAV_VALIDATE_RESOURCE + | DAV_VALIDATE_NO_MODIFY) + | DAV_VALIDATE_USE_424, + NULL)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not %s %s due to a failed " + "precondition on the source " + "(e.g. locks).", + is_move ? "MOVE" : "COPY", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, multi_response); + } + + /* + * Check If-Headers and existing locks for destination. Note that we + * use depth==infinity since the target (hierarchy) will be deleted + * before the move/copy is completed. + * + * Note that we are overwriting the target, which implies a DELETE, so + * we are subject to the error/response rules as a DELETE. Namely, we + * will return a 424 error if any of the validations fail. + * (see dav_method_delete() for more information) + */ + if ((err = dav_validate_request(lookup.rnew, resnew, DAV_INFINITY, NULL, + &multi_response, + DAV_VALIDATE_PARENT + | DAV_VALIDATE_USE_424, NULL)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not MOVE/COPY %s due to a " + "failed precondition on the " + "destination (e.g. locks).", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, multi_response); + } + + if (is_dir + && depth == DAV_INFINITY + && (*resource->hooks->is_parent_resource)(resource, resnew)) { + /* Supply some text for the error response body. */ + return dav_error_response(r, HTTP_FORBIDDEN, + "Source collection contains the " + "Destination."); + + } + if (is_dir + && (*resnew->hooks->is_parent_resource)(resnew, resource)) { + /* The destination must exist (since it contains the source), and + * a condition above implies Overwrite==T. Obviously, we cannot + * delete the Destination before the MOVE/COPY, as that would + * delete the Source. + */ + + /* Supply some text for the error response body. */ + return dav_error_response(r, HTTP_FORBIDDEN, + "Destination collection contains the Source " + "and Overwrite has been specified."); + } + + /* ### for now, we don't need anything in the body */ + if ((result = ap_discard_request_body(r)) != OK) { + return result; + } + + if ((err = dav_open_lockdb(r, 0, &lockdb)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + + /* remove any locks from the old resources */ + /* + * ### this is Yet Another Traversal. if we do a rename(), then we + * ### really don't have to do this in some cases since the inode + * ### values will remain constant across the move. but we can't + * ### know that fact from outside the provider :-( + * + * ### note that we now have a problem atomicity in the move/copy + * ### since a failure after this would have removed locks (technically, + * ### this is okay to do, but really...) + */ + if (is_move && lockdb != NULL) { + /* ### this is wrong! it blasts direct locks on parent resources */ + /* ### pass lockdb! */ + (void)dav_unlock(r, resource, NULL); + } + + /* if this is a move, then the source parent collection will be modified */ + if (is_move) { + if ((err = dav_auto_checkout(r, resource, 1 /* parent_only */, + &src_av_info)) != NULL) { + if (lockdb != NULL) + (*lockdb->hooks->close_lockdb)(lockdb); + + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + } + + /* + * Remember the initial state of the destination, so the lock system + * can be notified as to how it changed. + */ + resnew_state = dav_get_resource_state(lookup.rnew, resnew); + + /* In a MOVE operation, the destination is replaced by the source. + * In a COPY operation, if the destination exists, is under version + * control, and is the same resource type as the source, + * then it should not be replaced, but modified to be a copy of + * the source. + */ + if (!resnew->exists) + replace_dest = 0; + else if (is_move || !resource->versioned) + replace_dest = 1; + else if (resource->type != resnew->type) + replace_dest = 1; + else if ((resource->collection == 0) != (resnew->collection == 0)) + replace_dest = 1; + else + replace_dest = 0; + + /* If the destination must be created or replaced, + * make sure the parent collection is writable + */ + if (!resnew->exists || replace_dest) { + if ((err = dav_auto_checkout(r, resnew, 1 /*parent_only*/, + &dst_av_info)) != NULL) { + /* could not make destination writable: + * if move, restore state of source parent + */ + if (is_move) { + (void)dav_auto_checkin(r, NULL, 1 /* undo */, + 0 /*unlock*/, &src_av_info); + } + + if (lockdb != NULL) + (*lockdb->hooks->close_lockdb)(lockdb); + + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + } + + /* If source and destination parents are the same, then + * use the same resource object, so status updates to one are reflected + * in the other, when doing auto-versioning. Otherwise, + * we may try to checkin the parent twice. + */ + if (src_av_info.parent_resource != NULL + && dst_av_info.parent_resource != NULL + && (*src_av_info.parent_resource->hooks->is_same_resource) + (src_av_info.parent_resource, dst_av_info.parent_resource)) { + + dst_av_info.parent_resource = src_av_info.parent_resource; + } + + /* If destination is being replaced, remove it first + * (we know Ovewrite must be TRUE). Then try to copy/move the resource. + */ + if (replace_dest) + err = (*resnew->hooks->remove_resource)(resnew, &multi_response); + + if (err == NULL) { + if (is_move) + err = (*resource->hooks->move_resource)(resource, resnew, + &multi_response); + else + err = (*resource->hooks->copy_resource)(resource, resnew, depth, + &multi_response); + } + + /* perform any auto-versioning cleanup */ + err2 = dav_auto_checkin(r, NULL, err != NULL /* undo if error */, + 0 /*unlock*/, &dst_av_info); + + if (is_move) { + err3 = dav_auto_checkin(r, NULL, err != NULL /* undo if error */, + 0 /*unlock*/, &src_av_info); + } + else + err3 = NULL; + + /* check for error from remove/copy/move operations */ + if (err != NULL) { + if (lockdb != NULL) + (*lockdb->hooks->close_lockdb)(lockdb); + + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not MOVE/COPY %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, multi_response); + } + + /* check for errors from auto-versioning */ + if (err2 != NULL) { + /* just log a warning */ + err = dav_push_error(r->pool, err2->status, 0, + "The MOVE/COPY was successful, but there was a " + "problem automatically checking in the " + "source parent collection.", + err2); + dav_log_err(r, err, APLOG_WARNING); + } + if (err3 != NULL) { + /* just log a warning */ + err = dav_push_error(r->pool, err3->status, 0, + "The MOVE/COPY was successful, but there was a " + "problem automatically checking in the " + "destination or its parent collection.", + err3); + dav_log_err(r, err, APLOG_WARNING); + } + + /* propagate any indirect locks at the target */ + if (lockdb != NULL) { + + /* notify lock system that we have created/replaced a resource */ + err = dav_notify_created(r, lockdb, resnew, resnew_state, depth); + + (*lockdb->hooks->close_lockdb)(lockdb); + + if (err != NULL) { + /* The move/copy was successful, but the locking failed. */ + err = dav_push_error(r->pool, err->status, 0, + "The MOVE/COPY was successful, but there " + "was a problem updating the lock " + "information.", + err); + return dav_handle_err(r, err, NULL); + } + } + + /* return an appropriate response (HTTP_CREATED or HTTP_NO_CONTENT) */ + return dav_created(r, lookup.rnew->unparsed_uri, "Destination", + resnew_state == DAV_RESOURCE_EXISTS); +} + +/* dav_method_lock: Handler to implement the DAV LOCK method + * Returns appropriate HTTP_* response. + */ +static int dav_method_lock(request_rec *r) +{ + dav_error *err; + dav_resource *resource; + dav_resource *parent; + const dav_hooks_locks *locks_hooks; + int result; + int depth; + int new_lock_request = 0; + apr_xml_doc *doc; + dav_lock *lock; + dav_response *multi_response = NULL; + dav_lockdb *lockdb; + int resource_state; + + /* If no locks provider, decline the request */ + locks_hooks = DAV_GET_HOOKS_LOCKS(r); + if (locks_hooks == NULL) + return DECLINED; + + if ((result = ap_xml_parse_input(r, &doc)) != OK) + return result; + + depth = dav_get_depth(r, DAV_INFINITY); + if (depth != 0 && depth != DAV_INFINITY) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00595) + "Depth must be 0 or \"infinity\" for LOCK."); + return HTTP_BAD_REQUEST; + } + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + /* Check if parent collection exists */ + if ((err = resource->hooks->get_parent_resource(resource, &parent)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + if (parent && (!parent->exists || parent->collection != 1)) { + err = dav_new_error(r->pool, HTTP_CONFLICT, 0, 0, + apr_psprintf(r->pool, + "The parent resource of %s does not " + "exist or is not a collection.", + ap_escape_html(r->pool, r->uri))); + return dav_handle_err(r, err, NULL); + } + + /* + * Open writable. Unless an error occurs, we'll be + * writing into the database. + */ + if ((err = (*locks_hooks->open_lockdb)(r, 0, 0, &lockdb)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, NULL); + } + + if (doc != NULL) { + if ((err = dav_lock_parse_lockinfo(r, resource, lockdb, doc, + &lock)) != NULL) { + /* ### add a higher-level description to err? */ + goto error; + } + new_lock_request = 1; + + lock->auth_user = apr_pstrdup(r->pool, r->user); + } + + resource_state = dav_get_resource_state(r, resource); + + /* + * Check If-Headers and existing locks. + * + * If this will create a locknull resource, then the LOCK will affect + * the parent collection (much like a PUT/MKCOL). For that case, we must + * validate the parent resource's conditions. + */ + if ((err = dav_validate_request(r, resource, depth, NULL, &multi_response, + (resource_state == DAV_RESOURCE_NULL + ? DAV_VALIDATE_PARENT + : DAV_VALIDATE_RESOURCE) + | (new_lock_request ? lock->scope : 0) + | DAV_VALIDATE_ADD_LD, + lockdb)) != OK) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not LOCK %s due to a failed " + "precondition (e.g. other locks).", + ap_escape_html(r->pool, r->uri)), + err); + goto error; + } + + if (new_lock_request == 0) { + dav_locktoken_list *ltl; + + /* + * Refresh request + * ### Assumption: We can renew multiple locks on the same resource + * ### at once. First harvest all the positive lock-tokens given in + * ### the If header. Then modify the lock entries for this resource + * ### with the new Timeout val. + */ + + if ((err = dav_get_locktoken_list(r, <l)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "The lock refresh for %s failed " + "because no lock tokens were " + "specified in an \"If:\" " + "header.", + ap_escape_html(r->pool, r->uri)), + err); + goto error; + } + + if ((err = (*locks_hooks->refresh_locks)(lockdb, resource, ltl, + dav_get_timeout(r), + &lock)) != NULL) { + /* ### add a higher-level description to err? */ + goto error; + } + } else { + /* New lock request */ + char *locktoken_txt; + dav_dir_conf *conf; + + conf = (dav_dir_conf *)ap_get_module_config(r->per_dir_config, + &dav_module); + + /* apply lower bound (if any) from DAVMinTimeout directive */ + if (lock->timeout != DAV_TIMEOUT_INFINITE + && lock->timeout < time(NULL) + conf->locktimeout) + lock->timeout = time(NULL) + conf->locktimeout; + + err = dav_add_lock(r, resource, lockdb, lock, &multi_response); + if (err != NULL) { + /* ### add a higher-level description to err? */ + goto error; + } + + locktoken_txt = apr_pstrcat(r->pool, "<", + (*locks_hooks->format_locktoken)(r->pool, + lock->locktoken), + ">", NULL); + + apr_table_setn(r->headers_out, "Lock-Token", locktoken_txt); + } + + (*locks_hooks->close_lockdb)(lockdb); + + r->status = HTTP_OK; + ap_set_content_type(r, DAV_XML_CONTENT_TYPE); + + ap_rputs(DAV_XML_HEADER DEBUG_CR "<D:prop xmlns:D=\"DAV:\">" DEBUG_CR, r); + if (lock == NULL) + ap_rputs("<D:lockdiscovery/>" DEBUG_CR, r); + else { + ap_rprintf(r, + "<D:lockdiscovery>" DEBUG_CR + "%s" DEBUG_CR + "</D:lockdiscovery>" DEBUG_CR, + dav_lock_get_activelock(r, lock, NULL)); + } + ap_rputs("</D:prop>", r); + + /* the response has been sent. */ + return DONE; + + error: + (*locks_hooks->close_lockdb)(lockdb); + return dav_handle_err(r, err, multi_response); +} + +/* dav_method_unlock: Handler to implement the DAV UNLOCK method + * Returns appropriate HTTP_* response. + */ +static int dav_method_unlock(request_rec *r) +{ + dav_error *err; + dav_resource *resource; + const dav_hooks_locks *locks_hooks; + int result; + const char *const_locktoken_txt; + char *locktoken_txt; + dav_locktoken *locktoken = NULL; + int resource_state; + dav_response *multi_response; + + /* If no locks provider, decline the request */ + locks_hooks = DAV_GET_HOOKS_LOCKS(r); + if (locks_hooks == NULL) + return DECLINED; + + if ((const_locktoken_txt = apr_table_get(r->headers_in, + "Lock-Token")) == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00596) + "Unlock failed (%s): " + "No Lock-Token specified in header", r->filename); + return HTTP_BAD_REQUEST; + } + + locktoken_txt = apr_pstrdup(r->pool, const_locktoken_txt); + if (locktoken_txt[0] != '<') { + /* ### should provide more specifics... */ + return HTTP_BAD_REQUEST; + } + locktoken_txt++; + + if (locktoken_txt[strlen(locktoken_txt) - 1] != '>') { + /* ### should provide more specifics... */ + return HTTP_BAD_REQUEST; + } + locktoken_txt[strlen(locktoken_txt) - 1] = '\0'; + + if ((err = (*locks_hooks->parse_locktoken)(r->pool, locktoken_txt, + &locktoken)) != NULL) { + err = dav_push_error(r->pool, HTTP_BAD_REQUEST, 0, + apr_psprintf(r->pool, + "The UNLOCK on %s failed -- an " + "invalid lock token was specified " + "in the \"If:\" header.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, NULL); + } + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + resource_state = dav_get_resource_state(r, resource); + + /* + * Check If-Headers and existing locks. + * + * Note: depth == 0 normally requires no multistatus response. However, + * if we pass DAV_VALIDATE_PARENT, then we could get an error on a URI + * other than the Request-URI, thereby requiring a multistatus. + * + * If the resource is a locknull resource, then the UNLOCK will affect + * the parent collection (much like a delete). For that case, we must + * validate the parent resource's conditions. + */ + if ((err = dav_validate_request(r, resource, 0, locktoken, + &multi_response, + resource_state == DAV_RESOURCE_LOCK_NULL + ? DAV_VALIDATE_PARENT + : DAV_VALIDATE_RESOURCE, NULL)) != NULL) { + /* ### add a higher-level description? */ + return dav_handle_err(r, err, multi_response); + } + + /* ### RFC 2518 s. 8.11: If this resource is locked by locktoken, + * _all_ resources locked by locktoken are released. It does not say + * resource has to be the root of an infinite lock. Thus, an UNLOCK + * on any part of an infinite lock will remove the lock on all resources. + * + * For us, if r->filename represents an indirect lock (part of an infinity lock), + * we must actually perform an UNLOCK on the direct lock for this resource. + */ + if ((result = dav_unlock(r, resource, locktoken)) != OK) { + return result; + } + + return HTTP_NO_CONTENT; +} + +static int dav_method_vsn_control(request_rec *r) +{ + dav_resource *resource; + int resource_state; + dav_auto_version_info av_info; + const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r); + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + dav_error *err; + apr_xml_doc *doc; + const char *target = NULL; + int result; + + /* if no versioning provider, decline the request */ + if (vsn_hooks == NULL) + return DECLINED; + + /* ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* parse the request body (may be a version-control element) */ + if ((result = ap_xml_parse_input(r, &doc)) != OK) { + return result; + } + /* note: doc == NULL if no request body */ + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + /* remember the pre-creation resource state */ + resource_state = dav_get_resource_state(r, resource); + + if (doc != NULL) { + const apr_xml_elem *child; + apr_size_t tsize; + + if (!dav_validate_root(doc, "version-control")) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00597) + "The request body does not contain " + "a \"version-control\" element."); + return HTTP_BAD_REQUEST; + } + + /* get the version URI */ + if ((child = dav_find_child(doc->root, "version")) == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00598) + "The \"version-control\" element does not contain " + "a \"version\" element."); + return HTTP_BAD_REQUEST; + } + + if ((child = dav_find_child(child, "href")) == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00599) + "The \"version\" element does not contain " + "an \"href\" element."); + return HTTP_BAD_REQUEST; + } + + /* get version URI */ + apr_xml_to_text(r->pool, child, APR_XML_X2T_INNER, NULL, NULL, + &target, &tsize); + if (tsize == 0) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00600) + "An \"href\" element does not contain a URI."); + return HTTP_BAD_REQUEST; + } + } + + /* Check request preconditions */ + + /* ### need a general mechanism for reporting precondition violations + * ### (should be returning XML document for 403/409 responses) + */ + + /* if not versioning existing resource, must specify version to select */ + if (!resource->exists && target == NULL) { + err = dav_new_error(r->pool, HTTP_CONFLICT, 0, 0, + "<DAV:initial-version-required/>"); + return dav_handle_err(r, err, NULL); + } + else if (resource->exists) { + /* cannot add resource to existing version history */ + if (target != NULL) { + err = dav_new_error(r->pool, HTTP_CONFLICT, 0, 0, + "<DAV:cannot-add-to-existing-history/>"); + return dav_handle_err(r, err, NULL); + } + + /* resource must be unversioned and versionable, or version selector */ + if (resource->type != DAV_RESOURCE_TYPE_REGULAR + || (!resource->versioned && !(vsn_hooks->versionable)(resource))) { + err = dav_new_error(r->pool, HTTP_CONFLICT, 0, 0, + "<DAV:must-be-versionable/>"); + return dav_handle_err(r, err, NULL); + } + + /* the DeltaV spec says if resource is a version selector, + * then VERSION-CONTROL is a no-op + */ + if (resource->versioned) { + /* set the Cache-Control header, per the spec */ + apr_table_setn(r->headers_out, "Cache-Control", "no-cache"); + + /* no body */ + ap_set_content_length(r, 0); + + return DONE; + } + } + + /* Check If-Headers and existing locks */ + /* Note: depth == 0. Implies no need for a multistatus response. */ + if ((err = dav_validate_request(r, resource, 0, NULL, NULL, + resource_state == DAV_RESOURCE_NULL ? + DAV_VALIDATE_PARENT : + DAV_VALIDATE_RESOURCE, NULL)) != NULL) { + return dav_handle_err(r, err, NULL); + } + + /* if in versioned collection, make sure parent is checked out */ + if ((err = dav_auto_checkout(r, resource, 1 /* parent_only */, + &av_info)) != NULL) { + return dav_handle_err(r, err, NULL); + } + + /* attempt to version-control the resource */ + if ((err = (*vsn_hooks->vsn_control)(resource, target)) != NULL) { + dav_auto_checkin(r, resource, 1 /*undo*/, 0 /*unlock*/, &av_info); + err = dav_push_error(r->pool, HTTP_CONFLICT, 0, + apr_psprintf(r->pool, + "Could not VERSION-CONTROL resource %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, NULL); + } + + /* revert writability of parent directory */ + err = dav_auto_checkin(r, resource, 0 /*undo*/, 0 /*unlock*/, &av_info); + if (err != NULL) { + /* just log a warning */ + err = dav_push_error(r->pool, err->status, 0, + "The VERSION-CONTROL was successful, but there " + "was a problem automatically checking in " + "the parent collection.", + err); + dav_log_err(r, err, APLOG_WARNING); + } + + /* if the resource is lockable, let lock system know of new resource */ + if (locks_hooks != NULL + && (*locks_hooks->get_supportedlock)(resource) != NULL) { + dav_lockdb *lockdb; + + if ((err = (*locks_hooks->open_lockdb)(r, 0, 0, &lockdb)) != NULL) { + /* The resource creation was successful, but the locking failed. */ + err = dav_push_error(r->pool, err->status, 0, + "The VERSION-CONTROL was successful, but there " + "was a problem opening the lock database " + "which prevents inheriting locks from the " + "parent resources.", + err); + return dav_handle_err(r, err, NULL); + } + + /* notify lock system that we have created/replaced a resource */ + err = dav_notify_created(r, lockdb, resource, resource_state, 0); + + (*locks_hooks->close_lockdb)(lockdb); + + if (err != NULL) { + /* The dir creation was successful, but the locking failed. */ + err = dav_push_error(r->pool, err->status, 0, + "The VERSION-CONTROL was successful, but there " + "was a problem updating its lock " + "information.", + err); + return dav_handle_err(r, err, NULL); + } + } + + /* set the Cache-Control header, per the spec */ + apr_table_setn(r->headers_out, "Cache-Control", "no-cache"); + + /* return an appropriate response (HTTP_CREATED) */ + return dav_created(r, resource->uri, "Version selector", 0 /*replaced*/); +} + +/* handle the CHECKOUT method */ +static int dav_method_checkout(request_rec *r) +{ + dav_resource *resource; + dav_resource *working_resource; + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + dav_error *err; + int result; + apr_xml_doc *doc; + int apply_to_vsn = 0; + int is_unreserved = 0; + int is_fork_ok = 0; + int create_activity = 0; + apr_array_header_t *activities = NULL; + + /* If no versioning provider, decline the request */ + if (vsn_hooks == NULL) + return DECLINED; + + if ((result = ap_xml_parse_input(r, &doc)) != OK) + return result; + + if (doc != NULL) { + const apr_xml_elem *aset; + + if (!dav_validate_root(doc, "checkout")) { + /* This supplies additional information for the default msg. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00601) + "The request body, if present, must be a " + "DAV:checkout element."); + return HTTP_BAD_REQUEST; + } + + if (dav_find_child(doc->root, "apply-to-version") != NULL) { + if (apr_table_get(r->headers_in, "label") != NULL) { + /* ### we want generic 403/409 XML reporting here */ + /* ### DAV:must-not-have-label-and-apply-to-version */ + return dav_error_response(r, HTTP_CONFLICT, + "DAV:apply-to-version cannot be " + "used in conjunction with a " + "Label header."); + } + apply_to_vsn = 1; + } + + is_unreserved = dav_find_child(doc->root, "unreserved") != NULL; + is_fork_ok = dav_find_child(doc->root, "fork-ok") != NULL; + + if ((aset = dav_find_child(doc->root, "activity-set")) != NULL) { + if (dav_find_child(aset, "new") != NULL) { + create_activity = 1; + } + else { + const apr_xml_elem *child = aset->first_child; + + activities = apr_array_make(r->pool, 1, sizeof(const char *)); + + for (; child != NULL; child = child->next) { + if (child->ns == APR_XML_NS_DAV_ID + && strcmp(child->name, "href") == 0) { + const char *href; + + href = dav_xml_get_cdata(child, r->pool, + 1 /* strip_white */); + *(const char **)apr_array_push(activities) = href; + } + } + + if (activities->nelts == 0) { + /* no href's is a DTD violation: + <!ELEMENT activity-set (href+ | new)> + */ + + /* This supplies additional info for the default msg. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00602) + "Within the DAV:activity-set element, the " + "DAV:new element must be used, or at least " + "one DAV:href must be specified."); + return HTTP_BAD_REQUEST; + } + } + } + } + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 1 /*label_allowed*/, apply_to_vsn, &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* Check the state of the resource: must be a file or collection, + * must be versioned, and must not already be checked out. + */ + if (resource->type != DAV_RESOURCE_TYPE_REGULAR + && resource->type != DAV_RESOURCE_TYPE_VERSION) { + return dav_error_response(r, HTTP_CONFLICT, + "Cannot checkout this type of resource."); + } + + if (!resource->versioned) { + return dav_error_response(r, HTTP_CONFLICT, + "Cannot checkout unversioned resource."); + } + + if (resource->working) { + return dav_error_response(r, HTTP_CONFLICT, + "The resource is already checked out to the workspace."); + } + + /* ### do lock checks, once behavior is defined */ + + /* Do the checkout */ + if ((err = (*vsn_hooks->checkout)(resource, 0 /*auto_checkout*/, + is_unreserved, is_fork_ok, + create_activity, activities, + &working_resource)) != NULL) { + err = dav_push_error(r->pool, HTTP_CONFLICT, 0, + apr_psprintf(r->pool, + "Could not CHECKOUT resource %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, NULL); + } + + /* set the Cache-Control header, per the spec */ + apr_table_setn(r->headers_out, "Cache-Control", "no-cache"); + + /* if no working resource created, return OK, + * else return CREATED with working resource URL in Location header + */ + if (working_resource == NULL) { + /* no body */ + ap_set_content_length(r, 0); + return DONE; + } + + return dav_created(r, working_resource->uri, "Checked-out resource", 0); +} + +/* handle the UNCHECKOUT method */ +static int dav_method_uncheckout(request_rec *r) +{ + dav_resource *resource; + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + dav_error *err; + int result; + + /* If no versioning provider, decline the request */ + if (vsn_hooks == NULL) + return DECLINED; + + if ((result = ap_discard_request_body(r)) != OK) { + return result; + } + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* Check the state of the resource: must be a file or collection, + * must be versioned, and must be checked out. + */ + if (resource->type != DAV_RESOURCE_TYPE_REGULAR) { + return dav_error_response(r, HTTP_CONFLICT, + "Cannot uncheckout this type of resource."); + } + + if (!resource->versioned) { + return dav_error_response(r, HTTP_CONFLICT, + "Cannot uncheckout unversioned resource."); + } + + if (!resource->working) { + return dav_error_response(r, HTTP_CONFLICT, + "The resource is not checked out to the workspace."); + } + + /* ### do lock checks, once behavior is defined */ + + /* Do the uncheckout */ + if ((err = (*vsn_hooks->uncheckout)(resource)) != NULL) { + err = dav_push_error(r->pool, HTTP_CONFLICT, 0, + apr_psprintf(r->pool, + "Could not UNCHECKOUT resource %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, NULL); + } + + /* no body */ + ap_set_content_length(r, 0); + + return DONE; +} + +/* handle the CHECKIN method */ +static int dav_method_checkin(request_rec *r) +{ + dav_resource *resource; + dav_resource *new_version; + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + dav_error *err; + int result; + apr_xml_doc *doc; + int keep_checked_out = 0; + + /* If no versioning provider, decline the request */ + if (vsn_hooks == NULL) + return DECLINED; + + if ((result = ap_xml_parse_input(r, &doc)) != OK) + return result; + + if (doc != NULL) { + if (!dav_validate_root(doc, "checkin")) { + /* This supplies additional information for the default msg. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00603) + "The request body, if present, must be a " + "DAV:checkin element."); + return HTTP_BAD_REQUEST; + } + + keep_checked_out = dav_find_child(doc->root, "keep-checked-out") != NULL; + } + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* Check the state of the resource: must be a file or collection, + * must be versioned, and must be checked out. + */ + if (resource->type != DAV_RESOURCE_TYPE_REGULAR) { + return dav_error_response(r, HTTP_CONFLICT, + "Cannot checkin this type of resource."); + } + + if (!resource->versioned) { + return dav_error_response(r, HTTP_CONFLICT, + "Cannot checkin unversioned resource."); + } + + if (!resource->working) { + return dav_error_response(r, HTTP_CONFLICT, + "The resource is not checked out."); + } + + /* ### do lock checks, once behavior is defined */ + + /* Do the checkin */ + if ((err = (*vsn_hooks->checkin)(resource, keep_checked_out, &new_version)) + != NULL) { + err = dav_push_error(r->pool, HTTP_CONFLICT, 0, + apr_psprintf(r->pool, + "Could not CHECKIN resource %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, NULL); + } + + return dav_created(r, new_version->uri, "Version", 0); +} + +static int dav_method_update(request_rec *r) +{ + dav_resource *resource; + dav_resource *version = NULL; + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + apr_xml_doc *doc; + apr_xml_elem *child; + int is_label = 0; + int depth; + int result; + apr_size_t tsize; + const char *target; + dav_response *multi_response; + dav_error *err; + dav_lookup_result lookup; + + /* If no versioning provider, or UPDATE not supported, + * decline the request */ + if (vsn_hooks == NULL || vsn_hooks->update == NULL) + return DECLINED; + + if ((depth = dav_get_depth(r, 0)) < 0) { + /* dav_get_depth() supplies additional information for the + * default message. */ + return HTTP_BAD_REQUEST; + } + + /* parse the request body */ + if ((result = ap_xml_parse_input(r, &doc)) != OK) { + return result; + } + + if (doc == NULL || !dav_validate_root(doc, "update")) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00604) + "The request body does not contain " + "an \"update\" element."); + return HTTP_BAD_REQUEST; + } + + /* check for label-name or version element, but not both */ + if ((child = dav_find_child(doc->root, "label-name")) != NULL) + is_label = 1; + else if ((child = dav_find_child(doc->root, "version")) != NULL) { + /* get the href element */ + if ((child = dav_find_child(child, "href")) == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00605) + "The version element does not contain " + "an \"href\" element."); + return HTTP_BAD_REQUEST; + } + } + else { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00606) + "The \"update\" element does not contain " + "a \"label-name\" or \"version\" element."); + return HTTP_BAD_REQUEST; + } + + /* a depth greater than zero is only allowed for a label */ + if (!is_label && depth != 0) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00607) + "Depth must be zero for UPDATE with a version"); + return HTTP_BAD_REQUEST; + } + + /* get the target value (a label or a version URI) */ + apr_xml_to_text(r->pool, child, APR_XML_X2T_INNER, NULL, NULL, + &target, &tsize); + if (tsize == 0) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00608) + "A \"label-name\" or \"href\" element does not contain " + "any content."); + return HTTP_BAD_REQUEST; + } + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* ### need a general mechanism for reporting precondition violations + * ### (should be returning XML document for 403/409 responses) + */ + if (resource->type != DAV_RESOURCE_TYPE_REGULAR + || !resource->versioned || resource->working) { + return dav_error_response(r, HTTP_CONFLICT, + "<DAV:must-be-checked-in-version-controlled-resource>"); + } + + /* if target is a version, resolve the version resource */ + /* ### dav_lookup_uri only allows absolute URIs; is that OK? */ + if (!is_label) { + lookup = dav_lookup_uri(target, r, 0 /* must_be_absolute */); + if (lookup.rnew == NULL) { + if (lookup.err.status == HTTP_BAD_REQUEST) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00609) + "%s", lookup.err.desc); + return HTTP_BAD_REQUEST; + } + + /* ### this assumes that dav_lookup_uri() only generates a status + * ### that Apache can provide a status line for!! */ + + return dav_error_response(r, lookup.err.status, lookup.err.desc); + } + if (lookup.rnew->status != HTTP_OK) { + /* ### how best to report this... */ + return dav_error_response(r, lookup.rnew->status, + "Version URI had an error."); + } + + /* resolve version resource */ + err = dav_get_resource(lookup.rnew, 0 /* label_allowed */, + 0 /* use_checked_in */, &version); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* NULL out target, since we're using a version resource */ + target = NULL; + } + + /* do the UPDATE operation */ + err = (*vsn_hooks->update)(resource, version, target, depth, &multi_response); + + if (err != NULL) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not UPDATE %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, multi_response); + } + + /* set the Cache-Control header, per the spec */ + apr_table_setn(r->headers_out, "Cache-Control", "no-cache"); + + /* no body */ + ap_set_content_length(r, 0); + + return DONE; +} + +/* context maintained during LABEL treewalk */ +typedef struct dav_label_walker_ctx +{ + /* input: */ + dav_walk_params w; + + /* original request */ + request_rec *r; + + /* label being manipulated */ + const char *label; + + /* label operation */ + int label_op; +#define DAV_LABEL_ADD 1 +#define DAV_LABEL_SET 2 +#define DAV_LABEL_REMOVE 3 + + /* version provider hooks */ + const dav_hooks_vsn *vsn_hooks; + +} dav_label_walker_ctx; + +static dav_error * dav_label_walker(dav_walk_resource *wres, int calltype) +{ + dav_label_walker_ctx *ctx = wres->walk_ctx; + dav_error *err = NULL; + + /* check for any method preconditions */ + if (dav_run_method_precondition(ctx->r, NULL, wres->resource, NULL, &err) != DECLINED + && err) { + /* precondition failed, dropping through */ + } + + /* Check the state of the resource: must be a version or + * non-checkedout version selector + */ + /* ### need a general mechanism for reporting precondition violations + * ### (should be returning XML document for 403/409 responses) + */ + else if (wres->resource->type != DAV_RESOURCE_TYPE_VERSION && + (wres->resource->type != DAV_RESOURCE_TYPE_REGULAR + || !wres->resource->versioned)) { + err = dav_new_error(ctx->w.pool, HTTP_CONFLICT, 0, 0, + "<DAV:must-be-version-or-version-selector/>"); + } + else if (wres->resource->working) { + err = dav_new_error(ctx->w.pool, HTTP_CONFLICT, 0, 0, + "<DAV:must-not-be-checked-out/>"); + } + else { + /* do the label operation */ + if (ctx->label_op == DAV_LABEL_REMOVE) + err = (*ctx->vsn_hooks->remove_label)(wres->resource, ctx->label); + else + err = (*ctx->vsn_hooks->add_label)(wres->resource, ctx->label, + ctx->label_op == DAV_LABEL_SET); + } + + if (err != NULL) { + /* ### need utility routine to add response with description? */ + dav_add_response(wres, err->status, NULL); + wres->response->desc = err->desc; + } + + return NULL; +} + +static int dav_method_label(request_rec *r) +{ + dav_resource *resource; + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + apr_xml_doc *doc; + apr_xml_elem *child; + int depth; + int result; + apr_size_t tsize; + dav_error *err; + dav_label_walker_ctx ctx = { { 0 } }; + dav_response *multi_status; + + /* If no versioning provider, or the provider doesn't support + * labels, decline the request */ + if (vsn_hooks == NULL || vsn_hooks->add_label == NULL) + return DECLINED; + + /* parse the request body */ + if ((result = ap_xml_parse_input(r, &doc)) != OK) { + return result; + } + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 1 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + if ((depth = dav_get_depth(r, 0)) < 0) { + /* dav_get_depth() supplies additional information for the + * default message. */ + return HTTP_BAD_REQUEST; + } + + if (doc == NULL || !dav_validate_root(doc, "label")) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00610) + "The request body does not contain " + "a \"label\" element."); + return HTTP_BAD_REQUEST; + } + + /* check for add, set, or remove element */ + if ((child = dav_find_child(doc->root, "add")) != NULL) { + ctx.label_op = DAV_LABEL_ADD; + } + else if ((child = dav_find_child(doc->root, "set")) != NULL) { + ctx.label_op = DAV_LABEL_SET; + } + else if ((child = dav_find_child(doc->root, "remove")) != NULL) { + ctx.label_op = DAV_LABEL_REMOVE; + } + else { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00611) + "The \"label\" element does not contain " + "an \"add\", \"set\", or \"remove\" element."); + return HTTP_BAD_REQUEST; + } + + /* get the label string */ + if ((child = dav_find_child(child, "label-name")) == NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00612) + "The label command element does not contain " + "a \"label-name\" element."); + return HTTP_BAD_REQUEST; + } + + apr_xml_to_text(r->pool, child, APR_XML_X2T_INNER, NULL, NULL, + &ctx.label, &tsize); + if (tsize == 0) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00613) + "A \"label-name\" element does not contain " + "a label name."); + return HTTP_BAD_REQUEST; + } + + /* do the label operation walk */ + ctx.w.walk_type = DAV_WALKTYPE_NORMAL; + ctx.w.func = dav_label_walker; + ctx.w.walk_ctx = &ctx; + ctx.w.pool = r->pool; + ctx.w.root = resource; + ctx.r = r; + ctx.vsn_hooks = vsn_hooks; + + err = (*resource->hooks->walk)(&ctx.w, depth, &multi_status); + + if (err != NULL) { + /* some sort of error occurred which terminated the walk */ + err = dav_push_error(r->pool, err->status, 0, + "The LABEL operation was terminated prematurely.", + err); + return dav_handle_err(r, err, multi_status); + } + + if (multi_status != NULL) { + /* One or more resources had errors. If depth was zero, convert + * response to simple error, else make sure there is an + * overall error to pass to dav_handle_err() + */ + if (depth == 0) { + err = dav_new_error(r->pool, multi_status->status, 0, 0, + multi_status->desc); + multi_status = NULL; + } + else { + err = dav_new_error(r->pool, HTTP_MULTI_STATUS, 0, 0, + "Errors occurred during the LABEL operation."); + } + + return dav_handle_err(r, err, multi_status); + } + + /* set the Cache-Control header, per the spec */ + apr_table_setn(r->headers_out, "Cache-Control", "no-cache"); + + /* no body */ + ap_set_content_length(r, 0); + + return DONE; +} + +static int dav_core_deliver_report(request_rec *r, + const dav_resource *resource, + const apr_xml_doc *doc, + ap_filter_t *output, dav_error **err) +{ + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + + if (vsn_hooks) { + *err = (*vsn_hooks->deliver_report)(r, resource, doc, + r->output_filters); + return OK; + } + + return DECLINED; +} + +static void dav_core_gather_reports( + request_rec *r, + const dav_resource *resource, + apr_array_header_t *reports, + dav_error **err) +{ + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + + if (vsn_hooks) { + const dav_report_elem *rp; + + (*err) = (*vsn_hooks->avail_reports)(resource, &rp); + while (rp && rp->name) { + + dav_report_elem *report = apr_array_push(reports); + + report->nmspace = rp->nmspace; + report->name = rp->name; + + rp++; + } + } + +} + +static int dav_method_report(request_rec *r) +{ + dav_resource *resource; + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + apr_xml_doc *doc; + dav_error *err = NULL; + + int result; + int label_allowed; + + if ((result = ap_xml_parse_input(r, &doc)) != OK) { + return result; + } + if (doc == NULL) { + /* This supplies additional information for the default msg. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00614) + "The request body must specify a report."); + return HTTP_BAD_REQUEST; + } + + /* Ask repository module to resolve the resource. + * First determine whether a Target-Selector header is allowed + * for this report. + */ + label_allowed = vsn_hooks ? (*vsn_hooks->report_label_header_allowed)(doc) : 0; + err = dav_get_resource(r, label_allowed, 0 /* use_checked_in */, + &resource); + if (err != NULL) { + return dav_handle_err(r, err, NULL); + } + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* set up defaults for the report response */ + r->status = HTTP_OK; + ap_set_content_type(r, DAV_XML_CONTENT_TYPE); + err = NULL; + + /* run report hook */ + result = dav_run_deliver_report(r, resource, doc, + r->output_filters, &err); + if (err != NULL) { + + if (! r->sent_bodyct) { + /* No data has been sent to client yet; throw normal error. */ + return dav_handle_err(r, err, NULL); + } + + /* If an error occurred during the report delivery, there's + basically nothing we can do but abort the connection and + log an error. This is one of the limitations of HTTP; it + needs to "know" the entire status of the response before + generating it, which is just impossible in these streamy + response situations. */ + err = dav_push_error(r->pool, err->status, 0, + "Provider encountered an error while streaming" + " a REPORT response.", err); + dav_log_err(r, err, APLOG_ERR); + r->connection->aborted = 1; + + return DONE; + } + switch (result) { + case OK: + return DONE; + case DECLINED: + /* No one handled the report */ + return HTTP_NOT_IMPLEMENTED; + default: + return DONE; + } + + return DONE; +} + +static int dav_method_make_workspace(request_rec *r) +{ + dav_resource *resource; + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + dav_error *err; + apr_xml_doc *doc; + int result; + + /* if no versioning provider, or the provider does not support workspaces, + * decline the request + */ + if (vsn_hooks == NULL || vsn_hooks->make_workspace == NULL) + return DECLINED; + + /* ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* parse the request body (must be a mkworkspace element) */ + if ((result = ap_xml_parse_input(r, &doc)) != OK) { + return result; + } + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (doc == NULL + || !dav_validate_root(doc, "mkworkspace")) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00615) + "The request body does not contain " + "a \"mkworkspace\" element."); + return HTTP_BAD_REQUEST; + } + + /* Check request preconditions */ + + /* ### need a general mechanism for reporting precondition violations + * ### (should be returning XML document for 403/409 responses) + */ + + /* resource must not already exist */ + if (resource->exists) { + err = dav_new_error(r->pool, HTTP_CONFLICT, 0, 0, + "<DAV:resource-must-be-null/>"); + return dav_handle_err(r, err, NULL); + } + + /* ### what about locking? */ + + /* attempt to create the workspace */ + if ((err = (*vsn_hooks->make_workspace)(resource, doc)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not create workspace %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, NULL); + } + + /* set the Cache-Control header, per the spec */ + apr_table_setn(r->headers_out, "Cache-Control", "no-cache"); + + /* return an appropriate response (HTTP_CREATED) */ + return dav_created(r, resource->uri, "Workspace", 0 /*replaced*/); +} + +static int dav_method_make_activity(request_rec *r) +{ + dav_resource *resource; + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + dav_error *err; + int result; + + /* if no versioning provider, or the provider does not support activities, + * decline the request + */ + if (vsn_hooks == NULL || vsn_hooks->make_activity == NULL) + return DECLINED; + + /* ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + /* MKACTIVITY does not have a defined request body. */ + if ((result = ap_discard_request_body(r)) != OK) { + return result; + } + + /* Check request preconditions */ + + /* ### need a general mechanism for reporting precondition violations + * ### (should be returning XML document for 403/409 responses) + */ + + /* resource must not already exist */ + if (resource->exists) { + err = dav_new_error(r->pool, HTTP_CONFLICT, 0, 0, + "<DAV:resource-must-be-null/>"); + return dav_handle_err(r, err, NULL); + } + + /* the provider must say whether the resource can be created as + an activity, i.e. whether the location is ok. */ + if (vsn_hooks->can_be_activity != NULL + && !(*vsn_hooks->can_be_activity)(resource)) { + err = dav_new_error(r->pool, HTTP_FORBIDDEN, 0, 0, + "<DAV:activity-location-ok/>"); + return dav_handle_err(r, err, NULL); + } + + /* ### what about locking? */ + + /* attempt to create the activity */ + if ((err = (*vsn_hooks->make_activity)(resource)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not create activity %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, NULL); + } + + /* set the Cache-Control header, per the spec */ + apr_table_setn(r->headers_out, "Cache-Control", "no-cache"); + + /* return an appropriate response (HTTP_CREATED) */ + return dav_created(r, resource->uri, "Activity", 0 /*replaced*/); +} + +static int dav_method_baseline_control(request_rec *r) +{ + /* ### */ + return HTTP_METHOD_NOT_ALLOWED; +} + +static int dav_method_merge(request_rec *r) +{ + dav_resource *resource; + dav_resource *source_resource; + const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r); + dav_error *err; + int result; + apr_xml_doc *doc; + apr_xml_elem *source_elem; + apr_xml_elem *href_elem; + apr_xml_elem *prop_elem; + const char *source; + int no_auto_merge; + int no_checkout; + dav_lookup_result lookup; + + /* If no versioning provider, decline the request */ + if (vsn_hooks == NULL) + return DECLINED; + + if ((result = ap_xml_parse_input(r, &doc)) != OK) + return result; + + if (doc == NULL || !dav_validate_root(doc, "merge")) { + /* This supplies additional information for the default msg. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00616) + "The request body must be present and must be a " + "DAV:merge element."); + return HTTP_BAD_REQUEST; + } + + if ((source_elem = dav_find_child(doc->root, "source")) == NULL) { + /* This supplies additional information for the default msg. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00617) + "The DAV:merge element must contain a DAV:source " + "element."); + return HTTP_BAD_REQUEST; + } + if ((href_elem = dav_find_child(source_elem, "href")) == NULL) { + /* This supplies additional information for the default msg. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00618) + "The DAV:source element must contain a DAV:href " + "element."); + return HTTP_BAD_REQUEST; + } + source = dav_xml_get_cdata(href_elem, r->pool, 1 /* strip_white */); + + /* get a subrequest for the source, so that we can get a dav_resource + for that source. */ + lookup = dav_lookup_uri(source, r, 0 /* must_be_absolute */); + if (lookup.rnew == NULL) { + if (lookup.err.status == HTTP_BAD_REQUEST) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00619) + "%s", lookup.err.desc); + return HTTP_BAD_REQUEST; + } + + /* ### this assumes that dav_lookup_uri() only generates a status + * ### that Apache can provide a status line for!! */ + + return dav_error_response(r, lookup.err.status, lookup.err.desc); + } + if (lookup.rnew->status != HTTP_OK) { + /* ### how best to report this... */ + return dav_error_response(r, lookup.rnew->status, + "Merge source URI had an error."); + } + err = dav_get_resource(lookup.rnew, 0 /* label_allowed */, + 0 /* use_checked_in */, &source_resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, source_resource, NULL, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + no_auto_merge = dav_find_child(doc->root, "no-auto-merge") != NULL; + no_checkout = dav_find_child(doc->root, "no-checkout") != NULL; + + prop_elem = dav_find_child(doc->root, "prop"); + + /* ### check RFC. I believe the DAV:merge element may contain any + ### element also allowed within DAV:checkout. need to extract them + ### here, and pass them along. + ### if so, then refactor the CHECKOUT method handling so we can reuse + ### the code. maybe create a structure to hold CHECKOUT parameters + ### which can be passed to the checkout() and merge() hooks. */ + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, source_resource, resource, doc, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* ### check the source and target resources flags/types */ + + /* ### do lock checks, once behavior is defined */ + + /* set the Cache-Control header, per the spec */ + /* ### correct? */ + apr_table_setn(r->headers_out, "Cache-Control", "no-cache"); + + /* Initialize these values for a standard MERGE response. If the MERGE + is going to do something different (i.e. an error), then it must + return a dav_error, and we'll reset these values properly. */ + r->status = HTTP_OK; + ap_set_content_type(r, "text/xml"); + + /* ### should we do any preliminary response generation? probably not, + ### because we may have an error, thus demanding something else in + ### the response body. */ + + /* Do the merge, including any response generation. */ + if ((err = (*vsn_hooks->merge)(resource, source_resource, + no_auto_merge, no_checkout, + prop_elem, + r->output_filters)) != NULL) { + /* ### is err->status the right error here? */ + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not MERGE resource \"%s\" " + "into \"%s\".", + ap_escape_html(r->pool, source), + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, NULL); + } + + /* the response was fully generated by the merge() hook. */ + /* ### urk. does this prevent logging? need to check... */ + return DONE; +} + +static int dav_method_bind(request_rec *r) +{ + dav_resource *resource; + dav_resource *binding; + dav_auto_version_info av_info; + const dav_hooks_binding *binding_hooks = DAV_GET_HOOKS_BINDING(r); + const char *dest; + dav_error *err; + dav_error *err2; + dav_response *multi_response = NULL; + dav_lookup_result lookup; + int overwrite; + + /* If no bindings provider, decline the request */ + if (binding_hooks == NULL) + return DECLINED; + + /* Ask repository module to resolve the resource */ + err = dav_get_resource(r, 0 /* label_allowed */, 0 /* use_checked_in */, + &resource); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, NULL, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + if (!resource->exists) { + /* Apache will supply a default error for this. */ + return HTTP_NOT_FOUND; + } + + /* get the destination URI */ + dest = apr_table_get(r->headers_in, "Destination"); + if (dest == NULL) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00620) + "The request is missing a Destination header."); + return HTTP_BAD_REQUEST; + } + + lookup = dav_lookup_uri(dest, r, 0 /* must_be_absolute */); + if (lookup.rnew == NULL) { + if (lookup.err.status == HTTP_BAD_REQUEST) { + /* This supplies additional information for the default message. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00621) + "%s", lookup.err.desc); + return HTTP_BAD_REQUEST; + } + else if (lookup.err.status == HTTP_BAD_GATEWAY) { + /* ### Bindings protocol draft 02 says to return 507 + * ### (Cross Server Binding Forbidden); Apache already defines 507 + * ### as HTTP_INSUFFICIENT_STORAGE. So, for now, we'll return + * ### HTTP_FORBIDDEN + */ + return dav_error_response(r, HTTP_FORBIDDEN, + "Cross server bindings are not " + "allowed by this server."); + } + + /* ### this assumes that dav_lookup_uri() only generates a status + * ### that Apache can provide a status line for!! */ + + return dav_error_response(r, lookup.err.status, lookup.err.desc); + } + if (lookup.rnew->status != HTTP_OK) { + /* ### how best to report this... */ + return dav_error_response(r, lookup.rnew->status, + "Destination URI had an error."); + } + + /* resolve binding resource */ + err = dav_get_resource(lookup.rnew, 0 /* label_allowed */, + 0 /* use_checked_in */, &binding); + if (err != NULL) + return dav_handle_err(r, err, NULL); + + /* check for any method preconditions */ + if (dav_run_method_precondition(r, resource, binding, NULL, &err) != DECLINED + && err) { + return dav_handle_err(r, err, NULL); + } + + /* are the two resources handled by the same repository? */ + if (resource->hooks != binding->hooks) { + /* ### this message exposes some backend config, but screw it... */ + return dav_error_response(r, HTTP_BAD_GATEWAY, + "Destination URI is handled by a " + "different repository than the source URI. " + "BIND between repositories is not possible."); + } + + /* get and parse the overwrite header value */ + if ((overwrite = dav_get_overwrite(r)) < 0) { + /* dav_get_overwrite() supplies additional information for the + * default message. */ + return HTTP_BAD_REQUEST; + } + + /* quick failure test: if dest exists and overwrite is false. */ + if (binding->exists && !overwrite) { + return dav_error_response(r, HTTP_PRECONDITION_FAILED, + "Destination is not empty and " + "Overwrite is not \"T\""); + } + + /* are the source and destination the same? */ + if ((*resource->hooks->is_same_resource)(resource, binding)) { + return dav_error_response(r, HTTP_FORBIDDEN, + "Source and Destination URIs are the same."); + } + + /* + * Check If-Headers and existing locks for destination. Note that we + * use depth==infinity since the target (hierarchy) will be deleted + * before the move/copy is completed. + * + * Note that we are overwriting the target, which implies a DELETE, so + * we are subject to the error/response rules as a DELETE. Namely, we + * will return a 424 error if any of the validations fail. + * (see dav_method_delete() for more information) + */ + if ((err = dav_validate_request(lookup.rnew, binding, DAV_INFINITY, NULL, + &multi_response, + DAV_VALIDATE_PARENT + | DAV_VALIDATE_USE_424, NULL)) != NULL) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not BIND %s due to a " + "failed precondition on the " + "destination (e.g. locks).", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, multi_response); + } + + /* guard against creating circular bindings */ + if (resource->collection + && (*resource->hooks->is_parent_resource)(resource, binding)) { + return dav_error_response(r, HTTP_FORBIDDEN, + "Source collection contains the Destination."); + } + if (resource->collection + && (*resource->hooks->is_parent_resource)(binding, resource)) { + /* The destination must exist (since it contains the source), and + * a condition above implies Overwrite==T. Obviously, we cannot + * delete the Destination before the BIND, as that would + * delete the Source. + */ + + return dav_error_response(r, HTTP_FORBIDDEN, + "Destination collection contains the Source and " + "Overwrite has been specified."); + } + + /* prepare the destination collection for modification */ + if ((err = dav_auto_checkout(r, binding, 1 /* parent_only */, + &av_info)) != NULL) { + /* could not make destination writable */ + return dav_handle_err(r, err, NULL); + } + + /* If target exists, remove it first (we know Ovewrite must be TRUE). + * Then try to bind to the resource. + */ + if (binding->exists) + err = (*resource->hooks->remove_resource)(binding, &multi_response); + + if (err == NULL) { + err = (*binding_hooks->bind_resource)(resource, binding); + } + + /* restore parent collection states */ + err2 = dav_auto_checkin(r, NULL, + err != NULL /* undo if error */, + 0 /* unlock */, &av_info); + + /* check for error from remove/bind operations */ + if (err != NULL) { + err = dav_push_error(r->pool, err->status, 0, + apr_psprintf(r->pool, + "Could not BIND %s.", + ap_escape_html(r->pool, r->uri)), + err); + return dav_handle_err(r, err, multi_response); + } + + /* check for errors from reverting writability */ + if (err2 != NULL) { + /* just log a warning */ + err = dav_push_error(r->pool, err2->status, 0, + "The BIND was successful, but there was a " + "problem automatically checking in the " + "source parent collection.", + err2); + dav_log_err(r, err, APLOG_WARNING); + } + + /* return an appropriate response (HTTP_CREATED) */ + /* ### spec doesn't say what happens when destination was replaced */ + return dav_created(r, lookup.rnew->unparsed_uri, "Binding", 0); +} + + +/* + * Response handler for DAV resources + */ +static int dav_handler(request_rec *r) +{ + if (strcmp(r->handler, DAV_HANDLER_NAME) != 0) + return DECLINED; + + /* Reject requests with an unescaped hash character, as these may + * be more destructive than the user intended. */ + if (r->parsed_uri.fragment != NULL) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(00622) + "buggy client used un-escaped hash in Request-URI"); + return dav_error_response(r, HTTP_BAD_REQUEST, + "The request was invalid: the URI included " + "an un-escaped hash character"); + } + + /* ### do we need to do anything with r->proxyreq ?? */ + + /* + * ### anything else to do here? could another module and/or + * ### config option "take over" the handler here? i.e. how do + * ### we lock down this hierarchy so that we are the ultimate + * ### arbiter? (or do we simply depend on the administrator + * ### to avoid conflicting configurations?) + */ + + /* + * Set up the methods mask, since that's one of the reasons this handler + * gets called, and lower-level things may need the info. + * + * First, set the mask to the methods we handle directly. Since by + * definition we own our managed space, we unconditionally set + * the r->allowed field rather than ORing our values with anything + * any other module may have put in there. + * + * These are the HTTP-defined methods that we handle directly. + */ + r->allowed = 0 + | (AP_METHOD_BIT << M_GET) + | (AP_METHOD_BIT << M_PUT) + | (AP_METHOD_BIT << M_DELETE) + | (AP_METHOD_BIT << M_OPTIONS) + | (AP_METHOD_BIT << M_INVALID); + + /* + * These are the DAV methods we handle. + */ + r->allowed |= 0 + | (AP_METHOD_BIT << M_COPY) + | (AP_METHOD_BIT << M_LOCK) + | (AP_METHOD_BIT << M_UNLOCK) + | (AP_METHOD_BIT << M_MKCOL) + | (AP_METHOD_BIT << M_MOVE) + | (AP_METHOD_BIT << M_PROPFIND) + | (AP_METHOD_BIT << M_PROPPATCH); + + /* + * These are methods that we don't handle directly, but let the + * server's default handler do for us as our agent. + */ + r->allowed |= 0 + | (AP_METHOD_BIT << M_POST); + + /* ### hrm. if we return HTTP_METHOD_NOT_ALLOWED, then an Allow header + * ### is sent; it will need the other allowed states; since the default + * ### handler is not called on error, then it doesn't add the other + * ### allowed states, so we must + */ + + /* ### we might need to refine this for just where we return the error. + * ### also, there is the issue with other methods (see ISSUES) + */ + + /* dispatch the appropriate method handler */ + if (r->method_number == M_GET) { + return dav_method_get(r); + } + + if (r->method_number == M_PUT) { + return dav_method_put(r); + } + + if (r->method_number == M_POST) { + return dav_method_post(r); + } + + if (r->method_number == M_DELETE) { + return dav_method_delete(r); + } + + if (r->method_number == M_OPTIONS) { + return dav_method_options(r); + } + + if (r->method_number == M_PROPFIND) { + return dav_method_propfind(r); + } + + if (r->method_number == M_PROPPATCH) { + return dav_method_proppatch(r); + } + + if (r->method_number == M_MKCOL) { + return dav_method_mkcol(r); + } + + if (r->method_number == M_COPY) { + return dav_method_copymove(r, DAV_DO_COPY); + } + + if (r->method_number == M_MOVE) { + return dav_method_copymove(r, DAV_DO_MOVE); + } + + if (r->method_number == M_LOCK) { + return dav_method_lock(r); + } + + if (r->method_number == M_UNLOCK) { + return dav_method_unlock(r); + } + + if (r->method_number == M_VERSION_CONTROL) { + return dav_method_vsn_control(r); + } + + if (r->method_number == M_CHECKOUT) { + return dav_method_checkout(r); + } + + if (r->method_number == M_UNCHECKOUT) { + return dav_method_uncheckout(r); + } + + if (r->method_number == M_CHECKIN) { + return dav_method_checkin(r); + } + + if (r->method_number == M_UPDATE) { + return dav_method_update(r); + } + + if (r->method_number == M_LABEL) { + return dav_method_label(r); + } + + if (r->method_number == M_REPORT) { + return dav_method_report(r); + } + + if (r->method_number == M_MKWORKSPACE) { + return dav_method_make_workspace(r); + } + + if (r->method_number == M_MKACTIVITY) { + return dav_method_make_activity(r); + } + + if (r->method_number == M_BASELINE_CONTROL) { + return dav_method_baseline_control(r); + } + + if (r->method_number == M_MERGE) { + return dav_method_merge(r); + } + + /* BIND method */ + if (r->method_number == dav_methods[DAV_M_BIND]) { + return dav_method_bind(r); + } + + /* DASL method */ + if (r->method_number == dav_methods[DAV_M_SEARCH]) { + return dav_method_search(r); + } + + /* ### add'l methods for Advanced Collections, ACLs */ + + return DECLINED; +} + +static int dav_fixups(request_rec *r) +{ + dav_dir_conf *conf; + + /* quickly ignore any HTTP/0.9 requests which aren't subreqs. */ + if (r->assbackwards && !r->main) { + return DECLINED; + } + + conf = (dav_dir_conf *)ap_get_module_config(r->per_dir_config, + &dav_module); + + /* if DAV is not enabled, then we've got nothing to do */ + if (conf->provider == NULL) { + return DECLINED; + } + + /* We are going to handle almost every request. In certain cases, + the provider maps to the filesystem (thus, handle_get is + FALSE), and core Apache will handle it. a For that case, we + just return right away. */ + if (r->method_number == M_GET) { + /* + * ### need some work to pull Content-Type and Content-Language + * ### from the property database. + */ + + /* + * If the repository hasn't indicated that it will handle the + * GET method, then just punt. + * + * ### this isn't quite right... taking over the response can break + * ### things like mod_negotiation. need to look into this some more. + */ + if (!conf->provider->repos->handle_get) { + return DECLINED; + } + } + + /* ### this is wrong. We should only be setting the r->handler for the + * requests that mod_dav knows about. If we set the handler for M_POST + * requests, then CGI scripts that use POST will return the source for the + * script. However, mod_dav DOES handle POST, so something else needs + * to be fixed. + */ + if (r->method_number != M_POST) { + + /* We are going to be handling the response for this resource. */ + r->handler = DAV_HANDLER_NAME; + return OK; + } + + return DECLINED; +} + +static void register_hooks(apr_pool_t *p) +{ + ap_hook_handler(dav_handler, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_post_config(dav_init_handler, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_fixups(dav_fixups, NULL, NULL, APR_HOOK_MIDDLE); + + dav_hook_find_liveprop(dav_core_find_liveprop, NULL, NULL, APR_HOOK_LAST); + dav_hook_insert_all_liveprops(dav_core_insert_all_liveprops, + NULL, NULL, APR_HOOK_MIDDLE); + + dav_hook_deliver_report(dav_core_deliver_report, + NULL, NULL, APR_HOOK_LAST); + dav_hook_gather_reports(dav_core_gather_reports, + NULL, NULL, APR_HOOK_LAST); + + dav_core_register_uris(p); +} + +/*--------------------------------------------------------------------------- + * + * Configuration info for the module + */ + +static const command_rec dav_cmds[] = +{ + /* per directory/location */ + AP_INIT_TAKE1("DAV", dav_cmd_dav, NULL, ACCESS_CONF, + "specify the DAV provider for a directory or location"), + + /* per directory/location, or per server */ + AP_INIT_TAKE1("DAVMinTimeout", dav_cmd_davmintimeout, NULL, + ACCESS_CONF|RSRC_CONF, + "specify minimum allowed timeout"), + + /* per directory/location, or per server */ + AP_INIT_FLAG("DAVDepthInfinity", dav_cmd_davdepthinfinity, NULL, + ACCESS_CONF|RSRC_CONF, + "allow Depth infinity PROPFIND requests"), + + /* per directory/location, or per server */ + AP_INIT_FLAG("DAVLockDiscovery", dav_cmd_davlockdiscovery, NULL, + ACCESS_CONF|RSRC_CONF, + "allow lock discovery by PROPFIND requests"), + + { NULL } +}; + +module DAV_DECLARE_DATA dav_module = +{ + STANDARD20_MODULE_STUFF, + dav_create_dir_config, /* dir config creater */ + dav_merge_dir_config, /* dir merger --- default is to override */ + dav_create_server_config, /* server config */ + dav_merge_server_config, /* merge server config */ + dav_cmds, /* command table */ + register_hooks, /* register hooks */ +}; + +APR_HOOK_STRUCT( + APR_HOOK_LINK(gather_propsets) + APR_HOOK_LINK(find_liveprop) + APR_HOOK_LINK(insert_all_liveprops) + APR_HOOK_LINK(deliver_report) + APR_HOOK_LINK(gather_reports) + APR_HOOK_LINK(method_precondition) + ) + +APR_IMPLEMENT_EXTERNAL_HOOK_VOID(dav, DAV, gather_propsets, + (apr_array_header_t *uris), + (uris)) + +APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST(dav, DAV, int, find_liveprop, + (const dav_resource *resource, + const char *ns_uri, const char *name, + const dav_hooks_liveprop **hooks), + (resource, ns_uri, name, hooks), 0) + +APR_IMPLEMENT_EXTERNAL_HOOK_VOID(dav, DAV, insert_all_liveprops, + (request_rec *r, const dav_resource *resource, + dav_prop_insert what, apr_text_header *phdr), + (r, resource, what, phdr)) + +APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST(dav, DAV, int, deliver_report, + (request_rec *r, + const dav_resource *resource, + const apr_xml_doc *doc, + ap_filter_t *output, dav_error **err), + (r, resource, doc, output, err), DECLINED) + +APR_IMPLEMENT_EXTERNAL_HOOK_VOID(dav, DAV, gather_reports, + (request_rec *r, const dav_resource *resource, + apr_array_header_t *reports, dav_error **err), + (r, resource, reports, err)) + +APR_IMPLEMENT_EXTERNAL_HOOK_RUN_FIRST(dav, DAV, int, method_precondition, + (request_rec *r, + dav_resource *src, const dav_resource *dest, + const apr_xml_doc *doc, + dav_error **err), + (r, src, dest, doc, err), DECLINED) |