1 /* ====================================================================
2 * The Apache Software License, Version 1.1
4 * Copyright (c) 2000 The Apache Software Foundation. All rights
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
11 * 1. Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
14 * 2. Redistributions in binary form must reproduce the above copyright
15 * notice, this list of conditions and the following disclaimer in
16 * the documentation and/or other materials provided with the
19 * 3. The end-user documentation included with the redistribution,
20 * if any, must include the following acknowledgment:
21 * "This product includes software developed by the
22 * Apache Software Foundation (http://www.apache.org/)."
23 * Alternately, this acknowledgment may appear in the software itself,
24 * if and wherever such third-party acknowledgments normally appear.
26 * 4. The names "Apache" and "Apache Software Foundation" must
27 * not be used to endorse or promote products derived from this
28 * software without prior written permission. For written
29 * permission, please contact apache@apache.org.
31 * 5. Products derived from this software may not be called "Apache",
32 * nor may "Apache" appear in their name, without prior written
33 * permission of the Apache Software Foundation.
35 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
36 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
37 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
39 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
42 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
44 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
45 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
47 * ====================================================================
49 * This software consists of voluntary contributions made by many
50 * individuals on behalf of the Apache Software Foundation. For more
51 * information on the Apache Software Foundation, please see
52 * <http://www.apache.org/>.
56 ** DAV extension module for Apache 2.0.*
57 ** - various utilities, repository-independent
62 #include "http_request.h"
63 #include "http_config.h"
64 #include "http_vhost.h"
66 #include "http_protocol.h"
68 #include "apr_strings.h"
70 dav_error *dav_new_error(apr_pool_t *p, int status, int error_id, const char *desc)
72 int save_errno = errno;
73 dav_error *err = apr_pcalloc(p, sizeof(*err));
75 /* DBG3("dav_new_error: %d %d %s", status, error_id, desc ? desc : "(no desc)"); */
78 err->error_id = error_id;
80 err->save_errno = save_errno;
85 dav_error *dav_push_error(apr_pool_t *p, int status, int error_id, const char *desc,
88 dav_error *err = apr_pcalloc(p, sizeof(*err));
91 err->error_id = error_id;
98 void dav_check_bufsize(apr_pool_t * p, dav_buffer *pbuf, size_t extra_needed)
100 /* grow the buffer if necessary */
101 if (pbuf->cur_len + extra_needed > pbuf->alloc_len) {
104 pbuf->alloc_len += extra_needed + DAV_BUFFER_PAD;
105 newbuf = apr_palloc(p, pbuf->alloc_len);
106 memcpy(newbuf, pbuf->buf, pbuf->cur_len);
111 void dav_set_bufsize(apr_pool_t * p, dav_buffer *pbuf, size_t size)
113 /* NOTE: this does not retain prior contents */
115 /* NOTE: this function is used to init the first pointer, too, since
116 the PAD will be larger than alloc_len (0) for zeroed structures */
118 /* grow if we don't have enough for the requested size plus padding */
119 if (size + DAV_BUFFER_PAD > pbuf->alloc_len) {
120 /* set the new length; min of MINSIZE */
121 pbuf->alloc_len = size + DAV_BUFFER_PAD;
122 if (pbuf->alloc_len < DAV_BUFFER_MINSIZE)
123 pbuf->alloc_len = DAV_BUFFER_MINSIZE;
125 pbuf->buf = apr_palloc(p, pbuf->alloc_len);
127 pbuf->cur_len = size;
131 /* initialize a buffer and copy the specified (null-term'd) string into it */
132 void dav_buffer_init(apr_pool_t *p, dav_buffer *pbuf, const char *str)
134 dav_set_bufsize(p, pbuf, strlen(str));
135 memcpy(pbuf->buf, str, pbuf->cur_len + 1);
138 /* append a string to the end of the buffer, adjust length */
139 void dav_buffer_append(apr_pool_t *p, dav_buffer *pbuf, const char *str)
141 size_t len = strlen(str);
143 dav_check_bufsize(p, pbuf, len + 1);
144 memcpy(pbuf->buf + pbuf->cur_len, str, len + 1);
145 pbuf->cur_len += len;
148 /* place a string on the end of the buffer, do NOT adjust length */
149 void dav_buffer_place(apr_pool_t *p, dav_buffer *pbuf, const char *str)
151 size_t len = strlen(str);
153 dav_check_bufsize(p, pbuf, len + 1);
154 memcpy(pbuf->buf + pbuf->cur_len, str, len + 1);
157 /* place some memory on the end of a buffer; do NOT adjust length */
158 void dav_buffer_place_mem(apr_pool_t *p, dav_buffer *pbuf, const void *mem,
159 size_t amt, size_t pad)
161 dav_check_bufsize(p, pbuf, amt + pad);
162 memcpy(pbuf->buf + pbuf->cur_len, mem, amt);
168 ** Extension for ap_sub_req_lookup_uri() which can't handle absolute
171 ** If NULL is returned, then an error occurred with parsing the URI or
172 ** the URI does not match the current server.
174 dav_lookup_result dav_lookup_uri(const char *uri, request_rec * r)
176 dav_lookup_result result = { 0 };
178 unsigned short port = ntohs(r->connection->local_addr.sin_port);
183 /* first thing to do is parse the URI into various components */
184 if (ap_parse_uri_components(r->pool, uri, &comp) != HTTP_OK) {
185 result.err.status = HTTP_BAD_REQUEST;
186 result.err.desc = "Invalid syntax in Destination URI.";
190 /* the URI must be an absoluteURI (WEBDAV S9.3) */
191 if (comp.scheme == NULL) {
192 result.err.status = HTTP_BAD_REQUEST;
193 result.err.desc = "Destination URI must be an absolute URI.";
197 /* ### not sure this works if the current request came in via https: */
198 scheme = r->parsed_uri.scheme;
200 scheme = ap_http_method(r);
202 /* insert a port if the URI did not contain one */
204 comp.port = ap_default_port_for_scheme(comp.scheme);
206 /* now, verify that the URI uses the same scheme as the current request.
207 the port, must match our port.
208 the URI must not have a query (args) or a fragment
210 if (strcasecmp(comp.scheme, scheme) != 0 ||
212 result.err.status = HTTP_BAD_GATEWAY;
213 result.err.desc = apr_psprintf(r->pool,
214 "Destination URI refers to different "
215 "scheme or port (%s://hostname:%d)"
216 APR_EOL_STR "(want: %s://hostname:%d)",
217 comp.scheme ? comp.scheme : scheme,
218 comp.port ? comp.port : port,
222 if (comp.query != NULL || comp.fragment != NULL) {
223 result.err.status = HTTP_BAD_REQUEST;
225 "Destination URI contains invalid components "
226 "(a query or a fragment).";
230 /* we have verified the scheme, port, and general structure */
233 ** Hrm. IE5 will pass unqualified hostnames for both the
234 ** Host: and Destination: headers. This breaks the
235 ** http_vhost.c::matches_aliases function.
237 ** For now, qualify unqualified comp.hostnames with
238 ** r->server->server_hostname.
240 ** ### this is a big hack. Apache should provide a better way.
241 ** ### maybe the admin should list the unqualified hosts in a
242 ** ### <ServerAlias> block?
244 if (strrchr(comp.hostname, '.') == NULL &&
245 (domain = strchr(r->server->server_hostname, '.')) != NULL) {
246 comp.hostname = apr_pstrcat(r->pool, comp.hostname, domain, NULL);
249 /* now, if a hostname was provided, then verify that it represents the
250 same server as the current connection. note that we just use our
251 port, since we've verified the URI matches ours */
252 if (comp.hostname != NULL &&
253 !ap_matches_request_vhost(r, comp.hostname, port)) {
254 result.err.status = HTTP_BAD_GATEWAY;
255 result.err.desc = "Destination URI refers to a different server.";
259 /* we have verified that the requested URI denotes the same server as
260 the current request. Therefore, we can use ap_sub_req_lookup_uri() */
262 /* reconstruct a URI as just the path */
263 new_file = ap_unparse_uri_components(r->pool, &comp, UNP_OMITSITEPART);
266 * Lookup the URI and return the sub-request. Note that we use the
267 * same HTTP method on the destination. This allows the destination
268 * to apply appropriate restrictions (e.g. readonly).
270 result.rnew = ap_sub_req_method_uri(r->method, new_file, r);
275 /* ---------------------------------------------------------------
277 ** XML UTILITY FUNCTIONS
280 /* validate that the root element uses a given DAV: tagname (TRUE==valid) */
281 int dav_validate_root(const ap_xml_doc *doc, const char *tagname)
284 doc->root->ns == AP_XML_NS_DAV_ID &&
285 strcmp(doc->root->name, tagname) == 0;
288 /* find and return the (unique) child with a given DAV: tagname */
289 ap_xml_elem *dav_find_child(const ap_xml_elem *elem, const char *tagname)
291 ap_xml_elem *child = elem->first_child;
293 for (; child; child = child->next)
294 if (child->ns == AP_XML_NS_DAV_ID && !strcmp(child->name, tagname))
299 /* ---------------------------------------------------------------
301 ** Timeout header processing
305 /* dav_get_timeout: If the Timeout: header exists, return a time_t
306 * when this lock is expected to expire. Otherwise, return
307 * a time_t of DAV_TIMEOUT_INFINITE.
309 * It's unclear if DAV clients are required to understand
310 * Seconds-xxx and Infinity time values. We assume that they do.
311 * In addition, for now, that's all we understand, too.
313 time_t dav_get_timeout(request_rec *r)
315 time_t now, expires = DAV_TIMEOUT_INFINITE;
317 const char *timeout_const = apr_table_get(r->headers_in, "Timeout");
318 const char *timeout = apr_pstrdup(r->pool, timeout_const), *val;
321 return DAV_TIMEOUT_INFINITE;
323 /* Use the first thing we understand, or infinity if
324 * we don't understand anything.
327 while ((val = ap_getword_white(r->pool, &timeout)) && strlen(val)) {
328 if (!strncmp(val, "Infinite", 8)) {
329 return DAV_TIMEOUT_INFINITE;
332 if (!strncmp(val, "Second-", 7)) {
334 /* ### We need to handle overflow better:
335 * ### timeout will be <= 2^32 - 1
339 return now + expires;
343 return DAV_TIMEOUT_INFINITE;
346 /* ---------------------------------------------------------------
348 ** If Header processing
352 /* add_if_resource returns a new if_header, linking it to next_ih.
354 static dav_if_header *dav_add_if_resource(apr_pool_t *p, dav_if_header *next_ih,
355 const char *uri, size_t uri_len)
359 if ((ih = apr_pcalloc(p, sizeof(*ih))) == NULL)
363 ih->uri_len = uri_len;
369 /* add_if_state adds a condition to an if_header.
371 static dav_error * dav_add_if_state(apr_pool_t *p, dav_if_header *ih,
372 const char *state_token,
373 dav_if_state_type t, int condition,
374 const dav_hooks_locks *locks_hooks)
376 dav_if_state_list *new_sl;
378 new_sl = apr_pcalloc(p, sizeof(*new_sl));
380 new_sl->condition = condition;
383 if (t == dav_if_opaquelock) {
386 if ((err = (*locks_hooks->parse_locktoken)(p, state_token,
387 &new_sl->locktoken)) != NULL) {
388 /* ### maybe add a higher-level description */
393 new_sl->etag = state_token;
395 new_sl->next = ih->state;
401 /* fetch_next_token returns the substring from str+1
402 * to the next occurence of char term, or \0, whichever
403 * occurs first. Leading whitespace is ignored.
405 static char *dav_fetch_next_token(char **str, char term)
412 while (*token && (*token == ' ' || *token == '\t'))
415 if ((sp = strchr(token, term)) == NULL)
423 /* dav_process_if_header:
425 * If NULL (no error) is returned, then **if_header points to the
426 * "If" productions structure (or NULL if "If" is not present).
428 * ### this part is bogus:
429 * If an error is encountered, the error is logged. Parent should
430 * return err->status.
432 static dav_error * dav_process_if_header(request_rec *r, dav_if_header **p_ih)
437 const char *state_token;
438 const char *uri = NULL; /* scope of current production; NULL=no-tag */
440 dav_if_header *ih = NULL;
441 uri_components parsed_uri;
442 const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r);
443 enum {no_tagged, tagged, unknown} list_type = unknown;
448 if ((str = apr_pstrdup(r->pool, apr_table_get(r->headers_in, "If"))) == NULL)
454 /* Tagged-list production - following states apply to this uri */
455 if (list_type == no_tagged
456 || ((uri = dav_fetch_next_token(&str, '>')) == NULL)) {
457 return dav_new_error(r->pool, HTTP_BAD_REQUEST,
459 "Invalid If-header: unclosed \"<\" or "
460 "unexpected tagged-list production.");
463 /* 2518 specifies this must be an absolute URI; just take the
464 * relative part for later comparison against r->uri */
465 if (ap_parse_uri_components(r->pool, uri, &parsed_uri) != HTTP_OK) {
466 return dav_new_error(r->pool, HTTP_BAD_REQUEST,
468 "Invalid URI in tagged If-header.");
470 /* note that parsed_uri.path is allocated; we can trash it */
472 /* clean up the URI a bit */
473 ap_getparents(parsed_uri.path);
474 uri_len = strlen(parsed_uri.path);
475 if (uri_len > 1 && parsed_uri.path[uri_len - 1] == '/')
476 parsed_uri.path[--uri_len] = '\0';
478 uri = parsed_uri.path;
483 /* List production */
485 /* If a uri has not been encountered, this is a No-Tagged-List */
486 if (list_type == unknown)
487 list_type = no_tagged;
489 if ((list = dav_fetch_next_token(&str, ')')) == NULL) {
490 return dav_new_error(r->pool, HTTP_BAD_REQUEST,
491 DAV_ERR_IF_UNCLOSED_PAREN,
492 "Invalid If-header: unclosed \"(\".");
495 if ((ih = dav_add_if_resource(r->pool, ih, uri, uri_len)) == NULL) {
496 /* ### dav_add_if_resource() should return an error for us! */
497 return dav_new_error(r->pool, HTTP_BAD_REQUEST,
499 "Internal server error parsing \"If:\" "
503 condition = DAV_IF_COND_NORMAL;
506 /* List is the entire production (in a uri scope) */
510 if ((state_token = dav_fetch_next_token(&list, '>')) == NULL) {
511 /* ### add a description to this error */
512 return dav_new_error(r->pool, HTTP_BAD_REQUEST,
513 DAV_ERR_IF_PARSE, NULL);
516 if ((err = dav_add_if_state(r->pool, ih, state_token, dav_if_opaquelock,
517 condition, locks_hooks)) != NULL) {
518 /* ### maybe add a higher level description */
521 condition = DAV_IF_COND_NORMAL;
525 if ((state_token = dav_fetch_next_token(&list, ']')) == NULL) {
526 /* ### add a description to this error */
527 return dav_new_error(r->pool, HTTP_BAD_REQUEST,
528 DAV_ERR_IF_PARSE, NULL);
531 if ((err = dav_add_if_state(r->pool, ih, state_token, dav_if_etag,
532 condition, locks_hooks)) != NULL) {
533 /* ### maybe add a higher level description */
536 condition = DAV_IF_COND_NORMAL;
540 if (list[1] == 'o' && list[2] == 't') {
541 if (condition != DAV_IF_COND_NORMAL) {
542 return dav_new_error(r->pool, HTTP_BAD_REQUEST,
543 DAV_ERR_IF_MULTIPLE_NOT,
544 "Invalid \"If:\" header: "
545 "Multiple \"not\" entries "
546 "for the same state.");
548 condition = DAV_IF_COND_NOT;
558 return dav_new_error(r->pool, HTTP_BAD_REQUEST,
560 apr_psprintf(r->pool,
562 "header: Unexpected "
563 "character encountered "
577 return dav_new_error(r->pool, HTTP_BAD_REQUEST,
579 apr_psprintf(r->pool,
580 "Invalid \"If:\" header: "
581 "Unexpected character "
582 "encountered (0x%02x, '%c').",
593 static int dav_find_submitted_locktoken(const dav_if_header *if_header,
594 const dav_lock *lock_list,
595 const dav_hooks_locks *locks_hooks)
597 for (; if_header != NULL; if_header = if_header->next) {
598 const dav_if_state_list *state_list;
600 for (state_list = if_header->state;
602 state_list = state_list->next) {
604 if (state_list->type == dav_if_opaquelock) {
605 const dav_lock *lock;
607 /* given state_list->locktoken, match it */
610 ** The resource will have one or more lock tokens. We only
611 ** need to match one of them against any token in the
614 ** One token case: It is an exclusive or shared lock. Either
615 ** way, we must find it.
617 ** N token case: They are shared locks. By policy, we need
618 ** to match only one. The resource's other
619 ** tokens may belong to somebody else (so we
620 ** shouldn't see them in the If: header anyway)
622 for (lock = lock_list; lock != NULL; lock = lock->next) {
624 if (!(*locks_hooks->compare_locktoken)(state_list->locktoken, lock->locktoken)) {
635 /* dav_validate_resource_state:
636 * Returns NULL if path/uri meets if-header and lock requirements
638 static dav_error * dav_validate_resource_state(apr_pool_t *p,
639 const dav_resource *resource,
641 const dav_if_header *if_header,
649 const dav_hooks_locks *locks_hooks = (lockdb ? lockdb->hooks : NULL);
650 const dav_if_header *ifhdr_scan;
651 dav_if_state_list *state_list;
658 const char *reason = NULL;
660 /* DBG1("validate: <%s>", resource->uri); */
663 ** The resource will have one of three states:
665 ** 1) No locks. We have no special requirements that the user supply
666 ** specific locktokens. One of the state lists must match, and
669 ** 2) One exclusive lock. The locktoken must appear *anywhere* in the
670 ** If: header. Of course, asserting the token in a "Not" term will
671 ** quickly fail that state list :-). If the locktoken appears in
672 ** one of the state lists *and* one state list matches, then we're
675 ** 3) One or more shared locks. One of the locktokens must appear
676 ** *anywhere* in the If: header. If one of the locktokens appears,
677 ** and we match one state list, then we are done.
679 ** The <seen_locktoken> variable determines whether we have seen one
680 ** of this resource's locktokens in the If: header.
684 ** If this is a new lock request, <flags> will contain the requested
685 ** lock scope. Three rules apply:
687 ** 1) Do not require a (shared) locktoken to be seen (when we are
688 ** applying another shared lock)
689 ** 2) If the scope is exclusive and we see any locks, fail.
690 ** 3) If the scope is shared and we see an exclusive lock, fail.
693 if (lockdb == NULL) {
694 /* we're in State 1. no locks. */
699 ** ### hrm... we don't need to have these fully
700 ** ### resolved since we're only looking at the
703 ** ### use get_locks w/ calltype=PARTIAL
705 if ((err = dav_lock_query(lockdb, resource, &lock_list)) != NULL) {
706 return dav_push_error(p,
707 HTTP_INTERNAL_SERVER_ERROR, 0,
708 "The locks could not be queried for "
709 "verification against a possible \"If:\" "
714 /* lock_list now determines whether we're in State 1, 2, or 3. */
718 ** For a new, exclusive lock: if any locks exist, fail.
719 ** For a new, shared lock: if an exclusive lock exists, fail.
720 ** else, do not require a token to be seen.
722 if (flags & DAV_LOCKSCOPE_EXCLUSIVE) {
723 if (lock_list != NULL) {
724 return dav_new_error(p, HTTP_LOCKED, 0,
725 "Existing lock(s) on the requested resource "
726 "prevent an exclusive lock.");
730 ** There are no locks, so we can pretend that we've already met
731 ** any requirement to find the resource's locks in an If: header.
735 else if (flags & DAV_LOCKSCOPE_SHARED) {
737 ** Strictly speaking, we don't need this loop. Either the first
738 ** (and only) lock will be EXCLUSIVE, or none of them will be.
740 for (lock = lock_list; lock != NULL; lock = lock->next) {
741 if (lock->scope == DAV_LOCKSCOPE_EXCLUSIVE) {
742 return dav_new_error(p, HTTP_LOCKED, 0,
743 "The requested resource is already "
744 "locked exclusively.");
749 ** The locks on the resource (if any) are all shared. Set the
750 ** <seen_locktoken> flag to indicate that we do not need to find
751 ** the locks in an If: header.
757 ** For methods other than LOCK:
759 ** If we have no locks, then <seen_locktoken> can be set to true --
760 ** pretending that we've already met the requirement of seeing one
761 ** of the resource's locks in the If: header.
763 ** Otherwise, it must be cleared and we'll look for one.
765 seen_locktoken = (lock_list == NULL);
769 ** If there is no If: header, then we can shortcut some logic:
771 ** 1) if we do not need to find a locktoken in the (non-existent) If:
772 ** header, then we are successful.
774 ** 2) if we must find a locktoken in the (non-existent) If: header, then
777 if (if_header == NULL) {
781 return dav_new_error(p, HTTP_LOCKED, 0,
782 "This resource is locked and an \"If:\" header "
783 "was not supplied to allow access to the "
786 /* the If: header is present */
789 ** If a dummy header is present (because of a Lock-Token: header), then
790 ** we are required to find that token in this resource's set of locks.
791 ** If we have no locks, then we immediately fail.
793 ** This is a 400 (Bad Request) since they should only submit a locktoken
794 ** that actually exists.
796 ** Don't issue this response if we're talking about the parent resource.
797 ** It is okay for that resource to NOT have this locktoken.
798 ** (in fact, it certainly will not: a dummy_header only occurs for the
799 ** UNLOCK method, the parent is checked only for locknull resources,
800 ** and the parent certainly does not have the (locknull's) locktoken)
802 if (lock_list == NULL && if_header->dummy_header) {
803 if (flags & DAV_VALIDATE_IS_PARENT)
805 return dav_new_error(p, HTTP_BAD_REQUEST, 0,
806 "The locktoken specified in the \"Lock-Token:\" "
807 "header is invalid because this resource has no "
808 "outstanding locks.");
812 ** Prepare the input URI. We want the URI to never have a trailing slash.
814 ** When URIs are placed into the dav_if_header structure, they are
815 ** guaranteed to never have a trailing slash. If the URIs are equivalent,
816 ** then it doesn't matter if they both lack a trailing slash -- they're
819 ** Note: we could also ensure that a trailing slash is present on both
820 ** URIs, but the majority of URIs provided to us via a resource walk
821 ** will not contain that trailing slash.
824 uri_len = strlen(uri);
825 if (uri[uri_len - 1] == '/') {
826 dav_set_bufsize(p, pbuf, uri_len);
827 memcpy(pbuf->buf, uri, uri_len);
828 pbuf->buf[--uri_len] = '\0';
832 /* get the resource's etag; we may need it during the checks */
833 etag = (*resource->hooks->getetag)(resource);
835 /* how many state_lists apply to this URI? */
838 /* If there are if-headers, fail if this resource
839 * does not match at least one state_list.
841 for (ifhdr_scan = if_header;
843 ifhdr_scan = ifhdr_scan->next) {
845 /* DBG2("uri=<%s> if_uri=<%s>", uri, ifhdr_scan->uri ? ifhdr_scan->uri : "(no uri)"); */
847 if (ifhdr_scan->uri != NULL
848 && (uri_len != ifhdr_scan->uri_len
849 || memcmp(uri, ifhdr_scan->uri, uri_len) != 0)) {
851 ** A tagged-list's URI doesn't match this resource's URI.
852 ** Skip to the next state_list to see if it will match.
857 /* this state_list applies to this resource */
860 ** ### only one state_list should ever apply! a no-tag, or a tagged
861 ** ### where S9.4.2 states only one can match.
863 ** ### revamp this code to loop thru ifhdr_scan until we find the
864 ** ### matching state_list. process it. stop.
868 /* To succeed, resource must match *all* of the states
869 * specified in the state_list.
871 for (state_list = ifhdr_scan->state;
873 state_list = state_list->next) {
875 switch(state_list->type) {
878 int mismatch = strcmp(state_list->etag, etag);
880 if (state_list->condition == DAV_IF_COND_NORMAL && mismatch) {
882 ** The specified entity-tag does not match the
883 ** entity-tag on the resource. This state_list is
884 ** not going to match. Bust outta here.
887 "an entity-tag was specified, but the resource's "
888 "actual ETag does not match.";
889 goto state_list_failed;
891 else if (state_list->condition == DAV_IF_COND_NOT
894 ** The specified entity-tag DOES match the
895 ** entity-tag on the resource. This state_list is
896 ** not going to match. Bust outta here.
899 "an entity-tag was specified using the \"Not\" form, "
900 "but the resource's actual ETag matches the provided "
902 goto state_list_failed;
907 case dav_if_opaquelock:
908 if (lockdb == NULL) {
909 if (state_list->condition == DAV_IF_COND_NOT) {
910 /* the locktoken is definitely not there! (success) */
914 /* condition == DAV_IF_COND_NORMAL */
917 ** If no lockdb is provided, then validation fails for
918 ** this state_list (NORMAL means we were supposed to
919 ** find the token, which we obviously cannot do without
922 ** Go and try the next state list.
925 "a State-token was supplied, but a lock database "
926 "is not available for to provide the required lock.";
927 goto state_list_failed;
930 /* Resource validation 'fails' if:
931 * ANY of the lock->locktokens match
932 * a NOT state_list->locktoken,
934 * NONE of the lock->locktokens match
935 * a NORMAL state_list->locktoken.
938 for (lock = lock_list; lock != NULL; lock = lock->next) {
941 DBG2("compare: rsrc=%s ifhdr=%s",
942 (*locks_hooks->format_locktoken)(p, lock->locktoken),
943 (*locks_hooks->format_locktoken)(p, state_list->locktoken));
946 /* nothing to do if the locktokens do not match. */
947 if ((*locks_hooks->compare_locktoken)(state_list->locktoken, lock->locktoken)) {
952 ** We have now matched up one of the resource's locktokens
953 ** to a locktoken in a State-token in the If: header.
954 ** Note this fact, so that we can pass the overall
955 ** requirement of seeing at least one of the resource's
960 if (state_list->condition == DAV_IF_COND_NOT) {
962 ** This state requires that the specified locktoken
963 ** is NOT present on the resource. But we just found
964 ** it. There is no way this state-list can now
965 ** succeed, so go try another one.
968 "a State-token was supplied, which used a "
969 "\"Not\" condition. The State-token was found "
970 "in the locks on this resource";
971 goto state_list_failed;
974 /* condition == DAV_IF_COND_NORMAL */
976 /* Validate auth_user: If an authenticated user created
977 ** the lock, only the same user may submit that locktoken
978 ** to manipulate a resource.
980 if (lock->auth_user &&
982 strcmp(lock->auth_user, r->user))) {
985 errmsg = apr_pstrcat(p, "User \"",
987 "\" submitted a locktoken created "
989 lock->auth_user, "\".", NULL);
990 return dav_new_error(p, HTTP_UNAUTHORIZED, 0, errmsg);
994 ** We just matched a specified State-Token to one of the
995 ** resource's locktokens.
997 ** Break out of the lock scan -- we only needed to find
998 ** one match (actually, there shouldn't be any other
999 ** matches in the lock list).
1005 if (num_matched == 0
1006 && state_list->condition == DAV_IF_COND_NORMAL) {
1008 ** We had a NORMAL state, meaning that we should have
1009 ** found the State-Token within the locks on this
1010 ** resource. We didn't, so this state_list must fail.
1013 "a State-token was supplied, but it was not found "
1014 "in the locks on this resource.";
1015 goto state_list_failed;
1021 } /* foreach ( state_list ) */
1024 ** We've checked every state in this state_list and none of them
1025 ** have failed. Since all of them succeeded, then we have a matching
1026 ** state list and we may be done.
1028 ** The next requirement is that we have seen one of the resource's
1029 ** locktokens (if any). If we have, then we can just exit. If we
1030 ** haven't, then we need to keep looking.
1032 if (seen_locktoken) {
1038 ** Haven't seen one. Let's break out of the search and just look
1039 ** for a matching locktoken.
1044 ** This label is used when we detect that a state_list is not
1045 ** going to match this resource. We bust out and try the next
1051 } /* foreach ( ifhdr_scan ) */
1054 ** The above loop exits for one of two reasons:
1055 ** 1) a state_list matched and seen_locktoken is false.
1056 ** 2) all if_header structures were scanned, without (1) occurring
1059 if (ifhdr_scan == NULL) {
1061 ** We finished the loop without finding any matching state lists.
1065 ** If none of the state_lists apply to this resource, then we
1066 ** may have succeeded. Note that this scenario implies a
1067 ** tagged-list with no matching state_lists. If the If: header
1068 ** was a no-tag-list, then it would have applied to this resource.
1070 ** S9.4.2 states that when no state_lists apply, then the header
1071 ** should be ignored.
1073 ** If we saw one of the resource's locktokens, then we're done.
1074 ** If we did not see a locktoken, then we fail.
1076 if (num_that_apply == 0) {
1081 ** We may have aborted the scan before seeing the locktoken.
1082 ** Rescan the If: header to see if we can find the locktoken
1085 if (dav_find_submitted_locktoken(if_header, lock_list,
1088 ** We found a match! We're set... none of the If: header
1089 ** assertions apply (implicit success), and the If: header
1090 ** specified the locktoken somewhere. We're done.
1095 return dav_new_error(p, HTTP_LOCKED, 0 /* error_id */,
1096 "This resource is locked and the \"If:\" "
1097 "header did not specify one of the "
1098 "locktokens for this resource's lock(s).");
1100 /* else: one or more state_lists were applicable, but failed. */
1103 ** If the dummy_header did not match, then they specified an
1104 ** incorrect token in the Lock-Token header. Forget whether the
1105 ** If: statement matched or not... we'll tell them about the
1106 ** bad Lock-Token first. That is considered a 400 (Bad Request).
1108 if (if_header->dummy_header) {
1109 return dav_new_error(p, HTTP_BAD_REQUEST, 0,
1110 "The locktoken specified in the "
1111 "\"Lock-Token:\" header did not specify one "
1112 "of this resource's locktoken(s).");
1115 if (reason == NULL) {
1116 return dav_new_error(p, HTTP_PRECONDITION_FAILED, 0,
1117 "The preconditions specified by the \"If:\" "
1118 "header did not match this resource.");
1121 return dav_new_error(p, HTTP_PRECONDITION_FAILED, 0,
1123 "The precondition(s) specified by "
1124 "the \"If:\" header did not match "
1125 "this resource. At least one "
1126 "failure is because: %s", reason));
1129 /* assert seen_locktoken == 0 */
1132 ** ifhdr_scan != NULL implies we found a matching state_list.
1134 ** Since we're still here, it also means that we have not yet found
1135 ** one the resource's locktokens in the If: header.
1137 ** Scan all the if_headers and states looking for one of this
1138 ** resource's locktokens. Note that we need to go back and scan them
1139 ** all -- we may have aborted a scan with a failure before we saw a
1142 ** Note that seen_locktoken == 0 implies lock_list != NULL which implies
1143 ** locks_hooks != NULL.
1145 if (dav_find_submitted_locktoken(if_header, lock_list, locks_hooks)) {
1147 ** We found a match! We're set... we have a matching state list,
1148 ** and the If: header specified the locktoken somewhere. We're done.
1154 ** We had a matching state list, but the user agent did not specify one
1155 ** of this resource's locktokens. Tell them so.
1157 ** Note that we need to special-case the message on whether a "dummy"
1158 ** header exists. If it exists, yet we didn't see a needed locktoken,
1159 ** then that implies the dummy header (Lock-Token header) did NOT
1160 ** specify one of this resource's locktokens. (this implies something
1161 ** in the real If: header matched)
1163 ** We want to note the 400 (Bad Request) in favor of a 423 (Locked).
1165 if (if_header->dummy_header) {
1166 return dav_new_error(p, HTTP_BAD_REQUEST, 0,
1167 "The locktoken specified in the "
1168 "\"Lock-Token:\" header did not specify one "
1169 "of this resource's locktoken(s).");
1172 return dav_new_error(p, HTTP_LOCKED, 1 /* error_id */,
1173 "This resource is locked and the \"If:\" header "
1174 "did not specify one of the "
1175 "locktokens for this resource's lock(s).");
1178 /* dav_validate_walker: Walker callback function to validate resource state */
1179 static dav_error * dav_validate_walker(dav_walker_ctx *ctx, int calltype)
1183 if ((err = dav_validate_resource_state(ctx->pool, ctx->resource,
1185 ctx->if_header, ctx->flags,
1186 &ctx->work_buf, ctx->r)) == NULL) {
1187 /* There was no error, so just bug out. */
1192 ** If we have a serious server error, or if the request itself failed,
1193 ** then just return error (not a multistatus).
1195 if (ap_is_HTTP_SERVER_ERROR(err->status)
1196 || (*ctx->resource->hooks->is_same_resource)(ctx->resource,
1198 /* ### maybe push a higher-level description? */
1202 /* associate the error with the current URI */
1203 dav_add_response(ctx, ctx->uri.buf, err->status, NULL);
1209 ** dav_validate_request: Validate if-headers (and check for locks) on:
1210 ** (1) r->filename @ depth;
1211 ** (2) Parent of r->filename if check_parent == 1
1213 ** The check of parent should be done when it is necessary to verify that
1214 ** the parent collection will accept a new member (ie current resource
1217 ** Return OK on successful validation.
1218 ** On error, return appropriate HTTP_* code, and log error. If a multi-stat
1219 ** error is necessary, response will point to it, else NULL.
1221 dav_error * dav_validate_request(request_rec *r, dav_resource *resource,
1222 int depth, dav_locktoken *locktoken,
1223 dav_response **response, int flags,
1228 dav_if_header *if_header;
1229 int lock_db_opened_locally = 0;
1230 const dav_hooks_locks *locks_hooks = DAV_GET_HOOKS_LOCKS(r);
1231 const dav_hooks_repository *repos_hooks = resource->hooks;
1232 dav_buffer work_buf = { 0 };
1233 dav_response *new_response;
1236 if (depth && response == NULL) {
1238 ** ### bleck. we can't return errors for other URIs unless we have
1239 ** ### a "response" ptr.
1241 return dav_new_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
1242 "DESIGN ERROR: dav_validate_request called "
1243 "with depth>0, but no response ptr.");
1247 if (response != NULL)
1250 /* Do the standard checks for conditional requests using
1251 * If-..-Since, If-Match etc */
1252 if ((result = ap_meets_conditions(r)) != OK) {
1253 /* ### fix this up... how? */
1254 return dav_new_error(r->pool, result, 0, NULL);
1257 /* always parse (and later process) the If: header */
1258 if ((err = dav_process_if_header(r, &if_header)) != NULL) {
1259 /* ### maybe add higher-level description */
1263 /* If a locktoken was specified, create a dummy if_header with which
1264 * to validate resources. In the interim, figure out why DAV uses
1265 * locktokens in an if-header without a Lock-Token header to refresh
1266 * locks, but a Lock-Token header without an if-header to remove them.
1268 if (locktoken != NULL) {
1269 dav_if_header *ifhdr_new;
1271 ifhdr_new = apr_pcalloc(r->pool, sizeof(*ifhdr_new));
1272 ifhdr_new->uri = resource->uri;
1273 ifhdr_new->uri_len = strlen(resource->uri);
1274 ifhdr_new->dummy_header = 1;
1276 ifhdr_new->state = apr_pcalloc(r->pool, sizeof(*ifhdr_new->state));
1277 ifhdr_new->state->type = dav_if_opaquelock;
1278 ifhdr_new->state->condition = DAV_IF_COND_NORMAL;
1279 ifhdr_new->state->locktoken = locktoken;
1281 ifhdr_new->next = if_header;
1282 if_header = ifhdr_new;
1286 ** If necessary, open the lock database (read-only, lazily);
1287 ** the validation process may need to retrieve or update lock info.
1288 ** Otherwise, assume provided lockdb is valid and opened rw.
1290 if (lockdb == NULL) {
1291 if (locks_hooks != NULL) {
1292 if ((err = (*locks_hooks->open_lockdb)(r, 0, 0, &lockdb)) != NULL) {
1293 /* ### maybe insert higher-level comment */
1296 lock_db_opened_locally = 1;
1299 return dav_new_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
1300 "Resource validation failed because no "
1301 "lock hooks were found.");
1305 /* (1) Validate the specified resource, at the specified depth */
1306 if (resource->exists && depth > 0) {
1307 dav_walker_ctx ctx = { 0 };
1309 ctx.walk_type = DAV_WALKTYPE_ALL;
1311 ctx.func = dav_validate_walker;
1313 ctx.if_header = if_header;
1316 ctx.resource = resource;
1318 if (lockdb != NULL) {
1319 ctx.lockdb = lockdb;
1320 ctx.walk_type |= DAV_WALKTYPE_LOCKNULL;
1323 dav_buffer_init(r->pool, &ctx.uri, resource->uri);
1325 err = (*repos_hooks->walk)(&ctx, DAV_INFINITY);
1327 *response = ctx.response;
1329 /* else: implies a 5xx status code occurred. */
1332 err = dav_validate_resource_state(r->pool, resource, lockdb,
1333 if_header, flags, &work_buf, r);
1336 /* (2) Validate the parent resource if requested */
1337 if (err == NULL && (flags & DAV_VALIDATE_PARENT)) {
1338 dav_resource *parent_resource = (*repos_hooks->get_parent_resource)(resource);
1340 if (parent_resource == NULL) {
1341 err = dav_new_error(r->pool, HTTP_FORBIDDEN, 0,
1342 "Cannot access parent of repository root.");
1345 err = dav_validate_resource_state(r->pool, parent_resource, lockdb,
1347 flags | DAV_VALIDATE_IS_PARENT,
1351 ** This error occurred on the parent resource. This implies that
1352 ** we have to create a multistatus response (to report the error
1353 ** against a URI other than the Request-URI). "Convert" this error
1354 ** into a multistatus response.
1357 new_response = apr_pcalloc(r->pool, sizeof(*new_response));
1359 new_response->href = parent_resource->uri;
1360 new_response->status = err->status;
1361 new_response->desc =
1362 "A validation error has occurred on the parent resource, "
1363 "preventing the operation on the resource specified by "
1365 if (err->desc != NULL) {
1366 new_response->desc = apr_pstrcat(r->pool,
1372 /* assert: DAV_VALIDATE_PARENT implies response != NULL */
1373 new_response->next = *response;
1374 *response = new_response;
1381 if (lock_db_opened_locally)
1382 (*locks_hooks->close_lockdb)(lockdb);
1385 ** If we don't have a (serious) error, and we have multistatus responses,
1386 ** then we need to construct an "error". This error will be the overall
1387 ** status returned, and the multistatus responses will go into its body.
1389 ** For certain methods, the overall error will be a 424. The default is
1390 ** to construct a standard 207 response.
1392 if (err == NULL && response != NULL && *response != NULL) {
1393 ap_text *propstat = NULL;
1395 if ((flags & DAV_VALIDATE_USE_424) != 0) {
1396 /* manufacture a 424 error to hold the multistatus response(s) */
1397 return dav_new_error(r->pool, HTTP_FAILED_DEPENDENCY, 0,
1398 "An error occurred on another resource, "
1399 "preventing the requested operation on "
1404 ** Whatever caused the error, the Request-URI should have a 424
1405 ** associated with it since we cannot complete the method.
1407 ** For a LOCK operation, insert an empty DAV:lockdiscovery property.
1408 ** For other methods, return a simple 424.
1410 if ((flags & DAV_VALIDATE_ADD_LD) != 0) {
1411 propstat = apr_pcalloc(r->pool, sizeof(*propstat));
1413 "<D:propstat>" DEBUG_CR
1414 "<D:prop><D:lockdiscovery/></D:prop>" DEBUG_CR
1415 "<D:status>HTTP/1.1 424 Failed Dependency</D:status>" DEBUG_CR
1416 "</D:propstat>" DEBUG_CR;
1419 /* create the 424 response */
1420 new_response = apr_pcalloc(r->pool, sizeof(*new_response));
1421 new_response->href = resource->uri;
1422 new_response->status = HTTP_FAILED_DEPENDENCY;
1423 new_response->propresult.propstats = propstat;
1424 new_response->desc =
1425 "An error occurred on another resource, preventing the "
1426 "requested operation on this resource.";
1428 new_response->next = *response;
1429 *response = new_response;
1431 /* manufacture a 207 error for the multistatus response(s) */
1432 return dav_new_error(r->pool, HTTP_MULTI_STATUS, 0,
1433 "Error(s) occurred on resources during the "
1434 "validation process.");
1440 /* dav_get_locktoken_list:
1442 * Sets ltl to a locktoken_list of all positive locktokens in header,
1443 * else NULL if no If-header, or no positive locktokens.
1445 dav_error * dav_get_locktoken_list(request_rec *r, dav_locktoken_list **ltl)
1448 dav_if_header *if_header;
1449 dav_if_state_list *if_state;
1450 dav_locktoken_list *lock_token = NULL;
1454 if ((err = dav_process_if_header(r, &if_header)) != NULL) {
1455 /* ### add a higher-level description? */
1459 while (if_header != NULL) {
1460 if_state = if_header->state; /* Begining of the if_state linked list */
1461 while (if_state != NULL) {
1462 if (if_state->condition == DAV_IF_COND_NORMAL
1463 && if_state->type == dav_if_opaquelock) {
1464 lock_token = apr_pcalloc(r->pool, sizeof(dav_locktoken_list));
1465 lock_token->locktoken = if_state->locktoken;
1466 lock_token->next = *ltl;
1469 if_state = if_state->next;
1471 if_header = if_header->next;
1474 /* No nodes added */
1475 return dav_new_error(r->pool, HTTP_BAD_REQUEST, DAV_ERR_IF_ABSENT,
1476 "No locktokens were specified in the \"If:\" "
1477 "header, so the refresh could not be performed.");
1483 static const char *strip_white(const char *s, apr_pool_t *pool)
1487 /* trim leading whitespace */
1488 while (apr_isspace(*s)) /* assume: return false for '\0' */
1491 /* trim trailing whitespace */
1492 idx = strlen(s) - 1;
1493 if (apr_isspace(s[idx])) {
1494 char *s2 = apr_pstrdup(pool, s);
1496 while (apr_isspace(s2[idx]) && idx > 0)
1505 /* see mod_dav.h for docco */
1506 const char *dav_get_target_selector(request_rec *r, const ap_xml_elem *version)
1508 if (version != NULL) {
1509 /* DAV:version contains a DAV:href element. find it. */
1510 if ((version = dav_find_child(version, "href")) == NULL) {
1511 /* ### this should generate an error... fallthru for now */
1514 /* return the contents of the DAV:href element */
1515 /* ### this presumes no child elements */
1516 return strip_white(version->first_cdata.first->text, r->pool);
1520 /* no element. see if a Target-Selector header was provided. */
1521 return apr_table_get(r->headers_in, "Target-Selector");
1524 /* see mod_dav.h for docco */
1525 dav_error *dav_ensure_resource_writable(request_rec *r,
1526 dav_resource *resource,
1528 dav_resource **parent_resource,
1529 int *resource_existed,
1530 int *resource_was_writable,
1531 int *parent_was_writable)
1533 const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r);
1534 dav_resource *parent = NULL;
1538 const char *location;
1540 if (parent_resource != NULL)
1541 *parent_resource = NULL;
1544 *resource_existed = resource->exists;
1545 *resource_was_writable = 0;
1548 if (parent_was_writable != NULL)
1549 *parent_was_writable = 0;
1551 /* if a Target-Selector header is present, then the client knows about
1552 * versioning, so it should not be relying on implicit versioning
1554 auto_version = (dav_get_target_selector(r, NULL) == NULL);
1556 /* check parent resource if requested or if resource must be created */
1557 if (!resource->exists || parent_only) {
1558 parent = (*resource->hooks->get_parent_resource)(resource);
1559 if (parent == NULL || !parent->exists) {
1560 body = apr_psprintf(r->pool,
1561 "Missing one or more intermediate collections. "
1562 "Cannot create resource %s.",
1563 ap_escape_html(r->pool, resource->uri));
1564 return dav_new_error(r->pool, HTTP_CONFLICT, 0, body);
1567 if (parent_resource != NULL)
1568 *parent_resource = parent;
1570 /* if parent not versioned, assume child can be created */
1571 if (!parent->versioned) {
1573 *resource_was_writable = 1;
1575 if (parent_was_writable != NULL)
1576 *parent_was_writable = 1;
1580 /* if no versioning provider, something is terribly wrong */
1581 if (vsn_hooks == NULL) {
1582 return dav_new_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
1584 "versioned resource with no versioning "
1588 /* remember whether parent was already writable */
1589 if (parent_was_writable != NULL)
1590 *parent_was_writable = parent->working;
1592 /* parent must be checked out */
1593 if (!parent->working) {
1594 if ((err = (*vsn_hooks->checkout)(parent, &location)) != NULL) {
1595 body = apr_psprintf(r->pool,
1596 "Unable to checkout parent collection. "
1597 "Cannot create resource %s.",
1598 ap_escape_html(r->pool, resource->uri));
1599 return dav_push_error(r->pool, HTTP_CONFLICT, 0, body, err);
1602 /* ### what to do with the location? */
1605 /* if not just checking parent, create new child resource */
1607 if ((err = (*vsn_hooks->mkresource)(resource)) != NULL) {
1608 body = apr_psprintf(r->pool,
1609 "Unable to create versioned resource %s.",
1610 ap_escape_html(r->pool, resource->uri));
1611 return dav_push_error(r->pool, HTTP_CONFLICT, 0, body, err);
1616 /* resource exists: if not versioned, then assume it is writable */
1617 if (!resource->versioned) {
1618 *resource_was_writable = 1;
1622 *resource_was_writable = resource->working;
1625 /* if not just checking parent, make sure child resource is checked out */
1626 if (!parent_only && !resource->working) {
1627 if ((err = (*vsn_hooks->checkout)(resource, &location)) != NULL) {
1628 body = apr_psprintf(r->pool,
1629 "Unable to checkout resource %s.",
1630 ap_escape_html(r->pool, resource->uri));
1631 return dav_push_error(r->pool, HTTP_CONFLICT, 0, body, err);
1634 /* ### what to do with the location? */
1640 /* see mod_dav.h for docco */
1641 dav_error *dav_revert_resource_writability(request_rec *r,
1642 dav_resource *resource,
1643 dav_resource *parent_resource,
1645 int resource_existed,
1646 int resource_was_writable,
1647 int parent_was_writable)
1649 const dav_hooks_vsn *vsn_hooks = DAV_GET_HOOKS_VSN(r);
1653 if (resource != NULL) {
1654 if (!resource_was_writable
1655 && resource->versioned && resource->working) {
1658 err = (*vsn_hooks->uncheckout)(resource);
1660 err = (*vsn_hooks->checkin)(resource);
1663 body = apr_psprintf(r->pool,
1664 "Unable to %s resource %s.",
1665 undo ? "uncheckout" : "checkin",
1666 ap_escape_html(r->pool, resource->uri));
1667 return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
1672 if (undo && !resource_existed && resource->exists) {
1673 dav_response *response;
1675 /* ### should we do anything with the response? */
1676 if ((err = (*resource->hooks->remove_resource)(resource,
1677 &response)) != NULL) {
1678 body = apr_psprintf(r->pool,
1679 "Unable to undo creation of resource %s.",
1680 ap_escape_html(r->pool, resource->uri));
1681 return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
1687 if (parent_resource != NULL && !parent_was_writable
1688 && parent_resource->versioned && parent_resource->working) {
1691 err = (*vsn_hooks->uncheckout)(parent_resource);
1693 err = (*vsn_hooks->checkin)(parent_resource);
1696 body = apr_psprintf(r->pool,
1697 "Unable to %s parent collection of %s.",
1698 undo ? "uncheckout" : "checkin",
1699 ap_escape_html(r->pool, resource->uri));
1700 return dav_push_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,