| /* |
| * repos.c: mod_dav_svn repository provider functions for Subversion |
| * |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| */ |
| |
| #define APR_WANT_STRFUNC |
| #include <apr_want.h> |
| #include <apr_strings.h> |
| #include <apr_hash.h> |
| #include <apr_lib.h> |
| |
| #include <httpd.h> |
| #include <http_request.h> |
| #include <http_protocol.h> |
| #include <http_log.h> |
| #include <http_core.h> /* for ap_construct_url */ |
| #include <mod_dav.h> |
| |
| #define CORE_PRIVATE /* To make ap_show_mpm public in 2.2 */ |
| #include <http_config.h> |
| |
| #include "svn_hash.h" |
| #include "svn_types.h" |
| #include "svn_pools.h" |
| #include "svn_error.h" |
| #include "svn_time.h" |
| #include "svn_fs.h" |
| #include "svn_repos.h" |
| #include "svn_dav.h" |
| #include "svn_sorts.h" |
| #include "svn_version.h" |
| #include "svn_props.h" |
| #include "svn_ctype.h" |
| #include "svn_subst.h" |
| #include "mod_dav_svn.h" |
| #include "svn_ra.h" /* for SVN_RA_CAPABILITY_* */ |
| #include "svn_dirent_uri.h" |
| #include "private/svn_log.h" |
| #include "private/svn_fspath.h" |
| #include "private/svn_repos_private.h" |
| #include "private/svn_sorts_private.h" |
| |
| #include "dav_svn.h" |
| |
| |
| #define DEFAULT_ACTIVITY_DB "dav/activities.d" |
| |
| |
| struct dav_stream { |
| const dav_resource *res; |
| |
| /* for reading from the FS */ |
| svn_stream_t *rstream; |
| |
| /* for writing to the FS. we use wstream OR the handler/baton. */ |
| svn_stream_t *wstream; |
| svn_txdelta_window_handler_t delta_handler; |
| void *delta_baton; |
| }; |
| |
| |
| /* Convenience structure that facilitates combined memory allocation of |
| a dav_resource and dav_resource_private pair. */ |
| typedef struct dav_resource_combined { |
| dav_resource res; |
| dav_resource_private priv; |
| } dav_resource_combined; |
| |
| |
| /* Helper-wrapper around svn_fs_check_path(), which takes the same |
| arguments. But: if we attempt to stat a path like "file1/file2", |
| then still return 'svn_node_none' to signal nonexistence, rather |
| than a full-blown filesystem error. This allows mod_dav to throw |
| 404 instead of 500. */ |
| static dav_error * |
| fs_check_path(svn_node_kind_t *kind, |
| svn_fs_root_t *root, |
| const char *path, |
| apr_pool_t *pool) |
| { |
| svn_error_t *serr; |
| svn_node_kind_t my_kind; |
| |
| serr = svn_fs_check_path(&my_kind, root, path, pool); |
| |
| /* Possibly trap other fs-errors here someday -- errors which may |
| simply indicate the path's nonexistence, rather than a critical |
| problem. */ |
| if (serr && serr->apr_err == SVN_ERR_FS_NOT_DIRECTORY) |
| { |
| svn_error_clear(serr); |
| *kind = svn_node_none; |
| return NULL; |
| } |
| else if (serr) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| apr_psprintf(pool, "Error checking kind of " |
| "path '%s' in repository", |
| path), |
| pool); |
| } |
| |
| *kind = my_kind; |
| return NULL; |
| } |
| |
| |
| static int |
| parse_version_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| const char *slash; |
| const char *created_rev_str; |
| |
| /* format: CREATED_REV/REPOS_PATH */ |
| |
| /* ### what to do with LABEL and USE_CHECKED_IN ?? */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_VERSION; |
| comb->res.versioned = TRUE; |
| |
| slash = ap_strchr_c(path, '/'); |
| if (slash == NULL) |
| { |
| /* http://host.name/repos/$svn/ver/0 |
| |
| This URL form refers to the root path of the repository. |
| */ |
| created_rev_str = apr_pstrndup(comb->res.pool, path, strlen(path)); |
| comb->priv.root.rev = SVN_STR_TO_REV(created_rev_str); |
| comb->priv.repos_path = "/"; |
| } |
| else if (slash == path) |
| { |
| /* the CREATED_REV was missing(?) |
| |
| ### not sure this can happen, though, because it would imply two |
| ### slashes, yet those are cleaned out within get_resource |
| */ |
| return TRUE; |
| } |
| else |
| { |
| apr_size_t len = slash - path; |
| |
| created_rev_str = apr_pstrndup(comb->res.pool, path, len); |
| comb->priv.root.rev = SVN_STR_TO_REV(created_rev_str); |
| comb->priv.repos_path = slash; |
| } |
| |
| /* if the CREATED_REV parsing blew, then propagate it. */ |
| if (comb->priv.root.rev == SVN_INVALID_REVNUM) |
| return TRUE; |
| |
| /* We have idempotent resource. */ |
| comb->priv.idempotent = TRUE; |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_history_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| /* format: ??? */ |
| |
| /* ### what to do with LABEL and USE_CHECKED_IN ?? */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_HISTORY; |
| |
| /* ### parse path */ |
| comb->priv.repos_path = path; |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_working_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| const char *slash; |
| |
| /* format: ACTIVITY_ID/REPOS_PATH */ |
| |
| /* ### what to do with LABEL and USE_CHECKED_IN ?? */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_WORKING; |
| comb->res.working = TRUE; |
| comb->res.versioned = TRUE; |
| |
| slash = ap_strchr_c(path, '/'); |
| |
| /* This sucker starts with a slash. That's bogus. */ |
| if (slash == path) |
| return TRUE; |
| |
| if (slash == NULL) |
| { |
| /* There's no slash character in our path. Assume it's just an |
| ACTIVITY_ID pointing to the root path. That should be cool. |
| We'll just drop through to the normal case handling below. */ |
| comb->priv.root.activity_id = apr_pstrdup(comb->res.pool, path); |
| comb->priv.repos_path = "/"; |
| } |
| else |
| { |
| comb->priv.root.activity_id = apr_pstrndup(comb->res.pool, path, |
| slash - path); |
| comb->priv.repos_path = slash; |
| } |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_activity_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| /* format: ACTIVITY_ID */ |
| |
| /* ### what to do with LABEL and USE_CHECKED_IN ?? */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_ACTIVITY; |
| |
| comb->priv.root.activity_id = path; |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_vcc_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| /* format: "default" (a singleton) */ |
| |
| if (strcmp(path, DAV_SVN__DEFAULT_VCC_NAME) != 0) |
| return TRUE; |
| |
| if (label == NULL && !use_checked_in) |
| { |
| /* Version Controlled Configuration (baseline selector) */ |
| |
| /* ### mod_dav has a proper model for these. technically, they are |
| ### version-controlled resources (REGULAR), but that just monkeys |
| ### up a lot of stuff for us. use a PRIVATE for now. */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_PRIVATE; /* _REGULAR */ |
| comb->priv.restype = DAV_SVN_RESTYPE_VCC; |
| |
| comb->res.exists = TRUE; |
| comb->res.versioned = TRUE; |
| comb->res.baselined = TRUE; |
| |
| /* NOTE: comb->priv.repos_path == NULL */ |
| } |
| else |
| { |
| /* a specific Version Resource; in this case, a Baseline */ |
| |
| svn_revnum_t revnum; |
| |
| if (label != NULL) |
| { |
| revnum = SVN_STR_TO_REV(label); /* assume slash terminates */ |
| if (!SVN_IS_VALID_REVNUM(revnum)) |
| return TRUE; /* ### be nice to get better feedback */ |
| } |
| else /* use_checked_in */ |
| { |
| /* use the DAV:checked-in value of the VCC. this is always the |
| "latest" (or "youngest") revision. */ |
| |
| /* signal prep_version to look it up */ |
| revnum = SVN_INVALID_REVNUM; |
| } |
| |
| comb->res.type = DAV_RESOURCE_TYPE_VERSION; |
| |
| /* exists? need to wait for now */ |
| comb->res.versioned = TRUE; |
| comb->res.baselined = TRUE; |
| |
| /* which baseline (revision tree) to access */ |
| comb->priv.root.rev = revnum; |
| |
| /* NOTE: comb->priv.repos_path == NULL */ |
| /* NOTE: comb->priv.created_rev == SVN_INVALID_REVNUM */ |
| } |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_me_resource_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| /* In HTTP protocol v2, this uri represents the repository itself, |
| and is the place where custom REPORTs get sent to. (It replaces |
| the older vcc uri form.) It has no trailing components. */ |
| |
| if (path[0] != '\0') |
| return TRUE; |
| |
| comb->res.type = DAV_RESOURCE_TYPE_PRIVATE; |
| comb->priv.restype = DAV_SVN_RESTYPE_ME; |
| |
| /* We're keeping these the same as the VCC resource, to make things |
| smoother for our report requests. */ |
| comb->res.exists = TRUE; |
| comb->res.versioned = TRUE; |
| comb->res.baselined = TRUE; |
| /* NOTE: comb->priv.repos_path == NULL */ |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_baseline_coll_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| const char *slash; |
| svn_revnum_t revnum; |
| |
| /* format: REVISION/REPOS_PATH */ |
| |
| /* ### what to do with LABEL and USE_CHECKED_IN ?? */ |
| |
| slash = ap_strchr_c(path, '/'); |
| if (slash == NULL) |
| slash = "/"; /* they are referring to the root of the BC */ |
| else if (slash == path) |
| return TRUE; /* the REVISION was missing(?) |
| ### not sure this can happen, though, because |
| ### it would imply two slashes, yet those are |
| ### cleaned out within get_resource */ |
| |
| revnum = SVN_STR_TO_REV(path); /* assume slash terminates conversion */ |
| if (!SVN_IS_VALID_REVNUM(revnum)) |
| return TRUE; /* ### be nice to get better feedback */ |
| |
| /* ### mod_dav doesn't have a proper model for these. they are standard |
| ### VCRs, but we need some additional semantics attached to them. |
| ### need to figure out a way to label them as special. */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_REGULAR; |
| comb->res.versioned = TRUE; |
| comb->priv.root.rev = revnum; |
| comb->priv.repos_path = slash; |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_baseline_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| svn_revnum_t revnum; |
| |
| /* format: REVISION */ |
| |
| /* ### what to do with LABEL and USE_CHECKED_IN ?? */ |
| |
| revnum = SVN_STR_TO_REV(path); |
| if (!SVN_IS_VALID_REVNUM(revnum)) |
| return TRUE; /* ### be nice to get better feedback */ |
| |
| /* create a Baseline resource (a special Version Resource) */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_VERSION; |
| |
| /* exists? need to wait for now */ |
| comb->res.versioned = TRUE; |
| comb->res.baselined = TRUE; |
| |
| /* which baseline (revision tree) to access */ |
| comb->priv.root.rev = revnum; |
| |
| /* NOTE: comb->priv.repos_path == NULL */ |
| /* NOTE: comb->priv.created_rev == SVN_INVALID_REVNUM */ |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_revstub_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| /* format: !svn/rev/REVISION |
| |
| In HTTP protocol v2, this represents a specific revision in the |
| repository. Clients perform PROPFIND and PROPPATCH against it to |
| read and write revprops. (This uri replaces baseline (bln) and |
| working baseline (wbl) forms.) |
| */ |
| |
| svn_revnum_t revnum = SVN_STR_TO_REV(path); |
| if (!SVN_IS_VALID_REVNUM(revnum)) |
| return TRUE; /* fail */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_VERSION; |
| comb->res.versioned = TRUE; |
| comb->res.baselined = TRUE; |
| /* exists? need to wait for now */ |
| |
| /* which baseline (revision tree) to access */ |
| comb->priv.root.rev = revnum; |
| |
| /* all resource parameters are fixed in URI. */ |
| comb->priv.idempotent = TRUE; |
| |
| /* NOTE: comb->priv.repos_path == NULL */ |
| /* NOTE: comb->priv.created_rev == SVN_INVALID_REVNUM */ |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_revroot_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| /* format: !svn/rvr/REVISION/[PATH] |
| |
| In HTTP protocol v2, this represents a path within a specific |
| revision. Clients perform PROPFIND and GET against it to read |
| versioned file/dir properties and file contents. (This uri |
| replaces baseline collection (bc) forms.) |
| */ |
| |
| /* Right now, we treat 'rvr' URIs exactly the same as 'bc' ones. |
| Same expected format, same utility, etc. */ |
| return parse_baseline_coll_uri(comb, path, label, use_checked_in); |
| } |
| |
| |
| static int |
| parse_txnstub_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| /* format: !svn/txn/TXN_NAME |
| |
| In HTTP protocol v2, this represents a specific uncommitted |
| transaction. Clients perform PROPFIND and PROPPATCH against it |
| to read and write txnprops during a commit. They can also issue |
| a DELETE against it to abort the txn. |
| */ |
| |
| if (path == NULL) |
| return TRUE; /* fail, we need a txn_name. */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_PRIVATE; |
| comb->priv.restype = DAV_SVN_RESTYPE_TXN_COLLECTION; |
| comb->priv.root.txn_name = apr_pstrdup(comb->res.pool, path); |
| |
| return FALSE; |
| } |
| |
| static int |
| parse_vtxnstub_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| /* format: !svn/vtxn/TXN_NAME */ |
| |
| if (parse_txnstub_uri(comb, path, label, use_checked_in)) |
| return TRUE; |
| |
| if (!comb->priv.root.txn_name) |
| return TRUE; |
| |
| comb->priv.root.vtxn_name = comb->priv.root.txn_name; |
| comb->priv.root.txn_name = dav_svn__get_txn(comb->priv.repos, |
| comb->priv.root.vtxn_name); |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_txnroot_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| /* format: !svn/txr/TXN_NAME/[PATH] |
| |
| In HTTP protocol v2, this represents a path within a specific |
| uncommitted transaction. Clients perform PUT, COPY, DELETE, MOVE |
| against it to modify the path. |
| */ |
| const char *slash; |
| |
| /* Note that we're calling this a WORKING resource, rather than |
| PRIVATE, so that we can let prep_working() do the same work for |
| us that it does on DeltaV 'working resources'. */ |
| comb->res.type = DAV_RESOURCE_TYPE_WORKING; |
| |
| /* ...but setting this restype can let parse_working() know whether |
| this is a !svn/wrk/ (DeltaV) or a !svn/txr (protocol v2) */ |
| comb->priv.restype = DAV_SVN_RESTYPE_TXNROOT_COLLECTION; |
| comb->res.working = TRUE; |
| comb->res.versioned = TRUE; |
| |
| slash = ap_strchr_c(path, '/'); |
| |
| /* This sucker starts with a slash. That's bogus. */ |
| if (slash == path) |
| return TRUE; |
| |
| if (slash == NULL) |
| { |
| /* There's no slash character in our path. Assume it's just an |
| TXN_NAME pointing to the root path. That should be cool. |
| We'll just drop through to the normal case handling below. */ |
| comb->priv.root.txn_name = apr_pstrdup(comb->res.pool, path); |
| comb->priv.repos_path = "/"; |
| } |
| else |
| { |
| comb->priv.root.txn_name = apr_pstrndup(comb->res.pool, path, |
| slash - path); |
| comb->priv.repos_path = slash; |
| } |
| |
| return FALSE; |
| } |
| |
| static int |
| parse_vtxnroot_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| /* format: !svn/vtxr/TXN_NAME/[PATH] */ |
| |
| if (parse_txnroot_uri(comb, path, label, use_checked_in)) |
| return TRUE; |
| |
| if (!comb->priv.root.txn_name) |
| return TRUE; |
| |
| comb->priv.root.vtxn_name = comb->priv.root.txn_name; |
| comb->priv.root.txn_name = dav_svn__get_txn(comb->priv.repos, |
| comb->priv.root.vtxn_name); |
| |
| return FALSE; |
| } |
| |
| |
| static int |
| parse_wrk_baseline_uri(dav_resource_combined *comb, |
| const char *path, |
| const char *label, |
| int use_checked_in) |
| { |
| const char *slash; |
| |
| /* format: ACTIVITY_ID/REVISION */ |
| |
| /* ### what to do with LABEL and USE_CHECKED_IN ?? */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_WORKING; |
| comb->res.working = TRUE; |
| comb->res.versioned = TRUE; |
| comb->res.baselined = TRUE; |
| |
| if ((slash = ap_strchr_c(path, '/')) == NULL |
| || slash == path |
| || slash[1] == '\0') |
| return TRUE; |
| |
| comb->priv.root.activity_id = apr_pstrndup(comb->res.pool, path, |
| slash - path); |
| comb->priv.root.rev = SVN_STR_TO_REV(slash + 1); |
| |
| /* NOTE: comb->priv.repos_path == NULL */ |
| |
| return FALSE; |
| } |
| |
| |
| static const struct special_defn |
| { |
| const char *name; |
| |
| /* |
| * COMB is the resource that we are constructing. Any elements that |
| * can be determined from the PATH may be set in COMB. However, further |
| * operations are not allowed (we don't want anything besides a parse |
| * error to occur). |
| * |
| * At a minimum, the parse function must set COMB->res.type and |
| * COMB->priv.repos_path. |
| * |
| * PATH does not contain a leading slash. Given "/root/$svn/xxx/the/path" |
| * as the request URI, the PATH variable will be "the/path" |
| */ |
| int (*parse)(dav_resource_combined *comb, const char *path, |
| const char *label, int use_checked_in); |
| |
| /* The number of subcompenents after the !svn/xxx/... before we |
| reach the actual path within the repository. */ |
| int numcomponents; |
| |
| /* Boolean: are the subcomponents followed by a repos path? */ |
| int has_repos_path; |
| |
| /* The private resource type for the /$svn/xxx/ collection. */ |
| enum dav_svn_private_restype restype; |
| |
| } special_subdirs[] = |
| { |
| /* Our original delta-V-ish protocol uses all these: */ |
| { "ver", parse_version_uri, 1, TRUE, DAV_SVN_RESTYPE_VER_COLLECTION }, |
| { "his", parse_history_uri, 0, FALSE, DAV_SVN_RESTYPE_HIS_COLLECTION }, |
| { "wrk", parse_working_uri, 1, TRUE, DAV_SVN_RESTYPE_WRK_COLLECTION }, |
| { "act", parse_activity_uri, 1, FALSE, DAV_SVN_RESTYPE_ACT_COLLECTION }, |
| { "vcc", parse_vcc_uri, 1, FALSE, DAV_SVN_RESTYPE_VCC_COLLECTION }, |
| { "bc", parse_baseline_coll_uri, 1, TRUE, DAV_SVN_RESTYPE_BC_COLLECTION }, |
| { "bln", parse_baseline_uri, 1, FALSE, DAV_SVN_RESTYPE_BLN_COLLECTION }, |
| { "wbl", parse_wrk_baseline_uri, 2, FALSE, DAV_SVN_RESTYPE_WBL_COLLECTION }, |
| |
| /* The new v2 protocol uses these new 'stub' uris: */ |
| { "me", parse_me_resource_uri, 0, FALSE, DAV_SVN_RESTYPE_ME }, |
| { "rev", parse_revstub_uri, 1, FALSE, DAV_SVN_RESTYPE_REV_COLLECTION }, |
| { "rvr", parse_revroot_uri, 1, TRUE, DAV_SVN_RESTYPE_REVROOT_COLLECTION }, |
| { "txn", parse_txnstub_uri, 1, FALSE, DAV_SVN_RESTYPE_TXN_COLLECTION}, |
| { "txr", parse_txnroot_uri, 1, TRUE, DAV_SVN_RESTYPE_TXNROOT_COLLECTION}, |
| { "vtxn", parse_vtxnstub_uri, 1, FALSE, DAV_SVN_RESTYPE_TXN_COLLECTION}, |
| { "vtxr", parse_vtxnroot_uri, 1, TRUE, DAV_SVN_RESTYPE_TXNROOT_COLLECTION}, |
| |
| { NULL } /* sentinel */ |
| }; |
| |
| |
| /* |
| * parse_uri: parse the provided URI into its various bits |
| * |
| * URI will contain a path relative to our configured root URI. It should |
| * not have a leading "/". The root is identified by "". |
| * |
| * On output: *COMB will contain all of the information parsed out of |
| * the URI -- the resource type, activity ID, path, etc. |
| * |
| * Note: this function will only parse the URI. Validation of the pieces, |
| * opening data stores, etc, are not part of this function. |
| * |
| * TRUE is returned if a parsing error occurred. FALSE for success. |
| */ |
| static int |
| parse_uri(dav_resource_combined *comb, |
| const char *uri, |
| const char *label, |
| int use_checked_in) |
| { |
| const char *special_uri = comb->priv.repos->special_uri; |
| apr_size_t len1; |
| apr_size_t len2; |
| char ch; |
| |
| len1 = strlen(uri); |
| len2 = strlen(special_uri); |
| if (len1 > len2 |
| && ((ch = uri[len2]) == '/' || ch == '\0') |
| && memcmp(uri, special_uri, len2) == 0) |
| { |
| if (ch == '\0') |
| { |
| /* URI was "/root/!svn". It exists, but has restricted usage. */ |
| comb->res.type = DAV_RESOURCE_TYPE_PRIVATE; |
| comb->priv.restype = DAV_SVN_RESTYPE_ROOT_COLLECTION; |
| } |
| else |
| { |
| const struct special_defn *defn; |
| |
| /* skip past the "!svn/" prefix */ |
| uri += len2 + 1; |
| len1 -= len2 + 1; |
| |
| for (defn = special_subdirs ; defn->name != NULL; ++defn) |
| { |
| apr_size_t len3 = strlen(defn->name); |
| |
| if (len1 >= len3 && memcmp(uri, defn->name, len3) == 0) |
| { |
| /* If we find a slash after our special subdir, or |
| if we don't and this subdir isn't *supposed* to have |
| anything following it (such as the !svn/me |
| resource), hand off the custom parser for this |
| subdir type. */ |
| if (uri[len3] == '/') |
| { |
| if ((*defn->parse)(comb, uri + len3 + 1, label, |
| use_checked_in)) |
| return TRUE; |
| } |
| else if (uri[len3] == '\0') |
| { |
| if ((defn->numcomponents == 0) |
| && (! defn->has_repos_path)) |
| { |
| if ((*defn->parse)(comb, "", label, use_checked_in)) |
| return TRUE; |
| } |
| else |
| { |
| /* URI was "/root/!svn/XXX". The location |
| exists, but has restricted usage. */ |
| comb->res.type = DAV_RESOURCE_TYPE_PRIVATE; |
| |
| /* Store the resource type so that we can |
| PROPFIND on this collection. */ |
| comb->priv.restype = defn->restype; |
| } |
| } |
| else |
| { |
| /* e.g. "/root/!svn/activity" (we just know "act") */ |
| return TRUE; |
| } |
| |
| break; |
| } |
| } |
| |
| /* if completed the loop, then it is an unrecognized subdir */ |
| if (defn->name == NULL) |
| return TRUE; |
| } |
| } |
| else |
| { |
| /* Anything under the root, but not under "!svn". These are all |
| version-controlled resources. */ |
| comb->res.type = DAV_RESOURCE_TYPE_REGULAR; |
| comb->res.versioned = TRUE; |
| |
| /* The location of these resources corresponds directly to the URI, |
| and we keep the leading "/". */ |
| comb->priv.repos_path = comb->priv.uri_path->data; |
| } |
| |
| return FALSE; |
| } |
| |
| |
| static dav_error * |
| prep_regular(dav_resource_combined *comb) |
| { |
| apr_pool_t *pool = comb->res.pool; |
| dav_svn_repos *repos = comb->priv.repos; |
| svn_error_t *serr; |
| dav_error *derr; |
| svn_node_kind_t kind; |
| |
| /* A REGULAR resource might have a specific revision already (e.g. if it |
| is part of a baseline collection). However, if it doesn't, then we |
| will assume that we need the youngest revision. |
| ### other cases besides a BC? */ |
| if (comb->priv.root.rev == SVN_INVALID_REVNUM) |
| { |
| serr = dav_svn__get_youngest_rev(&comb->priv.root.rev, repos, pool); |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not determine the proper " |
| "revision to access", |
| pool); |
| } |
| } |
| else |
| { |
| /* Did we have a query for this REGULAR resource? */ |
| if (comb->priv.r->parsed_uri.query) |
| { |
| /* If yes, it's 'idempotent' only if peg revision is specified. */ |
| comb->priv.idempotent = comb->priv.pegged; |
| } |
| else |
| { |
| /* Otherwise, we have the specific revision in URI, so the resource |
| is 'idempotent'. */ |
| comb->priv.idempotent = TRUE; |
| } |
| } |
| |
| /* get the root of the tree */ |
| serr = svn_fs_revision_root(&comb->priv.root.root, repos->fs, |
| comb->priv.root.rev, pool); |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not open the root of the " |
| "repository", |
| pool); |
| } |
| |
| derr = fs_check_path(&kind, comb->priv.root.root, |
| comb->priv.repos_path, pool); |
| if (derr != NULL) |
| return derr; |
| |
| comb->res.exists = (kind != svn_node_none); |
| comb->res.collection = (kind == svn_node_dir); |
| |
| /* HACK: dav_get_resource_state() is making shortcut assumptions |
| about how to distinguish a null resource from a lock-null |
| resource. This is the only way to get around that problem. |
| Without it, it won't ever detect lock-nulls, and thus 'svn unlock |
| nonexistentURL' will always return 404's. */ |
| if (! comb->res.exists) |
| comb->priv.r->path_info = (char *) ""; |
| |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| prep_version(dav_resource_combined *comb) |
| { |
| svn_error_t *serr; |
| apr_pool_t *pool = comb->res.pool; |
| |
| /* we are accessing the Version Resource by REV/PATH */ |
| |
| /* ### assert: .baselined = TRUE */ |
| |
| /* if we don't have a revision, then assume the youngest */ |
| if (!SVN_IS_VALID_REVNUM(comb->priv.root.rev)) |
| { |
| serr = dav_svn__get_youngest_rev(&comb->priv.root.rev, |
| comb->priv.repos, |
| pool); |
| if (serr != NULL) |
| { |
| /* ### might not be a baseline */ |
| |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not fetch 'youngest' revision " |
| "to enable accessing the latest " |
| "baseline resource.", |
| pool); |
| } |
| } |
| |
| /* ### baselines have no repos_path, and we don't need to open |
| ### a root (yet). we just needed to ensure that we have the proper |
| ### revision number. */ |
| |
| if (!comb->priv.root.root) |
| { |
| serr = svn_fs_revision_root(&comb->priv.root.root, |
| comb->priv.repos->fs, |
| comb->priv.root.rev, |
| pool); |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not open a revision root.", |
| pool); |
| } |
| } |
| |
| /* ### we should probably check that the revision is valid */ |
| comb->res.exists = TRUE; |
| |
| /* Set up the proper URI. Most likely, we arrived here via a VCC, |
| so the URI will be incorrect. Set the canonical form. */ |
| /* ### assuming a baseline */ |
| comb->res.uri = dav_svn__build_uri(comb->priv.repos, |
| DAV_SVN__BUILD_URI_BASELINE, |
| comb->priv.root.rev, NULL, |
| FALSE /* add_href */, |
| pool); |
| |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| prep_history(dav_resource_combined *comb) |
| { |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| prep_working(dav_resource_combined *comb) |
| { |
| apr_pool_t *pool = comb->res.pool; |
| svn_error_t *serr; |
| dav_error *derr; |
| svn_node_kind_t kind; |
| const char *txn_name = comb->priv.root.txn_name; |
| |
| /* A txnroot object will already have the txn_name filled in, but a |
| DeltaV 'working resource' will only have the activity_id at this |
| point. */ |
| if (txn_name == NULL) |
| { |
| if (!comb->priv.root.activity_id) |
| return dav_svn__new_error(comb->res.pool, HTTP_BAD_REQUEST, 0, 0, |
| "The request did not specify an activity ID"); |
| |
| txn_name = dav_svn__get_txn(comb->priv.repos, |
| comb->priv.root.activity_id); |
| if (txn_name == NULL) |
| { |
| return dav_svn__new_error(pool, HTTP_BAD_REQUEST, 0, 0, |
| "An unknown activity was specified in the " |
| "URL. This is generally caused by a " |
| "problem in the client software."); |
| } |
| comb->priv.root.txn_name = txn_name; |
| } |
| |
| /* get the FS transaction, given its name */ |
| serr = svn_fs_open_txn(&comb->priv.root.txn, comb->priv.repos->fs, txn_name, |
| pool); |
| if (serr != NULL) |
| { |
| if (serr->apr_err == SVN_ERR_FS_NO_SUCH_TRANSACTION) |
| { |
| svn_error_clear(serr); |
| return dav_svn__new_error(pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, |
| "An activity was specified and found, but " |
| "the corresponding SVN FS transaction was " |
| "not found."); |
| } |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not open the SVN FS transaction " |
| "corresponding to the specified activity.", |
| pool); |
| } |
| |
| if (comb->res.baselined) |
| { |
| /* a Working Baseline */ |
| |
| /* if the transaction exists, then the working resource exists */ |
| comb->res.exists = TRUE; |
| |
| return NULL; |
| } |
| |
| /* Set the txn author if not previously set. Protect against multi-author |
| * commits by verifying authenticated user associated with the current |
| * request is the same as the txn author. |
| * Note that anonymous requests are being excluded as being a change |
| * in author, because the commit may touch areas of the repository |
| * that are anonymous writeable as well as areas that are not. |
| */ |
| if (comb->priv.repos->username) |
| { |
| svn_string_t *current_author; |
| svn_string_t request_author; |
| |
| serr = svn_fs_txn_prop(¤t_author, comb->priv.root.txn, |
| SVN_PROP_REVISION_AUTHOR, pool); |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Failed to retrieve author of the SVN FS transaction " |
| "corresponding to the specified activity.", |
| pool); |
| } |
| |
| request_author.data = comb->priv.repos->username; |
| request_author.len = strlen(request_author.data); |
| if (!current_author) |
| { |
| serr = svn_fs_change_txn_prop(comb->priv.root.txn, |
| SVN_PROP_REVISION_AUTHOR, |
| &request_author, pool); |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Failed to set the author of the SVN FS transaction " |
| "corresponding to the specified activity.", |
| pool); |
| } |
| } |
| else if (!svn_string_compare(current_author, &request_author)) |
| { |
| return dav_svn__new_error(pool, HTTP_NOT_IMPLEMENTED, 0, 0, |
| "Multi-author commits not supported."); |
| } |
| } |
| |
| /* get the root of the tree */ |
| serr = svn_fs_txn_root(&comb->priv.root.root, comb->priv.root.txn, pool); |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not open the (transaction) root of " |
| "the repository", |
| pool); |
| } |
| |
| derr = fs_check_path(&kind, comb->priv.root.root, |
| comb->priv.repos_path, pool); |
| if (derr != NULL) |
| return derr; |
| |
| comb->res.exists = (kind != svn_node_none); |
| comb->res.collection = (kind == svn_node_dir); |
| |
| if (comb->res.exists |
| && comb->priv.r->method_number == M_MKCOL |
| && comb->priv.repos->is_svn_client) |
| { |
| /* mod_dav will now continue returning a generic HTTP_METHOD_NOT_ALLOWED |
| error, which doesn't produce nice output on SVN, nor gives any details |
| on why the operation failed. |
| |
| Let's error out a bit earlier and produce an error message that is |
| easier to understand for both clients and users. */ |
| |
| /* It would be nice if we could error out a bit later (see issue #2295), |
| like in create_collection(), but mod_dav outsmarts us by just |
| returning the error when the node exists. */ |
| |
| return dav_svn__convert_err( |
| svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL, |
| "Path already exists, path '%s'", |
| comb->priv.repos_path), |
| HTTP_METHOD_NOT_ALLOWED, NULL, pool); |
| } |
| |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| prep_activity(dav_resource_combined *comb) |
| { |
| const char *txn_name; |
| |
| if (!comb->priv.root.activity_id) |
| return dav_svn__new_error(comb->res.pool, HTTP_BAD_REQUEST, 0, 0, |
| "The request did not specify an activity ID"); |
| |
| txn_name = dav_svn__get_txn(comb->priv.repos, comb->priv.root.activity_id); |
| |
| comb->priv.root.txn_name = txn_name; |
| comb->res.exists = txn_name != NULL; |
| |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| prep_private(dav_resource_combined *comb) |
| { |
| svn_error_t *serr; |
| apr_pool_t *pool = comb->res.pool; |
| |
| if (comb->priv.restype == DAV_SVN_RESTYPE_VCC) |
| { |
| /* ### what to do */ |
| } |
| else if (comb->priv.restype == DAV_SVN_RESTYPE_TXN_COLLECTION) |
| { |
| /* Open the named transaction. */ |
| |
| if (comb->priv.root.txn_name == NULL) |
| return dav_svn__new_error(pool, HTTP_BAD_REQUEST, 0, 0, |
| "An unknown txn name was specified in the " |
| "URL."); |
| |
| serr = svn_fs_open_txn(&comb->priv.root.txn, |
| comb->priv.repos->fs, |
| comb->priv.root.txn_name, pool); |
| if (serr != NULL) |
| { |
| if (serr->apr_err == SVN_ERR_FS_NO_SUCH_TRANSACTION) |
| { |
| svn_error_clear(serr); |
| comb->res.exists = FALSE; |
| return dav_svn__new_error(pool, HTTP_NOT_FOUND, 0, 0, |
| "Named transaction doesn't exist."); |
| } |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not open specified transaction.", |
| pool); |
| } |
| comb->res.exists = TRUE; |
| } |
| |
| return NULL; |
| } |
| |
| |
| static const struct res_type_handler |
| { |
| dav_resource_type type; |
| dav_error * (*prep)(dav_resource_combined *comb); |
| |
| } res_type_handlers[] = |
| { |
| /* skip UNKNOWN */ |
| { DAV_RESOURCE_TYPE_REGULAR, prep_regular }, |
| { DAV_RESOURCE_TYPE_VERSION, prep_version }, |
| { DAV_RESOURCE_TYPE_HISTORY, prep_history }, |
| { DAV_RESOURCE_TYPE_WORKING, prep_working }, |
| /* skip WORKSPACE */ |
| { DAV_RESOURCE_TYPE_ACTIVITY, prep_activity }, |
| { DAV_RESOURCE_TYPE_PRIVATE, prep_private }, |
| |
| { 0, NULL } /* sentinel */ |
| }; |
| |
| |
| /* |
| ** ### docco... |
| ** |
| ** Set .exists and .collection |
| ** open other, internal bits... |
| */ |
| static dav_error * |
| prep_resource(dav_resource_combined *comb) |
| { |
| const struct res_type_handler *scan; |
| |
| for (scan = res_type_handlers; scan->prep != NULL; ++scan) |
| { |
| if (comb->res.type == scan->type) |
| return (*scan->prep)(comb); |
| } |
| |
| return dav_svn__new_error(comb->res.pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, |
| "DESIGN FAILURE: unknown resource type"); |
| } |
| |
| |
| static dav_resource * |
| create_private_resource(const dav_resource *base, |
| enum dav_svn_private_restype restype) |
| { |
| dav_resource_combined *comb; |
| svn_stringbuf_t *path; |
| const struct special_defn *defn; |
| |
| for (defn = special_subdirs; defn->name != NULL; ++defn) |
| if (defn->restype == restype) |
| break; |
| /* assert: defn->name != NULL */ |
| |
| path = svn_stringbuf_createf(base->pool, "/%s/%s", |
| base->info->repos->special_uri, defn->name); |
| |
| comb = apr_pcalloc(base->pool, sizeof(*comb)); |
| |
| /* ### can/should we leverage prep_resource */ |
| |
| comb->res.type = DAV_RESOURCE_TYPE_PRIVATE; |
| |
| comb->res.exists = TRUE; |
| comb->res.collection = TRUE; /* ### always true? */ |
| /* versioned = baselined = working = FALSE */ |
| |
| if (base->info->repos->root_path[1]) |
| comb->res.uri = apr_pstrcat(base->pool, base->info->repos->root_path, |
| path->data, SVN_VA_NULL); |
| else |
| comb->res.uri = path->data; |
| comb->res.info = &comb->priv; |
| comb->res.hooks = &dav_svn__hooks_repository; |
| comb->res.pool = base->pool; |
| |
| comb->priv.uri_path = path; |
| comb->priv.repos = base->info->repos; |
| comb->priv.root.rev = SVN_INVALID_REVNUM; |
| return &comb->res; |
| } |
| |
| static void log_warning_req(void *baton, svn_error_t *err) |
| { |
| request_rec *r = baton; |
| const char *continuation = ""; |
| |
| /* Not showing file/line so no point in tracing */ |
| err = svn_error_purge_tracing(err); |
| while (err) |
| { |
| ap_log_rerror(APLOG_MARK, APLOG_ERR, APR_EGENERAL, r, "%s%s", |
| continuation, err->message); |
| continuation = "-"; |
| err = err->child; |
| } |
| } |
| |
| static void log_warning_conn(void *baton, svn_error_t *err) |
| { |
| conn_rec *c = baton; |
| const char *continuation = ""; |
| |
| /* Not showing file/line so no point in tracing */ |
| err = svn_error_purge_tracing(err); |
| while (err) |
| { |
| ap_log_cerror(APLOG_MARK, APLOG_ERR, APR_EGENERAL, c, "%s%s", |
| continuation, err->message); |
| continuation = "-"; |
| err = err->child; |
| } |
| } |
| |
| |
| AP_MODULE_DECLARE(dav_error *) |
| dav_svn_split_uri2(request_rec *r, |
| const char *uri_to_split, |
| const char *root_path, |
| const char **cleaned_uri, |
| int *trailing_slash, |
| const char **repos_basename, |
| const char **relative_path, |
| const char **repos_path, |
| apr_pool_t *pool) |
| { |
| apr_size_t len1; |
| int had_slash; |
| const char *fs_path; |
| const char *fs_parent_path; |
| const char *relative; |
| char *uri; |
| |
| /* one of these is NULL, the other non-NULL. */ |
| fs_path = dav_svn__get_fs_path(r); |
| fs_parent_path = dav_svn__get_fs_parent_path(r); |
| |
| if ((fs_path == NULL) && (fs_parent_path == NULL)) |
| { |
| /* ### are SVN_ERR_APMOD codes within the right numeric space? */ |
| return dav_svn__new_error(pool, HTTP_INTERNAL_SERVER_ERROR, |
| SVN_ERR_APMOD_MISSING_PATH_TO_FS, 0, |
| "The server is misconfigured: " |
| "either an SVNPath or SVNParentPath " |
| "directive is required to specify the location " |
| "of this resource's repository."); |
| } |
| |
| /* make a copy so that we can do some work on it */ |
| uri = apr_pstrdup(pool, uri_to_split); |
| |
| /* remove duplicate slashes, and make sure URI has no trailing '/' */ |
| ap_no2slash(uri); |
| len1 = strlen(uri); |
| had_slash = (len1 > 0 && uri[len1 - 1] == '/'); |
| if (len1 > 1 && had_slash) |
| uri[len1 - 1] = '\0'; |
| |
| if (had_slash) |
| *trailing_slash = TRUE; |
| else |
| *trailing_slash = FALSE; |
| |
| /* return the first item. */ |
| *cleaned_uri = apr_pstrdup(pool, uri); |
| |
| /* The URL space defined by the SVN provider is always a virtual |
| space. Construct the path relative to the configured Location |
| (root_path). So... the relative location is simply the URL used, |
| skipping the root_path. |
| |
| Note: mod_dav has canonialized root_path. It will not have a trailing |
| slash (unless it is "/"). |
| |
| Note: given a URI of /something and a root of /some, then it is |
| impossible to be here (and end up with "thing"). This is simply |
| because we control /some and are dispatched to here for its |
| URIs. We do not control /something, so we don't get here. Or, |
| if we *do* control /something, then it is for THAT root. |
| */ |
| relative = ap_stripprefix(uri, root_path); |
| |
| /* We want a leading slash on the path specified by <relative>. This |
| will almost always be the case since root_path does not have a trailing |
| slash. However, if the root is "/", then the slash will be removed |
| from <relative>. Backing up a character will put the leading slash |
| back. |
| |
| Watch out for the empty string! This can happen when URI == ROOT_PATH. |
| We simply turn the path into "/" for this case. */ |
| if (*relative == '\0') |
| relative = "/"; |
| else if (*relative != '/') |
| --relative; |
| /* ### need a better name... it isn't "relative" because of the leading |
| ### slash. something about SVN-private-path */ |
| |
| /* Depending on whether SVNPath or SVNParentPath was used, we need |
| to compute 'relative' and 'repos_basename' differently. */ |
| |
| /* Normal case: the SVNPath command was used to specify a |
| particular repository. */ |
| if (fs_path != NULL) |
| { |
| /* the repos_basename is the last component of root_path. */ |
| *repos_basename = svn_dirent_basename(root_path, pool); |
| |
| /* 'relative' is already correct for SVNPath; the root_path |
| already contains the name of the repository, so relative is |
| everything beyond that. */ |
| } |
| |
| else |
| { |
| /* SVNParentPath was used instead: assume the first component of |
| 'relative' is the name of a repository. */ |
| const char *magic_component, *magic_end; |
| |
| /* A repository name is required here. |
| Remember that 'relative' always starts with a "/". */ |
| if (relative[1] == '\0') |
| { |
| /* ### are SVN_ERR_APMOD codes within the right numeric space? */ |
| return dav_svn__new_error(pool, HTTP_FORBIDDEN, |
| SVN_ERR_APMOD_MALFORMED_URI, 0, |
| "The URI does not contain the name " |
| "of a repository."); |
| } |
| |
| magic_end = ap_strchr_c(relative + 1, '/'); |
| if (!magic_end) |
| { |
| /* ### Request was for parent directory with no trailing |
| slash; we probably ought to just redirect to same with |
| trailing slash appended. */ |
| magic_component = relative + 1; |
| relative = "/"; |
| } |
| else |
| { |
| magic_component = apr_pstrndup(pool, relative + 1, |
| magic_end - relative - 1); |
| relative = magic_end; |
| } |
| |
| /* return answer */ |
| *repos_basename = magic_component; |
| } |
| |
| /* We can return 'relative' at this point too. */ |
| *relative_path = apr_pstrdup(pool, relative); |
| |
| /* Code to remove the !svn junk from the front of the relative path, |
| mainly stolen from parse_uri(). This code assumes that |
| the 'relative' string being parsed doesn't start with '/'. */ |
| relative++; |
| |
| { |
| const char *special_uri = dav_svn__get_special_uri(r); |
| apr_size_t len2; |
| char ch; |
| |
| len1 = strlen(relative); |
| len2 = strlen(special_uri); |
| if (len1 > len2 |
| && ((ch = relative[len2]) == '/' || ch == '\0') |
| && memcmp(relative, special_uri, len2) == 0) |
| { |
| if (ch == '\0') |
| { |
| /* relative is just "!svn", which is malformed. */ |
| return dav_svn__new_error(pool, HTTP_NOT_FOUND, |
| SVN_ERR_APMOD_MALFORMED_URI, 0, |
| "Nothing follows the svn special_uri."); |
| } |
| else |
| { |
| const struct special_defn *defn; |
| |
| /* skip past the "!svn/" prefix */ |
| relative += len2 + 1; |
| len1 -= len2 + 1; |
| |
| for (defn = special_subdirs ; defn->name != NULL; ++defn) |
| { |
| apr_size_t len3 = strlen(defn->name); |
| |
| if (len1 >= len3 && memcmp(relative, defn->name, len3) == 0) |
| { |
| /* Found a matching special dir. */ |
| |
| if (relative[len3] == '\0') |
| { |
| /* relative is "!svn/xxx" */ |
| if (defn->numcomponents == 0) |
| *repos_path = NULL; |
| else |
| return dav_svn__new_error( |
| pool, HTTP_NOT_FOUND, |
| SVN_ERR_APMOD_MALFORMED_URI, 0, |
| "Missing info after special_uri."); |
| } |
| else if (relative[len3] == '/') |
| { |
| /* Skip past defn->numcomponents components, |
| return everything beyond that.*/ |
| int j; |
| const char *end = NULL, *start = relative + len3 + 1; |
| |
| for (j = 0; j < defn->numcomponents; j++) |
| { |
| end = ap_strchr_c(start, '/'); |
| if (! end) |
| break; |
| start = end + 1; |
| } |
| |
| if (! end) |
| { |
| /* Did we break from the loop prematurely? */ |
| if (j != (defn->numcomponents - 1)) |
| return dav_svn__new_error( |
| pool, HTTP_NOT_FOUND, |
| SVN_ERR_APMOD_MALFORMED_URI, 0, |
| "Not enough components after " |
| "special_uri."); |
| |
| if (! defn->has_repos_path) |
| /* It's okay to not have found a slash. */ |
| *repos_path = NULL; |
| else |
| *repos_path = "/"; |
| } |
| else |
| { |
| /* Found a slash after the special components. */ |
| *repos_path = apr_pstrdup(pool, start - 1); |
| } |
| } |
| else |
| { |
| return |
| dav_svn__new_error(pool, HTTP_NOT_FOUND, |
| SVN_ERR_APMOD_MALFORMED_URI, 0, |
| "Unknown data after special_uri."); |
| } |
| |
| break; |
| } |
| } |
| |
| if (defn->name == NULL) |
| return |
| dav_svn__new_error(pool, HTTP_NOT_FOUND, |
| SVN_ERR_APMOD_MALFORMED_URI, 0, |
| "Couldn't match subdir after special_uri."); |
| } |
| } |
| else |
| { |
| /* There's no "!svn/" at all, so the relative path is already |
| a valid path within the repository. */ |
| *repos_path = apr_pstrdup(pool, relative - 1); |
| } |
| } |
| |
| return NULL; |
| } |
| |
| AP_MODULE_DECLARE(dav_error *) |
| dav_svn_split_uri(request_rec *r, |
| const char *uri_to_split, |
| const char *root_path, |
| const char **cleaned_uri, |
| int *trailing_slash, |
| const char **repos_basename, |
| const char **relative_path, |
| const char **repos_path) |
| { |
| return dav_svn_split_uri2(r, uri_to_split, root_path, cleaned_uri, |
| trailing_slash, repos_basename, relative_path, |
| repos_path, r->pool); |
| } |
| |
| /* Context for cleanup handler. */ |
| struct cleanup_fs_access_baton |
| { |
| svn_fs_t *fs; |
| apr_pool_t *pool; |
| }; |
| |
| |
| /* Pool cleanup handler. Make sure fs's access ctx points to NULL |
| when request pool is destroyed. */ |
| static apr_status_t |
| cleanup_fs_access(void *data) |
| { |
| svn_error_t *serr; |
| struct cleanup_fs_access_baton *baton = data; |
| |
| serr = svn_fs_set_access(baton->fs, NULL); |
| if (serr) |
| { |
| ap_log_perror(APLOG_MARK, APLOG_ERR, serr->apr_err, baton->pool, |
| "cleanup_fs_access: error clearing fs access context"); |
| svn_error_clear(serr); |
| } |
| |
| return APR_SUCCESS; |
| } |
| |
| /* Context for cleanup handler. */ |
| struct cleanup_req_logging_baton |
| { |
| svn_fs_t *fs; |
| conn_rec *connection; |
| }; |
| |
| static apr_status_t |
| cleanup_req_logging(void *data) |
| { |
| struct cleanup_req_logging_baton *baton = data; |
| |
| /* The request about to be freed. Log future warnings with a connection |
| * context instead of a request context. */ |
| svn_fs_set_warning_func(baton->fs, log_warning_conn, baton->connection); |
| |
| return APR_SUCCESS; |
| } |
| |
| /* Helper func to construct a special 'parentpath' private resource. */ |
| static dav_error * |
| get_parentpath_resource(request_rec *r, |
| dav_resource **resource) |
| { |
| const char *new_uri; |
| dav_svn_root *droot = apr_pcalloc(r->pool, sizeof(*droot)); |
| dav_svn_repos *repos = apr_pcalloc(r->pool, sizeof(*repos)); |
| dav_resource_combined *comb = apr_pcalloc(r->pool, sizeof(*comb)); |
| apr_size_t len = strlen(r->uri); |
| |
| comb->res.exists = TRUE; |
| comb->res.collection = TRUE; |
| comb->res.uri = apr_pstrdup(r->pool, r->uri); |
| comb->res.info = &comb->priv; |
| comb->res.hooks = &dav_svn__hooks_repository; |
| comb->res.pool = r->pool; |
| comb->res.type = DAV_RESOURCE_TYPE_PRIVATE; |
| |
| comb->priv.restype = DAV_SVN_RESTYPE_PARENTPATH_COLLECTION; |
| comb->priv.r = r; |
| comb->priv.repos_path = "Collection of Repositories"; |
| comb->priv.root = *droot; |
| droot->rev = SVN_INVALID_REVNUM; |
| |
| comb->priv.repos = repos; |
| repos->pool = r->pool; |
| repos->xslt_uri = dav_svn__get_xslt_uri(r); |
| repos->autoversioning = dav_svn__get_autoversioning_flag(r); |
| repos->bulk_updates = dav_svn__get_bulk_updates_flag(r); |
| repos->v2_protocol = dav_svn__check_httpv2_support(r); |
| repos->base_url = ap_construct_url(r->pool, "", r); |
| repos->special_uri = dav_svn__get_special_uri(r); |
| repos->username = r->user; |
| repos->client_capabilities = apr_hash_make(repos->pool); |
| repos->youngest_rev = SVN_INVALID_REVNUM; |
| |
| /* Make sure this type of resource always has a trailing slash; if |
| not, redirect to a URI that does. */ |
| if (r->uri[len-1] != '/') |
| { |
| new_uri = apr_pstrcat(r->pool, ap_escape_uri(r->pool, r->uri), |
| "/", SVN_VA_NULL); |
| apr_table_setn(r->headers_out, "Location", |
| ap_construct_url(r->pool, new_uri, r)); |
| return dav_svn__new_error(r->pool, HTTP_MOVED_PERMANENTLY, 0, 0, |
| "Requests for a collection must have a " |
| "trailing slash on the URI."); |
| } |
| |
| /* No other "prepping" of resource needs to happen -- no opening |
| of a repository or anything like that, because, well, there's |
| no repository to open. */ |
| *resource = &comb->res; |
| return NULL; |
| } |
| |
| /* --------------- Borrowed from httpd's mod_negotiation.c -------------- */ |
| |
| typedef struct accept_rec { |
| char *name; /* MUST be lowercase */ |
| float quality; |
| } accept_rec; |
| |
| /* |
| * Get a single Accept-encoding line from ACCEPT_LINE, and place the |
| * information we have parsed out of it into RESULT. |
| */ |
| |
| static const char *get_entry(apr_pool_t *p, accept_rec *result, |
| const char *accept_line) |
| { |
| result->quality = 1.0f; |
| |
| /* |
| * Note that this handles what I gather is the "old format", |
| * |
| * Accept: text/html text/plain moo/zot |
| * |
| * without any compatibility kludges --- if the token after the |
| * MIME type begins with a semicolon, we know we're looking at parms, |
| * otherwise, we know we aren't. (So why all the pissing and moaning |
| * in the CERN server code? I must be missing something). |
| */ |
| |
| result->name = ap_get_token(p, &accept_line, 0); |
| ap_str_tolower(result->name); /* You want case insensitive, |
| * you'll *get* case insensitive. |
| */ |
| |
| while (*accept_line == ';') |
| { |
| /* Parameters ... */ |
| |
| char *parm; |
| char *cp; |
| char *end; |
| |
| ++accept_line; |
| parm = ap_get_token(p, &accept_line, 1); |
| |
| /* Look for 'var = value' --- and make sure the var is in lcase. */ |
| |
| for (cp = parm; (*cp && !svn_ctype_isspace(*cp) && *cp != '='); ++cp) |
| { |
| *cp = (char)apr_tolower(*cp); |
| } |
| |
| if (!*cp) |
| { |
| continue; /* No '='; just ignore it. */ |
| } |
| |
| *cp++ = '\0'; /* Delimit var */ |
| while (*cp && (svn_ctype_isspace(*cp) || *cp == '=')) |
| { |
| ++cp; |
| } |
| |
| if (*cp == '"') |
| { |
| ++cp; |
| for (end = cp; |
| (*end && *end != '\n' && *end != '\r' && *end != '\"'); |
| end++); |
| } |
| else |
| { |
| for (end = cp; (*end && !svn_ctype_isspace(*end)); end++); |
| } |
| if (*end) |
| { |
| *end = '\0'; /* strip ending quote or return */ |
| } |
| ap_str_tolower(cp); |
| |
| if (parm[0] == 'q' |
| && (parm[1] == '\0' || (parm[1] == 's' && parm[2] == '\0'))) |
| { |
| result->quality = (float) atof(cp); |
| } |
| } |
| |
| if (*accept_line == ',') |
| { |
| ++accept_line; |
| } |
| |
| return accept_line; |
| } |
| |
| /* @a accept_line is the Accept-Encoding header, which is of the |
| format: |
| |
| Accept-Encoding: name; q=N; |
| |
| This function will return an array of accept_rec structures that |
| contain the accepted encodings and the quality each one has |
| associated with them. |
| */ |
| static apr_array_header_t *do_header_line(apr_pool_t *p, |
| const char *accept_line) |
| { |
| apr_array_header_t *accept_recs; |
| |
| if (!accept_line) |
| return NULL; |
| |
| accept_recs = apr_array_make(p, 10, sizeof(accept_rec)); |
| |
| while (*accept_line) |
| { |
| accept_rec *prefs = (accept_rec *) apr_array_push(accept_recs); |
| accept_line = get_entry(p, prefs, accept_line); |
| } |
| |
| return accept_recs; |
| } |
| |
| /* ---------------------------------------------------------------------- */ |
| |
| |
| /* qsort comparison function for the quality field of the accept_rec |
| structure */ |
| static int sort_encoding_pref(const void *accept_rec1, const void *accept_rec2) |
| { |
| float diff = ((const accept_rec *) accept_rec1)->quality - |
| ((const accept_rec *) accept_rec2)->quality; |
| return (diff == 0 ? 0 : (diff > 0 ? -1 : 1)); |
| } |
| |
| static int get_svndiff_version(const struct accept_rec *rec) |
| { |
| if (strcmp(rec->name, "svndiff2") == 0) |
| return 2; |
| else if (strcmp(rec->name, "svndiff1") == 0) |
| return 1; |
| else if (strcmp(rec->name, "svndiff") == 0) |
| return 0; |
| else |
| return -1; |
| } |
| |
| /* Parse and handle any possible Accept-Encoding header that has been |
| sent as part of the request. */ |
| static void |
| negotiate_encoding_prefs(request_rec *r, int *svndiff_version) |
| { |
| /* It would be nice if mod_negotiation |
| <http://httpd.apache.org/docs-2.1/mod/mod_negotiation.html> could |
| handle the Accept-Encoding header parsing for us. Sadly, it |
| looks like its data structures and routines are private (see |
| httpd/modules/mappers/mod_negotiation.c). Thus, we duplicate the |
| necessary ones in this file. */ |
| int i; |
| apr_array_header_t *encoding_prefs; |
| apr_array_header_t *svndiff_encodings; |
| svn_boolean_t accepts_svndiff2 = FALSE; |
| |
| encoding_prefs = do_header_line(r->pool, |
| apr_table_get(r->headers_in, |
| "Accept-Encoding")); |
| |
| if (!encoding_prefs || apr_is_empty_array(encoding_prefs)) |
| { |
| *svndiff_version = 0; |
| return; |
| } |
| |
| svndiff_encodings = apr_array_make(r->pool, 3, sizeof(struct accept_rec)); |
| for (i = 0; i < encoding_prefs->nelts; i++) |
| { |
| const struct accept_rec *rec = &APR_ARRAY_IDX(encoding_prefs, i, |
| struct accept_rec); |
| int version = get_svndiff_version(rec); |
| |
| if (version > 0) |
| APR_ARRAY_PUSH(svndiff_encodings, struct accept_rec) = *rec; |
| |
| if (version == 2) |
| accepts_svndiff2 = TRUE; |
| } |
| |
| if (dav_svn__get_compression_level(r) == 0) |
| { |
| /* If the compression is disabled on the server, use the uncompressed |
| * svndiff0 format, which we assume is always supported. */ |
| *svndiff_version = 0; |
| } |
| else if (accepts_svndiff2 && dav_svn__get_compression_level(r) == 1) |
| { |
| /* Enable svndiff2 if the client can read it, and if the server-side |
| * compression level is set to 1. Svndiff2 offers better speed and |
| * compression ratio comparable to svndiff1 with compression level 1, |
| * but not with other compression levels. |
| */ |
| *svndiff_version = 2; |
| } |
| else if (svndiff_encodings->nelts > 0) |
| { |
| const struct accept_rec *rec; |
| |
| /* Otherwise, use what the client prefers to see. */ |
| svn_sort__array(svndiff_encodings, sort_encoding_pref); |
| rec = &APR_ARRAY_IDX(svndiff_encodings, 0, struct accept_rec); |
| *svndiff_version = get_svndiff_version(rec); |
| } |
| else |
| { |
| *svndiff_version = 0; |
| } |
| } |
| |
| |
| /* The only two possible values for a capability. */ |
| static const char *capability_yes = "yes"; |
| static const char *capability_no = "no"; |
| |
| /* Convert CAPABILITIES, a hash table mapping 'const char *' keys to |
| * "yes" or "no" values, to a list of all keys whose value is "yes". |
| * Return the list, allocated in POOL, and use POOL for all temporary |
| * allocation. |
| */ |
| static apr_array_header_t * |
| capabilities_as_list(apr_hash_t *capabilities, apr_pool_t *pool) |
| { |
| apr_array_header_t *list = apr_array_make(pool, apr_hash_count(capabilities), |
| sizeof(char *)); |
| apr_hash_index_t *hi; |
| |
| for (hi = apr_hash_first(pool, capabilities); hi; hi = apr_hash_next(hi)) |
| { |
| const void *key; |
| void *val; |
| apr_hash_this(hi, &key, NULL, &val); |
| if (strcmp((const char *) val, "yes") == 0) |
| APR_ARRAY_PUSH(list, const char *) = key; |
| } |
| |
| return list; |
| } |
| |
| |
| /* Given a non-NULL QUERY string of the form "key1=val1&key2=val2&...", |
| * parse the keys and values into an apr table. Allocate the table in |
| * POOL; dup all keys and values into POOL as well. |
| * |
| * Note that repeating the same key will cause table overwrites |
| * (e.g. "r=3&r=5"), and that a lack of value ("p=") is legal, but |
| * equivalent to not specifying the key at all. |
| */ |
| static apr_table_t * |
| querystring_to_table(const char *query, apr_pool_t *pool) |
| { |
| apr_table_t *table = apr_table_make(pool, 2); |
| apr_array_header_t *array = svn_cstring_split(query, "&", TRUE, pool); |
| int i; |
| for (i = 0; i < array->nelts; i++) |
| { |
| char *keyval = APR_ARRAY_IDX(array, i, char *); |
| char *equals = strchr(keyval, '='); |
| if (equals != NULL) |
| { |
| *equals = '\0'; |
| apr_table_set(table, keyval, equals + 1); |
| } |
| } |
| return table; |
| } |
| |
| |
| /* Helper for get_resource(), called after COMB is fully parsed and prepped. */ |
| static dav_error * |
| do_out_of_date_check(dav_resource_combined *comb, request_rec *r) |
| { |
| svn_revnum_t created_rev; |
| svn_error_t *serr; |
| |
| /* Do we have an X-SVN-Version-Name header? */ |
| if (! SVN_IS_VALID_REVNUM(comb->priv.version_name)) |
| return NULL; |
| |
| /* Note: LOCK and DELETE handlers already notice the header and do |
| their own out-of-dateness checks. MKCOL, COPY, MOVE don't supply |
| the header at all, nor do MKACTIVITY, POST, or MERGE. */ |
| if (! ((r->method_number == M_PUT) |
| || (r->method_number == M_PROPPATCH))) |
| return NULL; |
| |
| /* Do an out-of-dateness check. */ |
| if ((serr = svn_fs_node_created_rev(&created_rev, comb->priv.root.root, |
| comb->priv.repos_path, r->pool))) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not get created rev of " |
| "resource", r->pool); |
| |
| if (SVN_IS_VALID_REVNUM(created_rev)) |
| { |
| if (comb->priv.version_name < created_rev) |
| { |
| serr = svn_error_createf(SVN_ERR_RA_OUT_OF_DATE, NULL, |
| comb->res.collection |
| ? "Directory '%s' is out of date" |
| : (comb->res.exists |
| ? "File '%s' is out of date" |
| : "'%s' is out of date"), |
| comb->priv.repos_path); |
| return dav_svn__convert_err(serr, HTTP_CONFLICT, |
| "Attempting to modify out-of-date resource.", |
| r->pool); |
| } |
| else if (comb->priv.version_name > created_rev) |
| { |
| svn_revnum_t txn_base_rev; |
| |
| txn_base_rev = svn_fs_txn_base_revision(comb->res.info->root.txn); |
| if (comb->priv.version_name > txn_base_rev) |
| { |
| serr = svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, |
| "No such revision %ld", |
| comb->priv.version_name); |
| |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Unknown base revision", |
| r->pool); |
| } |
| } |
| } |
| else if (comb->res.collection) |
| { |
| /* Issue #4480: With HTTPv2 we can receive the first change for a |
| directory after it has been made mutable, because one of its |
| descendants was changed before changing the directory. |
| |
| We have to check if whatever the node is in HEAD is equivalent |
| to what it was in the provided BASE revision. |
| |
| If the node was copied, we would process it before its decendants |
| and we already performed quite a few checks when making it mutable |
| via its descendant, so what we should really check here is if the |
| properties changed since the BASE version. |
| |
| ### I think svn_fs_node_relation() checks for more changes than we |
| should check for here. Needs further review. But it looks like |
| this check matches the checks in the libsvn_fs commit editor. |
| |
| For now I would say reporting out of date in a few too many |
| cases is safer than not reporting out of date when we should. |
| */ |
| svn_revnum_t txn_base_rev; |
| svn_fs_root_t *txn_base_root; |
| svn_fs_root_t *rev_root; |
| svn_fs_node_relation_t node_relation; |
| |
| txn_base_rev = svn_fs_txn_base_revision(comb->res.info->root.txn); |
| |
| if (comb->priv.version_name == txn_base_rev) |
| return NULL; /* Easy out: Nothing changed */ |
| |
| serr = svn_fs_revision_root(&txn_base_root, comb->res.info->repos->fs, |
| txn_base_rev, r->pool); |
| |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not open the transaction revision " |
| "for verification against the base " |
| "revision", r->pool); |
| } |
| |
| serr = svn_fs_revision_root(&rev_root, comb->res.info->repos->fs, |
| comb->priv.version_name, r->pool); |
| |
| if (serr != NULL) |
| { |
| svn_fs_close_root(txn_base_root); |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not open the base revision " |
| "for verification against the " |
| "transaction revision", r->pool); |
| } |
| |
| serr = svn_fs_node_relation(&node_relation, rev_root, |
| comb->priv.repos_path, |
| txn_base_root, |
| comb->priv.repos_path, |
| r->pool); |
| |
| svn_fs_close_root(rev_root); |
| svn_fs_close_root(txn_base_root); |
| |
| if (serr != NULL) |
| { |
| /* ### correct HTTP error? */ |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Unable to fetch the node revision id " |
| "of the version resource within the " |
| "revision", |
| r->pool); |
| } |
| |
| if (node_relation != svn_fs_node_unchanged) |
| { |
| serr = svn_error_createf(SVN_ERR_RA_OUT_OF_DATE, NULL, |
| "Directory '%s' is out of date", |
| comb->priv.repos_path); |
| return dav_svn__convert_err(serr, HTTP_CONFLICT, |
| "Attempting to modify out-of-date resource.", |
| r->pool); |
| } |
| } |
| |
| return NULL; |
| } |
| |
| |
| /* Helper for get_resource(). |
| * |
| * Given a fully fleshed out COMB object which has already been parsed |
| * via parse_uri(), parse the querystring in QUERY. |
| * |
| * Specifically, look for optional 'p=PEGREV' and 'r=WORKINGREV' |
| * values in the querystring, and modify COMB so that prep_regular() |
| * opens the correct revision and path. |
| */ |
| static dav_error * |
| parse_querystring(request_rec *r, const char *query, |
| dav_resource_combined *comb, apr_pool_t *pool) |
| { |
| svn_error_t *serr; |
| svn_revnum_t working_rev, peg_rev; |
| apr_table_t *pairs = querystring_to_table(query, pool); |
| const char *prevstr = apr_table_get(pairs, "p"); |
| const char *wrevstr; |
| const char *keyword_subst; |
| |
| /* Will we be doing keyword substitution? */ |
| keyword_subst = apr_table_get(pairs, "kw"); |
| if (keyword_subst && (strcmp(keyword_subst, "1") == 0)) |
| comb->priv.keyword_subst = TRUE; |
| |
| if (prevstr) |
| { |
| while (*prevstr == 'r') |
| prevstr++; |
| peg_rev = SVN_STR_TO_REV(prevstr); |
| if (!SVN_IS_VALID_REVNUM(peg_rev)) |
| return dav_svn__new_error(pool, HTTP_BAD_REQUEST, 0, 0, |
| "invalid peg rev in query string"); |
| } |
| else |
| { |
| /* No peg-rev? Default to HEAD, just like the cmdline client. */ |
| serr = dav_svn__get_youngest_rev(&peg_rev, comb->priv.repos, pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Couldn't fetch youngest rev.", pool); |
| } |
| |
| wrevstr = apr_table_get(pairs, "r"); |
| if (wrevstr) |
| { |
| while (*wrevstr == 'r') |
| wrevstr++; |
| working_rev = SVN_STR_TO_REV(wrevstr); |
| if (!SVN_IS_VALID_REVNUM(working_rev)) |
| return dav_svn__new_error(pool, HTTP_BAD_REQUEST, 0, 0, |
| "invalid working rev in query string"); |
| } |
| else |
| { |
| /* No working-rev? Assume it's equal to the peg-rev, just |
| like the cmdline client does. */ |
| working_rev = peg_rev; |
| } |
| |
| /* If WORKING_REV is younger than PEG_REV, we have a problem. |
| Our node-tracing algorithms can't handle that scenario, so we'll |
| disallow it here. */ |
| if (working_rev > peg_rev) |
| return dav_svn__new_error(pool, HTTP_BAD_REQUEST, 0, 0, |
| "working rev greater than peg rev."); |
| |
| /* If WORKING_REV and PEG_REV are equivalent, we want to return the |
| resource at the revision. Otherwise, WORKING_REV is older than |
| PEG_REV, so we need to crawl back through the history of |
| REPOS_PATH@PEG_REV until we hit WORKING_REV. We'll then redirect |
| the client to the new location/revision pair found by that crawl. */ |
| if (working_rev == peg_rev) |
| { |
| comb->priv.root.rev = peg_rev; |
| |
| /* Did we have a peg revision? Remember this little fact (in |
| case deliver() needs to know it). */ |
| if (prevstr) |
| comb->priv.pegged = TRUE; |
| } |
| else |
| { |
| const char *newpath, *location; |
| apr_hash_t *locations; |
| apr_array_header_t *loc_revs = apr_array_make(pool, 1, |
| sizeof(svn_revnum_t)); |
| |
| dav_svn__authz_read_baton *arb = apr_pcalloc(pool, sizeof(*arb)); |
| arb->r = comb->priv.r; |
| arb->repos = comb->priv.repos; |
| |
| APR_ARRAY_PUSH(loc_revs, svn_revnum_t) = working_rev; |
| if ((serr = svn_repos_trace_node_locations(comb->priv.repos->fs, |
| &locations, |
| comb->priv.repos_path, |
| peg_rev, |
| loc_revs, |
| dav_svn__authz_read_func(arb), |
| arb, |
| pool))) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Couldn't trace history.", pool); |
| |
| newpath = apr_hash_get(locations, &working_rev, sizeof(working_rev)); |
| if (! newpath) |
| return dav_svn__new_error(pool, HTTP_NOT_FOUND, 0, 0, |
| "path doesn't exist in that revision."); |
| |
| /* Redirect folks to a canonical, peg-revision-only location. |
| If they used a peg revision in this request, we can use a |
| permanent redirect. If they didn't (peg-rev is HEAD), we can |
| only use a temporary redirect. In either case, preserve the |
| "keyword_subst" state in the redirected location, too. */ |
| location = ap_construct_url(r->pool, |
| apr_psprintf(r->pool, "%s%s?p=%ld%s", |
| (comb->priv.repos->root_path[1] |
| ? comb->priv.repos->root_path |
| : ""), |
| newpath, working_rev, |
| keyword_subst ? "&kw=1" : ""), |
| r); |
| apr_table_setn(r->headers_out, "Location", location); |
| return dav_svn__new_error(r->pool, |
| prevstr ? HTTP_MOVED_PERMANENTLY |
| : HTTP_MOVED_TEMPORARILY, |
| 0, 0, "redirecting to canonical location"); |
| } |
| |
| return NULL; |
| } |
| |
| static dav_error * |
| get_resource(request_rec *r, |
| const char *root_path, |
| const char *label, |
| int use_checked_in, |
| dav_resource **resource) |
| { |
| const char *fs_path; |
| const char *repo_name; |
| const char *xslt_uri; |
| const char *fs_parent_path; |
| dav_resource_combined *comb; |
| dav_svn_repos *repos; |
| const char *cleaned_uri; |
| const char *repo_basename; |
| const char *relative; |
| const char *repos_path; |
| const char *repos_key; |
| const char *version_name; |
| svn_error_t *serr; |
| dav_error *err; |
| int had_slash; |
| dav_locktoken_list *ltl; |
| struct cleanup_fs_access_baton *cleanup_baton; |
| struct cleanup_req_logging_baton *cleanup_req_logging_baton; |
| void *userdata; |
| apr_hash_t *fs_config; |
| |
| repo_name = dav_svn__get_repo_name(r); |
| xslt_uri = dav_svn__get_xslt_uri(r); |
| fs_parent_path = dav_svn__get_fs_parent_path(r); |
| |
| if (r->method_number == M_COPY) |
| { |
| /* Workaround for issue #4531: Avoid a depth-infinity walk on |
| the copy source by overriding the Depth header here. |
| mod_dav defaults to infinite depth if this header is not set |
| which makes copies O(size of source) rather than the desired O(1). |
| ### Should be fixed by an explicit provider API feature in mod_dav. */ |
| apr_table_setn(r->headers_in, "Depth", "0"); |
| } |
| |
| /* Special case: detect and build the SVNParentPath as a unique type |
| of private resource, iff the SVNListParentPath directive is 'on'. */ |
| if (dav_svn__is_parentpath_list(r)) |
| { |
| /* Only allow GET and HEAD on the parentpath resource |
| * httpd uses the same method_number for HEAD as GET */ |
| if (r->method_number != M_GET) |
| { |
| int status; |
| |
| /* Marshall the error back to the client by generating by |
| * way of the dav_svn__error_response_tag trick. */ |
| err = dav_svn__new_error(r->pool, HTTP_METHOD_NOT_ALLOWED, |
| SVN_ERR_APMOD_MALFORMED_URI, 0, |
| "The URI does not contain the name " |
| "of a repository."); |
| /* can't use r->allowed since the default handler isn't called */ |
| apr_table_setn(r->headers_out, "Allow", "GET,HEAD"); |
| status = dav_svn__error_response_tag(r, err); |
| |
| return dav_push_error(r->pool, status, err->error_id, NULL, err); |
| } |
| |
| err = get_parentpath_resource(r, resource); |
| if (err) |
| return err; |
| return NULL; |
| } |
| |
| /* This does all the work of interpreting/splitting the request uri. */ |
| err = dav_svn_split_uri(r, r->uri, root_path, |
| &cleaned_uri, &had_slash, |
| &repo_basename, &relative, &repos_path); |
| if (err) |
| return err; |
| |
| /* The path that we will eventually try to open as an svn |
| repository. Normally defined by the SVNPath directive. */ |
| fs_path = dav_svn__get_fs_path(r); |
| |
| /* If the SVNParentPath directive was used instead... */ |
| if (fs_parent_path != NULL) |
| { |
| /* ...then the URL to the repository is actually one implicit |
| component longer... */ |
| root_path = svn_urlpath__join(root_path, repo_basename, r->pool); |
| /* ...and we need to specify exactly what repository to open. */ |
| fs_path = svn_dirent_join(fs_parent_path, repo_basename, r->pool); |
| } |
| |
| /* Start building and filling a 'combination' object. */ |
| comb = apr_pcalloc(r->pool, sizeof(*comb)); |
| comb->res.info = &comb->priv; |
| comb->res.hooks = &dav_svn__hooks_repository; |
| comb->res.pool = r->pool; |
| comb->res.uri = cleaned_uri; |
| |
| /* Original request, off which to generate subrequests later. */ |
| comb->priv.r = r; |
| |
| /* ### ugly hack to carry over Content-Type data to the open_stream, which |
| ### does not have access to the request headers. */ |
| { |
| const char *ct = apr_table_get(r->headers_in, "content-type"); |
| |
| comb->priv.is_svndiff = |
| ct != NULL |
| && strcmp(ct, SVN_SVNDIFF_MIME_TYPE) == 0; |
| } |
| |
| negotiate_encoding_prefs(r, &comb->priv.svndiff_version); |
| |
| /* ### and another hack for computing diffs to send to the client */ |
| comb->priv.delta_base = apr_table_get(r->headers_in, |
| SVN_DAV_DELTA_BASE_HEADER); |
| |
| /* Gather any options requested by an svn client. */ |
| comb->priv.svn_client_options = apr_table_get(r->headers_in, |
| SVN_DAV_OPTIONS_HEADER); |
| |
| /* See if the client sent a custom 'version name' request header. */ |
| version_name = apr_table_get(r->headers_in, SVN_DAV_VERSION_NAME_HEADER); |
| comb->priv.version_name |
| = version_name ? SVN_STR_TO_REV(version_name): SVN_INVALID_REVNUM; |
| |
| /* Remember checksums, if any. */ |
| comb->priv.base_checksum = |
| apr_table_get(r->headers_in, SVN_DAV_BASE_FULLTEXT_MD5_HEADER); |
| comb->priv.result_checksum = |
| apr_table_get(r->headers_in, SVN_DAV_RESULT_FULLTEXT_MD5_HEADER); |
| |
| /* "relative" is part of the "uri" string, so it has the proper |
| lifetime to store here. */ |
| /* ### that comment no longer applies. we're creating a string with its |
| ### own lifetime now. so WHY are we using a string? hmm... */ |
| comb->priv.uri_path = svn_stringbuf_create(relative, r->pool); |
| |
| /* initialize this until we put something real here */ |
| comb->priv.root.rev = SVN_INVALID_REVNUM; |
| |
| /* create the repository structure and stash it away */ |
| repos = apr_pcalloc(r->pool, sizeof(*repos)); |
| repos->pool = r->pool; |
| repos->youngest_rev = SVN_INVALID_REVNUM; |
| |
| comb->priv.repos = repos; |
| |
| /* We are assuming the root_path will live at least as long as this |
| resource. Considering that it typically comes from the per-dir |
| config in mod_dav, this is valid for now. */ |
| repos->root_path = svn_path_uri_encode(root_path, r->pool); |
| |
| /* where is the SVN FS for this resource? */ |
| repos->fs_path = fs_path; |
| |
| /* A name for the repository */ |
| repos->repo_name = repo_name; |
| |
| /* The repository filesystem basename */ |
| repos->repo_basename = repo_basename; |
| |
| /* An XSL transformation */ |
| repos->xslt_uri = xslt_uri; |
| |
| /* Is autoversioning active in this repos? */ |
| repos->autoversioning = dav_svn__get_autoversioning_flag(r); |
| |
| /* Are bulk updates allowed in this repos? */ |
| repos->bulk_updates = dav_svn__get_bulk_updates_flag(r); |
| |
| /* Are we advertising HTTP v2 protocol support? */ |
| repos->v2_protocol = dav_svn__check_httpv2_support(r); |
| |
| /* Path to activities database */ |
| repos->activities_db = dav_svn__get_activities_db(r); |
| if (repos->activities_db == NULL) |
| /* If not specified, use default ($repos/dav/activities.d). */ |
| repos->activities_db = svn_dirent_join(repos->fs_path, |
| DEFAULT_ACTIVITY_DB, |
| r->pool); |
| else if (fs_parent_path != NULL) |
| /* If this is a ParentPath-based repository, treat the specified |
| path as a similar parent directory. */ |
| repos->activities_db = svn_dirent_join(repos->activities_db, |
| svn_dirent_basename(repos->fs_path, |
| r->pool), |
| r->pool); |
| |
| /* Remember various bits for later URL construction */ |
| repos->base_url = ap_construct_url(r->pool, "", r); |
| repos->special_uri = dav_svn__get_special_uri(r); |
| |
| /* Remember who is making this request */ |
| repos->username = r->user; |
| |
| /* Allocate room for capabilities, but don't search for any until |
| we know that this is a Subversion client. */ |
| repos->client_capabilities = apr_hash_make(repos->pool); |
| |
| /* Remember if the requesting client is a Subversion client, and if |
| so, what its capabilities are. */ |
| { |
| const char *val = apr_table_get(r->headers_in, "User-Agent"); |
| |
| if (val && (ap_strstr_c(val, "SVN/") == val)) |
| { |
| repos->is_svn_client = TRUE; |
| |
| /* Client capabilities are self-reported. There is no |
| guarantee the client actually has the capabilities it says |
| it has, we just assume it is in the client's interests to |
| report accurately. Also, we only remember the capabilities |
| the server cares about (even though the client may send |
| more than that). */ |
| |
| /* Start out assuming no capabilities. */ |
| svn_hash_sets(repos->client_capabilities, |
| SVN_RA_CAPABILITY_MERGEINFO, |
| capability_no); |
| |
| /* Then see what we can find. */ |
| val = apr_table_get(r->headers_in, "DAV"); |
| if (val) |
| { |
| apr_array_header_t *vals |
| = svn_cstring_split(val, ",", TRUE, r->pool); |
| |
| if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_MERGEINFO, vals)) |
| { |
| svn_hash_sets(repos->client_capabilities, |
| SVN_RA_CAPABILITY_MERGEINFO, capability_yes); |
| } |
| } |
| } |
| } |
| |
| /* Retrieve/cache open repository */ |
| repos_key = apr_pstrcat(r->pool, "mod_dav_svn:", fs_path, SVN_VA_NULL); |
| apr_pool_userdata_get(&userdata, repos_key, r->connection->pool); |
| repos->repos = userdata; |
| if (repos->repos == NULL) |
| { |
| const char *fs_type; |
| |
| /* construct FS configuration parameters */ |
| fs_config = apr_hash_make(r->connection->pool); |
| svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_CACHE_DELTAS, |
| dav_svn__get_txdelta_cache_flag(r) ? "1" :"0"); |
| svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_CACHE_FULLTEXTS, |
| dav_svn__get_fulltext_cache_flag(r) ? "1" :"0"); |
| svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_CACHE_REVPROPS, |
| dav_svn__get_revprop_cache_flag(r) ? "2" :"0"); |
| svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_CACHE_NODEPROPS, |
| dav_svn__get_nodeprop_cache_flag(r) ? "1" :"0"); |
| svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_BLOCK_READ, |
| dav_svn__get_block_read_flag(r) ? "1" :"0"); |
| |
| /* Disallow BDB/event until issue 4157 is fixed. */ |
| if (!strcmp(ap_show_mpm(), "event")) |
| { |
| serr = svn_repos__fs_type(&fs_type, fs_path, r->connection->pool); |
| if (serr) |
| { |
| /* svn_repos_open2 is going to fail, use that error. */ |
| svn_error_clear(serr); |
| serr = NULL; |
| } |
| else if (!strcmp(fs_type, "bdb")) |
| serr = svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, |
| "BDB repository at '%s' is not compatible " |
| "with event MPM", |
| fs_path); |
| } |
| else |
| serr = NULL; |
| |
| /* open the FS */ |
| if (!serr) |
| serr = svn_repos_open3(&(repos->repos), fs_path, fs_config, |
| r->connection->pool, r->pool); |
| if (serr != NULL) |
| { |
| /* The error returned by svn_repos_open2 might contain the |
| actual path to the failed repository. We don't want to |
| leak that path back to the client, because that would be |
| a security risk, but we do want to log the real error on |
| the server side. */ |
| |
| apr_status_t cause = svn_error_root_cause(serr)->apr_err; |
| if (APR_STATUS_IS_ENOENT(cause) || APR_STATUS_IS_ENOTDIR(cause)) |
| return dav_svn__sanitize_error( |
| serr, "Could not find the requested SVN filesystem", |
| HTTP_NOT_FOUND, r); |
| else |
| return dav_svn__sanitize_error( |
| serr, "Could not open the requested SVN filesystem", |
| HTTP_INTERNAL_SERVER_ERROR, r); |
| } |
| |
| /* Cache the open repos for the next request on this connection */ |
| apr_pool_userdata_set(repos->repos, repos_key, |
| NULL, r->connection->pool); |
| |
| /* Store the capabilities of the current connection, making sure |
| to use the same pool repos->repos itself was created in. */ |
| serr = svn_repos_remember_client_capabilities |
| (repos->repos, capabilities_as_list(repos->client_capabilities, |
| r->connection->pool)); |
| if (serr != NULL) |
| { |
| return dav_svn__sanitize_error(serr, |
| "Error storing client capabilities " |
| "in repos object", |
| HTTP_INTERNAL_SERVER_ERROR, r); |
| } |
| |
| /* Configure hook script environment variables. */ |
| serr = svn_repos_hooks_setenv(repos->repos, dav_svn__get_hooks_env(r), |
| r->pool); |
| if (serr) |
| return dav_svn__sanitize_error(serr, |
| "Error settings hooks environment", |
| HTTP_INTERNAL_SERVER_ERROR, r); |
| } |
| |
| /* cache the filesystem object */ |
| repos->fs = svn_repos_fs(repos->repos); |
| |
| /* capture warnings during cleanup of the FS */ |
| svn_fs_set_warning_func(repos->fs, log_warning_req, r); |
| |
| /* if an authenticated username is present, attach it to the FS */ |
| if (r->user) |
| { |
| svn_fs_access_t *access_ctx; |
| |
| /* The fs is cached in connection->pool, but the fs access |
| context lives in r->pool. Because the username or token |
| could change on each request, we need to make sure that the |
| fs points to a NULL access context after the request is gone. */ |
| cleanup_baton = apr_pcalloc(r->pool, sizeof(*cleanup_baton)); |
| cleanup_baton->pool = r->pool; |
| cleanup_baton->fs = repos->fs; |
| apr_pool_cleanup_register(r->pool, cleanup_baton, cleanup_fs_access, |
| apr_pool_cleanup_null); |
| |
| /* We must degrade the logging context when the request is freed. */ |
| cleanup_req_logging_baton = |
| apr_pcalloc(r->pool, sizeof(*cleanup_req_logging_baton)); |
| cleanup_req_logging_baton->fs = repos->fs; |
| cleanup_req_logging_baton->connection = r->connection; |
| apr_pool_pre_cleanup_register(r->pool, cleanup_req_logging_baton, |
| cleanup_req_logging); |
| |
| /* Create an access context based on the authenticated username. */ |
| serr = svn_fs_create_access(&access_ctx, r->user, r->pool); |
| if (serr) |
| { |
| return dav_svn__sanitize_error(serr, |
| "Could not create fs access context", |
| HTTP_INTERNAL_SERVER_ERROR, r); |
| } |
| |
| /* Attach the access context to the fs. */ |
| serr = svn_fs_set_access(repos->fs, access_ctx); |
| if (serr) |
| { |
| return dav_svn__sanitize_error(serr, "Could not attach access " |
| "context to fs", |
| HTTP_INTERNAL_SERVER_ERROR, r); |
| } |
| } |
| |
| /* Look for locktokens in the "If:" request header. */ |
| err = dav_get_locktoken_list(r, <l); |
| |
| /* dav_get_locktoken_list claims to return a NULL list when no |
| locktokens are present. But it actually throws this error |
| instead! So we're deliberately trapping/ignoring it. |
| |
| This is a workaround for a bug in mod_dav. Remove this when the |
| bug is fixed in mod_dav. See Subversion Issue #2248 */ |
| if (err && (err->error_id != DAV_ERR_IF_ABSENT)) |
| return err; |
| |
| /* If one or more locktokens are present in the header, push them |
| into the filesystem access context. */ |
| if (ltl) |
| { |
| svn_fs_access_t *access_ctx; |
| dav_locktoken_list *list = ltl; |
| |
| serr = svn_fs_get_access(&access_ctx, repos->fs); |
| if (serr || !access_ctx) |
| { |
| if (serr == NULL) |
| serr = svn_error_create(SVN_ERR_FS_LOCK_OWNER_MISMATCH, NULL, NULL); |
| return dav_svn__sanitize_error(serr, "Lock token is in request, " |
| "but no user name", |
| HTTP_BAD_REQUEST, r); |
| } |
| |
| do { |
| /* Note the path/lock pairs are only for lock token checking |
| in access, and the relative path is not actually accurate |
| as it contains the !svn bits. However, we're using only |
| the tokens anyway (for access control). */ |
| |
| serr = svn_fs_access_add_lock_token2(access_ctx, relative, |
| list->locktoken->uuid_str); |
| |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Error pushing token into filesystem.", |
| r->pool); |
| list = list->next; |
| |
| } while (list); |
| } |
| |
| |
| /* Figure out the type of the resource. Note that we have a PARSE step |
| which is separate from a PREP step. This is because the PARSE can |
| map multiple URLs to the same resource type. The PREP operates on |
| the type of the resource. */ |
| |
| /* skip over the leading "/" in the relative URI */ |
| if (parse_uri(comb, relative + 1, label, use_checked_in)) |
| goto malformed_URI; |
| |
| /* Check for a query string on a regular-type resource; this allows |
| us to discover and parse a "universal" rev-path URI of the form |
| "path?[r=REV][&p=PEGREV]" */ |
| if ((comb->res.type == DAV_RESOURCE_TYPE_REGULAR) |
| && (r->parsed_uri.query != NULL) |
| && ((err = parse_querystring(r, r->parsed_uri.query, comb, r->pool)))) |
| return err; |
| |
| #ifdef SVN_DEBUG |
| if (comb->res.type == DAV_RESOURCE_TYPE_UNKNOWN) |
| { |
| /* Unknown URI. Return NULL to indicate "no resource" */ |
| DBG0("DESIGN FAILURE: should not be UNKNOWN at this point"); |
| *resource = NULL; |
| return NULL; |
| } |
| #endif |
| |
| /* prepare the resource for operation */ |
| if ((err = prep_resource(comb)) != NULL) |
| return err; |
| |
| /* a GET request for a REGULAR collection resource MUST have a trailing |
| slash. Redirect to include one if it does not. */ |
| if (comb->res.collection && comb->res.type == DAV_RESOURCE_TYPE_REGULAR |
| && !had_slash && r->method_number == M_GET) |
| { |
| const char *new_path = apr_pstrcat(r->pool, |
| ap_escape_uri(r->pool, r->uri), |
| "/", |
| r->args ? "?" : "", |
| r->args ? r->args : "", |
| SVN_VA_NULL); |
| apr_table_setn(r->headers_out, "Location", |
| ap_construct_url(r->pool, new_path, r)); |
| return dav_svn__new_error(r->pool, HTTP_MOVED_PERMANENTLY, 0, 0, |
| "Requests for a collection must have a " |
| "trailing slash on the URI."); |
| } |
| |
| /* HTTPv2: for write-requests, out-of-dateness checks happen via |
| Base-Version header rather via CHECKOUT requests. |
| |
| If a Base-Version header is present on a write request, we need |
| to do the out-of-dateness check *here*, rather than in other |
| dav-provider vtable funcs. That's because a number of mod_dav |
| methods annoyingly trap and genericize our error messages. */ |
| if ((err = do_out_of_date_check(comb, r)) != NULL) |
| return err; |
| |
| *resource = &comb->res; |
| return NULL; |
| |
| malformed_URI: |
| /* A malformed URI error occurs when a URI indicates the "special" area, |
| yet it has an improper construction. Generally, this is because some |
| doofus typed it in manually or has a buggy client. */ |
| /* ### pick something other than HTTP_INTERNAL_SERVER_ERROR */ |
| /* ### are SVN_ERR_APMOD codes within the right numeric space? */ |
| return dav_svn__new_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, |
| SVN_ERR_APMOD_MALFORMED_URI, 0, |
| "The URI indicated a resource within Subversion's " |
| "special resource area, but does not exist. This " |
| "is generally caused by a problem in the client " |
| "software."); |
| } |
| |
| |
| /* Helper func: return the parent of PATH, allocated in POOL. If |
| IS_URLPATH is set, PATH is a urlpath; otherwise, it's either a |
| relpath or an fspath. */ |
| static const char * |
| get_parent_path(const char *path, |
| svn_boolean_t is_urlpath, |
| apr_pool_t *pool) |
| { |
| if (*path != '\0') /* not an empty string */ |
| { |
| if (is_urlpath) |
| return svn_urlpath__dirname(path, pool); |
| else |
| return svn_fspath__dirname(path, pool); |
| } |
| |
| return path; |
| } |
| |
| |
| static dav_error * |
| get_parent_resource(const dav_resource *resource, |
| dav_resource **parent_resource) |
| { |
| dav_resource *parent; |
| dav_resource_private *parentinfo; |
| svn_stringbuf_t *path = resource->info->uri_path; |
| |
| /* Initialize the return value. */ |
| *parent_resource = NULL; |
| |
| /* The root of the repository has no parent. */ |
| if (path->len == 1 && *path->data == '/') |
| return NULL; |
| |
| /* If possible, create a parent based on the type of RESOURCE. */ |
| switch (resource->type) |
| { |
| case DAV_RESOURCE_TYPE_REGULAR: |
| |
| parent = apr_pcalloc(resource->pool, sizeof(*parent)); |
| parentinfo = apr_pcalloc(resource->pool, sizeof(*parentinfo)); |
| |
| parent->type = DAV_RESOURCE_TYPE_REGULAR; |
| parent->exists = 1; |
| parent->collection = 1; |
| parent->versioned = 1; |
| parent->hooks = resource->hooks; |
| parent->pool = resource->pool; |
| parent->uri = get_parent_path(svn_urlpath__canonicalize(resource->uri, |
| resource->pool), |
| TRUE, resource->pool); |
| parent->info = parentinfo; |
| |
| parentinfo->uri_path = |
| svn_stringbuf_create( |
| get_parent_path( |
| svn_urlpath__canonicalize(resource->info->uri_path->data, |
| resource->pool), |
| TRUE, resource->pool), |
| resource->pool); |
| parentinfo->repos = resource->info->repos; |
| parentinfo->root = resource->info->root; |
| parentinfo->r = resource->info->r; |
| parentinfo->svn_client_options = resource->info->svn_client_options; |
| parentinfo->repos_path = get_parent_path(resource->info->repos_path, |
| FALSE, resource->pool); |
| |
| *parent_resource = parent; |
| break; |
| |
| case DAV_RESOURCE_TYPE_WORKING: |
| /* The "/" occurring within the URL of working resources is part of |
| its identifier; it does not establish parent resource relationships. |
| All working resources have the same parent, which is: |
| http://host.name/path2repos/$svn/wrk/ |
| */ |
| *parent_resource = |
| create_private_resource(resource, DAV_SVN_RESTYPE_WRK_COLLECTION); |
| break; |
| |
| case DAV_RESOURCE_TYPE_ACTIVITY: |
| *parent_resource = |
| create_private_resource(resource, DAV_SVN_RESTYPE_ACT_COLLECTION); |
| break; |
| |
| case DAV_RESOURCE_TYPE_PRIVATE: |
| if ((resource->info->restype == DAV_SVN_RESTYPE_TXN_COLLECTION) |
| || (resource->info->restype == DAV_SVN_RESTYPE_REV_COLLECTION)) |
| *parent_resource = |
| create_private_resource(resource, resource->info->restype); |
| /* ### FIXME: Need parents for other private resource types. */ |
| break; |
| |
| default: |
| /* ### FIXME: Need parents for other resource types. */ |
| break; |
| } |
| |
| /* If we didn't create parent resource above, complain. */ |
| if (! *parent_resource) |
| return dav_svn__new_error(resource->pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, |
| apr_psprintf(resource->pool, |
| "get_parent_resource was called for " |
| "%s (type %d)", |
| resource->uri, resource->type)); |
| |
| return NULL; |
| } |
| |
| |
| /* does RES2 live in the same repository as RES1? */ |
| static int |
| is_our_resource(const dav_resource *res1, const dav_resource *res2) |
| { |
| if (res1->hooks != res2->hooks |
| || strcmp(res1->info->repos->fs_path, res2->info->repos->fs_path) != 0) |
| { |
| /* a different provider, or a different FS repository */ |
| return 0; |
| } |
| |
| /* coalesce the repository */ |
| if (res1->info->repos != res2->info->repos) |
| { |
| /* ### might be nice to have a pool which we can clear to toss |
| ### out the old, redundant repos/fs. */ |
| |
| /* have res2 point to res1's filesystem */ |
| res2->info->repos = res1->info->repos; |
| |
| /* res2's fs_root object is now invalid. regenerate it using |
| the now-shared filesystem. */ |
| if (res2->info->root.txn_name) |
| { |
| /* reopen the txn by name */ |
| svn_error_clear(svn_fs_open_txn(&(res2->info->root.txn), |
| res2->info->repos->fs, |
| res2->info->root.txn_name, |
| res2->info->repos->pool)); |
| |
| /* regenerate the txn "root" object */ |
| svn_error_clear(svn_fs_txn_root(&(res2->info->root.root), |
| res2->info->root.txn, |
| res2->info->repos->pool)); |
| } |
| else if (res2->info->root.rev) |
| { |
| /* default: regenerate the revision "root" object */ |
| svn_error_clear(svn_fs_revision_root(&(res2->info->root.root), |
| res2->info->repos->fs, |
| res2->info->root.rev, |
| res2->info->repos->pool)); |
| } |
| } |
| |
| return 1; |
| } |
| |
| |
| static int |
| is_same_resource(const dav_resource *res1, const dav_resource *res2) |
| { |
| if (!is_our_resource(res1, res2)) |
| return 0; |
| |
| /* ### what if the same resource were reached via two URIs? */ |
| |
| return svn_stringbuf_compare(res1->info->uri_path, res2->info->uri_path); |
| } |
| |
| |
| static int |
| is_parent_resource(const dav_resource *res1, const dav_resource *res2) |
| { |
| apr_size_t len1 = strlen(res1->info->uri_path->data); |
| apr_size_t len2; |
| |
| if (!is_our_resource(res1, res2)) |
| return 0; |
| |
| /* ### what if a resource were reached via two URIs? we ought to define |
| ### parent/child relations for resources independent of URIs. |
| ### i.e. define a "canonical" location for each resource, then return |
| ### the parent based on that location. */ |
| |
| /* res2 is one of our resources, we can use its ->info ptr */ |
| len2 = strlen(res2->info->uri_path->data); |
| |
| return (len2 > len1 |
| && memcmp(res1->info->uri_path->data, res2->info->uri_path->data, |
| len1) == 0 |
| && res2->info->uri_path->data[len1] == '/'); |
| } |
| |
| |
| static dav_error * |
| open_stream(const dav_resource *resource, |
| dav_stream_mode mode, |
| dav_stream **stream) |
| { |
| svn_node_kind_t kind; |
| dav_error *derr; |
| svn_error_t *serr; |
| |
| if (mode == DAV_MODE_WRITE_TRUNC || mode == DAV_MODE_WRITE_SEEKABLE) |
| { |
| if (resource->type != DAV_RESOURCE_TYPE_WORKING) |
| { |
| return dav_svn__new_error(resource->pool, HTTP_METHOD_NOT_ALLOWED, |
| 0, 0, |
| "Resource body changes may only be made to " |
| "working resources (at this time)."); |
| } |
| if (!resource->info->root.root) |
| { |
| return dav_svn__new_error(resource->pool, HTTP_METHOD_NOT_ALLOWED, |
| 0, 0, |
| "Resource body changes may only be made to " |
| "checked-out resources (at this time)."); |
| } |
| } |
| |
| /* ### TODO: Can we support range writes someday? */ |
| if (mode == DAV_MODE_WRITE_SEEKABLE) |
| { |
| return dav_svn__new_error(resource->pool, HTTP_NOT_IMPLEMENTED, 0, 0, |
| "Resource body writes cannot use ranges " |
| "(at this time)."); |
| } |
| |
| /* start building the stream structure */ |
| *stream = apr_pcalloc(resource->pool, sizeof(**stream)); |
| (*stream)->res = resource; |
| |
| derr = fs_check_path(&kind, resource->info->root.root, |
| resource->info->repos_path, resource->pool); |
| if (derr != NULL) |
| return derr; |
| |
| if (kind == svn_node_none) /* No existing file. */ |
| { |
| serr = svn_fs_make_file(resource->info->root.root, |
| resource->info->repos_path, |
| resource->pool); |
| |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not create file within the " |
| "repository.", |
| resource->pool); |
| } |
| } |
| |
| /* if the working-resource was auto-checked-out (i.e. came into |
| existence through the autoversioning feature), then possibly set |
| the svn:mime-type property based on whatever value mod_mime has |
| chosen. If the path already has an svn:mime-type property |
| set, do nothing. */ |
| if (resource->info->auto_checked_out |
| && resource->info->r->content_type) |
| { |
| svn_string_t *mime_type; |
| |
| serr = svn_fs_node_prop(&mime_type, |
| resource->info->root.root, |
| resource->info->repos_path, |
| SVN_PROP_MIME_TYPE, |
| resource->pool); |
| |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Error fetching mime-type property.", |
| resource->pool); |
| } |
| |
| if (!mime_type) |
| { |
| serr = svn_fs_change_node_prop(resource->info->root.root, |
| resource->info->repos_path, |
| SVN_PROP_MIME_TYPE, |
| svn_string_create |
| (resource->info->r->content_type, |
| resource->pool), |
| resource->pool); |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not set mime-type property.", |
| resource->pool); |
| } |
| } |
| } |
| |
| serr = svn_fs_apply_textdelta(&(*stream)->delta_handler, |
| &(*stream)->delta_baton, |
| resource->info->root.root, |
| resource->info->repos_path, |
| resource->info->base_checksum, |
| resource->info->result_checksum, |
| resource->pool); |
| |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not prepare to write the file", |
| resource->pool); |
| } |
| |
| /* if the incoming data is an SVNDIFF, then create a stream that |
| will process the data into windows and invoke the FS window handler |
| when a window is ready. */ |
| /* ### we need a better way to check the content-type! this is bogus |
| ### because we're effectively looking at the request_rec. doubly |
| ### bogus because this means you cannot open arbitrary streams and |
| ### feed them content (the type is always tied to a request_rec). |
| ### probably ought to pass the type to open_stream */ |
| if (resource->info->is_svndiff) |
| { |
| (*stream)->wstream = |
| svn_txdelta_parse_svndiff((*stream)->delta_handler, |
| (*stream)->delta_baton, |
| TRUE, |
| resource->pool); |
| } |
| |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| close_stream(dav_stream *stream, int commit) |
| { |
| svn_error_t *serr; |
| apr_pool_t *pool = stream->res->pool; |
| |
| if (stream->rstream != NULL) |
| { |
| serr = svn_stream_close(stream->rstream); |
| if (serr) |
| return dav_svn__convert_err |
| (serr, HTTP_INTERNAL_SERVER_ERROR, |
| "mod_dav_svn close_stream: error closing read stream", |
| pool); |
| } |
| |
| /* if we have a write-stream, then closing it also takes care of the |
| handler (so make sure not to send a NULL to it, too) */ |
| if (stream->wstream != NULL) |
| { |
| serr = svn_stream_close(stream->wstream); |
| if (serr) |
| return dav_svn__convert_err |
| (serr, HTTP_INTERNAL_SERVER_ERROR, |
| "mod_dav_svn close_stream: error closing write stream", |
| pool); |
| } |
| else if (stream->delta_handler != NULL) |
| { |
| serr = (*stream->delta_handler)(NULL, stream->delta_baton); |
| if (serr) |
| return dav_svn__convert_err |
| (serr, HTTP_INTERNAL_SERVER_ERROR, |
| "mod_dav_svn close_stream: error sending final (null) delta window", |
| pool); |
| } |
| |
| if (stream->wstream != NULL || stream->delta_handler != NULL) |
| { |
| request_rec *r = stream->res->info->r; |
| svn_checksum_t *checksum; |
| |
| serr = svn_fs_file_checksum(&checksum, svn_checksum_md5, |
| stream->res->info->root.root, |
| stream->res->info->repos_path, |
| FALSE, pool); |
| if (serr) |
| return dav_svn__convert_err |
| (serr, HTTP_INTERNAL_SERVER_ERROR, |
| "mod_dav_svn close_stream: error getting file checksum", |
| pool); |
| |
| if (checksum) |
| apr_table_set(r->headers_out, SVN_DAV_RESULT_FULLTEXT_MD5_HEADER, |
| svn_checksum_to_cstring(checksum, pool)); |
| } |
| |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| write_stream(dav_stream *stream, const void *buf, apr_size_t bufsize) |
| { |
| svn_error_t *serr; |
| apr_pool_t *pool = stream->res->pool; |
| |
| if (stream->wstream != NULL) |
| { |
| serr = svn_stream_write(stream->wstream, buf, &bufsize); |
| /* ### would the returned bufsize ever not match the requested amt? */ |
| } |
| else |
| { |
| svn_txdelta_window_t window = { 0 }; |
| svn_txdelta_op_t op; |
| svn_string_t data; |
| |
| data.data = buf; |
| data.len = bufsize; |
| |
| op.action_code = svn_txdelta_new; |
| op.offset = 0; |
| op.length = bufsize; |
| |
| window.tview_len = bufsize; /* result will be this long */ |
| window.num_ops = 1; |
| window.ops = &op; |
| window.new_data = &data; |
| |
| serr = (*stream->delta_handler)(&window, stream->delta_baton); |
| } |
| |
| if (serr) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not write the file contents", |
| pool); |
| } |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| seek_stream(dav_stream *stream, apr_off_t abs_position) |
| { |
| /* ### fill this in */ |
| |
| return dav_svn__new_error(stream->res->pool, HTTP_NOT_IMPLEMENTED, 0, 0, |
| "Resource body read/write cannot use ranges " |
| "(at this time)"); |
| } |
| |
| /* Returns whether the DAV resource lacks potential for generation of |
| an ETag (defined as any of the following): |
| - it doesn't exist |
| - the resource type isn't REGULAR or VERSION |
| - the resource is a Baseline */ |
| #define RESOURCE_LACKS_ETAG_POTENTIAL(resource) \ |
| (!resource->exists \ |
| || (resource->type != DAV_RESOURCE_TYPE_REGULAR \ |
| && resource->type != DAV_RESOURCE_TYPE_VERSION) \ |
| || (resource->type == DAV_RESOURCE_TYPE_VERSION \ |
| && resource->baselined)) |
| |
| |
| const char * |
| dav_svn__getetag(const dav_resource *resource, apr_pool_t *pool) |
| { |
| svn_error_t *serr; |
| svn_revnum_t created_rev; |
| |
| if (RESOURCE_LACKS_ETAG_POTENTIAL(resource)) |
| return ""; |
| |
| /* ### what kind of etag to return for activities, etc.? */ |
| |
| if ((serr = svn_fs_node_created_rev(&created_rev, resource->info->root.root, |
| resource->info->repos_path, |
| pool))) |
| { |
| /* ### what to do? */ |
| svn_error_clear(serr); |
| return ""; |
| } |
| |
| /* Use the "weak" format of the etag for collections because our GET |
| requests on collections include dynamic data (the HEAD revision, |
| the build version of Subversion, etc.). */ |
| return apr_psprintf(pool, "%s\"%ld/%s\"", |
| resource->collection ? "W/" : "", |
| created_rev, |
| apr_xml_quote_string(pool, |
| resource->info->repos_path, 1)); |
| } |
| |
| |
| /* Since dav_svn__getetag() takes a pool argument, this wrapper is for |
| the mod_dav hooks vtable entry, which does not. */ |
| static const char * |
| getetag_pathetic(const dav_resource *resource) |
| { |
| return dav_svn__getetag(resource, resource->pool); |
| } |
| |
| /* Helper for set_headers(). Returns TRUE if request R to RESOURCE can be |
| * cached. Returns FALSe otherwise. */ |
| static svn_boolean_t |
| is_cacheable(request_rec *r, const dav_resource *resource) |
| { |
| /* Non-idempotent resource cannot be cached because actual |
| target could change when youngest revision or transacation |
| will change. */ |
| if (!resource->info->idempotent) |
| return FALSE; |
| |
| /* Our GET requests on collections include dynamic data (the |
| HEAD revision, the build version of Subversion, etc.). |
| Directory content is also subject of authz filtering.*/ |
| if (resource->collection) |
| return FALSE; |
| |
| if (resource->type == DAV_RESOURCE_TYPE_REGULAR || |
| resource->type == DAV_RESOURCE_TYPE_VERSION) |
| return TRUE; |
| else |
| return FALSE; |
| } |
| |
| static dav_error * |
| set_headers(request_rec *r, const dav_resource *resource) |
| { |
| svn_error_t *serr; |
| svn_filesize_t length; |
| const char *mimetype = NULL; |
| |
| /* As version resources don't change, encourage caching. */ |
| if (is_cacheable(r, resource)) |
| /* Cache resource for one week (specified in seconds). */ |
| apr_table_setn(r->headers_out, "Cache-Control", "max-age=604800"); |
| else |
| apr_table_setn(r->headers_out, "Cache-Control", "max-age=0"); |
| |
| if (!resource->exists) |
| return NULL; |
| |
| /* generate our etag and place it into the output */ |
| apr_table_setn(r->headers_out, "ETag", |
| dav_svn__getetag(resource, resource->pool)); |
| |
| /* we accept byte-ranges */ |
| apr_table_setn(r->headers_out, "Accept-Ranges", "bytes"); |
| |
| /* For a directory, we will send text/html or text/xml. If we have a delta |
| base, then we will always be generating an svndiff. Otherwise, |
| we need to fetch the appropriate MIME type from the resource's |
| properties (and use text/plain if it isn't there). */ |
| if (resource->collection) |
| { |
| if (resource->info->repos->xslt_uri) |
| mimetype = "text/xml"; |
| else |
| mimetype = "text/html; charset=UTF-8"; |
| } |
| else if (resource->info->delta_base != NULL) |
| { |
| dav_svn__uri_info info; |
| |
| /* First order of business is to parse it. */ |
| serr = dav_svn__simple_parse_uri(&info, resource, |
| resource->info->delta_base, |
| resource->pool); |
| |
| /* If we successfully parse the base URL, then send an svndiff. */ |
| if ((serr == NULL) && (info.rev != SVN_INVALID_REVNUM)) |
| { |
| mimetype = SVN_SVNDIFF_MIME_TYPE; |
| |
| /* Note the base that this svndiff is based on, and tell any |
| intermediate caching proxies that this header is |
| significant. */ |
| apr_table_setn(r->headers_out, "Vary", SVN_DAV_DELTA_BASE_HEADER); |
| apr_table_setn(r->headers_out, SVN_DAV_DELTA_BASE_HEADER, |
| resource->info->delta_base); |
| } |
| svn_error_clear(serr); |
| } |
| |
| if ((mimetype == NULL) |
| && ((resource->type == DAV_RESOURCE_TYPE_VERSION) |
| || (resource->type == DAV_RESOURCE_TYPE_REGULAR)) |
| && (resource->info->repos_path != NULL)) |
| { |
| svn_string_t *value; |
| |
| serr = svn_fs_node_prop(&value, |
| resource->info->root.root, |
| resource->info->repos_path, |
| SVN_PROP_MIME_TYPE, |
| resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not fetch the resource's MIME type", |
| resource->pool); |
| |
| if (value) |
| mimetype = value->data; |
| else if ((! resource->info->repos->is_svn_client) |
| && r->content_type) |
| mimetype = r->content_type; |
| |
| /* If we found a MIME type, we'll make sure it's Subversion-friendly. */ |
| if (mimetype) |
| { |
| if ((serr = svn_mime_type_validate(mimetype, resource->pool))) |
| { |
| /* Probably serr->apr == SVN_ERR_BAD_MIME_TYPE, but there's |
| no point even checking. No matter what the error is, we |
| can't use this MIME type. */ |
| svn_error_clear(serr); |
| mimetype = NULL; |
| } |
| } |
| |
| /* We've found/calculated/validated no usable MIME type. We |
| could fall back to "application/octet-stream" (aka "bag o' |
| bytes"), but many browsers have grown to expect "text/plain" |
| to mean "*shrug*", and kick off their own MIME type detection |
| routines when they see it. So we'll use "text/plain". |
| |
| ### Why not just avoid sending a Content-type at all? Is |
| ### that just bad form for HTTP? */ |
| if (! mimetype) |
| mimetype = "text/plain"; |
| |
| |
| /* if we aren't sending a diff and aren't expanding keywords, |
| then we know the exact length of the file, so set up the |
| Content-Length header. */ |
| if (! resource->info->keyword_subst) |
| { |
| serr = svn_fs_file_length(&length, |
| resource->info->root.root, |
| resource->info->repos_path, |
| resource->pool); |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not fetch the resource length", |
| resource->pool); |
| } |
| ap_set_content_length(r, (apr_off_t) length); |
| } |
| } |
| |
| /* set the discovered MIME type */ |
| /* ### it would be best to do this during the findct phase... */ |
| ap_set_content_type(r, mimetype); |
| |
| return NULL; |
| } |
| |
| |
| typedef struct diff_ctx_t { |
| dav_svn__output *output; |
| apr_bucket_brigade *bb; |
| } diff_ctx_t; |
| |
| |
| static svn_error_t * __attribute__((warn_unused_result)) |
| write_to_filter(void *baton, const char *buffer, apr_size_t *len) |
| { |
| diff_ctx_t *dc = baton; |
| |
| /* take the current data and shove it into the filter */ |
| SVN_ERR(dav_svn__brigade_write(dc->bb, dc->output, buffer, *len)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static svn_error_t * __attribute__((warn_unused_result)) |
| close_filter(void *baton) |
| { |
| diff_ctx_t *dc = baton; |
| apr_bucket *bkt; |
| |
| /* done with the file. write an EOS bucket now. */ |
| bkt = apr_bucket_eos_create(dav_svn__output_get_bucket_alloc(dc->output)); |
| APR_BRIGADE_INSERT_TAIL(dc->bb, bkt); |
| SVN_ERR(dav_svn__output_pass_brigade(dc->output, dc->bb)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static svn_error_t * |
| emit_collection_head(const dav_resource *resource, |
| apr_bucket_brigade *bb, |
| dav_svn__output *output, |
| svn_boolean_t gen_html, |
| apr_pool_t *pool) |
| { |
| /* XML schema for the directory index if xslt_uri is set: |
| |
| <?xml version="1.0"?> |
| <?xml-stylesheet type="text/xsl" href="[info->repos->xslt_uri]"?> */ |
| static const char xml_index_dtd[] = |
| "<!DOCTYPE svn [\n" |
| " <!ELEMENT svn (index)>\n" |
| " <!ATTLIST svn version CDATA #REQUIRED\n" |
| " href CDATA #REQUIRED>\n" |
| " <!ELEMENT index (updir?, (file | dir)*)>\n" |
| " <!ATTLIST index name CDATA #IMPLIED\n" |
| " path CDATA #IMPLIED\n" |
| " rev CDATA #IMPLIED\n" |
| " base CDATA #IMPLIED>\n" |
| " <!ELEMENT updir EMPTY>\n" |
| " <!ATTLIST updir href CDATA #REQUIRED>\n" |
| " <!ELEMENT file EMPTY>\n" |
| " <!ATTLIST file name CDATA #REQUIRED\n" |
| " href CDATA #REQUIRED>\n" |
| " <!ELEMENT dir EMPTY>\n" |
| " <!ATTLIST dir name CDATA #REQUIRED\n" |
| " href CDATA #REQUIRED>\n" |
| "]>\n"; |
| |
| if (gen_html) |
| { |
| const char *title; |
| if (resource->info->repos_path == NULL) |
| title = "unknown location"; |
| else |
| title = resource->info->repos_path; |
| |
| if (resource->info->restype != DAV_SVN_RESTYPE_PARENTPATH_COLLECTION) |
| { |
| if (SVN_IS_VALID_REVNUM(resource->info->root.rev)) |
| title = apr_psprintf(pool, |
| "Revision %ld: %s", |
| resource->info->root.rev, title); |
| if (resource->info->repos->repo_basename) |
| title = apr_psprintf(pool, "%s - %s", |
| resource->info->repos->repo_basename, |
| title); |
| if (resource->info->repos->repo_name) |
| title = apr_psprintf(pool, "%s: %s", |
| resource->info->repos->repo_name, |
| title); |
| } |
| |
| SVN_ERR(dav_svn__brigade_printf(bb, output, |
| "<html><head><title>%s</title></head>\n" |
| "<body>\n <h2>%s</h2>\n <ul>\n", |
| title, title)); |
| } |
| else |
| { |
| const char *name = resource->info->repos->repo_name; |
| const char *href = resource->info->repos_path; |
| const char *base = resource->info->repos->repo_basename; |
| |
| SVN_ERR(dav_svn__brigade_puts(bb, output, "<?xml version=\"1.0\"?>\n")); |
| SVN_ERR(dav_svn__brigade_printf(bb, output, |
| "<?xml-stylesheet type=\"text/xsl\" " |
| "href=\"%s\"?>\n", |
| resource->info->repos->xslt_uri)); |
| SVN_ERR(dav_svn__brigade_puts(bb, output, xml_index_dtd)); |
| SVN_ERR(dav_svn__brigade_puts(bb, output, |
| "<svn version=\"" SVN_VERSION "\"\n" |
| " href=\"http://subversion.apache.org/\">\n")); |
| SVN_ERR(dav_svn__brigade_puts(bb, output, " <index")); |
| |
| if (name) |
| SVN_ERR(dav_svn__brigade_printf(bb, output, |
| " name=\"%s\"", |
| apr_xml_quote_string(resource->pool, |
| name, 1))); |
| if (SVN_IS_VALID_REVNUM(resource->info->root.rev)) |
| SVN_ERR(dav_svn__brigade_printf(bb, output, " rev=\"%ld\"", |
| resource->info->root.rev)); |
| if (href) |
| SVN_ERR(dav_svn__brigade_printf(bb, output, " path=\"%s\"", |
| apr_xml_quote_string(resource->pool, |
| href, 1))); |
| if (base) |
| SVN_ERR(dav_svn__brigade_printf(bb, output, " base=\"%s\"", base)); |
| |
| SVN_ERR(dav_svn__brigade_puts(bb, output, ">\n")); |
| } |
| |
| if ((resource->info->restype != DAV_SVN_RESTYPE_PARENTPATH_COLLECTION) |
| && resource->info->repos_path |
| && ((resource->info->repos_path[1] != '\0') |
| || dav_svn__get_list_parentpath_flag(resource->info->r))) |
| { |
| const char *href; |
| if (resource->info->pegged) |
| { |
| href = apr_psprintf(pool, "../?p=%ld", resource->info->root.rev); |
| } |
| else |
| { |
| href = "../"; |
| } |
| |
| if (gen_html) |
| { |
| SVN_ERR(dav_svn__brigade_printf(bb, output, |
| " <li><a href=\"%s\">..</a></li>\n", |
| href)); |
| } |
| else |
| { |
| SVN_ERR(dav_svn__brigade_printf(bb, output, |
| " <updir href=\"%s\"/>\n", |
| href)); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static svn_error_t * |
| emit_collection_entry(const dav_resource *resource, |
| apr_bucket_brigade *bb, |
| dav_svn__output *output, |
| const svn_fs_dirent_t *entry, |
| svn_boolean_t gen_html, |
| apr_pool_t *pool) |
| { |
| const char *name = entry->name; |
| const char *href = name; |
| svn_boolean_t is_dir = (entry->kind == svn_node_dir); |
| |
| /* append a trailing slash onto the name for directories. we NEED |
| this for the href portion so that the relative reference will |
| descend properly. for the visible portion, it is just nice. */ |
| /* ### The xml output doesn't like to see a trailing slash on |
| ### the visible portion, so avoid that. */ |
| if (is_dir) |
| href = apr_pstrcat(pool, href, "/", SVN_VA_NULL); |
| |
| if (gen_html) |
| name = href; |
| |
| /* We quote special characters in both XML and HTML. */ |
| name = apr_xml_quote_string(pool, name, !gen_html); |
| |
| /* According to httpd-2.0.54/include/httpd.h, ap_os_escape_path() |
| behaves differently on different platforms. It claims to |
| "convert an OS path to a URL in an OS dependant way". |
| Nevertheless, there appears to be only one implementation |
| of the function in httpd, and the code seems completely |
| platform independent, so we'll assume it's appropriate for |
| mod_dav_svn to use it to quote outbound paths. */ |
| href = ap_os_escape_path(pool, href, 0); |
| href = apr_xml_quote_string(pool, href, 1); |
| |
| if (gen_html) |
| { |
| /* If our directory was access using the public peg-rev |
| CGI query interface, we'll let its dirents carry that |
| peg-rev, too. */ |
| if (resource->info->pegged) |
| { |
| SVN_ERR(dav_svn__brigade_printf(bb, output, |
| " <li><a href=\"%s?p=%ld\">%s</a></li>\n", |
| href, resource->info->root.rev, name)); |
| } |
| else |
| { |
| SVN_ERR(dav_svn__brigade_printf(bb, output, |
| " <li><a href=\"%s\">%s</a></li>\n", |
| href, name)); |
| } |
| } |
| else |
| { |
| const char *const tag = (is_dir ? "dir" : "file"); |
| |
| /* This is where we could search for props */ |
| |
| /* If our directory was access using the public peg-rev |
| CGI query interface, we'll let its dirents carry that |
| peg-rev, too. */ |
| if (resource->info->pegged) |
| { |
| SVN_ERR(dav_svn__brigade_printf(bb, output, |
| " <%s name=\"%s\" href=\"%s?p=%ld\" />\n", |
| tag, name, href, resource->info->root.rev)); |
| } |
| else |
| { |
| SVN_ERR(dav_svn__brigade_printf(bb, output, |
| " <%s name=\"%s\" href=\"%s\" />\n", |
| tag, name, href)); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static svn_error_t * |
| emit_collection_tail(const dav_resource *resource, |
| apr_bucket_brigade *bb, |
| dav_svn__output *output, |
| svn_boolean_t gen_html, |
| apr_pool_t *pool) |
| { |
| if (gen_html) |
| { |
| if (strcmp(ap_psignature("FOO", resource->info->r), "") != 0) |
| { |
| /* Apache's signature generation code didn't eat our prefix. |
| ServerSignature must be enabled. Print our version info. |
| |
| WARNING: This is a kludge!! ap_psignature() doesn't promise |
| to return the empty string when ServerSignature is off. We |
| know it does by code inspection, but this behavior is subject |
| to change. (Perhaps we should try to get the Apache folks to |
| make this promise, though. Seems harmless/useful enough...) |
| */ |
| SVN_ERR(dav_svn__brigade_puts(bb, output, |
| " </ul>\n <hr noshade><em>Powered by " |
| "<a href=\"http://subversion.apache.org/\">" |
| "Apache Subversion" |
| "</a> version " SVN_VERSION "." |
| "</em>\n</body></html>")); |
| } |
| else |
| SVN_ERR(dav_svn__brigade_puts(bb, output, " </ul>\n</body></html>")); |
| } |
| else |
| SVN_ERR(dav_svn__brigade_puts(bb, output, " </index>\n</svn>\n")); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static dav_error * |
| deliver(const dav_resource *resource, ap_filter_t *unused) |
| { |
| svn_error_t *serr; |
| apr_bucket_brigade *bb; |
| apr_bucket *bkt; |
| dav_svn__output *output; |
| |
| /* Check resource type */ |
| if (resource->baselined |
| || (resource->type != DAV_RESOURCE_TYPE_REGULAR |
| && resource->type != DAV_RESOURCE_TYPE_VERSION |
| && resource->type != DAV_RESOURCE_TYPE_WORKING |
| && resource->info->restype != DAV_SVN_RESTYPE_PARENTPATH_COLLECTION)) |
| { |
| return dav_svn__new_error(resource->pool, HTTP_CONFLICT, 0, 0, |
| "Cannot GET this type of resource."); |
| } |
| |
| output = dav_svn__output_create(resource->info->r, resource->pool); |
| |
| if (resource->collection) |
| { |
| const int gen_html = !resource->info->repos->xslt_uri; |
| apr_hash_t *entries; |
| apr_pool_t *iterpool; |
| apr_array_header_t *sorted; |
| svn_revnum_t dir_rev = SVN_INVALID_REVNUM; |
| int i; |
| |
| /* <svn version="1.3.0 (dev-build)" |
| href="http://subversion.apache.org"> |
| <index name="[info->repos->repo_name]" |
| path="[info->repos_path]" |
| rev="[info->root.rev]"> |
| <file name="foo" href="foo" /> |
| <dir name="bar" href="bar/" /> |
| </index> |
| </svn> */ |
| |
| |
| /* ### TO-DO: check for a new mod_dav_svn directive here also. */ |
| if (resource->info->restype == DAV_SVN_RESTYPE_PARENTPATH_COLLECTION) |
| { |
| apr_hash_index_t *hi; |
| apr_hash_t *dirents; |
| const char *fs_parent_path = |
| dav_svn__get_fs_parent_path(resource->info->r); |
| |
| serr = svn_io_get_dirents3(&dirents, fs_parent_path, TRUE, |
| resource->pool, resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not fetch dirents of " |
| "SVNParentPath", resource->pool); |
| |
| /* convert an io dirent hash to an fs dirent hash. */ |
| entries = apr_hash_make(resource->pool); |
| for (hi = apr_hash_first(resource->pool, dirents); |
| hi; hi = apr_hash_next(hi)) |
| { |
| const void *key; |
| void *val; |
| svn_io_dirent_t *dirent; |
| svn_fs_dirent_t *ent = apr_pcalloc(resource->pool, sizeof(*ent)); |
| |
| apr_hash_this(hi, &key, NULL, &val); |
| dirent = val; |
| |
| if (dirent->kind == svn_node_file && dirent->special) |
| { |
| svn_node_kind_t resolved_kind; |
| const char *link_path = |
| svn_dirent_join(fs_parent_path, key, resource->pool); |
| |
| serr = svn_io_check_resolved_path(link_path, &resolved_kind, |
| resource->pool); |
| if (serr) |
| return dav_svn__convert_err(serr, |
| HTTP_INTERNAL_SERVER_ERROR, |
| "could not resolve symlink " |
| "dirent of SVNParentPath", |
| resource->pool); |
| if (resolved_kind != svn_node_dir) |
| continue; |
| |
| dirent->kind = svn_node_dir; |
| } |
| else if (dirent->kind != svn_node_dir) |
| continue; |
| |
| ent->name = key; |
| ent->id = NULL; /* ### does it matter? */ |
| ent->kind = dirent->kind; |
| |
| svn_hash_sets(entries, key, ent); |
| } |
| |
| } |
| else |
| { |
| dir_rev = svn_fs_revision_root_revision(resource->info->root.root); |
| serr = svn_fs_dir_entries(&entries, resource->info->root.root, |
| resource->info->repos_path, resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not fetch directory entries", |
| resource->pool); |
| } |
| |
| bb = apr_brigade_create(resource->pool, |
| dav_svn__output_get_bucket_alloc(output)); |
| |
| serr = emit_collection_head(resource, bb, output, gen_html, |
| resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not output collection", |
| resource->pool); |
| |
| /* get a sorted list of the entries */ |
| sorted = svn_sort__hash(entries, svn_sort_compare_items_as_paths, |
| resource->pool); |
| |
| iterpool = svn_pool_create(resource->pool); |
| |
| for (i = 0; i < sorted->nelts; ++i) |
| { |
| const svn_sort__item_t *item = &APR_ARRAY_IDX(sorted, i, |
| const svn_sort__item_t); |
| const svn_fs_dirent_t *entry = item->value; |
| const char *name = item->key; |
| const char *repos_relpath = NULL; |
| |
| svn_pool_clear(iterpool); |
| |
| /* DIR_REV is set to a valid revision if we're looking at |
| the entries of a versioned directory. Otherwise, we're |
| looking at a parent-path listing. */ |
| if (SVN_IS_VALID_REVNUM(dir_rev)) |
| { |
| repos_relpath = svn_fspath__join(resource->info->repos_path, |
| name, iterpool); |
| if (! dav_svn__allow_read(resource->info->r, |
| resource->info->repos, |
| repos_relpath, |
| dir_rev, |
| iterpool)) |
| continue; |
| } |
| else |
| { |
| if (! dav_svn__allow_list_repos(resource->info->r, |
| entry->name, iterpool)) |
| continue; |
| } |
| |
| serr = emit_collection_entry(resource, bb, output, entry, gen_html, |
| iterpool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not output collection entry", |
| resource->pool); |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| serr = emit_collection_tail(resource, bb, output, gen_html, |
| resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not output collection", |
| resource->pool); |
| |
| bkt = apr_bucket_eos_create(dav_svn__output_get_bucket_alloc(output)); |
| APR_BRIGADE_INSERT_TAIL(bb, bkt); |
| serr = dav_svn__output_pass_brigade(output, bb); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not write EOS to filter.", |
| resource->pool); |
| |
| return NULL; |
| } |
| |
| |
| /* If we have a base for a delta, then we want to compute an svndiff |
| between the provided base and the requested resource. For a simple |
| request, then we just grab the file contents. */ |
| if (resource->info->delta_base != NULL) |
| { |
| dav_svn__uri_info info; |
| svn_fs_root_t *root; |
| svn_boolean_t is_file; |
| svn_txdelta_stream_t *txd_stream; |
| svn_stream_t *o_stream; |
| svn_txdelta_window_handler_t handler; |
| void * h_baton; |
| diff_ctx_t dc = { 0 }; |
| |
| /* First order of business is to parse it. */ |
| serr = dav_svn__simple_parse_uri(&info, resource, |
| resource->info->delta_base, |
| resource->pool); |
| |
| /* If we successfully parse the base URL, then send an svndiff. */ |
| if ((serr == NULL) && (info.rev != SVN_INVALID_REVNUM)) |
| { |
| /* We are always accessing the base resource by ID, so open |
| an ID root. */ |
| serr = svn_fs_revision_root(&root, resource->info->repos->fs, |
| info.rev, resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not open a root for the base", |
| resource->pool); |
| |
| /* verify that it is a file */ |
| serr = svn_fs_is_file(&is_file, root, info.repos_path, |
| resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not determine if the base " |
| "is really a file", |
| resource->pool); |
| if (!is_file) |
| return dav_svn__new_error(resource->pool, HTTP_BAD_REQUEST, 0, 0, |
| apr_psprintf(resource->pool, |
| "the delta base of '%s' does not refer " |
| "to a file in revision %ld", |
| info.repos_path, info.rev)); |
| |
| /* Okay. Let's open up a delta stream for the client to read. */ |
| serr = svn_fs_get_file_delta_stream(&txd_stream, |
| root, info.repos_path, |
| resource->info->root.root, |
| resource->info->repos_path, |
| resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not prepare to read a delta", |
| resource->pool); |
| |
| bb = apr_brigade_create(resource->pool, |
| dav_svn__output_get_bucket_alloc(output)); |
| |
| /* create a stream that svndiff data will be written to, |
| which will copy it to the network */ |
| dc.output = output; |
| dc.bb = bb; |
| o_stream = svn_stream_create(&dc, resource->pool); |
| svn_stream_set_write(o_stream, write_to_filter); |
| svn_stream_set_close(o_stream, close_filter); |
| |
| /* get a handler/baton for writing into the output stream */ |
| svn_txdelta_to_svndiff3(&handler, &h_baton, |
| o_stream, resource->info->svndiff_version, |
| dav_svn__get_compression_level(resource->info->r), |
| resource->pool); |
| |
| /* got everything set up. read in delta windows and shove them into |
| the handler, which pushes data into the output stream, which goes |
| to the network. */ |
| serr = svn_txdelta_send_txstream(txd_stream, handler, h_baton, |
| resource->pool); |
| apr_brigade_destroy(bb); |
| |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not deliver the txdelta stream", |
| resource->pool); |
| |
| |
| return NULL; |
| } |
| else |
| { |
| svn_error_clear(serr); |
| } |
| } |
| |
| /* resource->info->delta_base is NULL, or we had an invalid base URL */ |
| { |
| svn_stream_t *stream; |
| char *block; |
| |
| serr = svn_fs_file_contents(&stream, |
| resource->info->root.root, |
| resource->info->repos_path, |
| resource->pool); |
| if (serr != NULL) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not prepare to read the file", |
| resource->pool); |
| } |
| |
| /* Perform keywords substitution if requested by client */ |
| if (resource->info->keyword_subst) |
| { |
| svn_string_t *keywords; |
| |
| serr = svn_fs_node_prop(&keywords, |
| resource->info->root.root, |
| resource->info->repos_path, |
| SVN_PROP_KEYWORDS, |
| resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not get fetch '" |
| SVN_PROP_KEYWORDS "' property for " |
| "for keywords substitution", |
| resource->pool); |
| |
| if (keywords) |
| { |
| apr_hash_t *kw; |
| svn_revnum_t cmt_rev; |
| const char *str_cmt_rev, *str_uri, *str_root; |
| const char *cmt_date, *cmt_author; |
| apr_time_t when = 0; |
| |
| serr = svn_repos_get_committed_info(&cmt_rev, |
| &cmt_date, |
| &cmt_author, |
| resource->info->root.root, |
| resource->info->repos_path, |
| resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not fetch committed info " |
| "for keywords substitution", |
| resource->pool); |
| |
| serr = svn_time_from_cstring(&when, cmt_date, resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not parse committed date " |
| "for keywords substitution", |
| resource->pool); |
| str_cmt_rev = apr_psprintf(resource->pool, "%ld", cmt_rev); |
| str_uri = apr_pstrcat(resource->pool, |
| resource->info->repos->base_url, |
| ap_escape_uri(resource->pool, |
| resource->info->r->uri), |
| SVN_VA_NULL); |
| str_root = apr_pstrcat(resource->pool, |
| resource->info->repos->base_url, |
| resource->info->repos->root_path, |
| SVN_VA_NULL); |
| |
| serr = svn_subst_build_keywords3(&kw, keywords->data, |
| str_cmt_rev, str_uri, str_root, |
| when, cmt_author, |
| resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not perform keywords " |
| "substitution", resource->pool); |
| |
| /* Replace the raw file STREAM with a wrapper that |
| handles keyword translation. */ |
| stream = svn_subst_stream_translated( |
| svn_stream_disown(stream, resource->pool), |
| NULL, FALSE, kw, TRUE, resource->pool); |
| } |
| } |
| |
| /* ### one day in the future, we can create a custom bucket type |
| ### which will read from the FS stream on demand */ |
| |
| block = apr_palloc(resource->pool, SVN__STREAM_CHUNK_SIZE); |
| bb = apr_brigade_create(resource->pool, |
| dav_svn__output_get_bucket_alloc(output)); |
| |
| while (1) { |
| apr_size_t bufsize = SVN__STREAM_CHUNK_SIZE; |
| |
| /* read from the FS ... */ |
| serr = svn_stream_read_full(stream, block, &bufsize); |
| if (serr != NULL) |
| { |
| apr_brigade_destroy(bb); |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not read the file contents", |
| resource->pool); |
| } |
| if (bufsize == 0) |
| break; |
| |
| /* write to the filter ... */ |
| bkt = apr_bucket_transient_create( |
| block, bufsize, dav_svn__output_get_bucket_alloc(output)); |
| APR_BRIGADE_INSERT_TAIL(bb, bkt); |
| serr = dav_svn__output_pass_brigade(output, bb); |
| if (serr != NULL) |
| { |
| apr_brigade_destroy(bb); |
| /* ### that HTTP code... */ |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not write data to filter.", |
| resource->pool); |
| } |
| } |
| |
| /* done with the file. write an EOS bucket now. */ |
| bkt = apr_bucket_eos_create(dav_svn__output_get_bucket_alloc(output)); |
| APR_BRIGADE_INSERT_TAIL(bb, bkt); |
| serr = dav_svn__output_pass_brigade(output, bb); |
| if (serr != NULL) |
| { |
| apr_brigade_destroy(bb); |
| /* ### that HTTP code... */ |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not write EOS to filter.", |
| resource->pool); |
| } |
| |
| apr_brigade_destroy(bb); |
| return NULL; |
| } |
| } |
| |
| |
| static dav_error * |
| create_collection(dav_resource *resource) |
| { |
| svn_error_t *serr; |
| dav_error *err; |
| |
| if (resource->type != DAV_RESOURCE_TYPE_WORKING |
| && resource->type != DAV_RESOURCE_TYPE_REGULAR) |
| { |
| return dav_svn__new_error(resource->pool, HTTP_METHOD_NOT_ALLOWED, 0, 0, |
| "Collections can only be created within a " |
| "working or regular collection (at this " |
| "time)."); |
| } |
| |
| /* ...regular resources allowed only if autoversioning is turned on. */ |
| if (resource->type == DAV_RESOURCE_TYPE_REGULAR |
| && ! (resource->info->repos->autoversioning)) |
| return dav_svn__new_error(resource->pool, HTTP_METHOD_NOT_ALLOWED, 0, 0, |
| "MKCOL called on regular resource, but " |
| "autoversioning is not active."); |
| |
| /* ### note that the parent was checked out at some point, and this |
| ### is being preformed relative to the working rsrc for that parent */ |
| |
| /* Auto-versioning mkcol of regular resource: */ |
| if (resource->type == DAV_RESOURCE_TYPE_REGULAR) |
| { |
| /* Change the VCR into a WR, in place. This creates a txn and |
| changes resource->info->root from a rev-root into a txn-root. */ |
| err = dav_svn__checkout(resource, |
| 1 /* auto-checkout */, |
| 0, 0, 0, NULL, NULL); |
| if (err) |
| return err; |
| } |
| |
| if ((serr = svn_fs_make_dir(resource->info->root.root, |
| resource->info->repos_path, |
| resource->pool)) != NULL) |
| { |
| /* ### need a better error */ |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not create the collection.", |
| resource->pool); |
| } |
| |
| /* Auto-versioning commit of the txn. */ |
| if (resource->info->auto_checked_out) |
| { |
| /* This also changes the WR back into a VCR, in place. */ |
| err = dav_svn__checkin(resource, 0, NULL); |
| if (err) |
| return err; |
| } |
| |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| copy_resource(const dav_resource *src, |
| dav_resource *dst, |
| int depth, |
| dav_response **response) |
| { |
| svn_error_t *serr; |
| dav_error *err; |
| const char *src_repos_path, *dst_repos_path; |
| |
| /* ### source must be from a collection under baseline control. the |
| ### baseline will (implicitly) indicate the source revision, and the |
| ### path will be derived simply from the URL path */ |
| |
| /* ### the destination's parent must be a working collection */ |
| |
| /* ### ben goofing around: */ |
| /* char *msg; |
| apr_psprintf |
| (src->pool, "Got a COPY request with src arg '%s' and dst arg '%s'", |
| src->uri, dst->uri); |
| |
| return dav_svn__new_error(src->pool, HTTP_NOT_IMPLEMENTED, 0, msg); |
| */ |
| |
| /* ### Safeguard: see issue #916, whereby we're allowing an |
| auto-checkout of a baseline for PROPPATCHing, *without* creating |
| a new baseline afterwards. We need to safeguard here that nobody |
| is calling COPY with the baseline as a Destination! */ |
| if (dst->baselined && dst->type == DAV_RESOURCE_TYPE_VERSION) |
| return dav_svn__new_error(src->pool, HTTP_PRECONDITION_FAILED, 0, 0, |
| "Illegal: COPY Destination is a baseline."); |
| |
| if (dst->type == DAV_RESOURCE_TYPE_REGULAR |
| && !(dst->info->repos->autoversioning)) |
| return dav_svn__new_error(dst->pool, HTTP_METHOD_NOT_ALLOWED, 0, 0, |
| "COPY called on regular resource, but " |
| "autoversioning is not active."); |
| |
| /* Auto-versioning copy of regular resource: */ |
| if (dst->type == DAV_RESOURCE_TYPE_REGULAR) |
| { |
| /* Change the VCR into a WR, in place. This creates a txn and |
| changes dst->info->root from a rev-root into a txn-root. */ |
| err = dav_svn__checkout(dst, |
| 1 /* auto-checkout */, |
| 0, 0, 0, NULL, NULL); |
| if (err) |
| return err; |
| } |
| |
| src_repos_path = svn_repos_path(src->info->repos->repos, src->pool); |
| dst_repos_path = svn_repos_path(dst->info->repos->repos, dst->pool); |
| |
| if (strcmp(src_repos_path, dst_repos_path) != 0) |
| { |
| /* Perhaps the source and dst repos use different path formats? */ |
| serr = svn_error_compose_create( |
| svn_dirent_get_absolute(&src_repos_path, src_repos_path, |
| src->pool), |
| svn_dirent_get_absolute(&dst_repos_path, dst_repos_path, |
| dst->pool)); |
| |
| if (!serr && (strcmp(src_repos_path, dst_repos_path) != 0)) |
| return dav_svn__new_error_svn( |
| dst->pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, |
| "Copy source and destination are in different repositories"); |
| } |
| else |
| serr = SVN_NO_ERROR; |
| |
| if (!serr) |
| { |
| serr = svn_fs_copy(src->info->root.root, /* root object of src rev*/ |
| src->info->repos_path, /* relative path of src */ |
| dst->info->root.root, /* root object of dst txn*/ |
| dst->info->repos_path, /* relative path of dst */ |
| src->pool); |
| } |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Unable to make a filesystem copy.", |
| dst->pool); |
| |
| /* Auto-versioning commit of the txn. */ |
| if (dst->info->auto_checked_out) |
| { |
| /* This also changes the WR back into a VCR, in place. */ |
| err = dav_svn__checkin(dst, 0, NULL); |
| if (err) |
| return err; |
| } |
| |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| remove_resource(dav_resource *resource, dav_response **response) |
| { |
| svn_error_t *serr; |
| dav_error *err; |
| apr_hash_t *locks; |
| |
| /* Only activities, working resources, regular resources, and |
| certain private resources can be deleted... */ |
| if (! (resource->type == DAV_RESOURCE_TYPE_WORKING |
| || resource->type == DAV_RESOURCE_TYPE_REGULAR |
| || resource->type == DAV_RESOURCE_TYPE_ACTIVITY |
| || (resource->type == DAV_RESOURCE_TYPE_PRIVATE |
| && resource->info->restype == DAV_SVN_RESTYPE_TXN_COLLECTION))) |
| return dav_svn__new_error(resource->pool, HTTP_METHOD_NOT_ALLOWED, 0, 0, |
| "DELETE called on invalid resource type."); |
| |
| /* ...and regular resources only if autoversioning is turned on. */ |
| if (resource->type == DAV_RESOURCE_TYPE_REGULAR |
| && ! (resource->info->repos->autoversioning)) |
| return dav_svn__new_error(resource->pool, HTTP_METHOD_NOT_ALLOWED, 0, 0, |
| "DELETE called on regular resource, but " |
| "autoversioning is not active."); |
| |
| /* Handle activity deletions (early exit). */ |
| if (resource->type == DAV_RESOURCE_TYPE_ACTIVITY) |
| { |
| return dav_svn__delete_activity(resource->info->repos, |
| resource->info->root.activity_id); |
| } |
| |
| /* Handle deletions of transaction collections (early exit) */ |
| if (resource->type == DAV_RESOURCE_TYPE_PRIVATE |
| && resource->info->restype == DAV_SVN_RESTYPE_TXN_COLLECTION) |
| { |
| if (resource->info->root.vtxn_name) |
| return dav_svn__delete_activity(resource->info->repos, |
| resource->info->root.vtxn_name); |
| else |
| return dav_svn__abort_txn(resource->info->repos, |
| resource->info->root.txn_name, |
| resource->pool); |
| } |
| |
| /* ### note that the parent was checked out at some point, and this |
| ### is being preformed relative to the working rsrc for that parent */ |
| |
| /* NOTE: strictly speaking, we cannot determine whether the parent was |
| ever checked out, and that this working resource is relative to that |
| checked out parent. It is entirely possible the client checked out |
| the target resource and just deleted it. Subversion doesn't mind, but |
| this does imply we are not enforcing the "checkout the parent, then |
| delete from within" semantic. */ |
| |
| /* Auto-versioning delete of regular resource: */ |
| if (resource->type == DAV_RESOURCE_TYPE_REGULAR) |
| { |
| /* Change the VCR into a WR, in place. This creates a txn and |
| changes resource->info->root from a rev-root into a txn-root. */ |
| err = dav_svn__checkout(resource, |
| 1 /* auto-checkout */, |
| 0, 0, 0, NULL, NULL); |
| if (err) |
| return err; |
| } |
| |
| /* Sanity check: an svn client may have sent a custom request header |
| containing the revision of the item it thinks it's deleting. In |
| this case, we enforce the svn-specific semantic that the item |
| must be up-to-date. */ |
| if (SVN_IS_VALID_REVNUM(resource->info->version_name)) |
| { |
| svn_revnum_t created_rev; |
| serr = svn_fs_node_created_rev(&created_rev, |
| resource->info->root.root, |
| resource->info->repos_path, |
| resource->pool); |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not get created rev of resource", |
| resource->pool); |
| |
| if (resource->info->version_name < created_rev) |
| { |
| serr = svn_error_createf(SVN_ERR_RA_OUT_OF_DATE, NULL, |
| resource->collection |
| ? "Directory '%s' is out of date" |
| : (resource->exists |
| ? "File '%s' is out of date" |
| : "'%s' is out of date"), |
| resource->info->repos_path); |
| return dav_svn__convert_err(serr, HTTP_CONFLICT, |
| "Can't DELETE out-of-date resource", |
| resource->pool); |
| } |
| else if (resource->info->version_name > created_rev) |
| { |
| svn_revnum_t txn_base_rev; |
| |
| txn_base_rev = svn_fs_txn_base_revision(resource->info->root.txn); |
| if (resource->info->version_name > txn_base_rev) |
| { |
| serr = svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, |
| "No such revision %ld", |
| resource->info->version_name); |
| |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Unknown base revision", |
| resource->pool); |
| } |
| } |
| } |
| |
| /* Before attempting the filesystem delete, we need to push any |
| incoming lock-tokens into the filesystem's access_t. Normally |
| they come in via 'If:' header, and get_resource() |
| automatically notices them and does this work for us. In the |
| case of a directory deletion, however, older subversion clients |
| are sending 'child' lock-tokens in the non-standard DELETE |
| request body. */ |
| |
| err = dav_svn__build_lock_hash(&locks, resource->info->r, |
| resource->info->repos_path, resource->pool); |
| if (err != NULL) |
| return err; |
| |
| if (apr_hash_count(locks)) |
| { |
| err = dav_svn__push_locks(resource, locks, resource->pool); |
| if (err != NULL) |
| return err; |
| } |
| |
| if ((serr = svn_fs_delete(resource->info->root.root, |
| resource->info->repos_path, |
| resource->pool)) != NULL) |
| { |
| /* ### need a better error */ |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not delete the resource", |
| resource->pool); |
| } |
| |
| /* Auto-versioning commit of the txn. */ |
| if (resource->info->auto_checked_out) |
| { |
| /* This also changes the WR back into a VCR, in place. */ |
| err = dav_svn__checkin(resource, 0, NULL); |
| if (err) |
| return err; |
| } |
| |
| return NULL; |
| } |
| |
| |
| static dav_error * |
| move_resource(dav_resource *src, |
| dav_resource *dst, |
| dav_response **response) |
| { |
| svn_error_t *serr; |
| dav_error *err; |
| |
| /* NOTE: The svn client does not call the MOVE method yet. Strictly |
| speaking, we do not need to implement this repository function. |
| But we do so anyway, so non-deltaV clients can work against the |
| repository when autoversioning is turned on. Like the svn client, |
| itself, we define a move to be a copy + delete within a single txn. */ |
| |
| /* Because we have no 'atomic' move, we only allow this method on |
| two regular resources with autoversioning active. That way we |
| can auto-checkout a single resource and do the copy + delete |
| within a single txn. (If we had two working resources, which txn |
| would we use?) */ |
| if (src->type != DAV_RESOURCE_TYPE_REGULAR |
| || dst->type != DAV_RESOURCE_TYPE_REGULAR |
| || !(src->info->repos->autoversioning)) |
| return dav_svn__new_error(dst->pool, HTTP_METHOD_NOT_ALLOWED, 0, 0, |
| "MOVE only allowed on two public URIs, and " |
| "autoversioning must be active."); |
| |
| /* Change the dst VCR into a WR, in place. This creates a txn and |
| changes dst->info->root from a rev-root into a txn-root. */ |
| err = dav_svn__checkout(dst, |
| 1 /* auto-checkout */, |
| 0, 0, 0, NULL, NULL); |
| if (err) |
| return err; |
| |
| /* Copy the src to the dst. */ |
| serr = svn_fs_copy(src->info->root.root, /* the root object of src rev*/ |
| src->info->repos_path, /* the relative path of src */ |
| dst->info->root.root, /* the root object of dst txn*/ |
| dst->info->repos_path, /* the relative path of dst */ |
| src->pool); |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Unable to make a filesystem copy.", |
| dst->pool); |
| |
| /* Notice: we're deleting the src repos path from the dst's txn_root. */ |
| if ((serr = svn_fs_delete(dst->info->root.root, |
| src->info->repos_path, |
| dst->pool)) != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not delete the src resource.", |
| dst->pool); |
| |
| /* Commit: this also changes the WR back into a VCR, in place. */ |
| err = dav_svn__checkin(dst, 0, NULL); |
| if (err) |
| return err; |
| |
| return NULL; |
| } |
| |
| |
| typedef struct walker_ctx_t { |
| /* the input walk parameters */ |
| const dav_walk_params *params; |
| |
| /* reused as we walk */ |
| dav_walk_resource wres; |
| |
| /* the current resource */ |
| dav_resource res; /* wres.resource refers here */ |
| dav_resource_private info; /* the info in res */ |
| svn_stringbuf_t *uri; /* the uri within res */ |
| svn_stringbuf_t *repos_path; /* the repos_path within res */ |
| |
| } walker_ctx_t; |
| |
| /* Recursively walk a resource for walk(). When DEPTH != 0, recurse with |
| DEPTH-1 on child nodes. WALK_ROOT should be TRUE for the root and will be |
| FALSE for any descendants, to avoid unneeded work for every descendant |
| node. |
| */ |
| static dav_error * |
| do_walk(walker_ctx_t *ctx, |
| int depth, |
| svn_boolean_t walk_root, |
| apr_pool_t *scratch_pool) |
| { |
| const dav_walk_params *params = ctx->params; |
| int isdir = ctx->res.collection; |
| dav_error *err; |
| svn_error_t *serr; |
| apr_hash_index_t *hi; |
| apr_size_t path_len; |
| apr_size_t uri_len; |
| apr_size_t repos_len; |
| apr_hash_t *children; |
| apr_pool_t *iterpool; |
| |
| /* The current resource is a collection (possibly here thru recursion) |
| and this is the invocation for the collection. Alternatively, this is |
| the first [and only] entry to do_walk() for a member resource, so |
| this will be the invocation for the member. */ |
| err = (*params->func)(&ctx->wres, |
| isdir ? DAV_CALLTYPE_COLLECTION : DAV_CALLTYPE_MEMBER); |
| if (err != NULL) |
| return err; |
| |
| /* if we are not to recurse, or this is a member, then we're done */ |
| if (depth == 0 || !isdir) |
| return NULL; |
| |
| /* ### for now, let's say that working resources have no children. of |
| ### course, this isn't true (or "right") for working collections, but |
| ### we don't actually need to do a walk right now. */ |
| if (params->root->type == DAV_RESOURCE_TYPE_WORKING) |
| return NULL; |
| |
| /* ### need to allow more walking in the future */ |
| if (params->root->type != DAV_RESOURCE_TYPE_REGULAR) |
| { |
| return dav_svn__new_error(params->pool, HTTP_METHOD_NOT_ALLOWED, 0, 0, |
| "Walking the resource hierarchy can only be " |
| "done on 'regular' resources [at this time]."); |
| } |
| |
| /* assert: collection resource. isdir == TRUE. repos_path != NULL. */ |
| |
| /* append "/" to the paths, in preparation for appending child names. |
| don't add "/" if the paths are simply "/" */ |
| if (ctx->info.uri_path->data[ctx->info.uri_path->len - 1] != '/') |
| svn_stringbuf_appendcstr(ctx->info.uri_path, "/"); |
| if (ctx->repos_path->data[ctx->repos_path->len - 1] != '/') |
| svn_stringbuf_appendcstr(ctx->repos_path, "/"); |
| |
| /* NOTE: the URI should already have a trailing "/" */ |
| |
| /* fix up the dependent pointers */ |
| ctx->info.repos_path = ctx->repos_path->data; |
| |
| /* all of the children exist. also initialize the collection flag. */ |
| ctx->res.exists = TRUE; |
| ctx->res.collection = FALSE; |
| |
| /* remember these values so we can chop back to them after each time |
| we append a child name to the path/uri/repos */ |
| path_len = ctx->info.uri_path->len; |
| uri_len = ctx->uri->len; |
| repos_len = ctx->repos_path->len; |
| |
| if (walk_root) |
| { |
| /* Tell our logging subsystem that we're listing a directory. |
| |
| Note: if we cared, we could look at the 'User-Agent:' request |
| header and distinguish an svn client ('svn ls') from a generic |
| DAV client. */ |
| dav_svn__operational_log(&ctx->info, |
| svn_log__get_dir(ctx->info.repos_path, |
| ctx->info.root.rev, |
| TRUE, FALSE, SVN_DIRENT_ALL, |
| scratch_pool)); |
| } |
| |
| /* fetch this collection's children */ |
| serr = svn_fs_dir_entries(&children, ctx->info.root.root, |
| ctx->info.repos_path, scratch_pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "could not fetch collection members", |
| params->pool); |
| |
| /* iterate over the children in this collection */ |
| iterpool = svn_pool_create(scratch_pool); |
| for (hi = apr_hash_first(scratch_pool, children); hi; hi = apr_hash_next(hi)) |
| { |
| const void *key; |
| apr_ssize_t klen; |
| void *val; |
| svn_fs_dirent_t *dirent; |
| |
| svn_pool_clear(iterpool); |
| |
| /* fetch one of the children */ |
| apr_hash_this(hi, &key, &klen, &val); |
| dirent = val; |
| |
| /* authorize access to this resource, if applicable */ |
| if (params->walk_type & DAV_WALKTYPE_AUTH) |
| { |
| const char *repos_relpath = |
| apr_pstrcat(iterpool, |
| apr_pstrmemdup(iterpool, |
| ctx->repos_path->data, |
| ctx->repos_path->len), |
| key, SVN_VA_NULL); |
| if (! dav_svn__allow_read(ctx->info.r, ctx->info.repos, |
| repos_relpath, ctx->info.root.rev, |
| iterpool)) |
| continue; |
| } |
| |
| /* append this child to our buffers */ |
| svn_stringbuf_appendbytes(ctx->info.uri_path, key, klen); |
| svn_stringbuf_appendbytes(ctx->uri, key, klen); |
| svn_stringbuf_appendbytes(ctx->repos_path, key, klen); |
| |
| /* reset the pointers since the above may have changed them */ |
| ctx->res.uri = ctx->uri->data; |
| ctx->info.repos_path = ctx->repos_path->data; |
| |
| if (dirent->kind == svn_node_file) |
| { |
| err = (*params->func)(&ctx->wres, DAV_CALLTYPE_MEMBER); |
| if (err != NULL) |
| { |
| svn_pool_destroy(iterpool); |
| return err; |
| } |
| } |
| else |
| { |
| /* this resource is a collection */ |
| ctx->res.collection = TRUE; |
| |
| /* append a slash to the URI (the path doesn't need it yet) */ |
| svn_stringbuf_appendcstr(ctx->uri, "/"); |
| ctx->res.uri = ctx->uri->data; |
| |
| /* recurse on this collection */ |
| err = do_walk(ctx, depth - 1, FALSE, iterpool); |
| if (err != NULL) |
| { |
| svn_pool_destroy(iterpool); |
| return err; |
| } |
| |
| /* restore the data */ |
| ctx->res.collection = FALSE; |
| } |
| |
| /* chop the child off the paths and uri. NOTE: no null-term. */ |
| ctx->info.uri_path->len = path_len; |
| ctx->uri->len = uri_len; |
| ctx->repos_path->len = repos_len; |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return NULL; |
| } |
| |
| static dav_error * |
| walk(const dav_walk_params *params, int depth, dav_response **response) |
| { |
| /* Thinking about adding support for LOCKNULL resources in this |
| walker? Check out the (working) code that was removed here: |
| Author: cmpilato |
| Date: Fri Mar 18 14:54:02 2005 |
| New Revision: 13475 |
| */ |
| |
| walker_ctx_t ctx = { 0 }; |
| dav_error *err; |
| |
| if (params->root->info->restype == DAV_SVN_RESTYPE_PARENTPATH_COLLECTION) |
| { |
| /* Cannot walk an SVNParentPath collection, there is no repository. */ |
| return NULL; |
| } |
| |
| ctx.params = params; |
| |
| ctx.wres.walk_ctx = params->walk_ctx; |
| ctx.wres.pool = params->pool; |
| ctx.wres.resource = &ctx.res; |
| |
| /* copy the resource over and adjust the "info" reference */ |
| ctx.res = *params->root; |
| ctx.info = *ctx.res.info; |
| |
| ctx.res.info = &ctx.info; |
| |
| /* operate within the proper pool */ |
| ctx.res.pool = params->pool; |
| |
| /* Don't monkey with the path from params->root. Create a new one. |
| This path will then be extended/shortened as necessary. */ |
| ctx.info.uri_path = svn_stringbuf_dup(ctx.info.uri_path, params->pool); |
| |
| /* prep the URI buffer */ |
| ctx.uri = svn_stringbuf_create(params->root->uri, params->pool); |
| |
| /* same for repos_path */ |
| if (ctx.info.repos_path == NULL) |
| ctx.repos_path = NULL; |
| else |
| ctx.repos_path = svn_stringbuf_create(ctx.info.repos_path, params->pool); |
| |
| /* if we have a collection, then ensure the URI has a trailing "/" */ |
| /* ### get_resource always kills the trailing slash... */ |
| if (ctx.res.collection && ctx.uri->data[ctx.uri->len - 1] != '/') { |
| svn_stringbuf_appendcstr(ctx.uri, "/"); |
| } |
| |
| /* the current resource's URI is stored in the (telescoping) ctx.uri */ |
| ctx.res.uri = ctx.uri->data; |
| |
| /* the current resource's repos_path is stored in ctx.repos_path */ |
| if (ctx.repos_path != NULL) |
| ctx.info.repos_path = ctx.repos_path->data; |
| |
| /* ### is the root already/always open? need to verify */ |
| |
| /* always return the error, and any/all multistatus responses */ |
| err = do_walk(&ctx, depth, TRUE, params->pool); |
| *response = ctx.wres.response; |
| |
| return err; |
| } |
| |
| |
| |
| /*** Utility functions for resource management ***/ |
| |
| dav_resource * |
| dav_svn__create_working_resource(dav_resource *base, |
| const char *activity_id, |
| const char *txn_name, |
| int tweak_in_place) |
| { |
| const char *path; |
| dav_resource *res; |
| |
| if (base->baselined) |
| path = apr_psprintf(base->pool, |
| "/%s/wbl/%s/%ld", |
| base->info->repos->special_uri, |
| activity_id, base->info->root.rev); |
| else |
| path = apr_psprintf(base->pool, "/%s/wrk/%s%s", |
| base->info->repos->special_uri, |
| activity_id, base->info->repos_path); |
| path = svn_path_uri_encode(path, base->pool); |
| |
| if (tweak_in_place) |
| res = base; |
| else |
| { |
| res = apr_pcalloc(base->pool, sizeof(*res)); |
| res->info = apr_pcalloc(base->pool, sizeof(*res->info)); |
| } |
| |
| res->type = DAV_RESOURCE_TYPE_WORKING; |
| res->exists = TRUE; /* ### not necessarily correct */ |
| res->versioned = TRUE; |
| res->working = TRUE; |
| res->baselined = base->baselined; |
| /* collection = FALSE. ### not necessarily correct */ |
| |
| if (base->info->repos->root_path[1]) |
| res->uri = apr_pstrcat(base->pool, base->info->repos->root_path, |
| path, SVN_VA_NULL); |
| else |
| res->uri = path; |
| res->hooks = &dav_svn__hooks_repository; |
| res->pool = base->pool; |
| |
| res->info->uri_path = svn_stringbuf_create(path, base->pool); |
| res->info->repos = base->info->repos; |
| res->info->repos_path = base->info->repos_path; |
| res->info->root.rev = base->info->root.rev; |
| res->info->root.activity_id = activity_id; |
| res->info->root.txn_name = txn_name; |
| |
| if (tweak_in_place) |
| return NULL; |
| else |
| return res; |
| } |
| |
| |
| dav_error * |
| dav_svn__working_to_regular_resource(dav_resource *resource) |
| { |
| dav_resource_private *priv = resource->info; |
| dav_svn_repos *repos = priv->repos; |
| const char *path; |
| svn_error_t *serr; |
| |
| /* no need to change the repos object or repos_path */ |
| |
| /* set type back to REGULAR */ |
| resource->type = DAV_RESOURCE_TYPE_REGULAR; |
| |
| /* remove the working flag */ |
| resource->working = FALSE; |
| |
| /* Change the URL into either a baseline-collection or a public one. */ |
| if (priv->root.rev == SVN_INVALID_REVNUM) |
| { |
| serr = dav_svn__get_youngest_rev(&priv->root.rev, repos, resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not determine youngest rev.", |
| resource->pool); |
| |
| /* create public URL */ |
| path = apr_psprintf(resource->pool, "%s", priv->repos_path); |
| } |
| else |
| { |
| /* if rev was specific, create baseline-collection URL */ |
| path = dav_svn__build_uri(repos, DAV_SVN__BUILD_URI_BC, |
| priv->root.rev, priv->repos_path, |
| FALSE /* add_href */, resource->pool); |
| } |
| path = svn_path_uri_encode(path, resource->pool); |
| priv->uri_path = svn_stringbuf_create(path, resource->pool); |
| |
| /* change root.root back into a revision root. */ |
| serr = svn_fs_revision_root(&priv->root.root, repos->fs, |
| priv->root.rev, resource->pool); |
| if (serr != NULL) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not open revision root.", |
| resource->pool); |
| |
| return NULL; |
| } |
| |
| |
| dav_error * |
| dav_svn__create_version_resource(dav_resource **version_res, |
| const char *uri, |
| apr_pool_t *pool) |
| { |
| int result; |
| dav_error *err; |
| |
| dav_resource_combined *comb = apr_pcalloc(pool, sizeof(*comb)); |
| |
| result = parse_version_uri(comb, uri, NULL, 0); |
| if (result != 0) |
| return dav_svn__new_error(pool, HTTP_INTERNAL_SERVER_ERROR, 0, 0, |
| "Could not parse version resource uri."); |
| |
| err = prep_version(comb); |
| if (err) |
| return err; |
| |
| *version_res = &comb->res; |
| return NULL; |
| } |
| |
| |
| |
| static dav_error * |
| handle_post_request(request_rec *r, |
| dav_resource *resource, |
| dav_svn__output *output) |
| { |
| svn_skel_t *request_skel, *post_skel; |
| int status; |
| apr_pool_t *pool = resource->pool; |
| |
| /* Make sure our skel-based request parses okay, has an initial atom |
| that identifies what kind of action is expected, and that that |
| action is something we understand. */ |
| status = dav_svn__parse_request_skel(&request_skel, r, pool); |
| |
| if (status != OK) |
| return dav_svn__new_error(pool, status, 0, 0, |
| "Error parsing skel POST request body."); |
| |
| if (svn_skel__list_length(request_skel) < 1) |
| return dav_svn__new_error(pool, HTTP_BAD_REQUEST, 0, 0, |
| "Unable to identify skel POST request flavor."); |
| |
| post_skel = request_skel->children; |
| |
| /* NOTE: If you add POST handlers here, you'll want to advertise |
| that the server supports them, too. See version.c:get_option(). */ |
| |
| if (svn_skel__matches_atom(post_skel, "create-txn")) |
| { |
| return dav_svn__post_create_txn(resource, request_skel, output); |
| } |
| else if (svn_skel__matches_atom(post_skel, "create-txn-with-props")) |
| { |
| return dav_svn__post_create_txn_with_props(resource, |
| request_skel, output); |
| } |
| |
| return dav_svn__new_error(pool, HTTP_BAD_REQUEST, 0, 0, |
| "Unsupported skel POST request flavor."); |
| } |
| |
| |
| /* A stripped down version of mod_dav's dav_handle_err so that POST |
| errors, which are not passed via mod_dav, are handled in the same |
| way as errors for requests that are passed via mod_dav. */ |
| static int |
| handle_err(request_rec *r, dav_error *err) |
| { |
| dav_error *stackerr = err; |
| |
| dav_svn__log_err(r, err, APLOG_ERR); |
| |
| /* our error messages are safe; tell Apache this */ |
| apr_table_setn(r->notes, "verbose-error-to", "*"); |
| |
| /* We might be able to generate a standard <D:error> response. |
| Search the error stack for an errortag. */ |
| while (stackerr != NULL && stackerr->tagname == NULL) |
| stackerr = stackerr->prev; |
| |
| if (stackerr != NULL && stackerr->tagname != NULL) |
| return dav_svn__error_response_tag(r, stackerr); |
| |
| return err->status; |
| } |
| |
| |
| int dav_svn__method_post(request_rec *r) |
| { |
| dav_resource *resource; |
| dav_error *derr; |
| const char *content_type; |
| |
| /* We only allow POSTs against the "me resource" right now. */ |
| derr = get_resource(r, dav_svn__get_root_dir(r), |
| "ignored", 0, &resource); |
| if (derr != NULL) |
| return derr->status; |
| if (resource->info->restype != DAV_SVN_RESTYPE_ME) |
| return HTTP_BAD_REQUEST; |
| |
| /* Pass skel-type POST request handling off to a dispatcher; any |
| other type of request is considered bogus. */ |
| content_type = apr_table_get(r->headers_in, "content-type"); |
| if (content_type && (strcmp(content_type, SVN_SKEL_MIME_TYPE) == 0)) |
| { |
| dav_svn__output *output = dav_svn__output_create(resource->info->r, |
| resource->pool); |
| derr = handle_post_request(r, resource, output); |
| } |
| else |
| { |
| derr = dav_svn__new_error(resource->pool, HTTP_BAD_REQUEST, 0, 0, |
| "Unsupported POST request type."); |
| } |
| |
| /* If something went wrong above, we'll generate a response back to |
| the client with (hopefully) some helpful information. */ |
| if (derr) |
| { |
| /* POST is not a DAV method and so mod_dav isn't involved and |
| won't handle this error. Do it explicitly. */ |
| return handle_err(r, derr); |
| } |
| |
| return OK; |
| } |
| |
| |
| |
| const dav_hooks_repository dav_svn__hooks_repository = |
| { |
| 1, /* special GET handling */ |
| get_resource, |
| get_parent_resource, |
| is_same_resource, |
| is_parent_resource, |
| open_stream, |
| close_stream, |
| write_stream, |
| seek_stream, |
| set_headers, |
| deliver, |
| create_collection, |
| copy_resource, |
| move_resource, |
| remove_resource, |
| walk, |
| getetag_pathetic |
| }; |