| /* |
| * lock.c: mod_dav_svn locking provider functions |
| * |
| * ==================================================================== |
| * 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. |
| * ==================================================================== |
| */ |
| |
| #include <apr_uuid.h> |
| #include <apr_time.h> |
| |
| #include <httpd.h> |
| #include <http_log.h> |
| #include <mod_dav.h> |
| |
| #include "svn_hash.h" |
| #include "svn_fs.h" |
| #include "svn_repos.h" |
| #include "svn_dav.h" |
| #include "svn_time.h" |
| #include "svn_pools.h" |
| #include "svn_props.h" |
| #include "private/svn_log.h" |
| |
| #include "dav_svn.h" |
| |
| |
| struct dav_lockdb_private |
| { |
| /* These represent 'custom' request hearders only sent by svn clients: */ |
| svn_boolean_t lock_steal; |
| svn_boolean_t lock_break; |
| svn_boolean_t keep_locks; |
| svn_revnum_t working_revnum; |
| |
| /* The original request, so we can set 'custom' output headers. */ |
| request_rec *r; |
| }; |
| |
| |
| /* Helper func: convert an svn_lock_t to a dav_lock, allocated in |
| pool. EXISTS_P indicates whether slock->path actually exists or not. |
| If HIDE_AUTH_USER is set, then do not return the svn lock's 'owner' |
| as dlock->auth_user. |
| */ |
| static void |
| svn_lock_to_dav_lock(dav_lock **dlock, |
| const svn_lock_t *slock, |
| svn_boolean_t hide_auth_user, |
| svn_boolean_t exists_p, |
| apr_pool_t *pool) |
| { |
| dav_lock *lock = apr_pcalloc(pool, sizeof(*lock)); |
| dav_locktoken *token = apr_pcalloc(pool, sizeof(*token)); |
| |
| lock->rectype = DAV_LOCKREC_DIRECT; |
| lock->scope = DAV_LOCKSCOPE_EXCLUSIVE; |
| lock->type = DAV_LOCKTYPE_WRITE; |
| lock->depth = 0; |
| lock->is_locknull = exists_p; |
| |
| token->uuid_str = apr_pstrdup(pool, slock->token); |
| lock->locktoken = token; |
| |
| /* the svn_lock_t 'comment' field maps to the 'DAV:owner' field. */ |
| if (slock->comment) |
| { |
| if (! slock->is_dav_comment) |
| { |
| /* This comment was originally given to us by an svn client, |
| so, we need to wrap the naked comment with <DAV:owner>, |
| and xml-escape it for safe transport, lest we send back |
| an invalid http response. (mod_dav won't do it for us, |
| because it assumes that it personally created every lock |
| in the repository.) */ |
| lock->owner = apr_pstrcat(pool, |
| "<D:owner xmlns:D=\"DAV:\">", |
| apr_xml_quote_string(pool, |
| slock->comment, 1), |
| "</D:owner>", SVN_VA_NULL); |
| } |
| else |
| { |
| lock->owner = apr_pstrdup(pool, slock->comment); |
| } |
| } |
| else |
| lock->owner = NULL; |
| |
| /* the svn_lock_t 'owner' is the actual authenticated owner of the |
| lock, and maps to the 'auth_user' field in the mod_dav lock. */ |
| |
| /* (If the client ran 'svn unlock --force', then we don't want to |
| return lock->auth_user. Otherwise mod_dav will throw an error |
| when lock->auth_user and r->user don't match.) */ |
| if (! hide_auth_user) |
| lock->auth_user = apr_pstrdup(pool, slock->owner); |
| |
| /* This is absurd. apr_time.h has an apr_time_t->time_t func, |
| but not the reverse?? */ |
| if (slock->expiration_date) |
| lock->timeout = (time_t) (slock->expiration_date / APR_USEC_PER_SEC); |
| else |
| lock->timeout = DAV_TIMEOUT_INFINITE; |
| |
| *dlock = lock; |
| } |
| |
| |
| /* Helper func for dav_lock_to_svn_lock: take an incoming |
| "<D:owner><foo></D:owner>" tag and convert it to |
| "<foo>". */ |
| static dav_error * |
| unescape_xml(const char **output, |
| const char *input, |
| apr_pool_t *pool) |
| { |
| apr_xml_parser *xml_parser = apr_xml_parser_create(pool); |
| apr_xml_doc *xml_doc; |
| apr_status_t apr_err; |
| const char *xml_input = apr_pstrcat |
| (pool, "<?xml version=\"1.0\" encoding=\"utf-8\"?>", input, SVN_VA_NULL); |
| |
| apr_err = apr_xml_parser_feed(xml_parser, xml_input, strlen(xml_input)); |
| if (!apr_err) |
| apr_err = apr_xml_parser_done(xml_parser, &xml_doc); |
| |
| if (apr_err) |
| { |
| char errbuf[1024]; |
| |
| errbuf[0] = '\0'; |
| (void)apr_xml_parser_geterror(xml_parser, errbuf, sizeof(errbuf)); |
| return dav_svn__new_error(pool, HTTP_INTERNAL_SERVER_ERROR, |
| DAV_ERR_LOCK_SAVE_LOCK, apr_err, errbuf); |
| } |
| |
| apr_xml_to_text(pool, xml_doc->root, APR_XML_X2T_INNER, |
| xml_doc->namespaces, NULL, output, NULL); |
| return NULL; |
| } |
| |
| |
| /* Helper func: convert a dav_lock to an svn_lock_t, allocated in pool. */ |
| static dav_error * |
| dav_lock_to_svn_lock(svn_lock_t **slock, |
| const dav_lock *dlock, |
| const char *path, |
| dav_lockdb_private *info, |
| svn_boolean_t is_svn_client, |
| apr_pool_t *pool) |
| { |
| svn_lock_t *lock; |
| |
| /* Sanity checks */ |
| if (dlock->type != DAV_LOCKTYPE_WRITE) |
| return dav_svn__new_error(pool, HTTP_BAD_REQUEST, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Only 'write' locks are supported."); |
| |
| if (dlock->scope != DAV_LOCKSCOPE_EXCLUSIVE) |
| return dav_svn__new_error(pool, HTTP_BAD_REQUEST, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Only exclusive locks are supported."); |
| |
| lock = svn_lock_create(pool); |
| lock->path = apr_pstrdup(pool, path); |
| lock->token = apr_pstrdup(pool, dlock->locktoken->uuid_str); |
| |
| /* DAV has no concept of lock creationdate, so assume 'now' */ |
| lock->creation_date = apr_time_now(); |
| |
| if (dlock->auth_user) |
| lock->owner = apr_pstrdup(pool, dlock->auth_user); |
| |
| /* We need to be very careful about stripping the <D:owner> tag away |
| from the cdata. It's okay to do for svn clients, but not other |
| DAV clients! */ |
| if (dlock->owner) |
| { |
| if (is_svn_client) |
| { |
| /* mod_dav has forcibly xml-escaped the comment before |
| handing it to us; we need to xml-unescape it (and remove |
| the <D:owner> wrapper) when storing in the repository, so |
| it looks reasonable to the rest of svn. */ |
| dav_error *derr; |
| lock->is_dav_comment = 0; /* comment is NOT xml-wrapped. */ |
| derr = unescape_xml(&(lock->comment), dlock->owner, pool); |
| if (derr) |
| return derr; |
| } |
| else |
| { |
| /* The comment comes from a non-svn client; don't touch |
| this data at all. */ |
| lock->comment = apr_pstrdup(pool, dlock->owner); |
| lock->is_dav_comment = 1; /* comment IS xml-wrapped. */ |
| } |
| } |
| |
| if (dlock->timeout == DAV_TIMEOUT_INFINITE) |
| lock->expiration_date = 0; /* never expires */ |
| else |
| lock->expiration_date = ((apr_time_t)dlock->timeout) * APR_USEC_PER_SEC; |
| |
| *slock = lock; |
| return 0; |
| } |
| |
| |
| /* ---------------------------------------------------------------- */ |
| /* mod_dav locking vtable starts here: */ |
| |
| |
| /* Return the supportedlock property for a resource */ |
| static const char * |
| get_supportedlock(const dav_resource *resource) |
| { |
| /* This is imitating what mod_dav_fs is doing. Note that unlike |
| mod_dav_fs, however, we don't support "shared" locks, only |
| "exclusive" ones. Nor do we support locks on collections. */ |
| static const char supported[] = DEBUG_CR |
| "<D:lockentry>" DEBUG_CR |
| "<D:lockscope><D:exclusive/></D:lockscope>" DEBUG_CR |
| "<D:locktype><D:write/></D:locktype>" DEBUG_CR |
| "</D:lockentry>" DEBUG_CR; |
| |
| if (resource->collection) |
| return NULL; |
| else |
| return supported; |
| } |
| |
| |
| /* Parse a lock token URI, returning a lock token object allocated |
| * in the given pool. |
| */ |
| static dav_error * |
| parse_locktoken(apr_pool_t *pool, |
| const char *char_token, |
| dav_locktoken **locktoken_p) |
| { |
| dav_locktoken *token = apr_pcalloc(pool, sizeof(*token)); |
| |
| /* libsvn_fs already produces a valid locktoken URI. */ |
| token->uuid_str = apr_pstrdup(pool, char_token); |
| |
| *locktoken_p = token; |
| return 0; |
| } |
| |
| |
| /* Format a lock token object into a URI string, allocated in |
| * the given pool. |
| * |
| * Always returns non-NULL. |
| */ |
| static const char * |
| format_locktoken(apr_pool_t *p, const dav_locktoken *locktoken) |
| { |
| svn_stringbuf_t *formatted |
| = svn_stringbuf_create_ensure(strlen(locktoken->uuid_str), p); |
| |
| /* libsvn_fs produces a locktoken URI that will be valid XML when |
| escaped. */ |
| svn_xml_escape_cdata_cstring(&formatted, locktoken->uuid_str, p); |
| return formatted->data; |
| } |
| |
| |
| /* Compare two lock tokens. |
| * |
| * Result < 0 => lt1 < lt2 |
| * Result == 0 => lt1 == lt2 |
| * Result > 0 => lt1 > lt2 |
| */ |
| static int |
| compare_locktoken(const dav_locktoken *lt1, const dav_locktoken *lt2) |
| { |
| return strcmp(lt1->uuid_str, lt2->uuid_str); |
| } |
| |
| |
| /* Open the provider's lock database. |
| * |
| * The provider may or may not use a "real" database for locks |
| * (a lock could be an attribute on a resource, for example). |
| * |
| * The provider may choose to use the value of the DAVLockDB directive |
| * (as returned by dav_get_lockdb_path()) to decide where to place |
| * any storage it may need. |
| * |
| * The request storage pool should be associated with the lockdb, |
| * so it can be used in subsequent operations. |
| * |
| * If ro != 0, only readonly operations will be performed. |
| * If force == 0, the open can be "lazy"; no subsequent locking operations |
| * may occur. |
| * If force != 0, locking operations will definitely occur. |
| */ |
| static dav_error * |
| open_lockdb(request_rec *r, int ro, int force, dav_lockdb **lockdb) |
| { |
| const char *svn_client_options, *version_name; |
| dav_lockdb *db = apr_pcalloc(r->pool, sizeof(*db)); |
| dav_lockdb_private *info = apr_pcalloc(r->pool, sizeof(*info)); |
| |
| info->r = r; |
| |
| /* Is this an svn client? */ |
| |
| /* Check to see if an svn client sent any custom X-SVN-* headers in |
| the request. */ |
| svn_client_options = apr_table_get(r->headers_in, SVN_DAV_OPTIONS_HEADER); |
| if (svn_client_options) |
| { |
| /* 'svn [lock | unlock] --force' */ |
| if (ap_strstr_c(svn_client_options, SVN_DAV_OPTION_LOCK_BREAK)) |
| info->lock_break = TRUE; |
| if (ap_strstr_c(svn_client_options, SVN_DAV_OPTION_LOCK_STEAL)) |
| info->lock_steal = TRUE; |
| if (ap_strstr_c(svn_client_options, SVN_DAV_OPTION_KEEP_LOCKS)) |
| info->keep_locks = TRUE; |
| } |
| |
| /* 'svn lock' wants to make svn_fs_lock() do an out-of-dateness check. */ |
| version_name = apr_table_get(r->headers_in, SVN_DAV_VERSION_NAME_HEADER); |
| info->working_revnum = version_name ? |
| SVN_STR_TO_REV(version_name): SVN_INVALID_REVNUM; |
| |
| /* The generic lockdb structure. */ |
| db->hooks = &dav_svn__hooks_locks; |
| db->ro = ro; |
| db->info = info; |
| |
| *lockdb = db; |
| return 0; |
| } |
| |
| |
| /* Indicates completion of locking operations */ |
| static void |
| close_lockdb(dav_lockdb *lockdb) |
| { |
| /* nothing to do here. */ |
| return; |
| } |
| |
| |
| /* Take a resource out of the lock-null state. */ |
| static dav_error * |
| remove_locknull_state(dav_lockdb *lockdb, const dav_resource *resource) |
| { |
| |
| /* mod_dav_svn supports RFC2518bis which does not actually require |
| the server to create lock-null resources. Instead, we create |
| zero byte files when a lock comes in on a non-existent path. |
| mod_dav_svn never creates any lock-null resources, so this |
| function is never called by mod_dav. */ |
| |
| return 0; /* Just to suppress compiler warnings. */ |
| } |
| |
| |
| /* |
| ** Create a (direct) lock structure for the given resource. A locktoken |
| ** will be created. |
| ** |
| ** The lock provider may store private information into lock->info. |
| */ |
| static dav_error * |
| create_lock(dav_lockdb *lockdb, const dav_resource *resource, dav_lock **lock) |
| { |
| svn_error_t *serr; |
| dav_locktoken *token = apr_pcalloc(resource->pool, sizeof(*token)); |
| dav_lock *dlock = apr_pcalloc(resource->pool, sizeof(*dlock)); |
| |
| dlock->rectype = DAV_LOCKREC_DIRECT; |
| dlock->is_locknull = resource->exists; |
| dlock->scope = DAV_LOCKSCOPE_UNKNOWN; |
| dlock->type = DAV_LOCKTYPE_UNKNOWN; |
| dlock->depth = 0; |
| |
| serr = svn_fs_generate_lock_token(&(token->uuid_str), |
| resource->info->repos->fs, |
| resource->pool); |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Failed to generate a lock token.", |
| resource->pool); |
| dlock->locktoken = token; |
| |
| /* allowing mod_dav to fill in dlock->timeout, owner, auth_user. */ |
| /* dlock->info and dlock->next are NULL by default. */ |
| |
| *lock = dlock; |
| return 0; |
| } |
| |
| |
| /* |
| ** Get the locks associated with the specified resource. |
| ** |
| ** If resolve_locks is true (non-zero), then any indirect locks are |
| ** resolved to their actual, direct lock (i.e. the reference to followed |
| ** to the original lock). |
| ** |
| ** The locks, if any, are returned as a linked list in no particular |
| ** order. If no locks are present, then *locks will be NULL. |
| ** |
| ** #define DAV_GETLOCKS_RESOLVED 0 -- resolve indirects to directs |
| ** #define DAV_GETLOCKS_PARTIAL 1 -- leave indirects partially filled |
| ** #define DAV_GETLOCKS_COMPLETE 2 -- fill out indirect locks |
| */ |
| static dav_error * |
| get_locks(dav_lockdb *lockdb, |
| const dav_resource *resource, |
| int calltype, |
| dav_lock **locks) |
| { |
| dav_lockdb_private *info = lockdb->info; |
| svn_error_t *serr; |
| svn_lock_t *slock; |
| dav_lock *lock = NULL; |
| |
| /* We only support exclusive locks, not shared ones. So this |
| function always returns a "list" of exactly one lock, or just a |
| NULL list. The 'calltype' arg is also meaningless, since we |
| don't support locks on collections. */ |
| |
| /* Sanity check: if the resource has no associated path in the fs, |
| then there's nothing to do. */ |
| if (! resource->info->repos_path) |
| { |
| *locks = NULL; |
| return 0; |
| } |
| |
| /* The Big Lie: if the client ran 'svn lock', then we have |
| to pretend that there's no existing lock. Otherwise mod_dav will |
| throw '403 Locked' without even attempting to create a new |
| lock. For the --force case, this is required and for the non-force case, |
| we allow the filesystem to produce a better error for svn clients. |
| */ |
| if (info->r->method_number == M_LOCK |
| && resource->info->repos->is_svn_client) |
| { |
| *locks = NULL; |
| return 0; |
| } |
| |
| /* If the resource's fs path is unreadable, we don't want to say |
| anything about locks attached to it.*/ |
| if (! dav_svn__allow_read_resource(resource, SVN_INVALID_REVNUM, |
| resource->pool)) |
| return dav_svn__new_error(resource->pool, HTTP_FORBIDDEN, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Path is not accessible."); |
| |
| serr = svn_fs_get_lock(&slock, |
| resource->info->repos->fs, |
| resource->info->repos_path, |
| resource->pool); |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Failed to check path for a lock.", |
| resource->pool); |
| |
| if (slock != NULL) |
| { |
| svn_lock_to_dav_lock(&lock, slock, info->lock_break, |
| resource->exists, resource->pool); |
| |
| /* If we are talking to an svn client that wants to unlock a |
| path, tell mod_dav that the lock is owned by the user trying |
| to unlock. This stops it from returning an error that we |
| can't use in the client, and allows us to return the actual |
| unlock error */ |
| if (info->r->method_number == M_UNLOCK |
| && resource->info->repos->is_svn_client |
| && resource->info->repos->username) |
| { |
| lock->auth_user = resource->info->repos->username; |
| } |
| |
| /* Let svn clients know the creationdate of the slock. */ |
| apr_table_setn(info->r->headers_out, SVN_DAV_CREATIONDATE_HEADER, |
| svn_time_to_cstring(slock->creation_date, |
| resource->pool)); |
| |
| /* Let svn clients know who "owns" the slock. */ |
| apr_table_setn(info->r->headers_out, SVN_DAV_LOCK_OWNER_HEADER, |
| slock->owner); |
| } |
| |
| *locks = lock; |
| return 0; |
| } |
| |
| |
| /* |
| ** Find a particular lock on a resource (specified by its locktoken). |
| ** |
| ** *lock will be set to NULL if the lock is not found. |
| ** |
| ** Note that the provider can optimize the unmarshalling -- only one |
| ** lock (or none) must be constructed and returned. |
| ** |
| ** If partial_ok is true (non-zero), then an indirect lock can be |
| ** partially filled in. Otherwise, another lookup is done and the |
| ** lock structure will be filled out as a DAV_LOCKREC_INDIRECT. |
| */ |
| static dav_error * |
| find_lock(dav_lockdb *lockdb, |
| const dav_resource *resource, |
| const dav_locktoken *locktoken, |
| int partial_ok, |
| dav_lock **lock) |
| { |
| dav_lockdb_private *info = lockdb->info; |
| svn_error_t *serr; |
| svn_lock_t *slock; |
| dav_lock *dlock = NULL; |
| |
| /* If the resource's fs path is unreadable, we don't want to say |
| anything about locks attached to it.*/ |
| if (! dav_svn__allow_read_resource(resource, SVN_INVALID_REVNUM, |
| resource->pool)) |
| return dav_svn__new_error(resource->pool, HTTP_FORBIDDEN, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Path is not accessible."); |
| |
| serr = svn_fs_get_lock(&slock, |
| resource->info->repos->fs, |
| resource->info->repos_path, |
| resource->pool); |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Failed to look up lock by path.", |
| resource->pool); |
| |
| if (slock != NULL) |
| { |
| /* Sanity check. */ |
| if (strcmp(locktoken->uuid_str, slock->token) != 0) |
| return dav_svn__new_error(resource->pool, HTTP_BAD_REQUEST, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Incoming token doesn't match existing " |
| "lock."); |
| |
| svn_lock_to_dav_lock(&dlock, slock, FALSE, |
| resource->exists, resource->pool); |
| |
| /* Let svn clients know the creationdate of the slock. */ |
| apr_table_setn(info->r->headers_out, SVN_DAV_CREATIONDATE_HEADER, |
| svn_time_to_cstring(slock->creation_date, |
| resource->pool)); |
| |
| /* Let svn clients know the 'owner' of the slock. */ |
| apr_table_setn(info->r->headers_out, SVN_DAV_LOCK_OWNER_HEADER, |
| slock->owner); |
| } |
| |
| *lock = dlock; |
| return 0; |
| } |
| |
| |
| /* |
| ** Quick test to see if the resource has *any* locks on it. |
| ** |
| ** This is typically used to determine if a non-existent resource |
| ** has a lock and is (therefore) a locknull resource. |
| ** |
| ** WARNING: this function may return TRUE even when timed-out locks |
| ** exist (i.e. it may not perform timeout checks). |
| */ |
| static dav_error * |
| has_locks(dav_lockdb *lockdb, const dav_resource *resource, int *locks_present) |
| { |
| dav_lockdb_private *info = lockdb->info; |
| svn_error_t *serr; |
| svn_lock_t *slock; |
| |
| /* Sanity check: if the resource has no associated path in the fs, |
| then there's nothing to do. */ |
| if (! resource->info->repos_path) |
| { |
| *locks_present = 0; |
| return 0; |
| } |
| |
| /* The Big Lie: if the client ran 'svn lock', then we have |
| to pretend that there's no existing lock. Otherwise mod_dav will |
| throw '403 Locked' without even attempting to create a new |
| lock. For the --force case, this is required and for the non-force case, |
| we allow the filesystem to produce a better error for svn clients. |
| */ |
| if (info->r->method_number == M_LOCK |
| && resource->info->repos->is_svn_client) |
| { |
| *locks_present = 0; |
| return 0; |
| } |
| |
| /* If the resource's fs path is unreadable, we don't want to say |
| anything about locks attached to it.*/ |
| if (! dav_svn__allow_read_resource(resource, SVN_INVALID_REVNUM, |
| resource->pool)) |
| return dav_svn__new_error(resource->pool, HTTP_FORBIDDEN, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Path is not accessible."); |
| |
| serr = svn_fs_get_lock(&slock, |
| resource->info->repos->fs, |
| resource->info->repos_path, |
| resource->pool); |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Failed to check path for a lock.", |
| resource->pool); |
| |
| *locks_present = slock ? 1 : 0; |
| return 0; |
| } |
| |
| |
| /* |
| ** Append the specified lock(s) to the set of locks on this resource. |
| ** |
| ** If "make_indirect" is true (non-zero), then the specified lock(s) |
| ** should be converted to an indirect lock (if it is a direct lock) |
| ** before appending. Note that the conversion to an indirect lock does |
| ** not alter the passed-in lock -- the change is internal the |
| ** append_locks function. |
| ** |
| ** Multiple locks are specified using the lock->next links. |
| */ |
| static dav_error * |
| append_locks(dav_lockdb *lockdb, |
| const dav_resource *resource, |
| int make_indirect, |
| const dav_lock *lock) |
| { |
| dav_lockdb_private *info = lockdb->info; |
| svn_lock_t *slock; |
| svn_error_t *serr; |
| dav_error *derr; |
| dav_svn_repos *repos = resource->info->repos; |
| |
| /* We don't allow anonymous locks */ |
| if (! repos->username) |
| return dav_svn__new_error(resource->pool, HTTP_NOT_IMPLEMENTED, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Anonymous lock creation is not allowed."); |
| |
| /* Not a path in the repository so can't lock it. */ |
| if (! resource->info->repos_path) |
| return dav_svn__new_error(resource->pool, HTTP_BAD_REQUEST, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Attempted to lock path not in repository."); |
| |
| /* If the resource's fs path is unreadable, we don't allow a lock to |
| be created on it. */ |
| if (! dav_svn__allow_read_resource(resource, SVN_INVALID_REVNUM, |
| resource->pool)) |
| return dav_svn__new_error(resource->pool, HTTP_FORBIDDEN, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Path is not accessible."); |
| |
| if (lock->next) |
| return dav_svn__new_error(resource->pool, HTTP_BAD_REQUEST, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Tried to attach multiple locks to a resource."); |
| |
| /* RFC2518bis (section 7.4) doesn't require us to support |
| 'lock-null' resources at all. Instead, it asks that we treat |
| 'LOCK nonexistentURL' as a PUT (followed by a LOCK) of a 0-byte file. */ |
| if (! resource->exists) |
| { |
| svn_revnum_t rev, new_rev; |
| svn_fs_txn_t *txn; |
| svn_fs_root_t *txn_root; |
| const char *conflict_msg; |
| apr_hash_t *revprop_table = apr_hash_make(resource->pool); |
| svn_hash_sets(revprop_table, |
| SVN_PROP_REVISION_AUTHOR, |
| svn_string_create(repos->username, resource->pool)); |
| |
| if (resource->info->repos->is_svn_client) |
| return dav_svn__new_error(resource->pool, HTTP_METHOD_NOT_ALLOWED, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Subversion clients may not lock " |
| "nonexistent paths."); |
| |
| else if (! resource->info->repos->autoversioning) |
| return dav_svn__new_error(resource->pool, HTTP_METHOD_NOT_ALLOWED, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Attempted to lock non-existent path; " |
| "turn on autoversioning first."); |
| |
| /* Commit a 0-byte file: */ |
| |
| if ((serr = dav_svn__get_youngest_rev(&rev, repos, resource->pool))) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not determine youngest revision", |
| resource->pool); |
| |
| if ((serr = svn_repos_fs_begin_txn_for_commit2(&txn, repos->repos, rev, |
| revprop_table, |
| resource->pool))) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not begin a transaction", |
| resource->pool); |
| |
| if ((serr = svn_fs_txn_root(&txn_root, txn, resource->pool))) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not begin a transaction", |
| resource->pool); |
| |
| if ((serr = svn_fs_make_file(txn_root, resource->info->repos_path, |
| resource->pool))) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not create empty file.", |
| resource->pool); |
| |
| if ((serr = dav_svn__attach_auto_revprops(txn, |
| resource->info->repos_path, |
| resource->pool))) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Could not create empty file.", |
| resource->pool); |
| |
| serr = svn_repos_fs_commit_txn(&conflict_msg, repos->repos, |
| &new_rev, txn, resource->pool); |
| if (SVN_IS_VALID_REVNUM(new_rev)) |
| { |
| /* ### Log an error in post commit FS processing? */ |
| svn_error_clear(serr); |
| } |
| else |
| { |
| svn_error_clear(svn_fs_abort_txn(txn, resource->pool)); |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_CONFLICT, |
| apr_psprintf(resource->pool, |
| "Conflict when " |
| "committing '%s'.", |
| conflict_msg), |
| resource->pool); |
| else |
| return dav_svn__new_error(resource->pool, |
| HTTP_INTERNAL_SERVER_ERROR, |
| 0, 0, |
| "Commit failed but there was no error " |
| "provided."); |
| } |
| } |
| |
| /* Convert the dav_lock into an svn_lock_t. */ |
| derr = dav_lock_to_svn_lock(&slock, lock, resource->info->repos_path, |
| info, repos->is_svn_client, |
| resource->pool); |
| if (derr) |
| return derr; |
| |
| /* Now use the svn_lock_t to actually perform the lock. */ |
| serr = svn_repos_fs_lock(&slock, |
| repos->repos, |
| slock->path, |
| slock->token, |
| slock->comment, |
| slock->is_dav_comment, |
| slock->expiration_date, |
| info->working_revnum, |
| info->lock_steal, |
| resource->pool); |
| |
| if (serr && serr->apr_err == SVN_ERR_FS_NO_USER) |
| { |
| svn_error_clear(serr); |
| return dav_svn__new_error(resource->pool, HTTP_NOT_IMPLEMENTED, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Anonymous lock creation is not allowed."); |
| } |
| else if (serr && serr->apr_err == SVN_ERR_REPOS_POST_LOCK_HOOK_FAILED) |
| { |
| /* The lock was created in the repository, so we should report the node |
| as locked to the client */ |
| |
| /* First log the hook failure, for diagnostics. This clears serr */ |
| dav_svn__log_err(info->r, |
| dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Post lock hook failure.", |
| resource->pool), |
| APLOG_WARNING); |
| |
| /* How can we report the error to the client? |
| |
| We can't return an error code, as that would make it impossible |
| to return the lock details? |
| |
| Add yet another custom header? |
| Just an header doesn't handle a full error chain... |
| |
| ### Current behavior: we don't report an error. |
| */ |
| |
| } |
| else if (serr && (svn_error_find_cause(serr, SVN_ERR_REPOS_HOOK_FAILURE) || |
| serr->apr_err == SVN_ERR_FS_NO_SUCH_LOCK || |
| serr->apr_err == SVN_ERR_FS_LOCK_EXPIRED || |
| SVN_ERR_IS_LOCK_ERROR(serr))) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Failed to create new lock.", |
| resource->pool); |
| else if (serr) |
| return dav_svn__sanitize_error(serr, "Failed to create new lock.", |
| HTTP_INTERNAL_SERVER_ERROR, |
| resource->info->r); |
| |
| |
| /* A standard webdav LOCK response doesn't include any information |
| about the creation date. We send it in a custom header, so that |
| svn clients can fill in svn_lock_t->creation_date. A generic DAV |
| client should just ignore the header. */ |
| apr_table_setn(info->r->headers_out, SVN_DAV_CREATIONDATE_HEADER, |
| svn_time_to_cstring(slock->creation_date, resource->pool)); |
| |
| /* A standard webdav LOCK response doesn't include any information |
| about the owner of the lock. ('DAV:owner' has nothing to do with |
| authorization, it's just a comment that we map to |
| svn_lock_t->comment.) We send the owner in a custom header, so |
| that svn clients can fill in svn_lock_t->owner. A generic DAV |
| client should just ignore the header. */ |
| apr_table_setn(info->r->headers_out, SVN_DAV_LOCK_OWNER_HEADER, |
| slock->owner); |
| |
| /* Log the locking as a 'high-level' action. */ |
| dav_svn__operational_log(resource->info, |
| svn_log__lock_one_path(slock->path, info->lock_steal, |
| resource->info->r->pool)); |
| |
| return 0; |
| } |
| |
| |
| /* |
| ** Remove any lock that has the specified locktoken. |
| ** |
| ** If locktoken == NULL, then ALL locks are removed. |
| */ |
| static dav_error * |
| remove_lock(dav_lockdb *lockdb, |
| const dav_resource *resource, |
| const dav_locktoken *locktoken) |
| { |
| dav_lockdb_private *info = lockdb->info; |
| svn_error_t *serr; |
| svn_lock_t *slock; |
| const char *token = NULL; |
| |
| /* Sanity check: if the resource has no associated path in the fs, |
| then there's nothing to do. */ |
| if (! resource->info->repos_path) |
| return NULL; |
| |
| /* Another easy out: if an svn client sent a 'keep_locks' header |
| (typically in a DELETE request, as part of 'svn commit |
| --no-unlock'), then ignore dav_method_delete()'s attempt to |
| unconditionally remove the lock. */ |
| if (info->keep_locks) |
| return NULL; |
| |
| /* If the resource's fs path is unreadable, we don't allow a lock to |
| be removed from it. */ |
| if (! dav_svn__allow_read_resource(resource, SVN_INVALID_REVNUM, |
| resource->pool)) |
| return dav_svn__new_error(resource->pool, HTTP_FORBIDDEN, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Path is not accessible."); |
| |
| if (locktoken == NULL) |
| { |
| /* Need to manually discover any lock on the resource. */ |
| serr = svn_fs_get_lock(&slock, |
| resource->info->repos->fs, |
| resource->info->repos_path, |
| resource->pool); |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Failed to check path for a lock.", |
| resource->pool); |
| if (slock) |
| token = slock->token; |
| } |
| else |
| { |
| token = locktoken->uuid_str; |
| } |
| |
| if (token) |
| { |
| /* Notice that a generic DAV client is unable to forcibly |
| 'break' a lock, because info->lock_break will always be |
| FALSE. An svn client, however, can request a 'forced' break.*/ |
| serr = svn_repos_fs_unlock(resource->info->repos->repos, |
| resource->info->repos_path, |
| token, |
| info->lock_break, |
| resource->pool); |
| |
| if (serr && serr->apr_err == SVN_ERR_FS_NO_USER) |
| { |
| svn_error_clear(serr); |
| return dav_svn__new_error(resource->pool, HTTP_NOT_IMPLEMENTED, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Anonymous lock removal is not allowed."); |
| } |
| else if (serr && serr->apr_err == SVN_ERR_REPOS_POST_UNLOCK_HOOK_FAILED |
| && !resource->info->repos->is_svn_client) |
| { |
| /* Generic DAV clients don't understand the specific error code we |
| would produce here as being just a warning, so lets produce a |
| success result. We removed the lock anyway. */ |
| |
| /* First log the hook failure, for diagnostics. This clears serr */ |
| dav_svn__log_err(info->r, |
| dav_svn__convert_err(serr, |
| HTTP_INTERNAL_SERVER_ERROR, |
| "Post unlock hook failure.", |
| resource->pool), |
| APLOG_WARNING); |
| |
| } |
| else if (serr && serr->apr_err == SVN_ERR_FS_LOCK_OWNER_MISMATCH) |
| { |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| NULL, resource->pool); |
| } |
| else if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Failed to remove a lock.", |
| resource->pool); |
| |
| /* Log the unlocking as a 'high-level' action. */ |
| dav_svn__operational_log(resource->info, |
| svn_log__unlock_one_path( |
| resource->info->repos_path, |
| info->lock_break, |
| resource->info->r->pool)); |
| } |
| |
| return NULL; |
| } |
| |
| static dav_error * |
| remove_lock_svn_output(dav_lockdb *lockdb, |
| const dav_resource *resource, |
| const dav_locktoken *locktoken) |
| { |
| dav_error *derr = remove_lock(lockdb, resource, locktoken); |
| int status; |
| |
| if (!derr |
| || !resource->info->repos |
| || !resource->info->repos->is_svn_client |
| || (strcmp(lockdb->info->r->method, "UNLOCK") != 0)) |
| return derr; |
| |
| /* Ok, we have a nice error chain but mod_dav doesn't offer us a way to |
| present it to the client as it will only use the status code for |
| generating a standard error... |
| |
| Luckily the unlock processing for the "UNLOCK" method is very simple: |
| call this function and return the result. |
| |
| That allows us to just force a response and tell httpd that we are done */ |
| status = dav_svn__error_response_tag(lockdb->info->r, derr); |
| |
| /* status = DONE */ |
| |
| /* And push an error that will make mod_dav just report that it is done */ |
| return dav_push_error(resource->pool, status, derr->error_id, NULL, derr); |
| } |
| |
| |
| |
| /* |
| ** Refresh all locks, found on the specified resource, which has a |
| ** locktoken in the provided list. |
| ** |
| ** If the lock is indirect, then the direct lock is referenced and |
| ** refreshed. |
| ** |
| ** Each lock that is updated is returned in the <locks> argument. |
| ** Note that the locks will be fully resolved. |
| */ |
| static dav_error * |
| refresh_locks(dav_lockdb *lockdb, |
| const dav_resource *resource, |
| const dav_locktoken_list *ltl, |
| time_t new_time, |
| dav_lock **locks) |
| { |
| /* We're not looping over a list of locks, since we only support one |
| lock per resource. */ |
| dav_locktoken *token = ltl->locktoken; |
| svn_error_t *serr; |
| svn_lock_t *slock; |
| dav_lock *dlock; |
| |
| /* If the resource's fs path is unreadable, we don't want to say |
| anything about locks attached to it.*/ |
| if (! dav_svn__allow_read_resource(resource, SVN_INVALID_REVNUM, |
| resource->pool)) |
| return dav_svn__new_error(resource->pool, HTTP_FORBIDDEN, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Path is not accessible."); |
| |
| /* Convert the path into an svn_lock_t. */ |
| serr = svn_fs_get_lock(&slock, |
| resource->info->repos->fs, |
| resource->info->repos_path, |
| resource->pool); |
| if (serr) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Token doesn't point to a lock.", |
| resource->pool); |
| |
| /* Sanity check: does the incoming token actually represent the |
| current lock on the incoming resource? */ |
| if ((! slock) |
| || (strcmp(token->uuid_str, slock->token) != 0)) |
| return dav_svn__new_error(resource->pool, HTTP_PRECONDITION_FAILED, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Lock refresh request doesn't match existing " |
| "lock."); |
| |
| /* Now use the tweaked svn_lock_t to 'refresh' the existing lock. */ |
| serr = svn_repos_fs_lock(&slock, |
| resource->info->repos->repos, |
| slock->path, |
| slock->token, |
| slock->comment, |
| slock->is_dav_comment, |
| (new_time == DAV_TIMEOUT_INFINITE) |
| ? 0 : (apr_time_t)new_time * APR_USEC_PER_SEC, |
| SVN_INVALID_REVNUM, |
| TRUE, /* forcibly steal existing lock */ |
| resource->pool); |
| |
| if (serr && serr->apr_err == SVN_ERR_FS_NO_USER) |
| { |
| svn_error_clear(serr); |
| return dav_svn__new_error(resource->pool, HTTP_NOT_IMPLEMENTED, |
| DAV_ERR_LOCK_SAVE_LOCK, 0, |
| "Anonymous lock refreshing is not allowed."); |
| } |
| else if (serr && (svn_error_find_cause(serr, SVN_ERR_REPOS_HOOK_FAILURE) || |
| serr->apr_err == SVN_ERR_FS_NO_SUCH_LOCK || |
| serr->apr_err == SVN_ERR_FS_LOCK_EXPIRED || |
| SVN_ERR_IS_LOCK_ERROR(serr))) |
| return dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR, |
| "Failed to refresh existing lock.", |
| resource->pool); |
| else if (serr) |
| return dav_svn__sanitize_error(serr, "Failed to refresh existing lock.", |
| HTTP_INTERNAL_SERVER_ERROR, |
| resource->info->r); |
| |
| /* Convert the refreshed lock into a dav_lock and return it. */ |
| svn_lock_to_dav_lock(&dlock, slock, FALSE, resource->exists, resource->pool); |
| *locks = dlock; |
| |
| return 0; |
| } |
| |
| |
| /* The main locking vtable, provided to mod_dav */ |
| const dav_hooks_locks dav_svn__hooks_locks = { |
| get_supportedlock, |
| parse_locktoken, |
| format_locktoken, |
| compare_locktoken, |
| open_lockdb, |
| close_lockdb, |
| remove_locknull_state, |
| create_lock, |
| get_locks, |
| find_lock, |
| has_locks, |
| append_locks, |
| remove_lock_svn_output, |
| refresh_locks, |
| NULL, |
| NULL /* hook structure context */ |
| }; |