| /* |
| ** Copyright (C) 1998-2000 Greg Stein. All Rights Reserved. |
| ** |
| ** By using this file, you agree to the terms and conditions set forth in |
| ** the LICENSE.html file which can be found at the top level of the mod_dav |
| ** distribution or at http://www.webdav.org/mod_dav/license-1.html. |
| ** |
| ** Contact information: |
| ** Greg Stein, PO Box 760, Palo Alto, CA, 94302 |
| ** gstein@lyra.org, http://www.webdav.org/mod_dav/ |
| */ |
| |
| /* |
| ** DAV extension module for Apache 1.3.* |
| ** This module is repository-independent. It depends on hooks provided by a |
| ** repository implementation. |
| ** |
| ** Written by Greg Stein, gstein@lyra.org, http://www.lyra.org/ |
| ** |
| ** 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 "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 "dav_opaquelock.h" |
| |
| |
| enum { |
| DAV_ENABLED_UNSET = 0, |
| DAV_ENABLED_OFF, |
| DAV_ENABLED_ON |
| }; |
| |
| /* per-dir configuration */ |
| typedef struct { |
| int enabled; |
| const char *dir; |
| int locktimeout; |
| int handle_get; /* cached from repository hook structure */ |
| int allow_depthinfinity; |
| long limit_xml_body; |
| |
| table *d_params; /* per-directory DAV config parameters */ |
| struct dav_dyn_mod_ctx *dmc; |
| |
| dav_dyn_hooks propdb; |
| dav_dyn_hooks locks; |
| dav_dyn_hooks *liveprop; |
| dav_dyn_hooks repository; |
| dav_dyn_hooks vsn; |
| } dav_dir_conf; |
| |
| /* per-server configuration */ |
| typedef struct { |
| const char *lockdb_path; /* lock database path */ |
| |
| uuid_state st; /* UUID state for opaquelocktoken */ |
| |
| } dav_server_conf; |
| |
| #define DAV_INHERIT_VALUE(parent, child, field) \ |
| ((child)->field ? (child)->field : (parent)->field) |
| |
| /* LimitXMLRequestBody handling */ |
| #define DAV_LIMIT_UNSET ((long) -1) |
| #define DAV_DEFAULT_LIMIT_XML_BODY ((size_t)1000000) |
| |
| |
| /* forward-declare for use in configuration lookup */ |
| extern module MODULE_VAR_EXPORT dav_module; |
| |
| /* copy a module's providers into our per-directory configuration state */ |
| static void dav_copy_providers(pool *p, const char *name, dav_dir_conf *conf) |
| { |
| const dav_dyn_module *mod; |
| const dav_dyn_provider *provider; |
| dav_dyn_hooks hooks; |
| void *ctx; |
| |
| mod = dav_find_module(name); |
| /* ### if NULL? need to error out somehow... */ |
| |
| /* Set hooks for any providers in the module */ |
| ctx = dav_prepare_scan(p, mod); |
| if (ctx == NULL) { |
| /* ### how to signal an error? */ |
| return; |
| } |
| |
| while (!dav_scan_providers(ctx, &provider, &hooks)) { |
| |
| switch (provider->type) { |
| |
| case DAV_DYN_TYPE_PROPDB: |
| conf->propdb = hooks; |
| break; |
| |
| case DAV_DYN_TYPE_LOCKS: |
| conf->locks = hooks; |
| break; |
| |
| case DAV_DYN_TYPE_QUERY_GRAMMAR: |
| /* ### not yet defined */ |
| break; |
| |
| case DAV_DYN_TYPE_ACL: |
| /* ### not yet defined */ |
| break; |
| |
| case DAV_DYN_TYPE_VSN: |
| conf->vsn = hooks; |
| break; |
| |
| case DAV_DYN_TYPE_REPOSITORY: |
| conf->repository = hooks; |
| conf->handle_get = DAV_AS_HOOKS_REPOSITORY(&hooks)->handle_get; |
| break; |
| |
| case DAV_DYN_TYPE_LIVEPROP: |
| { |
| dav_dyn_hooks *ddh = ap_palloc(p, sizeof(*ddh)); |
| |
| *ddh = hooks; |
| ddh->next = conf->liveprop; |
| conf->liveprop = ddh; |
| break; |
| } |
| |
| default: |
| /* ### need to error out somehow... */ |
| break; |
| } |
| } |
| } |
| |
| static void dav_init_handler(server_rec *s, pool *p) |
| { |
| /* DBG0("dav_init_handler"); */ |
| |
| ap_add_version_component("DAV/" DAV_VERSION); |
| |
| dav_process_builtin_modules(p); |
| } |
| |
| static void *dav_create_server_config(pool *p, server_rec *s) |
| { |
| dav_server_conf *newconf; |
| |
| newconf = (dav_server_conf *) ap_pcalloc(p, sizeof(*newconf)); |
| |
| newconf->lockdb_path = NULL; |
| dav_create_uuid_state(&newconf->st); |
| |
| return newconf; |
| } |
| |
| static void *dav_merge_server_config(pool *p, void *base, void *overrides) |
| { |
| dav_server_conf *parent = base; |
| dav_server_conf *child = overrides; |
| dav_server_conf *newconf; |
| |
| newconf = (dav_server_conf *) ap_pcalloc(p, sizeof(*newconf)); |
| |
| newconf->lockdb_path = DAV_INHERIT_VALUE(parent, child, lockdb_path); |
| |
| memcpy(&newconf->st, &child->st, sizeof(newconf->st)); |
| |
| return newconf; |
| } |
| |
| static void *dav_create_dir_config(pool *p, char *dir) |
| { |
| /* NOTE: dir==NULL creates the default per-dir config */ |
| |
| dav_dir_conf *conf; |
| |
| conf = (dav_dir_conf *) ap_pcalloc(p, sizeof(*conf)); |
| conf->dir = ap_pstrdup(p, dir); |
| conf->d_params = ap_make_table(p, 1); |
| conf->limit_xml_body = DAV_LIMIT_UNSET; |
| |
| /* DBG1("dav_create_dir_config: %08lx", (long)conf); */ |
| |
| /* |
| ** Locate the appropriate module (NULL == default) and copy the module's |
| ** providers' hooks into our configuration state. |
| */ |
| dav_copy_providers(p, NULL, conf); |
| |
| return conf; |
| } |
| |
| static void *dav_merge_dir_config(pool *p, void *base, void *overrides) |
| { |
| dav_dir_conf *parent = base; |
| dav_dir_conf *child = overrides; |
| dav_dir_conf *newconf = (dav_dir_conf *) ap_pcalloc(p, sizeof(*newconf)); |
| |
| /* DBG3("dav_merge_dir_config: new=%08lx base=%08lx overrides=%08lx", |
| (long)newconf, (long)base, (long)overrides); */ |
| |
| newconf->enabled = DAV_INHERIT_VALUE(parent, child, enabled); |
| 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); |
| |
| if (child->limit_xml_body != DAV_LIMIT_UNSET) |
| newconf->limit_xml_body = child->limit_xml_body; |
| else |
| newconf->limit_xml_body = parent->limit_xml_body; |
| |
| newconf->d_params = ap_copy_table(p, parent->d_params); |
| ap_overlap_tables(newconf->d_params, child->d_params, |
| AP_OVERLAP_TABLES_SET); |
| |
| if (child->propdb.hooks != NULL) |
| newconf->propdb = child->propdb; |
| else |
| newconf->propdb = parent->propdb; |
| |
| if (child->locks.hooks != NULL) |
| newconf->locks = child->locks; |
| else |
| newconf->locks = parent->locks; |
| |
| if (child->vsn.hooks != NULL) |
| newconf->vsn = child->vsn; |
| else |
| newconf->vsn = parent->vsn; |
| |
| if (child->repository.hooks != NULL) |
| newconf->repository = child->repository; |
| else |
| newconf->repository = parent->repository; |
| newconf->handle_get = |
| newconf->repository.hooks != NULL |
| && DAV_AS_HOOKS_REPOSITORY(&newconf->repository)->handle_get; |
| |
| if (child->liveprop != NULL) |
| newconf->liveprop = child->liveprop; |
| else |
| newconf->liveprop = parent->liveprop; |
| |
| return newconf; |
| } |
| |
| uuid_state *dav_get_uuid_state(const request_rec *r) |
| { |
| dav_server_conf *conf; |
| |
| conf = ap_get_module_config(r->server->module_config, &dav_module); |
| |
| return &conf->st; |
| } |
| |
| const char *dav_get_lockdb_path(const request_rec *r) |
| { |
| dav_server_conf *conf; |
| |
| conf = ap_get_module_config(r->server->module_config, &dav_module); |
| return conf->lockdb_path; |
| } |
| |
| table *dav_get_dir_params(const request_rec *r) |
| { |
| dav_dir_conf *conf; |
| |
| conf = ap_get_module_config(r->per_dir_config, &dav_module); |
| return conf->d_params; |
| } |
| |
| size_t dav_get_limit_xml_body(const request_rec *r) |
| { |
| dav_dir_conf *conf; |
| |
| conf = ap_get_module_config(r->per_dir_config, &dav_module); |
| if (conf->limit_xml_body == DAV_LIMIT_UNSET) |
| return DAV_DEFAULT_LIMIT_XML_BODY; |
| return (size_t)conf->limit_xml_body; |
| } |
| |
| const dav_dyn_hooks *dav_get_provider_hooks(request_rec *r, int provider_type) |
| { |
| dav_dir_conf *conf; |
| const dav_dyn_hooks *hooks; |
| static const dav_dyn_hooks null_hooks = { { 0 } }; |
| |
| /* Call repository hook to resolve resource */ |
| conf = (dav_dir_conf *) ap_get_module_config(r->per_dir_config, |
| &dav_module); |
| switch (provider_type) { |
| |
| case DAV_DYN_TYPE_PROPDB: |
| hooks = &conf->propdb; |
| break; |
| |
| case DAV_DYN_TYPE_LOCKS: |
| hooks = &conf->locks; |
| break; |
| |
| case DAV_DYN_TYPE_QUERY_GRAMMAR: |
| /* ### not yet defined */ |
| hooks = &null_hooks; |
| break; |
| |
| case DAV_DYN_TYPE_ACL: |
| /* ### not yet defined */ |
| hooks = &null_hooks; |
| break; |
| |
| case DAV_DYN_TYPE_VSN: |
| hooks = &conf->vsn; |
| break; |
| |
| case DAV_DYN_TYPE_REPOSITORY: |
| hooks = &conf->repository; |
| break; |
| |
| case DAV_DYN_TYPE_LIVEPROP: |
| hooks = conf->liveprop; |
| break; |
| |
| default: |
| /* unknown provider type */ |
| hooks = &null_hooks; |
| break; |
| } |
| |
| return hooks; |
| } |
| |
| /* |
| * Command handler for the DAV directive, which is FLAG. |
| */ |
| static const char *dav_cmd_dav(cmd_parms *cmd, void *config, int arg) |
| { |
| dav_dir_conf *conf = (dav_dir_conf *) config; |
| |
| if (arg) |
| conf->enabled = DAV_ENABLED_ON; |
| else |
| conf->enabled = DAV_ENABLED_OFF; |
| 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 DAVLockDB directive, which is TAKE1 |
| */ |
| static const char *dav_cmd_davlockdb(cmd_parms *cmd, void *config, char *arg1) |
| { |
| dav_server_conf *conf; |
| |
| conf = (dav_server_conf *) ap_get_module_config(cmd->server->module_config, |
| &dav_module); |
| arg1 = ap_os_canonical_filename(cmd->pool, arg1); |
| conf->lockdb_path = ap_server_root_relative(cmd->pool, arg1); |
| |
| return NULL; |
| } |
| |
| /* |
| * Command handler for DAVMinTimeout directive, which is TAKE1 |
| */ |
| static const char *dav_cmd_davmintimeout(cmd_parms *cmd, void *config, |
| 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; |
| } |
| |
| /* |
| * Command handler for DAVParam directive, which is TAKE2 |
| */ |
| static const char *dav_cmd_davparam(cmd_parms *cmd, void *config, |
| char *arg1, char *arg2) |
| { |
| dav_dir_conf *conf = (dav_dir_conf *) config; |
| |
| ap_table_set(conf->d_params, arg1, arg2); |
| |
| return NULL; |
| } |
| |
| /* |
| * Command handler for LimitXMLRequestBody directive, which is TAKE1 |
| */ |
| static const char *dav_cmd_limitxmlrequestbody(cmd_parms *cmd, void *config, |
| char *arg1) |
| { |
| dav_dir_conf *conf = (dav_dir_conf *) config; |
| |
| conf->limit_xml_body = atol(arg1); |
| if (conf->limit_xml_body < 0) |
| return "LimitXMLRequestBody 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. |
| */ |
| static int dav_error_response(request_rec *r, int status, const char *body) |
| { |
| r->status = status; |
| r->content_type = "text/html"; |
| |
| /* since we're returning DONE, ensure the request body is consumed. */ |
| (void) ap_discard_request_body(r); |
| |
| /* begin the response now... */ |
| ap_send_http_header(r); |
| |
| /* ### hard or soft? */ |
| ap_soft_timeout("send error body", r); |
| |
| ap_rvputs(r, |
| DAV_RESPONSE_BODY_1, |
| r->status_line, |
| DAV_RESPONSE_BODY_2, |
| &r->status_line[4], |
| DAV_RESPONSE_BODY_3, |
| NULL); |
| |
| ap_rputs(body, r); |
| |
| ap_rputs(ap_psignature("\n<P><HR>\n", r), r); |
| ap_rputs(DAV_RESPONSE_BODY_4, r); |
| |
| ap_kill_timeout(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(pool *p, const char *uri) |
| { |
| const char *e_uri = ap_escape_uri(p, uri); |
| |
| /* check the easy case... */ |
| if (strchr(e_uri, '&') == NULL) |
| return e_uri; |
| |
| /* more work needed... sigh. */ |
| |
| /* |
| ** Note: this is a teeny bit of overkill since we know there are no |
| ** '<' or '>' characters, but who cares. |
| */ |
| return dav_quote_string(p, e_uri, 0); |
| } |
| |
| static void dav_send_multistatus(request_rec *r, int status, |
| dav_response *first, |
| array_header *namespaces) |
| { |
| /* Set the correct status and Content-Type */ |
| r->status = status; |
| r->content_type = DAV_XML_CONTENT_TYPE; |
| |
| /* Send all of the headers now */ |
| ap_send_http_header(r); |
| |
| /* Start a timeout for delivering the response. */ |
| ap_soft_timeout("sending multistatus response", r); |
| |
| /* Send the actual multistatus response now... */ |
| ap_rputs(DAV_XML_HEADER DEBUG_CR |
| "<D:multistatus xmlns:D=\"DAV:\"", r); |
| |
| if (namespaces != NULL) { |
| int i; |
| |
| for (i = namespaces->nelts; i--; ) { |
| ap_rprintf(r, " xmlns:ns%d=\"%s\"", i, |
| DAV_GET_URI_ITEM(namespaces, i)); |
| } |
| } |
| |
| /* ap_rputc('>', r); */ |
| ap_rputs(">" DEBUG_CR, r); |
| |
| for (; first != NULL; first = first->next) { |
| dav_text *t; |
| |
| if (first->propresult.xmlns == NULL) { |
| ap_rputs("<D:response>", r); |
| } |
| else { |
| ap_rputs("<D:response", r); |
| for (t = first->propresult.xmlns; t; t = t->next) { |
| ap_rputs(t->text, r); |
| } |
| ap_rputc('>', r); |
| } |
| |
| ap_rputs(DEBUG_CR "<D:href>", r); |
| ap_rputs(dav_xml_escape_uri(r->pool, first->href), r); |
| ap_rputs("</D:href>" DEBUG_CR, r); |
| |
| if (first->propresult.propstats == NULL) { |
| /* ### it would be nice to get a status line from Apache */ |
| ap_rprintf(r, |
| "<D:status>HTTP/1.1 %d status text goes here</D:status>" |
| DEBUG_CR, first->status); |
| } |
| else { |
| /* assume this includes <propstat> and is quoted properly */ |
| for (t = first->propresult.propstats; t; t = t->next) { |
| ap_rputs(t->text, r); |
| } |
| } |
| |
| if (first->desc != NULL) { |
| /* |
| ** We supply the description, so we know it doesn't have to |
| ** have any escaping/encoding applied to it. |
| */ |
| ap_rputs("<D:responsedescription>", r); |
| ap_rputs(first->desc, r); |
| ap_rputs("</D:responsedescription>" DEBUG_CR, r); |
| } |
| |
| ap_rputs("</D:response>" DEBUG_CR, r); |
| } |
| |
| ap_rputs("</D:multistatus>" DEBUG_CR, r); |
| |
| /* Done with sending and the timeout. */ |
| ap_kill_timeout(r); |
| } |
| |
| /* |
| ** 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; |
| if (errscan->save_errno != 0) { |
| errno = errscan->save_errno; |
| ap_log_rerror(APLOG_MARK, level, r, "%s [%d, #%d]", |
| errscan->desc, errscan->status, errscan->error_id); |
| } |
| else { |
| ap_log_rerror(APLOG_MARK, level | APLOG_NOERRNO, 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 |
| */ |
| static int dav_handle_err(request_rec *r, dav_error *err, |
| dav_response *response) |
| { |
| /* log the errors */ |
| dav_log_err(r, err, APLOG_ERR); |
| |
| if (response == NULL) { |
| /* our error messages are safe; tell Apache this */ |
| ap_table_setn(r->notes, "verbose-error-to", "*"); |
| return err->status; |
| } |
| |
| /* since we're returning DONE, ensure the request body is consumed. */ |
| (void) ap_discard_request_body(r); |
| |
| /* 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 */ |
| static int dav_created(request_rec *r, request_rec *rnew, |
| dav_resource *res, const char *what, |
| int replaced) |
| { |
| const char *body; |
| |
| if (rnew == NULL) { |
| rnew = r; |
| } |
| |
| /* 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. */ |
| |
| /* ### rnew->uri does not contain an absoluteURI. S14.30 states that |
| * ### the Location header requires an absoluteURI. where to get it? */ |
| /* ### disable until we get the right value */ |
| #if 0 |
| ap_table_setn(r->headers_out, "Location", rnew->uri); |
| #endif |
| |
| /* ### 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 = ap_psprintf(r->pool, "%s %s has been created.", |
| what, |
| ap_escape_html(rnew->pool, rnew->uri)); |
| return dav_error_response(r, HTTP_CREATED, body); |
| } |
| |
| /* ### move to dav_util? */ |
| int dav_get_depth(request_rec *r, int def_depth) |
| { |
| const char *depth = ap_table_get(r->headers_in, "Depth"); |
| |
| if (depth == NULL) { |
| return def_depth; |
| } |
| if (strcasecmp(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 | APLOG_NOERRNO, r, |
| "An invalid Depth header was specified."); |
| return -1; |
| } |
| |
| static int dav_get_overwrite(request_rec *r) |
| { |
| const char *overwrite = ap_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 | APLOG_NOERRNO, r, |
| "An invalid Overwrite header was specified."); |
| return -1; |
| } |
| |
| /* resolve a request URI to a resource descriptor */ |
| static int dav_get_resource(request_rec *r, dav_resource **res_p) |
| { |
| dav_dir_conf *conf; |
| const dav_hooks_repository *repos_hooks; |
| |
| /* Call repository hook to resolve resource */ |
| conf = (dav_dir_conf *) ap_get_module_config(r->per_dir_config, |
| &dav_module); |
| |
| repos_hooks = DAV_AS_HOOKS_REPOSITORY(&conf->repository); |
| if (repos_hooks == NULL || repos_hooks->get_resource == NULL) { |
| /* ### this should happen at startup rather than per-request */ |
| ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r, |
| "No %s has been configured.", |
| repos_hooks == NULL |
| ? "repository module" |
| : "GET handler"); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| *res_p = (*repos_hooks->get_resource)(r, conf->dir, |
| dav_get_target_selector(r)); |
| if (*res_p == NULL) { |
| /* Apache will supply a default error for this. */ |
| return HTTP_NOT_FOUND; |
| } |
| |
| return OK; |
| } |
| |
| static 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); |
| } |
| |
| static int dav_parse_range(request_rec *r, |
| off_t *range_start, off_t *range_end) |
| { |
| const char *range; |
| char *dash; |
| char *slash; |
| |
| range = ap_table_get(r->headers_in, "content-range"); |
| if (range == NULL) |
| return 0; |
| |
| range = ap_pstrdup(r->pool, range); |
| if (strncasecmp(range, "bytes ", 6) != 0 |
| || (dash = strchr(range, '-')) == NULL |
| || (slash = strchr(range, '/')) == NULL) { |
| /* malformed header. ignore it (per S14.16 of RFC2616) */ |
| return 0; |
| } |
| |
| *dash = *slash = '\0'; |
| *range_start = atol(range + 6); |
| *range_end = atol(dash + 1); |
| if (*range_end < *range_start |
| || (slash[1] != '*' && atol(slash + 1) <= *range_end)) { |
| /* invalid range. ignore it (per S14.16 of RFC2616) */ |
| return 0; |
| } |
| |
| /* we now have a valid range */ |
| return 1; |
| } |
| |
| /* handle the GET method */ |
| static int dav_method_get(request_rec *r) |
| { |
| dav_resource *resource; |
| int result; |
| |
| /* 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. |
| */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| if (!resource->exists) { |
| /* Apache will supply a default error for this. */ |
| return HTTP_NOT_FOUND; |
| } |
| |
| /* Check resource type */ |
| if (resource->type != DAV_RESOURCE_TYPE_REGULAR && |
| resource->type != DAV_RESOURCE_TYPE_REVISION) { |
| return dav_error_response(r, HTTP_CONFLICT, |
| "Cannot GET this type of resource."); |
| } |
| |
| /* Cannot handle GET of a collection from a repository */ |
| if (resource->collection) { |
| return dav_error_response(r, HTTP_CONFLICT, |
| "No default response to GET for a " |
| "collection."); |
| } |
| |
| /* |
| ** We can use two different approaches for a GET. |
| ** |
| ** 1) get_pathname will return a pathname to a file which should be |
| ** sent to the client. If the repository provides this, then we |
| ** use it. |
| ** |
| ** This is the best alternative since it allows us to do a sub- |
| ** request on the file, which gives the Apache framework a chance |
| ** to deal with negotiation, MIME types, or whatever. |
| ** |
| ** 2) open_stream and read_stream. |
| */ |
| if (resource->hooks->get_pathname != NULL) { |
| const char *pathname; |
| void *fhandle; |
| request_rec *new_req; |
| |
| /* Ask repository for copy of file */ |
| pathname = (*resource->hooks->get_pathname)(resource, &fhandle); |
| if (pathname == NULL) { |
| return HTTP_NOT_FOUND; |
| } |
| |
| /* Convert to canonical filename, so Apache detects component |
| * separators (on Windows, it only looks for '/', not '\') |
| */ |
| pathname = ap_os_case_canonical_filename(r->pool, pathname); |
| |
| /* Create a sub-request with the new filename */ |
| new_req = ap_sub_req_lookup_file(pathname, r); |
| if (new_req == NULL) { |
| (*resource->hooks->free_file)(fhandle); |
| return HTTP_INTERNAL_SERVER_ERROR; |
| } |
| |
| /* This may be a HEAD request */ |
| new_req->header_only = r->header_only; |
| |
| /* ### this enables header generation */ |
| new_req->assbackwards = 0; |
| |
| /* Run the sub-request */ |
| result = ap_run_sub_req(new_req); |
| ap_destroy_sub_req(new_req); |
| |
| /* Free resources */ |
| (*resource->hooks->free_file)(fhandle); |
| |
| return result; |
| } |
| else { |
| dav_stream_mode mode; |
| dav_stream *stream; |
| dav_error *err; |
| void *buffer; |
| int has_range; |
| off_t range_start; |
| off_t range_end; |
| |
| /* 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); |
| } |
| |
| /* use plain READ mode unless we see a Content-Range */ |
| mode = DAV_MODE_READ; |
| |
| /* process the Content-Range header (if present) */ |
| has_range = dav_parse_range(r, &range_start, &range_end); |
| if (has_range) { |
| /* use a read mode which is seekable */ |
| mode = DAV_MODE_READ_SEEKABLE; |
| |
| /* prep the output */ |
| r->status = HTTP_PARTIAL_CONTENT; |
| ap_table_setn(r->headers_out, |
| "Content-Range", |
| ap_psprintf(r->pool, "bytes %ld-%ld/*", |
| range_start, range_end)); |
| ap_set_content_length(r, range_end - range_start + 1); |
| } |
| |
| if (r->header_only) { |
| ap_send_http_header(r); |
| return DONE; |
| } |
| |
| if ((err = (*resource->hooks->open_stream)(resource, mode, |
| &stream)) != NULL) { |
| /* ### assuming FORBIDDEN is probably not quite right... */ |
| err = dav_push_error(r->pool, HTTP_FORBIDDEN, 0, |
| ap_psprintf(r->pool, |
| "Unable to GET contents for %s.", |
| ap_escape_html(r->pool, r->uri)), |
| err); |
| return dav_handle_err(r, err, NULL); |
| } |
| |
| if (has_range |
| && (err = (*resource->hooks->seek_stream)(stream, |
| range_start)) != NULL) { |
| err = dav_push_error(r->pool, err->status, 0, |
| "Could not seek to beginning of the " |
| "specified Content-Range.", err); |
| return dav_handle_err(r, err, NULL); |
| } |
| |
| /* all set. send the headers now. */ |
| ap_send_http_header(r); |
| |
| /* start a timeout for delivering the response. */ |
| ap_soft_timeout("sending GET response", r); |
| |
| buffer = ap_palloc(r->pool, DAV_READ_BLOCKSIZE); |
| while (1) { |
| size_t amt; |
| |
| if (!has_range) |
| amt = DAV_READ_BLOCKSIZE; |
| else if ((range_end - range_start + 1) > DAV_READ_BLOCKSIZE) |
| amt = DAV_READ_BLOCKSIZE; |
| else { |
| /* note: range_end - range_start is an ssize_t */ |
| amt = (size_t)(range_end - range_start + 1); |
| } |
| |
| if ((err = (*resource->hooks->read_stream)(stream, buffer, |
| &amt)) != NULL) { |
| break; |
| } |
| if (amt == 0) { |
| /* no more content */ |
| break; |
| } |
| if (ap_rwrite(buffer, amt, r) < 0) { |
| /* ### what to do with this error? */ |
| break; |
| } |
| |
| if (has_range) { |
| range_start += amt; |
| if (range_start > range_end) |
| break; |
| } |
| |
| /* reset the timeout after a successful write */ |
| ap_reset_timeout(r); |
| } |
| |
| /* Done with the request; clear its timeout */ |
| ap_kill_timeout(r); |
| |
| if (err != NULL) |
| return dav_handle_err(r, err, NULL); |
| |
| /* |
| ** ### range_start should equal range_end+1. if it doesn't, then |
| ** ### we did not send enough data to the client. the client will |
| ** ### hang (and timeout) waiting for the data. |
| ** |
| ** ### what to do? abort the connection? |
| */ |
| return DONE; |
| } |
| |
| /* NOTREACHED */ |
| } |
| |
| /* validate resource on POST, then pass it off to the default handler */ |
| static int dav_method_post(request_rec *r) |
| { |
| dav_resource *resource; |
| dav_error *err; |
| int result; |
| |
| /* Ask repository module to resolve the resource */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) { |
| return result; |
| } |
| |
| /* 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_resource *resource_parent; |
| const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r); |
| const char *body; |
| dav_error *err; |
| dav_error *err2; |
| int result; |
| int resource_existed = 0; |
| int resource_was_writable = 0; |
| int parent_was_writable = 0; |
| dav_stream_mode mode; |
| dav_stream *stream; |
| dav_response *multi_response; |
| int has_range; |
| off_t range_start; |
| off_t range_end; |
| |
| if ((result = ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK)) != OK) { |
| return result; |
| } |
| |
| /* Ask repository module to resolve the resource */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) { |
| return result; |
| } |
| |
| /* If not a file or collection resource, PUT not allowed */ |
| if (resource->type != DAV_RESOURCE_TYPE_REGULAR) { |
| body = ap_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); |
| } |
| |
| /* make sure the resource can be modified (if versioning repository) */ |
| if ((err = dav_ensure_resource_writable(r, resource, |
| 0 /* not parent_only */, |
| &resource_parent, |
| &resource_existed, |
| &resource_was_writable, |
| &parent_was_writable)) != NULL) { |
| /* ### add a higher-level description? */ |
| return dav_handle_err(r, err, NULL); |
| } |
| |
| /* truncate and rewrite the file unless we see a Content-Range */ |
| mode = DAV_MODE_WRITE_TRUNC; |
| |
| has_range = dav_parse_range(r, &range_start, &range_end); |
| if (has_range) { |
| mode = DAV_MODE_WRITE_SEEKABLE; |
| } |
| |
| /* Create the new file in the repository */ |
| if ((err = (*resource->hooks->open_stream)(resource, mode, |
| &stream)) != NULL) { |
| /* ### assuming FORBIDDEN is probably not quite right... */ |
| err = dav_push_error(r->pool, HTTP_FORBIDDEN, 0, |
| ap_psprintf(r->pool, |
| "Unable to PUT new contents for %s.", |
| ap_escape_html(r->pool, r->uri)), |
| err); |
| } |
| |
| if (err == NULL && has_range) { |
| /* a range was provided. seek to the start */ |
| err = (*resource->hooks->seek_stream)(stream, range_start); |
| } |
| |
| if (err == NULL) { |
| if (ap_should_client_block(r)) { |
| char *buffer = ap_palloc(r->pool, DAV_READ_BLOCKSIZE); |
| long len; |
| |
| /* |
| ** Once we start reading the request, then we must read the |
| ** whole darn thing. ap_discard_request_body() won't do anything |
| ** for a partially-read request. |
| */ |
| |
| while ((len = ap_get_client_block(r, buffer, |
| DAV_READ_BLOCKSIZE)) > 0) { |
| if (err == NULL) { |
| /* write whatever we read, until we see an error */ |
| err = (*resource->hooks->write_stream)(stream, |
| buffer, len); |
| } |
| } |
| |
| /* |
| ** ### what happens if we read more/less than the amount |
| ** ### specified in the Content-Range? eek... |
| */ |
| |
| if (len == -1) { |
| /* |
| ** Error reading request body. This has precedence over |
| ** prior errors. |
| */ |
| err = dav_new_error(r->pool, HTTP_BAD_REQUEST, 0, |
| "An error occurred while reading the " |
| "request body."); |
| } |
| } |
| |
| err2 = (*resource->hooks->close_stream)(stream, |
| err == NULL /* commit */); |
| if (err2 != NULL && err == NULL) { |
| /* no error during the write, but we hit one at close. use it. */ |
| 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_revert_resource_writability(r, resource, resource_parent, |
| err != NULL /* undo if error */, |
| resource_existed, |
| resource_was_writable, |
| parent_was_writable); |
| |
| /* check for errors now */ |
| if (err != NULL) { |
| return dav_handle_err(r, err, NULL); |
| } |
| if (err2 != NULL) { |
| /* just log a warning */ |
| err2 = dav_push_error(r->pool, err->status, 0, |
| "The PUT was successful, but there " |
| "was a problem reverting the writability of " |
| "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", resource_existed); |
| } |
| |
| /* ### move this to dav_util? */ |
| void dav_add_response(dav_walker_ctx *ctx, const char *href, int status, |
| dav_get_props_result *propstats) |
| { |
| dav_response *resp; |
| |
| /* just drop some data into an dav_response */ |
| resp = ap_pcalloc(ctx->pool, sizeof(*resp)); |
| resp->href = ap_pstrdup(ctx->pool, href); |
| resp->status = status; |
| if (propstats) { |
| resp->propresult = *propstats; |
| } |
| resp->next = ctx->response; |
| |
| ctx->response = resp; |
| } |
| |
| /* handle the DELETE method */ |
| static int dav_method_delete(request_rec *r) |
| { |
| dav_resource *resource; |
| dav_resource *resource_parent = NULL; |
| dav_error *err; |
| dav_error *err2; |
| dav_response *multi_response; |
| const char *body; |
| int result; |
| int depth; |
| int parent_was_writable = 0; |
| |
| /* 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 */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| 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 | APLOG_NOERRNO, r, |
| "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 | APLOG_NOERRNO, r, |
| "Depth of \"1\" is not allowed for DELETE."); |
| return HTTP_BAD_REQUEST; |
| } |
| |
| /* Check for valid resource type */ |
| /* ### allow DAV_RESOURCE_TYPE_REVISION with All-Bindings header */ |
| if (resource->type != DAV_RESOURCE_TYPE_REGULAR && |
| resource->type != DAV_RESOURCE_TYPE_WORKSPACE) { |
| body = ap_psprintf(r->pool, |
| "Cannot delete resource %s.", |
| ap_escape_html(r->pool, r->uri)); |
| return dav_error_response(r, HTTP_CONFLICT, body); |
| } |
| |
| /* |
| ** 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, |
| ap_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_ensure_resource_writable(r, resource, 1 /* parent_only */, |
| &resource_parent, |
| NULL, NULL, |
| &parent_was_writable)) != 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_revert_resource_writability(r, NULL, resource_parent, |
| err != NULL /* undo if error */, |
| 0, 0, parent_was_writable); |
| |
| /* check for errors now */ |
| if (err != NULL) { |
| err = dav_push_error(r->pool, err->status, 0, |
| ap_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 reverting the writability of " |
| "its 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; |
| } |
| |
| /* 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); |
| dav_resource *resource; |
| const char *options; |
| const char *dav_level; |
| const char *vsn_level; |
| int result; |
| const dav_dir_conf *conf; |
| const dav_dyn_hooks *lp; |
| |
| /* per HTTP/1.1 S9.2, we can discard this body */ |
| if ((result = ap_discard_request_body(r)) != OK) { |
| return result; |
| } |
| |
| /* no body */ |
| ap_set_content_length(r, 0); |
| |
| /* resolve the resource */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| |
| /* determine which providers are available */ |
| dav_level = "1"; |
| vsn_level = NULL; |
| |
| if (locks_hooks != NULL) { |
| dav_level = "1,2"; |
| } |
| if (vsn_hooks != NULL) { |
| vsn_level = (*vsn_hooks->get_vsn_header)(); |
| } |
| |
| /* |
| ** Iterate through the live property providers; add their URIs to |
| ** the dav_level string. |
| */ |
| conf = (dav_dir_conf *) ap_get_module_config(r->per_dir_config, |
| &dav_module); |
| for (lp = conf->liveprop; lp != NULL; lp = lp->next) { |
| const char *uri = DAV_AS_HOOKS_LIVEPROP(lp)->propset_uri; |
| |
| if (uri != NULL) |
| dav_level = ap_pstrcat(r->pool, dav_level, ",<", uri, ">", NULL); |
| } |
| |
| /* this tells MSFT products to skip looking for FrontPage extensions */ |
| ap_table_setn(r->headers_out, "MS-Author-Via", "DAV"); |
| |
| /* |
| ** Three cases: resource is null (3), is lock-null (7.4), or exists. |
| ** |
| ** All cases support OPTIONS and LOCK. |
| ** (Lock-) null resources also support MKCOL and PUT. |
| ** Lock-null support PROPFIND and UNLOCK. |
| ** Existing resources support lots of stuff. |
| */ |
| |
| /* ### take into account resource type */ |
| switch (dav_get_resource_state(r, resource)) |
| { |
| case DAV_RESOURCE_EXISTS: |
| /* resource exists */ |
| if (resource->collection) { |
| options = ap_pstrcat(r->pool, |
| "OPTIONS, " |
| "GET, HEAD, POST, DELETE, TRACE, " |
| "PROPFIND, PROPPATCH, COPY, MOVE", |
| locks_hooks != NULL ? ", LOCK, UNLOCK" : "", |
| NULL); |
| } |
| else { |
| /* files also support PUT */ |
| options = ap_pstrcat(r->pool, |
| "OPTIONS, " |
| "GET, HEAD, POST, DELETE, TRACE, " |
| "PROPFIND, PROPPATCH, COPY, MOVE, PUT", |
| locks_hooks != NULL ? ", LOCK, UNLOCK" : "", |
| NULL); |
| } |
| break; |
| |
| case DAV_RESOURCE_LOCK_NULL: |
| /* resource is lock-null. */ |
| options = ap_pstrcat(r->pool, "OPTIONS, MKCOL, PUT, PROPFIND", |
| locks_hooks != NULL ? ", LOCK, UNLOCK" : "", |
| NULL); |
| break; |
| |
| case DAV_RESOURCE_NULL: |
| /* resource is null. */ |
| options = ap_pstrcat(r->pool, "OPTIONS, MKCOL, PUT", |
| locks_hooks != NULL ? ", LOCK" : "", |
| NULL); |
| break; |
| |
| default: |
| /* ### internal error! */ |
| options = "OPTIONS"; |
| break; |
| } |
| |
| /* If there is a versioning provider, add versioning options */ |
| if (vsn_hooks != NULL) { |
| const char *vsn_options = NULL; |
| |
| /* ### take into account resource type */ |
| if (!resource->exists) { |
| if ((*vsn_hooks->versionable)(resource)) |
| vsn_options = ", MKRESOURCE"; |
| } |
| else if (!resource->versioned) { |
| if ((*vsn_hooks->versionable)(resource)) |
| vsn_options = ", CHECKIN"; |
| } |
| else if (resource->working) |
| vsn_options = ", CHECKIN, UNCHECKOUT"; |
| else |
| vsn_options = ", CHECKOUT"; |
| |
| if (vsn_options != NULL) |
| options = ap_pstrcat(r->pool, options, vsn_options, NULL); |
| } |
| |
| ap_table_setn(r->headers_out, "Allow", options); |
| ap_table_setn(r->headers_out, "DAV", dav_level); |
| |
| if (vsn_level != NULL) |
| ap_table_setn(r->headers_out, "Versioning", vsn_level); |
| |
| /* ### this will send a Content-Type. the default OPTIONS does not. */ |
| ap_send_http_header(r); |
| |
| /* ### 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. */ |
| |
| /* we've sent everything necessary to the client. */ |
| return DONE; |
| } |
| |
| static void dav_cache_badprops(dav_walker_ctx *ctx) |
| { |
| const dav_xml_elem *elem; |
| dav_text_header hdr = { 0 }; |
| |
| /* just return if we built the thing already */ |
| if (ctx->propstat_404 != NULL) { |
| return; |
| } |
| |
| dav_text_append(ctx->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) { |
| dav_text_append(ctx->pool, &hdr, dav_empty_elem(ctx->pool, elem)); |
| } |
| |
| dav_text_append(ctx->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_walker_ctx *ctx, int calltype) |
| { |
| dav_error *err; |
| dav_propdb *propdb; |
| dav_get_props_result propstats = { 0 }; |
| |
| /* |
| ** 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_open_propdb(ctx->r, ctx->lockdb, |
| (dav_resource *)ctx->resource, 1, |
| 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_add_response(ctx, ctx->uri.buf, 0, &badprops); |
| } |
| else { |
| /* no props on this collection/resource */ |
| dav_add_response(ctx, ctx->uri.buf, HTTP_OK, NULL); |
| } |
| 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 { |
| propstats = dav_get_allprops(propdb, |
| ctx->propfind_type == DAV_PROPFIND_IS_ALLPROP); |
| } |
| dav_close_propdb(propdb); |
| |
| dav_add_response(ctx, ctx->uri.buf, 0, &propstats); |
| |
| return NULL; |
| } |
| |
| /* handle the PROPFIND method */ |
| static int dav_method_propfind(request_rec *r) |
| { |
| dav_resource *resource; |
| int depth; |
| dav_error *err; |
| int result; |
| dav_xml_doc *doc; |
| const dav_xml_elem *child; |
| dav_walker_ctx ctx = { 0 }; |
| |
| /* Ask repository module to resolve the resource */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| |
| 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) { |
| 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, |
| ap_psprintf(r->pool, |
| "PROPFIND requests with a " |
| "Depth of \"infinity\" are " |
| "not allowed for %s.", |
| ap_escape_html(r->pool, |
| r->uri))); |
| } |
| } |
| |
| if ((result = dav_parse_input(r, &doc)) != OK) { |
| return result; |
| } |
| /* note: doc == NULL if no request body */ |
| |
| if (doc && !dav_validate_root(doc, "propfind")) { |
| /* This supplies additional information for the default message. */ |
| ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r, |
| "The \"propfind\" element was not found."); |
| return HTTP_BAD_REQUEST; |
| } |
| |
| /* ### validate that only one of these three elements is present */ |
| |
| if (doc == NULL |
| || (child = dav_find_child(doc->root, "allprop")) != NULL) { |
| /* note: no request body implies allprop */ |
| ctx.propfind_type = DAV_PROPFIND_IS_ALLPROP; |
| } |
| else if ((child = dav_find_child(doc->root, "propname")) != NULL) { |
| ctx.propfind_type = DAV_PROPFIND_IS_PROPNAME; |
| } |
| else if ((child = 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 | APLOG_NOERRNO, r, |
| "The \"propfind\" element does not contain one of " |
| "the required child elements (the specific command)."); |
| return HTTP_BAD_REQUEST; |
| } |
| |
| ctx.walk_type = DAV_WALKTYPE_ALL | DAV_WALKTYPE_AUTH; |
| ctx.func = dav_propfind_walker; |
| ctx.pool = r->pool; |
| ctx.doc = doc; |
| ctx.r = r; |
| ctx.resource = resource; |
| |
| dav_buffer_init(r->pool, &ctx.uri, r->uri); |
| |
| /* ### should open read-only */ |
| if ((err = dav_open_lockdb(r, 0, &ctx.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.lockdb != NULL) { |
| /* if we have a lock database, then we can walk locknull resources */ |
| ctx.walk_type |= DAV_WALKTYPE_LOCKNULL; |
| } |
| |
| err = (*resource->hooks->walk)(&ctx, depth); |
| |
| if (ctx.lockdb != NULL) { |
| (*ctx.lockdb->hooks->close_lockdb)(ctx.lockdb); |
| } |
| |
| if (err != NULL) { |
| /* ### add a higher-level description? */ |
| return dav_handle_err(r, err, NULL); |
| } |
| |
| /* return a 207 (Multi-Status) response now. */ |
| |
| /* 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. */ |
| /* NOTE: propstat_404 != NULL implies doc != NULL */ |
| if (ctx.propstat_404 != NULL) { |
| dav_send_multistatus(r, HTTP_MULTI_STATUS, ctx.response, |
| doc->namespaces); |
| } |
| else { |
| dav_send_multistatus(r, HTTP_MULTI_STATUS, ctx.response, NULL); |
| } |
| |
| /* the response has been sent. */ |
| return DONE; |
| } |
| |
| static dav_text * dav_failed_proppatch(pool *p, array_header *prop_ctx) |
| { |
| dav_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 ) { |
| dav_text_append(p, &hdr, |
| "<D:propstat>" DEBUG_CR |
| "<D:prop>"); |
| dav_text_append(p, &hdr, dav_empty_elem(p, ctx->prop)); |
| dav_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, |
| "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, |
| "Attempted DAV:remove " |
| "operation could not be " |
| "completed due to other " |
| "errors."); |
| ctx->err = err424_delete; |
| } |
| } |
| |
| s = ap_psprintf(p, |
| "<D:status>" |
| "HTTP/1.1 %d (status)" |
| "</D:status>" DEBUG_CR, |
| ctx->err->status); |
| dav_text_append(p, &hdr, s); |
| |
| /* ### we should use compute_desc if necessary... */ |
| if (ctx->err->desc != NULL) { |
| dav_text_append(p, &hdr, "<D:responsedescription>" DEBUG_CR); |
| dav_text_append(p, &hdr, ctx->err->desc); |
| dav_text_append(p, &hdr, "</D:responsedescription>" DEBUG_CR); |
| } |
| |
| dav_text_append(p, &hdr, "</D:propstat>" DEBUG_CR); |
| } |
| |
| return hdr.first; |
| } |
| |
| static dav_text * dav_success_proppatch(pool *p, array_header *prop_ctx) |
| { |
| dav_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. |
| */ |
| |
| dav_text_append(p, &hdr, |
| "<D:propstat>" DEBUG_CR |
| "<D:prop>" DEBUG_CR); |
| |
| for ( ; i-- > 0; ++ctx ) { |
| dav_text_append(p, &hdr, dav_empty_elem(p, ctx->prop)); |
| } |
| |
| dav_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), |
| array_header *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; |
| dav_xml_doc *doc; |
| dav_xml_elem *child; |
| dav_propdb *propdb; |
| int failure = 0; |
| dav_response resp = { 0 }; |
| dav_text *propstat_text; |
| array_header *ctx_list; |
| dav_prop_ctx *ctx; |
| |
| /* Ask repository module to resolve the resource */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| if (!resource->exists) { |
| /* Apache will supply a default error for this. */ |
| return HTTP_NOT_FOUND; |
| } |
| |
| if ((result = dav_parse_input(r, &doc)) != OK) { |
| return result; |
| } |
| /* note: doc == NULL if no request body */ |
| |
| if (doc == NULL || !dav_validate_root(doc, "propertyupdate")) { |
| /* This supplies additional information for the default message. */ |
| ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r, |
| "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); |
| } |
| |
| if ((err = dav_open_propdb(r, NULL, resource, 0, doc->namespaces, |
| &propdb)) != NULL) { |
| err = dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0, |
| ap_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 = ap_make_array(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; |
| dav_xml_elem *prop_group; |
| dav_xml_elem *one_prop; |
| |
| /* Ignore children that are not set/remove */ |
| if (child->ns != DAV_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); |
| |
| /* This supplies additional information for the default message. */ |
| ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r, |
| "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 *)ap_push_array(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); |
| |
| /* 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 = ap_table_get(r->headers_in, "Transfer-Encoding"); |
| const char *lenp = ap_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 (strcasecmp(tenc, "chunked")) { |
| /* Use this instead of Apache's default error string */ |
| ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r, |
| "Unknown Transfer-Encoding %s", tenc); |
| return HTTP_NOT_IMPLEMENTED; |
| } |
| |
| r->read_chunked = 1; |
| } |
| else if (lenp) { |
| const char *pos = lenp; |
| |
| while (ap_isdigit(*pos) || ap_isspace(*pos)) { |
| ++pos; |
| } |
| if (*pos != '\0') { |
| /* This supplies additional information for the default message. */ |
| ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r, |
| "Invalid Content-Length %s", lenp); |
| return HTTP_BAD_REQUEST; |
| } |
| |
| r->remaining = atol(lenp); |
| } |
| |
| 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_resource *resource_parent; |
| const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r); |
| dav_error *err; |
| dav_error *err2; |
| int result; |
| dav_dir_conf *conf; |
| int parent_was_writable = 0; |
| 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; |
| } |
| |
| conf = (dav_dir_conf *) ap_get_module_config(r->per_dir_config, |
| &dav_module); |
| |
| /* Ask repository module to resolve the resource */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| |
| 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_ensure_resource_writable(r, resource, 1 /* parent_only */, |
| &resource_parent, |
| NULL, NULL, |
| &parent_was_writable)) != 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)(r->pool, resource); |
| |
| /* restore modifiability of parent back to what it was */ |
| err2 = dav_revert_resource_writability(r, NULL, resource_parent, |
| err != NULL /* undo if error */, |
| 0, 0, parent_was_writable); |
| |
| /* 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, err->status, 0, |
| "The MKCOL was successful, but there " |
| "was a problem reverting the writability of " |
| "its 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, resource, "Collection", 0); |
| } |
| |
| /* handle the COPY and MOVE methods */ |
| static int dav_method_copymove(request_rec *r, int is_move) |
| { |
| dav_resource *resource; |
| dav_resource *resource_parent = NULL; |
| dav_resource *resnew; |
| dav_resource *resnew_parent = NULL; |
| 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 replaced; |
| int src_parent_was_writable = 0; |
| int dst_parent_was_writable = 0; |
| |
| /* Ask repository module to resolve the resource */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| 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 */ |
| if (resource->type != DAV_RESOURCE_TYPE_REGULAR) { |
| body = ap_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 = ap_table_get(r->headers_in, "Destination"); |
| if (dest == NULL) { |
| /* Look in headers provided by Netscape's Roaming Profiles */ |
| const char *nscp_host = ap_table_get(r->headers_in, "Host"); |
| const char *nscp_path = ap_table_get(r->headers_in, "New-uri"); |
| |
| if (nscp_host != NULL && nscp_path != NULL) |
| dest = ap_psprintf(r->pool, "http://%s%s", nscp_host, nscp_path); |
| } |
| if (dest == NULL) { |
| /* This supplies additional information for the default message. */ |
| ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r, |
| "The request is missing a Destination header."); |
| return HTTP_BAD_REQUEST; |
| } |
| |
| lookup = dav_lookup_uri(dest, r); |
| 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 | APLOG_NOERRNO, r, |
| 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, |
| "Destination URI had an error."); |
| } |
| |
| /* Resolve destination resource */ |
| result = dav_get_resource(lookup.rnew, &resnew); |
| if (result != OK) |
| return result; |
| |
| /* 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 | APLOG_NOERRNO, r, |
| "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 | APLOG_NOERRNO, r, |
| "Depth must be \"infinity\" when moving a collection."); |
| return HTTP_BAD_REQUEST; |
| } |
| |
| /* |
| ** Check If-Headers and existing locks for each resource in the source |
| ** if we are performing a MOVE. 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, since 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 (is_move |
| && (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, |
| ap_psprintf(r->pool, |
| "Could not MOVE %s due to a failed " |
| "precondition on the source " |
| "(e.g. locks).", |
| 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, |
| ap_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); |
| } |
| |
| /* remember whether target resource existed */ |
| replaced = resnew->exists; |
| |
| /* if this is a move, then the source parent collection will be modified */ |
| if (is_move) { |
| if ((err = dav_ensure_resource_writable(r, resource, |
| 1 /* parent_only */, |
| &resource_parent, |
| NULL, NULL, |
| &src_parent_was_writable)) != NULL) { |
| if (lockdb != NULL) |
| (*lockdb->hooks->close_lockdb)(lockdb); |
| |
| /* ### add a higher-level description? */ |
| return dav_handle_err(r, err, NULL); |
| } |
| } |
| |
| /* prepare the destination collection for modification */ |
| if ((err = dav_ensure_resource_writable(r, resnew, 1 /* parent_only */, |
| &resnew_parent, |
| NULL, NULL, |
| &dst_parent_was_writable)) != NULL) { |
| /* could not make destination writable: |
| * if move, restore state of source parent |
| */ |
| if (is_move) { |
| (void) dav_revert_resource_writability(r, NULL, resource_parent, |
| 1 /* undo */, |
| 0, 0, |
| src_parent_was_writable); |
| } |
| |
| 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 object, so status updates to one are reflected |
| * in the other. |
| */ |
| if (resource_parent != NULL |
| && (*resource_parent->hooks->is_same_resource)(resource_parent, |
| resnew_parent)) |
| resnew_parent = resource_parent; |
| |
| /* New resource will be same kind as source */ |
| resnew->collection = resource->collection; |
| |
| /* If target exists, remove it first (we know Ovewrite must be TRUE). |
| * Then try to copy/move the resource. |
| */ |
| if (resnew->exists) |
| 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); |
| } |
| |
| /* restore parent collection states */ |
| err2 = dav_revert_resource_writability(r, NULL, resnew_parent, |
| err != NULL /* undo if error */, |
| 0, 0, dst_parent_was_writable); |
| |
| if (is_move) { |
| err3 = dav_revert_resource_writability(r, NULL, resource_parent, |
| err != NULL /* undo if error */, |
| 0, 0, src_parent_was_writable); |
| } |
| 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, |
| ap_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 reverting writability */ |
| 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 reverting the writability of 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 reverting the writability of the " |
| "destination parent collection.", |
| err3); |
| dav_log_err(r, err, APLOG_WARNING); |
| } |
| |
| /* propagate any indirect locks at the target */ |
| if (lockdb != NULL) { |
| int resource_state = dav_get_resource_state(lookup.rnew, resnew); |
| |
| /* notify lock system that we have created/replaced a resource */ |
| err = dav_notify_created(r, lockdb, resnew, resource_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, resnew, "Destination", replaced); |
| } |
| |
| /* 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; |
| const dav_hooks_locks *locks_hooks; |
| int result; |
| int depth; |
| int new_lock_request = 0; |
| dav_xml_doc *doc = NULL; |
| 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 = dav_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 | APLOG_NOERRNO, r, |
| "Depth must be 0 or \"infinity\" for LOCK."); |
| return HTTP_BAD_REQUEST; |
| } |
| |
| /* Ask repository module to resolve the resource */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| |
| /* |
| ** 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 = ap_pstrdup(r->pool, r->connection->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, |
| ap_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, |
| ap_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 = ap_pstrcat(r->pool, "<", |
| (*locks_hooks->format_locktoken)(r->pool, lock->locktoken), |
| ">", NULL); |
| |
| ap_table_set(r->headers_out, "Lock-Token", locktoken_txt); |
| } |
| |
| (*locks_hooks->close_lockdb)(lockdb); |
| |
| r->status = HTTP_OK; |
| r->content_type = DAV_XML_CONTENT_TYPE; |
| |
| ap_send_http_header(r); |
| ap_soft_timeout("send LOCK response", r); |
| |
| 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); |
| |
| ap_kill_timeout(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 = ap_table_get(r->headers_in, "Lock-Token")) == NULL) { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, r, |
| "Unlock failed (%s): No Lock-Token specified in header", r->filename); |
| return HTTP_BAD_REQUEST; |
| } |
| |
| locktoken_txt = ap_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, |
| ap_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 */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| |
| 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 infinte lock. Thus, an UNLOCK |
| * on any part of an infinte 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; |
| } |
| |
| /* handle the SEARCH method from DASL */ |
| static int dav_method_search(request_rec *r) |
| { |
| /* ### we know this method, but we won't allow it yet */ |
| /* Apache will supply a default error for this. */ |
| return HTTP_METHOD_NOT_ALLOWED; |
| |
| /* Do some error checking, like if the querygrammar is |
| * supported by the content type, and then pass the |
| * request on to the appropriate query module. |
| */ |
| } |
| |
| /* handle the CHECKOUT method */ |
| static int dav_method_checkout(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; |
| |
| /* ### eventually check body for DAV:checkin-policy */ |
| if ((result = ap_discard_request_body(r)) != OK) { |
| return result; |
| } |
| |
| /* Ask repository module to resolve the resource */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| 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) { |
| 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)) != NULL) { |
| err = dav_push_error(r->pool, HTTP_CONFLICT, 0, |
| ap_psprintf(r->pool, |
| "Could not CHECKOUT 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); |
| ap_send_http_header(r); |
| |
| return DONE; |
| } |
| |
| /* 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 */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| 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, |
| ap_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); |
| ap_send_http_header(r); |
| |
| return DONE; |
| } |
| |
| /* handle the CHECKIN method */ |
| static int dav_method_checkin(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 */ |
| result = dav_get_resource(r, &resource); |
| if (result != OK) |
| return result; |
| 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 to the workspace."); |
| } |
| |
| /* ### do lock checks, once behavior is defined */ |
| |
| /* Do the checkin */ |
| if ((err = (*vsn_hooks->checkin)(resource)) != NULL) { |
| err = dav_push_error(r->pool, HTTP_CONFLICT, 0, |
| ap_psprintf(r->pool, |
| "Could not CHECKIN 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); |
| ap_send_http_header(r); |
| |
| return DONE; |
| } |
| |
| |
| /* |
| * Response handler for DAV resources |
| */ |
| static int dav_handler(request_rec *r) |
| { |
| dav_dir_conf *conf; |
| |
| /* quickly ignore any HTTP/0.9 requests */ |
| if (r->assbackwards) { |
| return DECLINED; |
| } |
| |
| /* ### do we need to do anything with r->proxyreq ?? */ |
| |
| conf = (dav_dir_conf *) ap_get_module_config(r->per_dir_config, |
| &dav_module); |
| |
| /* |
| * 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 |
| | (1 << M_GET) |
| | (1 << M_PUT) |
| | (1 << M_DELETE) |
| | (1 << M_OPTIONS) |
| | (1 << M_INVALID); |
| /* |
| * These are the DAV methods we handle. |
| */ |
| r->allowed |= 0 |
| | (1 << M_COPY) |
| | (1 << M_LOCK) |
| | (1 << M_UNLOCK) |
| | (1 << M_MKCOL) |
| | (1 << M_MOVE) |
| | (1 << M_PROPFIND) |
| | (1 << 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 |
| | (1 << 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) */ |
| /* ### more work necessary, now that we have M_foo for DAV methods */ |
| |
| /* 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); |
| } |
| |
| /* |
| * NOTE: When Apache moves creates defines for the add'l DAV methods, |
| * then it will no longer use M_INVALID. This code must be |
| * updated each time Apache adds method defines. |
| */ |
| if (r->method_number != M_INVALID) { |
| return DECLINED; |
| } |
| |
| if (!strcmp(r->method, "SEARCH")) { |
| return dav_method_search(r); |
| } |
| |
| if (!strcmp(r->method, "CHECKOUT")) { |
| return dav_method_checkout(r); |
| } |
| |
| if (!strcmp(r->method, "UNCHECKOUT")) { |
| return dav_method_uncheckout(r); |
| } |
| |
| if (!strcmp(r->method, "CHECKIN")) { |
| return dav_method_checkin(r); |
| } |
| |
| #if 0 |
| if (!strcmp(r->method, "MKRESOURCE")) { |
| return dav_method_mkresource(r); |
| } |
| |
| if (!strcmp(r->method, "REPORT")) { |
| return dav_method_report(r); |
| } |
| #endif |
| |
| /* ### add'l methods for Advanced Collections, ACLs, DASL */ |
| |
| return DECLINED; |
| } |
| |
| static int dav_type_checker(request_rec *r) |
| { |
| dav_dir_conf *conf; |
| |
| 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->enabled != DAV_ENABLED_ON) { |
| return DECLINED; |
| } |
| |
| 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->handle_get) |
| return DECLINED; |
| } |
| |
| /* ### we should (instead) trap the ones that we DO understand */ |
| /* ### the handler DOES handle POST, so we need to fix one of these */ |
| if (r->method_number != M_POST) { |
| |
| /* |
| ** ### 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?) |
| ** |
| ** ### I think the OK stops running type-checkers. need to look. |
| */ |
| r->handler = "dav-handler"; |
| return OK; |
| } |
| |
| return DECLINED; |
| } |
| |
| |
| /*--------------------------------------------------------------------------- |
| ** |
| ** Configuration info for the module |
| */ |
| |
| static const command_rec dav_cmds[] = |
| { |
| { |
| "DAV", |
| dav_cmd_dav, |
| NULL, |
| ACCESS_CONF, /* per directory/location */ |
| FLAG, |
| "turn DAV on/off for a directory or location" |
| }, |
| { |
| "DAVLockDB", |
| dav_cmd_davlockdb, |
| NULL, |
| RSRC_CONF, /* per server */ |
| TAKE1, |
| "specify a lock database" |
| }, |
| { |
| "DAVMinTimeout", |
| dav_cmd_davmintimeout, |
| NULL, |
| ACCESS_CONF|RSRC_CONF, /* per directory/location, or per server */ |
| TAKE1, |
| "specify minimum allowed timeout" |
| }, |
| { |
| "DAVDepthInfinity", |
| dav_cmd_davdepthinfinity, |
| NULL, |
| ACCESS_CONF|RSRC_CONF, /* per directory/location, or per server */ |
| FLAG, |
| "allow Depth infinity PROPFIND requests" |
| }, |
| { |
| "DAVParam", |
| dav_cmd_davparam, |
| NULL, |
| ACCESS_CONF|RSRC_CONF, /* per directory/location, or per server */ |
| TAKE2, |
| "DAVParam <parameter name> <parameter value>" |
| }, |
| { |
| "LimitXMLRequestBody", |
| dav_cmd_limitxmlrequestbody, |
| NULL, |
| ACCESS_CONF|RSRC_CONF, /* per directory/location, or per server */ |
| TAKE1, |
| "Limit (in bytes) on maximum size of an XML-based request body" |
| }, |
| { NULL } |
| }; |
| |
| static const handler_rec dav_handlers[] = |
| { |
| {"dav-handler", dav_handler}, |
| { NULL } |
| }; |
| |
| module MODULE_VAR_EXPORT dav_module = |
| { |
| STANDARD_MODULE_STUFF, |
| dav_init_handler, /* initializer */ |
| 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 */ |
| dav_handlers, /* handlers */ |
| NULL, /* filename translation */ |
| NULL, /* check_user_id */ |
| NULL, /* check auth */ |
| NULL, /* check access */ |
| dav_type_checker, /* type_checker */ |
| NULL, /* fixups */ |
| NULL, /* logger */ |
| NULL, /* header parser */ |
| NULL, /* child_init */ |
| NULL, /* child_exit */ |
| NULL /* post read-request */ |
| }; |