blob: 63ae2c880ac595152c21a0bb3dc5f3dbbf3586ca [file] [log] [blame]
/*
** 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.*
** - various utilities, repository-independent
**
** Written by Greg Stein, gstein@lyra.org, http://www.lyra.org/
*/
#include "mod_dav.h"
#include "http_request.h"
#include "http_config.h"
#include "http_vhost.h"
#include "http_log.h"
#include "http_protocol.h"
dav_error *dav_new_error(pool *p, int status, int error_id, const char *desc)
{
int save_errno = errno;
dav_error *err = ap_pcalloc(p, sizeof(*err));
/* DBG3("dav_new_error: %d %d %s", status, error_id, desc ? desc : "(no desc)"); */
err->status = status;
err->error_id = error_id;
err->desc = desc;
err->save_errno = save_errno;
return err;
}
dav_error *dav_push_error(pool *p, int status, int error_id, const char *desc,
dav_error *prev)
{
dav_error *err = ap_pcalloc(p, sizeof(*err));
err->status = status;
err->error_id = error_id;
err->desc = desc;
err->prev = prev;
return err;
}
void dav_text_append(pool * p, dav_text_header *hdr, const char *text)
{
dav_text *t = ap_palloc(p, sizeof(*t));
t->text = text;
t->next = NULL;
if (hdr->first == NULL) {
/* no text elements yet */
hdr->first = hdr->last = t;
}
else {
/* append to the last text element */
hdr->last->next = t;
hdr->last = t;
}
}
void dav_check_bufsize(pool * p, dav_buffer *pbuf, size_t extra_needed)
{
/* grow the buffer if necessary */
if (pbuf->cur_len + extra_needed > pbuf->alloc_len) {
char *newbuf;
pbuf->alloc_len += extra_needed + DAV_BUFFER_PAD;
newbuf = ap_palloc(p, pbuf->alloc_len);
memcpy(newbuf, pbuf->buf, pbuf->cur_len);
pbuf->buf = newbuf;
}
}
void dav_set_bufsize(pool * p, dav_buffer *pbuf, size_t size)
{
/* NOTE: this does not retain prior contents */
/* NOTE: this function is used to init the first pointer, too, since
the PAD will be larger than alloc_len (0) for zeroed structures */
/* grow if we don't have enough for the requested size plus padding */
if (size + DAV_BUFFER_PAD > pbuf->alloc_len) {
/* set the new length; min of MINSIZE */
pbuf->alloc_len = size + DAV_BUFFER_PAD;
if (pbuf->alloc_len < DAV_BUFFER_MINSIZE)
pbuf->alloc_len = DAV_BUFFER_MINSIZE;
pbuf->buf = ap_palloc(p, pbuf->alloc_len);
}
pbuf->cur_len = size;
}
/* initialize a buffer and copy the specified (null-term'd) string into it */
void dav_buffer_init(pool *p, dav_buffer *pbuf, const char *str)
{
dav_set_bufsize(p, pbuf, strlen(str));
memcpy(pbuf->buf, str, pbuf->cur_len + 1);
}
/* append a string to the end of the buffer, adjust length */
void dav_buffer_append(pool *p, dav_buffer *pbuf, const char *str)
{
size_t len = strlen(str);
dav_check_bufsize(p, pbuf, len + 1);
memcpy(pbuf->buf + pbuf->cur_len, str, len + 1);
pbuf->cur_len += len;
}
/* place a string on the end of the buffer, do NOT adjust length */
void dav_buffer_place(pool *p, dav_buffer *pbuf, const char *str)
{
size_t len = strlen(str);
dav_check_bufsize(p, pbuf, len + 1);
memcpy(pbuf->buf + pbuf->cur_len, str, len + 1);
}
/* place some memory on the end of a buffer; do NOT adjust length */
void dav_buffer_place_mem(pool *p, dav_buffer *pbuf, const void *mem,
size_t amt, size_t pad)
{
dav_check_bufsize(p, pbuf, amt + pad);
memcpy(pbuf->buf + pbuf->cur_len, mem, amt);
}
#if APACHE_RELEASE == 10304100
/* ### this code can be used for 1.3.4 installations. use this function
* ### instead of ap_sub_req_method_uri()
*/
/*
* ### don't look at this code.
* ### it is a Crime Against All That is Right and Good
*/
#include "http_core.h" /* for SATISFY_* */
static request_rec *gross_hack(const char *new_file, const request_rec * r)
{
request_rec *rnew = ap_sub_req_lookup_uri(new_file, r);
int res;
/* ### these aren't exported properly from the headers */
extern int ap_check_access(request_rec *); /* check access on non-auth basis */
extern int ap_check_user_id(request_rec *); /* obtain valid username from client auth */
extern int ap_check_auth(request_rec *); /* check (validated) user is authorized here */
if (rnew->status != HTTP_OK)
return rnew;
/* re-run portions with a modified method */
rnew->method = r->method;
rnew->method_number = r->method_number;
if ((ap_satisfies(rnew) == SATISFY_ALL
|| ap_satisfies(rnew) == SATISFY_NOSPEC)
? ((res = ap_check_access(rnew))
|| (ap_some_auth_required(rnew)
&& ((res = ap_check_user_id(rnew))
|| (res = ap_check_auth(rnew)))))
: ((res = ap_check_access(rnew))
&& (!ap_some_auth_required(rnew)
|| ((res = ap_check_user_id(rnew))
|| (res = ap_check_auth(rnew)))))
) {
rnew->status = res;
}
return rnew;
}
#endif /* APACHE_RELEASE == 10304100 */
/*
** dav_lookup_uri()
**
** Extension for ap_sub_req_lookup_uri() which can't handle absolute
** URIs properly.
**
** If NULL is returned, then an error occurred with parsing the URI or
** the URI does not match the current server.
*/
dav_lookup_result dav_lookup_uri(const char *uri, request_rec * r)
{
dav_lookup_result result = { 0 };
const char *scheme;
unsigned short port = ntohs(r->connection->local_addr.sin_port);
uri_components comp;
char *new_file;
const char *domain;
/* first thing to do is parse the URI into various components */
if (ap_parse_uri_components(r->pool, uri, &comp) != HTTP_OK) {
result.err.status = HTTP_BAD_REQUEST;
result.err.desc = "Invalid syntax in Destination URI.";
return result;
}
/* the URI must be an absoluteURI (WEBDAV S9.3) */
if (comp.scheme == NULL) {
result.err.status = HTTP_BAD_REQUEST;
result.err.desc = "Destination URI must be an absolute URI.";
return result;
}
/* ### not sure this works if the current request came in via https: */
scheme = r->parsed_uri.scheme;
if (scheme == NULL)
scheme = ap_http_method(r);
/* insert a port if the URI did not contain one */
if (comp.port == 0)
comp.port = ap_default_port_for_scheme(comp.scheme);
/* now, verify that the URI uses the same scheme as the current request.
the port, must match our port.
the URI must not have a query (args) or a fragment
*/
if (strcasecmp(comp.scheme, scheme) != 0 ||
comp.port != port) {
result.err.status = HTTP_BAD_GATEWAY;
result.err.desc = ap_psprintf(r->pool,
"Destination URI refers to different "
"scheme or port (%s://hostname:%d)\n"
"(want: %s://hostname:%d)",
comp.scheme ? comp.scheme : scheme,
comp.port ? comp.port : port,
scheme, port);
return result;
}
if (comp.query != NULL || comp.fragment != NULL) {
result.err.status = HTTP_BAD_REQUEST;
result.err.desc =
"Destination URI contains invalid components "
"(a query or a fragment).";
return result;
}
/* we have verified the scheme, port, and general structure */
/*
** Hrm. IE5 will pass unqualified hostnames for both the
** Host: and Destination: headers. This breaks the
** http_vhost.c::matches_aliases function.
**
** For now, qualify unqualified comp.hostnames with
** r->server->server_hostname.
**
** ### this is a big hack. Apache should provide a better way.
** ### maybe the admin should list the unqualified hosts in a
** ### <ServerAlias> block?
*/
if (strrchr(comp.hostname, '.') == NULL &&
(domain = strchr(r->server->server_hostname, '.')) != NULL) {
comp.hostname = ap_pstrcat(r->pool, comp.hostname, domain, NULL);
}
/* now, if a hostname was provided, then verify that it represents the
same server as the current connection. note that we just use our
port, since we've verified the URI matches ours */
if (comp.hostname != NULL &&
!ap_matches_request_vhost(r, comp.hostname, port)) {
result.err.status = HTTP_BAD_GATEWAY;
result.err.desc = "Destination URI refers to a different server.";
return result;
}
/* we have verified that the requested URI denotes the same server as
the current request. Therefore, we can use ap_sub_req_lookup_uri() */
/* reconstruct a URI as just the path */
new_file = ap_unparse_uri_components(r->pool, &comp, UNP_OMITSITEPART);
/*
* Lookup the URI and return the sub-request. Note that we use the
* same HTTP method on the destination. This allows the destination
* to apply appropriate restrictions (e.g. readonly).
*/
#if APACHE_RELEASE == 10304100
result.rnew = gross_hack(new_file, r);
#else
result.rnew = ap_sub_req_method_uri(r->method, new_file, r);
#endif
return result;
}
/* ---------------------------------------------------------------
**
** XML UTILITY FUNCTIONS
*/
/* validate that the root element uses a given DAV: tagname (TRUE==valid) */
int dav_validate_root(const dav_xml_doc *doc, const char *tagname)
{
return doc->root &&
doc->root->ns == DAV_NS_DAV_ID &&
strcmp(doc->root->name, tagname) == 0;
}
/* find and return the (unique) child with a given DAV: tagname */
dav_xml_elem *dav_find_child(const dav_xml_elem *elem, const char *tagname)
{
dav_xml_elem *child = elem->first_child;
for (; child; child = child->next)
if (child->ns == DAV_NS_DAV_ID && !strcmp(child->name, tagname))
return child;
return NULL;
}
/* how many characters for the given integer? */
#define DAV_NS_LEN(ns) ((ns) < 10 ? 1 : (ns) < 100 ? 2 : (ns) < 1000 ? 3 : \
(ns) < 10000 ? 4 : (ns) < 100000 ? 5 : \
(ns) < 1000000 ? 6 : (ns) < 10000000 ? 7 : \
(ns) < 100000000 ? 8 : (ns) < 1000000000 ? 9 : 10)
static int dav_text_size(const dav_text *t)
{
int size = 0;
for (; t; t = t->next)
size += strlen(t->text);
return size;
}
static size_t dav_elem_size(const dav_xml_elem *elem, int style,
array_header *namespaces, int *ns_map)
{
size_t size;
if (style == DAV_X2T_FULL || style == DAV_X2T_FULL_NS_LANG) {
const dav_xml_attr *attr;
size = 0;
if (style == DAV_X2T_FULL_NS_LANG) {
int i;
/*
** The outer element will contain xmlns:ns%d="%s" attributes
** and an xml:lang attribute, if applicable.
*/
for (i = namespaces->nelts; i--;) {
/* compute size of: ' xmlns:ns%d="%s"' */
size += (9 + DAV_NS_LEN(i) + 2 +
strlen(DAV_GET_URI_ITEM(namespaces, i)) + 1);
}
if (elem->lang != NULL) {
/* compute size of: ' xml:lang="%s"' */
size += 11 + strlen(elem->lang) + 1;
}
}
if (elem->ns == DAV_NS_NONE) {
/* compute size of: <%s> */
size += 1 + strlen(elem->name) + 1;
}
else {
int ns = ns_map ? ns_map[elem->ns] : elem->ns;
/* compute size of: <ns%d:%s> */
size += 3 + DAV_NS_LEN(ns) + 1 + strlen(elem->name) + 1;
}
if (DAV_ELEM_IS_EMPTY(elem)) {
/* insert a closing "/" */
size += 1;
}
else {
/*
* two of above plus "/":
* <ns%d:%s> ... </ns%d:%s>
* OR <%s> ... </%s>
*/
size = 2 * size + 1;
}
for (attr = elem->attr; attr; attr = attr->next) {
if (attr->ns == DAV_NS_NONE) {
/* compute size of: ' %s="%s"' */
size += 1 + strlen(attr->name) + 2 + strlen(attr->value) + 1;
}
else {
/* compute size of: ' ns%d:%s="%s"' */
size += 3 + DAV_NS_LEN(attr->ns) + 1 + strlen(attr->name) + 2 + strlen(attr->value) + 1;
}
}
/*
** If the element has an xml:lang value that is *different* from
** its parent, then add the thing in: ' xml:lang="%s"'.
**
** NOTE: we take advantage of the pointer equality established by
** the parsing for "inheriting" the xml:lang values from parents.
*/
if (elem->lang != NULL &&
(elem->parent == NULL || elem->lang != elem->parent->lang)) {
size += 11 + strlen(elem->lang) + 1;
}
}
else if (style == DAV_X2T_LANG_INNER) {
/*
* This style prepends the xml:lang value plus a null terminator.
* If a lang value is not present, then we insert a null term.
*/
size = elem->lang ? strlen(elem->lang) + 1 : 1;
}
else
size = 0;
size += dav_text_size(elem->first_cdata.first);
for (elem = elem->first_child; elem; elem = elem->next) {
/* the size of the child element plus the CDATA that follows it */
size += (dav_elem_size(elem, DAV_X2T_FULL, NULL, ns_map) +
dav_text_size(elem->following_cdata.first));
}
return size;
}
static char *dav_write_text(char *s, const dav_text *t)
{
for (; t; t = t->next) {
size_t len = strlen(t->text);
memcpy(s, t->text, len);
s += len;
}
return s;
}
static char *dav_write_elem(char *s, const dav_xml_elem *elem, int style,
array_header *namespaces, int *ns_map)
{
const dav_xml_elem *child;
size_t len;
int ns;
if (style == DAV_X2T_FULL || style == DAV_X2T_FULL_NS_LANG) {
int empty = DAV_ELEM_IS_EMPTY(elem);
const dav_xml_attr *attr;
if (elem->ns == DAV_NS_NONE) {
len = sprintf(s, "<%s", elem->name);
}
else {
ns = ns_map ? ns_map[elem->ns] : elem->ns;
len = sprintf(s, "<ns%d:%s", ns, elem->name);
}
s += len;
for (attr = elem->attr; attr; attr = attr->next) {
if (attr->ns == DAV_NS_NONE)
len = sprintf(s, " %s=\"%s\"", attr->name, attr->value);
else
len = sprintf(s, " ns%d:%s=\"%s\"", attr->ns, attr->name, attr->value);
s += len;
}
/* add the xml:lang value if necessary */
if (elem->lang != NULL &&
(style == DAV_X2T_FULL_NS_LANG ||
elem->parent == NULL ||
elem->lang != elem->parent->lang)) {
len = sprintf(s, " xml:lang=\"%s\"", elem->lang);
s += len;
}
/* add namespace definitions, if required */
if (style == DAV_X2T_FULL_NS_LANG) {
int i;
for (i = namespaces->nelts; i--;) {
len = sprintf(s, " xmlns:ns%d=\"%s\"", i,
DAV_GET_URI_ITEM(namespaces, i));
s += len;
}
}
/* no more to do. close it up and go. */
if (empty) {
*s++ = '/';
*s++ = '>';
return s;
}
/* just close it */
*s++ = '>';
}
else if (style == DAV_X2T_LANG_INNER) {
/* prepend the xml:lang value */
if (elem->lang != NULL) {
len = strlen(elem->lang);
memcpy(s, elem->lang, len);
s += len;
}
*s++ = '\0';
}
s = dav_write_text(s, elem->first_cdata.first);
for (child = elem->first_child; child; child = child->next) {
s = dav_write_elem(s, child, DAV_X2T_FULL, NULL, ns_map);
s = dav_write_text(s, child->following_cdata.first);
}
if (style == DAV_X2T_FULL || style == DAV_X2T_FULL_NS_LANG) {
if (elem->ns == DAV_NS_NONE) {
len = sprintf(s, "</%s>", elem->name);
}
else {
ns = ns_map ? ns_map[elem->ns] : elem->ns;
len = sprintf(s, "</ns%d:%s>", ns, elem->name);
}
s += len;
}
return s;
}
/* convert an element to a text string */
void dav_xml2text(pool * p,
const dav_xml_elem *elem,
int style,
array_header *namespaces,
int *ns_map,
const char **pbuf,
size_t *psize)
{
/* get the exact size, plus a null terminator */
size_t size = dav_elem_size(elem, style, namespaces, ns_map) + 1;
char *s = ap_palloc(p, size);
(void) dav_write_elem(s, elem, style, namespaces, ns_map);
s[size - 1] = '\0';
*pbuf = s;
if (psize)
*psize = size;
}
const char *dav_empty_elem(pool * p, const dav_xml_elem *elem)
{
if (elem->ns == DAV_NS_NONE) {
/*
* The prefix (xml...) is already within the prop name, or
* the element simply has no prefix.
*/
return ap_psprintf(p, "<%s/>" DEBUG_CR, elem->name);
}
return ap_psprintf(p, "<ns%d:%s/>" DEBUG_CR, elem->ns, elem->name);
}
/*
** dav_quote_string: quote an XML string
**
** Replace '<', '>', and '&' with '&lt;', '&gt;', and '&amp;'.
** If quotes is true, then replace '"' with '&quot;'.
**
** quotes is typically set to true for XML strings that will occur within
** double quotes -- attribute values.
*/
const char * dav_quote_string(pool *p, const char *s, int quotes)
{
const char *scan;
int len = 0;
int extra = 0;
char *qstr;
char *qscan;
char c;
for (scan = s; (c = *scan) != '\0'; ++scan, ++len) {
if (c == '<' || c == '>')
extra += 3; /* &lt; or &gt; */
else if (c == '&')
extra += 4; /* &amp; */
else if (quotes && c == '"')
extra += 5; /* &quot; */
}
/* nothing to do? */
if (extra == 0)
return s;
qstr = ap_palloc(p, len + extra + 1);
for (scan = s, qscan = qstr; (c = *scan) != '\0'; ++scan) {
if (c == '<') {
*qscan++ = '&';
*qscan++ = 'l';
*qscan++ = 't';
*qscan++ = ';';
}
else if (c == '>') {
*qscan++ = '&';
*qscan++ = 'g';
*qscan++ = 't';
*qscan++ = ';';
}
else if (c == '&') {
*qscan++ = '&';
*qscan++ = 'a';
*qscan++ = 'm';
*qscan++ = 'p';
*qscan++ = ';';
}
else if (quotes && c == '"') {
*qscan++ = '&';
*qscan++ = 'q';
*qscan++ = 'u';
*qscan++ = 'o';
*qscan++ = 't';
*qscan++ = ';';
}
else {
*qscan++ = c;
}
}
*qscan = '\0';
return qstr;
}
void dav_quote_xml_elem(pool *p, dav_xml_elem *elem)
{
dav_text *scan_txt;
dav_xml_attr *scan_attr;
dav_xml_elem *scan_elem;
/* convert the element's text */
for (scan_txt = elem->first_cdata.first;
scan_txt != NULL;
scan_txt = scan_txt->next) {
scan_txt->text = dav_quote_string(p, scan_txt->text, 0);
}
for (scan_txt = elem->following_cdata.first;
scan_txt != NULL;
scan_txt = scan_txt->next) {
scan_txt->text = dav_quote_string(p, scan_txt->text, 0);
}
/* convert the attribute values */
for (scan_attr = elem->attr;
scan_attr != NULL;
scan_attr = scan_attr->next) {
scan_attr->value = dav_quote_string(p, scan_attr->value, 1);
}
/* convert the child elements */
for (scan_elem = elem->first_child;
scan_elem != NULL;
scan_elem = scan_elem->next) {
dav_quote_xml_elem(p, scan_elem);
}
}
/* ---------------------------------------------------------------
**
** Timeout header processing
**
*/
/* dav_get_timeout: If the Timeout: header exists, return a time_t
* when this lock is expected to expire. Otherwise, return
* a time_t of DAV_TIMEOUT_INFINITE.
*
* It's unclear if DAV clients are required to understand
* Seconds-xxx and Infinity time values. We assume that they do.
* In addition, for now, that's all we understand, too.
*/
time_t dav_get_timeout(request_rec *r)
{
time_t now, expires = DAV_TIMEOUT_INFINITE;
const char *timeout_const = ap_table_get(r->headers_in, "Timeout");
const char *timeout = ap_pstrdup(r->pool, timeout_const), *val;
if (timeout == NULL)
return DAV_TIMEOUT_INFINITE;
/* Use the first thing we understand, or infinity if
* we don't understand anything.
*/
while ((val = ap_getword_white(r->pool, &timeout)) && strlen(val)) {
if (!strncmp(val, "Infinite", 8)) {
return DAV_TIMEOUT_INFINITE;
}
if (!strncmp(val, "Second-", 7)) {
val += 7;
/* ### We need to handle overflow better:
* ### timeout will be <= 2^32 - 1
*/
expires = atol(val);
now = time(NULL);
return now + expires;
}
}
return DAV_TIMEOUT_INFINITE;
}
/* ---------------------------------------------------------------
**
** If Header processing
**
*/
/* add_if_resource returns a new if_header, linking it to next_ih.
*/
static dav_if_header *dav_add_if_resource(pool *p, dav_if_header *next_ih,
const char *uri, size_t uri_len)
{
dav_if_header *ih;
if ((ih = ap_pcalloc(p, sizeof(*ih))) == NULL)
return NULL;
ih->uri = uri;
ih->uri_len = uri_len;
ih->next = next_ih;
return ih;
}
/* add_if_state adds a condition to an if_header.
*/
static dav_error * dav_add_if_state(pool *p, dav_if_header *ih,
const char *state_token,
dav_if_state_type t, int condition,
const dav_hooks_locks *locks_hooks)
{
dav_if_state_list *new_sl;
new_sl = ap_pcalloc(p, sizeof(*new_sl));
new_sl->condition = condition;
new_sl->type = t;
if (t == dav_if_opaquelock) {
dav_error *err;
if ((err = (*locks_hooks->parse_locktoken)(p, state_token,
&new_sl->locktoken)) != NULL) {
/* ### maybe add a higher-level description */
return err;
}
}
else
new_sl->etag = state_token;
new_sl->next = ih->state;
ih->state = new_sl;
return NULL;
}
/* fetch_next_token returns the substring from str+1
* to the next occurence of char term, or \0, whichever
* occurs first. Leading whitespace is ignored.
*/
static char *dav_fetch_next_token(char **str, char term)
{
char *sp;
char *token;
token = *str + 1;
while (*token && (*token == ' ' || *token == '\t'))
token++;
if ((sp = strchr(token, term)) == NULL)
return NULL;
*sp = '\0';
*str = sp;
return token;
}
/* dav_process_if_header:
*
* If NULL (no error) is returned, then **if_header points to the
* "If" productions structure (or NULL if "If" is not present).
*
* ### this part is bogus:
* If an error is encountered, the error is logged. Parent should
* return err->status.
*/
static dav_error * dav_process_if_header(request_rec *r, dav_if_header **p_ih)
{
dav_error *err;
char *str;
char *list;
const char *state_token;
const char *uri = NULL; /* scope of current production; NULL=no-tag */
size_t uri_len;
dav_if_header *ih = NULL;
uri_components parsed_uri;
const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r);
enum {no_tagged, tagged, unknown} list_type = unknown;
int condition;
*p_ih = NULL;
if ((str = ap_pstrdup(r->pool, ap_table_get(r->headers_in, "If"))) == NULL)
return NULL;
while (*str) {
switch(*str) {
case '<':
/* Tagged-list production - following states apply to this uri */
if (list_type == no_tagged
|| ((uri = dav_fetch_next_token(&str, '>')) == NULL)) {
return dav_new_error(r->pool, HTTP_BAD_REQUEST,
DAV_ERR_IF_TAGGED,
"Invalid If-header: unclosed \"<\" or "
"unexpected tagged-list production.");
}
/* 2518 specifies this must be an absolute URI; just take the
* relative part for later comparison against r->uri */
if (ap_parse_uri_components(r->pool, uri, &parsed_uri) != HTTP_OK) {
return dav_new_error(r->pool, HTTP_BAD_REQUEST,
DAV_ERR_IF_TAGGED,
"Invalid URI in tagged If-header.");
}
/* note that parsed_uri.path is allocated; we can trash it */
/* clean up the URI a bit */
ap_getparents(parsed_uri.path);
uri_len = strlen(parsed_uri.path);
if (uri_len > 1 && parsed_uri.path[uri_len - 1] == '/')
parsed_uri.path[--uri_len] = '\0';
uri = parsed_uri.path;
list_type = tagged;
break;
case '(':
/* List production */
/* If a uri has not been encountered, this is a No-Tagged-List */
if (list_type == unknown)
list_type = no_tagged;
if ((list = dav_fetch_next_token(&str, ')')) == NULL) {
return dav_new_error(r->pool, HTTP_BAD_REQUEST,
DAV_ERR_IF_UNCLOSED_PAREN,
"Invalid If-header: unclosed \"(\".");
}
if ((ih = dav_add_if_resource(r->pool, ih, uri, uri_len)) == NULL) {
/* ### dav_add_if_resource() should return an error for us! */
return dav_new_error(r->pool, HTTP_BAD_REQUEST,
DAV_ERR_IF_PARSE,
"Internal server error parsing \"If:\" "
"header.");
}
condition = DAV_IF_COND_NORMAL;
while (*list) {
/* List is the entire production (in a uri scope) */
switch (*list) {
case '<':
if ((state_token = dav_fetch_next_token(&list, '>')) == NULL) {
/* ### add a description to this error */
return dav_new_error(r->pool, HTTP_BAD_REQUEST,
DAV_ERR_IF_PARSE, NULL);
}
if ((err = dav_add_if_state(r->pool, ih, state_token, dav_if_opaquelock,
condition, locks_hooks)) != NULL) {
/* ### maybe add a higher level description */
return err;
}
condition = DAV_IF_COND_NORMAL;
break;
case '[':
if ((state_token = dav_fetch_next_token(&list, ']')) == NULL) {
/* ### add a description to this error */
return dav_new_error(r->pool, HTTP_BAD_REQUEST,
DAV_ERR_IF_PARSE, NULL);
}
if ((err = dav_add_if_state(r->pool, ih, state_token, dav_if_etag,
condition, locks_hooks)) != NULL) {
/* ### maybe add a higher level description */
return err;
}
condition = DAV_IF_COND_NORMAL;
break;
case 'N':
if (list[1] == 'o' && list[2] == 't') {
if (condition != DAV_IF_COND_NORMAL) {
return dav_new_error(r->pool, HTTP_BAD_REQUEST,
DAV_ERR_IF_MULTIPLE_NOT,
"Invalid \"If:\" header: "
"Multiple \"not\" entries "
"for the same state.");
}
condition = DAV_IF_COND_NOT;
}
list += 2;
break;
case ' ':
case '\t':
break;
default:
return dav_new_error(r->pool, HTTP_BAD_REQUEST,
DAV_ERR_IF_UNK_CHAR,
ap_psprintf(r->pool,
"Invalid \"If:\" "
"header: Unexpected "
"character encountered "
"(0x%02x, '%c').",
*list, *list));
}
list++;
}
break;
case ' ':
case '\t':
break;
default:
return dav_new_error(r->pool, HTTP_BAD_REQUEST,
DAV_ERR_IF_UNK_CHAR,
ap_psprintf(r->pool,
"Invalid \"If:\" header: "
"Unexpected character "
"encountered (0x%02x, '%c').",
*str, *str));
}
str++;
}
*p_ih = ih;
return NULL;
}
static int dav_find_submitted_locktoken(const dav_if_header *if_header,
const dav_lock *lock_list,
const dav_hooks_locks *locks_hooks)
{
for (; if_header != NULL; if_header = if_header->next) {
const dav_if_state_list *state_list;
for (state_list = if_header->state;
state_list != NULL;
state_list = state_list->next) {
if (state_list->type == dav_if_opaquelock) {
const dav_lock *lock;
/* given state_list->locktoken, match it */
/*
** The resource will have one or more lock tokens. We only
** need to match one of them against any token in the
** If: header.
**
** One token case: It is an exclusive or shared lock. Either
** way, we must find it.
**
** N token case: They are shared locks. By policy, we need
** to match only one. The resource's other
** tokens may belong to somebody else (so we
** shouldn't see them in the If: header anyway)
*/
for (lock = lock_list; lock != NULL; lock = lock->next) {
if (!(*locks_hooks->compare_locktoken)(state_list->locktoken, lock->locktoken)) {
return 1;
}
}
}
}
}
return 0;
}
/* dav_validate_resource_state:
* Returns NULL if path/uri meets if-header and lock requirements
*/
static dav_error * dav_validate_resource_state(pool *p,
const dav_resource *resource,
dav_lockdb *lockdb,
const dav_if_header *if_header,
int flags,
dav_buffer *pbuf,
request_rec *r)
{
dav_error *err;
const char *uri;
const char *etag;
const dav_hooks_locks *locks_hooks = (lockdb ? lockdb->hooks : NULL);
const dav_if_header *ifhdr_scan;
dav_if_state_list *state_list;
dav_lock *lock_list;
dav_lock *lock;
int num_matched;
int num_that_apply;
int seen_locktoken;
size_t uri_len;
const char *reason = NULL;
/* DBG1("validate: <%s>", resource->uri); */
/*
** The resource will have one of three states:
**
** 1) No locks. We have no special requirements that the user supply
** specific locktokens. One of the state lists must match, and
** we're done.
**
** 2) One exclusive lock. The locktoken must appear *anywhere* in the
** If: header. Of course, asserting the token in a "Not" term will
** quickly fail that state list :-). If the locktoken appears in
** one of the state lists *and* one state list matches, then we're
** done.
**
** 3) One or more shared locks. One of the locktokens must appear
** *anywhere* in the If: header. If one of the locktokens appears,
** and we match one state list, then we are done.
**
** The <seen_locktoken> variable determines whether we have seen one
** of this resource's locktokens in the If: header.
*/
/*
** If this is a new lock request, <flags> will contain the requested
** lock scope. Three rules apply:
**
** 1) Do not require a (shared) locktoken to be seen (when we are
** applying another shared lock)
** 2) If the scope is exclusive and we see any locks, fail.
** 3) If the scope is shared and we see an exclusive lock, fail.
*/
if (lockdb == NULL) {
/* we're in State 1. no locks. */
lock_list = NULL;
}
else {
/*
** ### hrm... we don't need to have these fully
** ### resolved since we're only looking at the
** ### locktokens...
**
** ### use get_locks w/ calltype=PARTIAL
*/
if ((err = dav_lock_query(lockdb, resource, &lock_list)) != NULL) {
return dav_push_error(p,
HTTP_INTERNAL_SERVER_ERROR, 0,
"The locks could not be queried for "
"verification against a possible \"If:\" "
"header.",
err);
}
/* lock_list now determines whether we're in State 1, 2, or 3. */
}
/*
** For a new, exclusive lock: if any locks exist, fail.
** For a new, shared lock: if an exclusive lock exists, fail.
** else, do not require a token to be seen.
*/
if (flags & DAV_LOCKSCOPE_EXCLUSIVE) {
if (lock_list != NULL) {
return dav_new_error(p, HTTP_LOCKED, 0,
"Existing lock(s) on the requested resource "
"prevent an exclusive lock.");
}
/*
** There are no locks, so we can pretend that we've already met
** any requirement to find the resource's locks in an If: header.
*/
seen_locktoken = 1;
}
else if (flags & DAV_LOCKSCOPE_SHARED) {
/*
** Strictly speaking, we don't need this loop. Either the first
** (and only) lock will be EXCLUSIVE, or none of them will be.
*/
for (lock = lock_list; lock != NULL; lock = lock->next) {
if (lock->scope == DAV_LOCKSCOPE_EXCLUSIVE) {
return dav_new_error(p, HTTP_LOCKED, 0,
"The requested resource is already "
"locked exclusively.");
}
}
/*
** The locks on the resource (if any) are all shared. Set the
** <seen_locktoken> flag to indicate that we do not need to find
** the locks in an If: header.
*/
seen_locktoken = 1;
}
else {
/*
** For methods other than LOCK:
**
** If we have no locks, then <seen_locktoken> can be set to true --
** pretending that we've already met the requirement of seeing one
** of the resource's locks in the If: header.
**
** Otherwise, it must be cleared and we'll look for one.
*/
seen_locktoken = (lock_list == NULL);
}
/*
** If there is no If: header, then we can shortcut some logic:
**
** 1) if we do not need to find a locktoken in the (non-existent) If:
** header, then we are successful.
**
** 2) if we must find a locktoken in the (non-existent) If: header, then
** we fail.
*/
if (if_header == NULL) {
if (seen_locktoken)
return NULL;
return dav_new_error(p, HTTP_LOCKED, 0,
"This resource is locked and an \"If:\" header "
"was not supplied to allow access to the "
"resource.");
}
/* the If: header is present */
/*
** If a dummy header is present (because of a Lock-Token: header), then
** we are required to find that token in this resource's set of locks.
** If we have no locks, then we immediately fail.
**
** This is a 400 (Bad Request) since they should only submit a locktoken
** that actually exists.
**
** Don't issue this response if we're talking about the parent resource.
** It is okay for that resource to NOT have this locktoken.
** (in fact, it certainly will not: a dummy_header only occurs for the
** UNLOCK method, the parent is checked only for locknull resources,
** and the parent certainly does not have the (locknull's) locktoken)
*/
if (lock_list == NULL && if_header->dummy_header) {
if (flags & DAV_VALIDATE_IS_PARENT)
return NULL;
return dav_new_error(p, HTTP_BAD_REQUEST, 0,
"The locktoken specified in the \"Lock-Token:\" "
"header is invalid because this resource has no "
"outstanding locks.");
}
/*
** Prepare the input URI. We want the URI to never have a trailing slash.
**
** When URIs are placed into the dav_if_header structure, they are
** guaranteed to never have a trailing slash. If the URIs are equivalent,
** then it doesn't matter if they both lack a trailing slash -- they're
** still equivalent.
**
** Note: we could also ensure that a trailing slash is present on both
** URIs, but the majority of URIs provided to us via a resource walk
** will not contain that trailing slash.
*/
uri = resource->uri;
uri_len = strlen(uri);
if (uri[uri_len - 1] == '/') {
dav_set_bufsize(p, pbuf, uri_len);
memcpy(pbuf->buf, uri, uri_len);
pbuf->buf[--uri_len] = '\0';
uri = pbuf->buf;
}
/* get the resource's etag; we may need it during the checks */
etag = (*resource->hooks->getetag)(resource);
/* how many state_lists apply to this URI? */
num_that_apply = 0;
/* If there are if-headers, fail if this resource
* does not match at least one state_list.
*/
for (ifhdr_scan = if_header;
ifhdr_scan != NULL;
ifhdr_scan = ifhdr_scan->next) {
/* DBG2("uri=<%s> if_uri=<%s>", uri, ifhdr_scan->uri ? ifhdr_scan->uri : "(no uri)"); */
if (ifhdr_scan->uri != NULL
&& (uri_len != ifhdr_scan->uri_len
|| memcmp(uri, ifhdr_scan->uri, uri_len) != 0)) {
/*
** A tagged-list's URI doesn't match this resource's URI.
** Skip to the next state_list to see if it will match.
*/
continue;
}
/* this state_list applies to this resource */
/*
** ### only one state_list should ever apply! a no-tag, or a tagged
** ### where S9.4.2 states only one can match.
**
** ### revamp this code to loop thru ifhdr_scan until we find the
** ### matching state_list. process it. stop.
*/
++num_that_apply;
/* To succeed, resource must match *all* of the states
* specified in the state_list.
*/
for (state_list = ifhdr_scan->state;
state_list != NULL;
state_list = state_list->next) {
switch(state_list->type) {
case dav_if_etag:
{
int mismatch = strcmp(state_list->etag, etag);
if (state_list->condition == DAV_IF_COND_NORMAL && mismatch) {
/*
** The specified entity-tag does not match the
** entity-tag on the resource. This state_list is
** not going to match. Bust outta here.
*/
reason =
"an entity-tag was specified, but the resource's "
"actual ETag does not match.";
goto state_list_failed;
}
else if (state_list->condition == DAV_IF_COND_NOT
&& !mismatch) {
/*
** The specified entity-tag DOES match the
** entity-tag on the resource. This state_list is
** not going to match. Bust outta here.
*/
reason =
"an entity-tag was specified using the \"Not\" form, "
"but the resource's actual ETag matches the provided "
"entity-tag.";
goto state_list_failed;
}
break;
}
case dav_if_opaquelock:
if (lockdb == NULL) {
if (state_list->condition == DAV_IF_COND_NOT) {
/* the locktoken is definitely not there! (success) */
continue;
}
/* condition == DAV_IF_COND_NORMAL */
/*
** If no lockdb is provided, then validation fails for
** this state_list (NORMAL means we were supposed to
** find the token, which we obviously cannot do without
** a lock database).
**
** Go and try the next state list.
*/
reason =
"a State-token was supplied, but a lock database "
"is not available for to provide the required lock.";
goto state_list_failed;
}
/* Resource validation 'fails' if:
* ANY of the lock->locktokens match
* a NOT state_list->locktoken,
* OR
* NONE of the lock->locktokens match
* a NORMAL state_list->locktoken.
*/
num_matched = 0;
for (lock = lock_list; lock != NULL; lock = lock->next) {
/*
DBG2("compare: rsrc=%s ifhdr=%s",
(*locks_hooks->format_locktoken)(p, lock->locktoken),
(*locks_hooks->format_locktoken)(p, state_list->locktoken));
*/
/* nothing to do if the locktokens do not match. */
if ((*locks_hooks->compare_locktoken)(state_list->locktoken, lock->locktoken)) {
continue;
}
/*
** We have now matched up one of the resource's locktokens
** to a locktoken in a State-token in the If: header.
** Note this fact, so that we can pass the overall
** requirement of seeing at least one of the resource's
** locktokens.
*/
seen_locktoken = 1;
if (state_list->condition == DAV_IF_COND_NOT) {
/*
** This state requires that the specified locktoken
** is NOT present on the resource. But we just found
** it. There is no way this state-list can now
** succeed, so go try another one.
*/
reason =
"a State-token was supplied, which used a "
"\"Not\" condition. The State-token was found "
"in the locks on this resource";
goto state_list_failed;
}
/* condition == DAV_IF_COND_NORMAL */
/* Validate auth_user: If an authenticated user created
** the lock, only the same user may submit that locktoken
** to manipulate a resource.
*/
if (lock->auth_user &&
(!r->connection->user ||
strcmp(lock->auth_user, r->connection->user))) {
const char *errmsg;
errmsg = ap_pstrcat(p, "User \"",
r->connection->user,
"\" submitted a locktoken created "
"by user \"",
lock->auth_user, "\".", NULL);
return dav_new_error(p, HTTP_UNAUTHORIZED, 0, errmsg);
}
/*
** We just matched a specified State-Token to one of the
** resource's locktokens.
**
** Break out of the lock scan -- we only needed to find
** one match (actually, there shouldn't be any other
** matches in the lock list).
*/
num_matched = 1;
break;
}
if (num_matched == 0
&& state_list->condition == DAV_IF_COND_NORMAL) {
/*
** We had a NORMAL state, meaning that we should have
** found the State-Token within the locks on this
** resource. We didn't, so this state_list must fail.
*/
reason =
"a State-token was supplied, but it was not found "
"in the locks on this resource.";
goto state_list_failed;
}
break;
} /* switch */
} /* foreach ( state_list ) */
/*
** We've checked every state in this state_list and none of them
** have failed. Since all of them succeeded, then we have a matching
** state list and we may be done.
**
** The next requirement is that we have seen one of the resource's
** locktokens (if any). If we have, then we can just exit. If we
** haven't, then we need to keep looking.
*/
if (seen_locktoken) {
/* woo hoo! */
return NULL;
}
/*
** Haven't seen one. Let's break out of the search and just look
** for a matching locktoken.
*/
break;
/*
** This label is used when we detect that a state_list is not
** going to match this resource. We bust out and try the next
** state_list.
*/
state_list_failed:
;
} /* foreach ( ifhdr_scan ) */
/*
** The above loop exits for one of two reasons:
** 1) a state_list matched and seen_locktoken is false.
** 2) all if_header structures were scanned, without (1) occurring
*/
if (ifhdr_scan == NULL) {
/*
** We finished the loop without finding any matching state lists.
*/
/*
** If none of the state_lists apply to this resource, then we
** may have succeeded. Note that this scenario implies a
** tagged-list with no matching state_lists. If the If: header
** was a no-tag-list, then it would have applied to this resource.
**
** S9.4.2 states that when no state_lists apply, then the header
** should be ignored.
**
** If we saw one of the resource's locktokens, then we're done.
** If we did not see a locktoken, then we fail.
*/
if (num_that_apply == 0) {
if (seen_locktoken)
return NULL;
/*
** We may have aborted the scan before seeing the locktoken.
** Rescan the If: header to see if we can find the locktoken
** somewhere.
*/
if (dav_find_submitted_locktoken(if_header, lock_list,
locks_hooks)) {
/*
** We found a match! We're set... none of the If: header
** assertions apply (implicit success), and the If: header
** specified the locktoken somewhere. We're done.
*/
return NULL;
}
return dav_new_error(p, HTTP_LOCKED, 0 /* error_id */,
"This resource is locked and the \"If:\" "
"header did not specify one of the "
"locktokens for this resource's lock(s).");
}
/* else: one or more state_lists were applicable, but failed. */
/*
** If the dummy_header did not match, then they specified an
** incorrect token in the Lock-Token header. Forget whether the
** If: statement matched or not... we'll tell them about the
** bad Lock-Token first. That is considered a 400 (Bad Request).
*/
if (if_header->dummy_header) {
return dav_new_error(p, HTTP_BAD_REQUEST, 0,
"The locktoken specified in the "
"\"Lock-Token:\" header did not specify one "
"of this resource's locktoken(s).");
}
if (reason == NULL) {
return dav_new_error(p, HTTP_PRECONDITION_FAILED, 0,
"The preconditions specified by the \"If:\" "
"header did not match this resource.");
}
return dav_new_error(p, HTTP_PRECONDITION_FAILED, 0,
ap_psprintf(p,
"The precondition(s) specified by "
"the \"If:\" header did not match "
"this resource. At least one "
"failure is because: %s", reason));
}
/* assert seen_locktoken == 0 */
/*
** ifhdr_scan != NULL implies we found a matching state_list.
**
** Since we're still here, it also means that we have not yet found
** one the resource's locktokens in the If: header.
**
** Scan all the if_headers and states looking for one of this
** resource's locktokens. Note that we need to go back and scan them
** all -- we may have aborted a scan with a failure before we saw a
** matching token.
**
** Note that seen_locktoken == 0 implies lock_list != NULL which implies
** locks_hooks != NULL.
*/
if (dav_find_submitted_locktoken(if_header, lock_list, locks_hooks)) {
/*
** We found a match! We're set... we have a matching state list,
** and the If: header specified the locktoken somewhere. We're done.
*/
return NULL;
}
/*
** We had a matching state list, but the user agent did not specify one
** of this resource's locktokens. Tell them so.
**
** Note that we need to special-case the message on whether a "dummy"
** header exists. If it exists, yet we didn't see a needed locktoken,
** then that implies the dummy header (Lock-Token header) did NOT
** specify one of this resource's locktokens. (this implies something
** in the real If: header matched)
**
** We want to note the 400 (Bad Request) in favor of a 423 (Locked).
*/
if (if_header->dummy_header) {
return dav_new_error(p, HTTP_BAD_REQUEST, 0,
"The locktoken specified in the "
"\"Lock-Token:\" header did not specify one "
"of this resource's locktoken(s).");
}
return dav_new_error(p, HTTP_LOCKED, 1 /* error_id */,
"This resource is locked and the \"If:\" header "
"did not specify one of the "
"locktokens for this resource's lock(s).");
}
/* dav_validate_walker: Walker callback function to validate resource state */
static dav_error * dav_validate_walker(dav_walker_ctx *ctx, int calltype)
{
dav_error *err;
if ((err = dav_validate_resource_state(ctx->pool, ctx->resource,
ctx->lockdb,
ctx->if_header, ctx->flags,
&ctx->work_buf, ctx->r)) == NULL) {
/* There was no error, so just bug out. */
return NULL;
}
/*
** If we have a serious server error, or if the request itself failed,
** then just return error (not a multistatus).
*/
if (ap_is_HTTP_SERVER_ERROR(err->status)
|| (*ctx->resource->hooks->is_same_resource)(ctx->resource,
ctx->root)) {
/* ### maybe push a higher-level description? */
return err;
}
/* associate the error with the current URI */
dav_add_response(ctx, ctx->uri.buf, err->status, NULL);
return NULL;
}
/*
** dav_validate_request: Validate if-headers (and check for locks) on:
** (1) r->filename @ depth;
** (2) Parent of r->filename if check_parent == 1
**
** The check of parent should be done when it is necessary to verify that
** the parent collection will accept a new member (ie current resource
** state is null).
**
** Return OK on successful validation.
** On error, return appropriate HTTP_* code, and log error. If a multi-stat
** error is necessary, response will point to it, else NULL.
*/
dav_error * dav_validate_request(request_rec *r, dav_resource *resource,
int depth, dav_locktoken *locktoken,
dav_response **response, int flags,
dav_lockdb *lockdb)
{
dav_error *err;
int result;
dav_if_header *if_header;
int lock_db_opened_locally = 0;
const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r);
const dav_hooks_repository *repos_hooks = resource->hooks;
dav_buffer work_buf = { 0 };
dav_response *new_response;
#if DAV_DEBUG
if (depth && response == NULL) {
/*
** ### bleck. we can't return errors for other URIs unless we have
** ### a "response" ptr.
*/
return dav_new_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
"DESIGN ERROR: dav_validate_request called "
"with depth>0, but no response ptr.");
}
#endif
if (response != NULL)
*response = NULL;
/* Do the standard checks for conditional requests using
* If-..-Since, If-Match etc */
if ((result = ap_meets_conditions(r)) != OK) {
/* ### fix this up... how? */
return dav_new_error(r->pool, result, 0, NULL);
}
/* always parse (and later process) the If: header */
if ((err = dav_process_if_header(r, &if_header)) != NULL) {
/* ### maybe add higher-level description */
return err;
}
/* If a locktoken was specified, create a dummy if_header with which
* to validate resources. In the interim, figure out why DAV uses
* locktokens in an if-header without a Lock-Token header to refresh
* locks, but a Lock-Token header without an if-header to remove them.
*/
if (locktoken != NULL) {
dav_if_header *ifhdr_new;
ifhdr_new = ap_pcalloc(r->pool, sizeof(*ifhdr_new));
ifhdr_new->uri = resource->uri;
ifhdr_new->uri_len = strlen(resource->uri);
ifhdr_new->dummy_header = 1;
ifhdr_new->state = ap_pcalloc(r->pool, sizeof(*ifhdr_new->state));
ifhdr_new->state->type = dav_if_opaquelock;
ifhdr_new->state->condition = DAV_IF_COND_NORMAL;
ifhdr_new->state->locktoken = locktoken;
ifhdr_new->next = if_header;
if_header = ifhdr_new;
}
/*
** If necessary, open the lock database (read-only, lazily);
** the validation process may need to retrieve or update lock info.
** Otherwise, assume provided lockdb is valid and opened rw.
*/
if (lockdb == NULL) {
if (locks_hooks != NULL) {
if ((err = (*locks_hooks->open_lockdb)(r, 0, 0, &lockdb)) != NULL) {
/* ### maybe insert higher-level comment */
return err;
}
lock_db_opened_locally = 1;
}
else {
return dav_new_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
"Resource validation failed because no "
"lock hooks were found.");
}
}
/* (1) Validate the specified resource, at the specified depth */
if (resource->exists && depth > 0) {
dav_walker_ctx ctx = { 0 };
ctx.walk_type = DAV_WALKTYPE_ALL;
ctx.postfix = 0;
ctx.func = dav_validate_walker;
ctx.pool = r->pool;
ctx.if_header = if_header;
ctx.r = r;
ctx.flags = flags;
ctx.resource = resource;
if (lockdb != NULL) {
ctx.lockdb = lockdb;
ctx.walk_type |= DAV_WALKTYPE_LOCKNULL;
}
dav_buffer_init(r->pool, &ctx.uri, resource->uri);
err = (*repos_hooks->walk)(&ctx, DAV_INFINITY);
if (err == NULL) {
*response = ctx.response;
}
/* else: implies a 5xx status code occurred. */
}
else {
err = dav_validate_resource_state(r->pool, resource, lockdb,
if_header, flags, &work_buf, r);
}
/* (2) Validate the parent resource if requested */
if (err == NULL && (flags & DAV_VALIDATE_PARENT)) {
dav_resource *parent_resource = (*repos_hooks->get_parent_resource)(resource);
if (parent_resource == NULL) {
err = dav_new_error(r->pool, HTTP_FORBIDDEN, 0,
"Cannot access parent of repository root.");
}
else {
err = dav_validate_resource_state(r->pool, parent_resource, lockdb,
if_header,
flags | DAV_VALIDATE_IS_PARENT,
&work_buf, r);
/*
** This error occurred on the parent resource. This implies that
** we have to create a multistatus response (to report the error
** against a URI other than the Request-URI). "Convert" this error
** into a multistatus response.
*/
if (err != NULL) {
new_response = ap_pcalloc(r->pool, sizeof(*new_response));
new_response->href = parent_resource->uri;
new_response->status = err->status;
new_response->desc =
"A validation error has occurred on the parent resource, "
"preventing the operation on the resource specified by "
"the Request-URI.";
if (err->desc != NULL) {
new_response->desc = ap_pstrcat(r->pool,
new_response->desc,
" The error was: ",
err->desc, NULL);
}
/* assert: DAV_VALIDATE_PARENT implies response != NULL */
new_response->next = *response;
*response = new_response;
err = NULL;
}
}
}
if (lock_db_opened_locally)
(*locks_hooks->close_lockdb)(lockdb);
/*
** If we don't have a (serious) error, and we have multistatus responses,
** then we need to construct an "error". This error will be the overall
** status returned, and the multistatus responses will go into its body.
**
** For certain methods, the overall error will be a 424. The default is
** to construct a standard 207 response.
*/
if (err == NULL && response != NULL && *response != NULL) {
dav_text *propstat = NULL;
if ((flags & DAV_VALIDATE_USE_424) != 0) {
/* manufacture a 424 error to hold the multistatus response(s) */
return dav_new_error(r->pool, HTTP_FAILED_DEPENDENCY, 0,
"An error occurred on another resource, "
"preventing the requested operation on "
"this resource.");
}
/*
** Whatever caused the error, the Request-URI should have a 424
** associated with it since we cannot complete the method.
**
** For a LOCK operation, insert an empty DAV:lockdiscovery property.
** For other methods, return a simple 424.
*/
if ((flags & DAV_VALIDATE_ADD_LD) != 0) {
propstat = ap_pcalloc(r->pool, sizeof(*propstat));
propstat->text =
"<D:propstat>" DEBUG_CR
"<D:prop><D:lockdiscovery/></D:prop>" DEBUG_CR
"<D:status>HTTP/1.1 424 Failed Dependency</D:status>" DEBUG_CR
"</D:propstat>" DEBUG_CR;
}
/* create the 424 response */
new_response = ap_pcalloc(r->pool, sizeof(*new_response));
new_response->href = resource->uri;
new_response->status = HTTP_FAILED_DEPENDENCY;
new_response->propresult.propstats = propstat;
new_response->desc =
"An error occurred on another resource, preventing the "
"requested operation on this resource.";
new_response->next = *response;
*response = new_response;
/* manufacture a 207 error for the multistatus response(s) */
return dav_new_error(r->pool, HTTP_MULTI_STATUS, 0,
"Error(s) occurred on resources during the "
"validation process.");
}
return err;
}
/* dav_get_locktoken_list:
*
* Sets ltl to a locktoken_list of all positive locktokens in header,
* else NULL if no If-header, or no positive locktokens.
*/
dav_error * dav_get_locktoken_list(request_rec *r, dav_locktoken_list **ltl)
{
dav_error *err;
dav_if_header *if_header;
dav_if_state_list *if_state;
dav_locktoken_list *lock_token = NULL;
*ltl = NULL;
if ((err = dav_process_if_header(r, &if_header)) != NULL) {
/* ### add a higher-level description? */
return err;
}
while (if_header != NULL) {
if_state = if_header->state; /* Begining of the if_state linked list */
while (if_state != NULL) {
if (if_state->condition == DAV_IF_COND_NORMAL
&& if_state->type == dav_if_opaquelock) {
lock_token = ap_pcalloc(r->pool, sizeof(dav_locktoken_list));
lock_token->locktoken = if_state->locktoken;
lock_token->next = *ltl;
*ltl = lock_token;
}
if_state = if_state->next;
}
if_header = if_header->next;
}
if (*ltl == NULL) {
/* No nodes added */
return dav_new_error(r->pool, HTTP_BAD_REQUEST, DAV_ERR_IF_ABSENT,
"No locktokens were specified in the \"If:\" "
"header, so the refresh could not be performed.");
}
return NULL;
}
/* dav_get_target_selector:
*
* Returns any Target-Selector header in a request
* (used by versioning clients)
*/
const char *dav_get_target_selector(request_rec *r)
{
return ap_table_get(r->headers_in, "Target-Selector");
}
/* Ensure that a resource is writable. If there is no versioning
* provider, then this is essentially a no-op. Versioning repositories
* require explicit resource creation and checkout before they can
* be written to. If a new resource is to be created, or an existing
* resource deleted, the parent collection must be checked out as well.
*
* Set the parent_only flag to only make the parent collection writable.
* Otherwise, both parent and child are made writable as needed. If the
* child does not exist, then a new versioned resource is created and
* checked out.
*
* The parent_resource and parent_was_writable arguments are optional
* (i.e. they may be NULL). If parent_only is set, then the
* resource_existed and resource_was_writable arguments are ignored.
*
* The previous states of the resources are returned, so they can be
* restored after the operation completes (see
* dav_revert_resource_writability())
*/
dav_error *dav_ensure_resource_writable(request_rec *r,
dav_resource *resource,
int parent_only,
dav_resource **parent_resource,
int *resource_existed,
int *resource_was_writable,
int *parent_was_writable)
{
const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r);
dav_resource *parent = NULL;
const char *body;
int auto_version;
dav_error *err;
if (parent_resource != NULL)
*parent_resource = NULL;
if (!parent_only) {
*resource_existed = resource->exists;
*resource_was_writable = 0;
}
if (parent_was_writable != NULL)
*parent_was_writable = 0;
/* if a Target-Selector header is present, then the client knows about
* versioning, so it should not be relying on implicit versioning
*/
auto_version = (dav_get_target_selector(r) == NULL);
/* check parent resource if requested or if resource must be created */
if (!resource->exists || parent_only) {
parent = (*resource->hooks->get_parent_resource)(resource);
if (parent == NULL || !parent->exists) {
body = ap_psprintf(r->pool,
"Missing one or more intermediate collections. "
"Cannot create resource %s.",
ap_escape_html(r->pool, resource->uri));
return dav_new_error(r->pool, HTTP_CONFLICT, 0, body);
}
if (parent_resource != NULL)
*parent_resource = parent;
/* if parent not versioned, assume child can be created */
if (!parent->versioned) {
if (!parent_only)
*resource_was_writable = 1;
if (parent_was_writable != NULL)
*parent_was_writable = 1;
return NULL;
}
/* if no versioning provider, something is terribly wrong */
if (vsn_hooks == NULL) {
return dav_new_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
"INTERNAL ERROR: "
"versioned resource with no versioning "
"provider?");
}
/* remember whether parent was already writable */
if (parent_was_writable != NULL)
*parent_was_writable = parent->working;
/* parent must be checked out */
if (!parent->working) {
if ((err = (*vsn_hooks->checkout)(parent)) != NULL) {
body = ap_psprintf(r->pool,
"Unable to checkout parent collection. "
"Cannot create resource %s.",
ap_escape_html(r->pool, resource->uri));
return dav_push_error(r->pool, HTTP_CONFLICT, 0, body, err);
}
}
/* if not just checking parent, create new child resource */
if (!parent_only) {
if ((err = (*vsn_hooks->mkresource)(resource)) != NULL) {
body = ap_psprintf(r->pool,
"Unable to create versioned resource %s.",
ap_escape_html(r->pool, resource->uri));
return dav_push_error(r->pool, HTTP_CONFLICT, 0, body, err);
}
}
}
else {
/* resource exists: if not versioned, then assume it is writable */
if (!resource->versioned) {
*resource_was_writable = 1;
return NULL;
}
*resource_was_writable = resource->working;
}
/* if not just checking parent, make sure child resource is checked out */
if (!parent_only && !resource->working) {
if ((err = (*vsn_hooks->checkout)(resource)) != NULL) {
body = ap_psprintf(r->pool,
"Unable to checkout resource %s.",
ap_escape_html(r->pool, resource->uri));
return dav_push_error(r->pool, HTTP_CONFLICT, 0, body, err);
}
}
return NULL;
}
/* Revert the writability of resources back to what they were
* before they were modified. If undo == 0, then the resource
* modifications are maintained (i.e. they are checked in).
* If undo != 0, then resource modifications are discarded
* (i.e. they are unchecked out).
*
* The resource and parent_resource arguments are optional
* (i.e. they may be NULL).
*/
dav_error *dav_revert_resource_writability(request_rec *r,
dav_resource *resource,
dav_resource *parent_resource,
int undo,
int resource_existed,
int resource_was_writable,
int parent_was_writable)
{
const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r);
const char *body;
dav_error *err;
if (resource != NULL) {
if (!resource_was_writable
&& resource->versioned && resource->working) {
if (undo)
err = (*vsn_hooks->uncheckout)(resource);
else
err = (*vsn_hooks->checkin)(resource);
if (err != NULL) {
body = ap_psprintf(r->pool,
"Unable to %s resource %s.",
undo ? "uncheckout" : "checkin",
ap_escape_html(r->pool, resource->uri));
return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
body, err);
}
}
if (undo && !resource_existed && resource->exists) {
dav_response *response;
/* ### should we do anything with the response? */
if ((err = (*resource->hooks->remove_resource)(resource,
&response)) != NULL) {
body = ap_psprintf(r->pool,
"Unable to undo creation of resource %s.",
ap_escape_html(r->pool, resource->uri));
return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
body, err);
}
}
}
if (parent_resource != NULL && !parent_was_writable
&& parent_resource->versioned && parent_resource->working) {
if (undo)
err = (*vsn_hooks->uncheckout)(parent_resource);
else
err = (*vsn_hooks->checkin)(parent_resource);
if (err != NULL) {
body = ap_psprintf(r->pool,
"Unable to %s parent collection of %s.",
undo ? "uncheckout" : "checkin",
ap_escape_html(r->pool, resource->uri));
return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
body, err);
}
}
return NULL;
}
/* return the URI's (existing) index, or insert it and return a new index */
int dav_insert_uri(array_header *uri_array, const char *uri)
{
int i;
const char **pelt;
for (i = uri_array->nelts; i--;) {
if (strcmp(uri, DAV_GET_URI_ITEM(uri_array, i)) == 0)
return i;
}
pelt = ap_push_array(uri_array);
*pelt = uri; /* assume uri is const or in a pool */
return uri_array->nelts - 1;
}