Roxen.git / server / base_server / module.pike

version» Context lines:

Roxen.git/server/base_server/module.pike:552:    string ext;    if(stringp(sub_id->extension)) {    sub_id->not_query += sub_id->extension;    ext = lower_case(Roxen.extension(sub_id->not_query, sub_id));    }    array(string) tmp=sub_id->conf->type_from_filename(sub_id->not_query, 1, ext);    if(tmp)    res = ([ "file":res, "type":tmp[0], "encoding":tmp[1] ]);    else    res = (["file": res]); +  } else if (!res) { +  res = Roxen.http_status(404, "File not found.");    }    response_headers = sub_id->make_response_headers (res);    destruct (sub_id);    }       return response_headers;    }   }      //! Return the set of properties for @[path].
Roxen.git/server/base_server/module.pike:890:   //! any above @[path]. (This is appropriate to use to get the   //! list of locks that need to be unlocked on DELETE.)   //!   //! @param exclude_shared   //! If @expr{1@} do not return shared locks that are held by users   //! other than the one the request is authenticated as. (This is   //! appropriate to get the list of locks that would conflict if the   //! current user were to make a shared lock.)   //!   //! @returns - //! Returns a multiset containing all applicable locks in + //! Returns a mapping containing all applicable locks in   //! this location module, or @expr{0@} (zero) if there are none.   //!   //! @note   //! @[DAVLock] objects may be created if the filesystem has some   //! persistent storage of them. The default implementation does not   //! store locks persistently.   //!   //! @note   //! The default implementation only handles the @expr{"DAV:write"@}   //! lock type. - multiset(DAVLock) find_locks(string path, + mapping(string:DAVLock) find_locks(string path,    int(-1..1) recursive,    int(0..1) exclude_shared,    RequestID id)   {    // Common case.    if (!sizeof(file_locks) && !sizeof(prefix_locks)) return 0;       TRACE_ENTER(sprintf("find_locks(%O, %O, %O, X)",    path, recursive, exclude_shared), this);       string rsc = resource_id (path, id);    -  multiset(DAVLock) locks = (<>); +  mapping(string:DAVLock) locks = ([]);    function(mapping(mixed:DAVLock):void) add_locks;       if (exclude_shared) {    mixed auth_user = authenticated_user_id (path, id);    add_locks = lambda (mapping(mixed:DAVLock) sub_locks) {    foreach (sub_locks; string user; DAVLock lock)    if (user == auth_user ||    lock->lockscope == "DAV:exclusive") -  locks[lock] = 1; +  locks[lock->locktoken] = lock;    };    }    else    add_locks = lambda (mapping(mixed:DAVLock) sub_locks) { -  locks |= mkmultiset (values (sub_locks)); +  locks |= mkmapping(values(sub_locks)->locktoken, +  values(sub_locks));    };       if (file_locks[rsc]) {    add_locks (file_locks[rsc]);    }       if (recursive >= 0) {    foreach(prefix_locks;    string prefix; mapping(mixed:DAVLock) sub_locks) {    if (has_prefix(rsc, prefix)) {
Roxen.git/server/base_server/module.pike:960:    });    }       add_locks = 0;       TRACE_LEAVE(sprintf("Done, found %d locks.", sizeof(locks)));       return sizeof(locks) && locks;   }    - //! Check if there are one or more locks that apply to @[path] for the - //! user the request is authenticated as. + //! Unlock any locks pertaining to @[path] or to anything + //! under @[path].   //! - //! WARNING: This function has some design issues and will very likely - //! get a different interface. Compatibility is NOT guaranteed. + //! This function is used to unlock locks after files or + //! directories have been deleted.   //! - //! @param path - //! Normalized path below the filesystem location. + //! The default implementation just unregisters the locks.   //! - //! @param recursive - //! If @expr{1@} also check recursively under @[path] for locks. - //! - //! @returns - //! The valid return values are: - //! @mixed - //! @type DAVLock - //! The lock owned by the authenticated user that apply to - //! @[path]. (It doesn't matter if the @expr{recursive@} flag in - //! the lock doesn't match the @[recursive] argument.) - //! @type LockFlag - //! @int - //! @value LOCK_NONE - //! No locks apply. (0) - //! @value LOCK_SHARED_BELOW - //! There are only one or more shared locks held by other - //! users somewhere below @[path] (but not on @[path] - //! itself). Only returned if @[recursive] is set. (2) - //! @value LOCK_SHARED_AT - //! There are only one or more shared locks held by other - //! users on @[path]. (3) - //! @value LOCK_OWN_BELOW - //! The authenticated user has locks under @[path] (but not - //! on @[path] itself) and there are no exclusive locks held - //! by other users. Only returned if @[recursive] is set. (4) - //! @value LOCK_EXCL_BELOW - //! There are one or more exclusive locks held by other - //! users somewhere below @[path] (but not on @[path] - //! itself). Only returned if @[recursive] is set. (6) - //! @value LOCK_EXCL_AT - //! There are one or more exclusive locks held by other - //! users on @[path]. (7) - //! @endint - //! Note that the lowest bit is set for all flags that apply to - //! @[path] itself. - //! @endmixed - //! - //! @note - //! @[DAVLock] objects may be created if the filesystem has some - //! persistent storage of them. The default implementation does not - //! store locks persistently. - //! - //! @note - //! The default implementation only handles the @expr{"DAV:write"@} - //! lock type. - DAVLock|LockFlag check_locks(string path, -  int(0..1) recursive, -  RequestID id) + //! @seealso + //! @[unregister_lock()] + void unlock_path(string path, RequestID id)   { -  TRACE_ENTER(sprintf("check_locks(%O, %d, X)", path, recursive), this); -  +     // Common case. -  if (!sizeof(file_locks) && !sizeof(prefix_locks)) { -  TRACE_LEAVE ("Got no locks"); -  return 0; -  } +  if (!sizeof(file_locks) && !sizeof(prefix_locks)) return 0;    -  mixed auth_user = authenticated_user_id (path, id); -  path = resource_id (path, id); +  TRACE_ENTER(sprintf("unlock_path(%O, %O)", path, id), this);    -  if (DAVLock lock = -  file_locks[path] && file_locks[path][auth_user] || -  prefix_locks[path] && prefix_locks[path][auth_user]) { -  TRACE_LEAVE(sprintf("Found own lock %O.", lock->locktoken)); -  return lock; -  } +  string rsc = resource_id(path, id);    -  LockFlag shared; +  // NB: The following code leaves dead locks in conf->active_locks!    -  if (mapping(mixed:DAVLock) locks = file_locks[path]) { -  foreach(locks;; DAVLock lock) { -  if (lock->lockscope == "DAV:exclusive") { -  TRACE_LEAVE(sprintf("Found other user's exclusive lock %O.", -  lock->locktoken)); -  return LOCK_EXCL_AT; +  foreach(file_locks; string prefix; mapping(mixed:DAVLock) sub_locks) { +  if (has_prefix(prefix, rsc)) { +  TRACE_ENTER(sprintf("Unlocking %d locks for path %O...", +  sizeof(sub_locks), prefix), this); +  m_delete(file_locks, prefix); +  TRACE_LEAVE("");    } -  shared = LOCK_SHARED_AT; -  break; +     } -  } +     -  foreach(prefix_locks; -  string prefix; mapping(mixed:DAVLock) locks) { -  if (has_prefix(path, prefix)) { -  if (DAVLock lock = locks[auth_user]) { -  SIMPLE_TRACE_LEAVE ("Found own lock %O on %O.", lock->locktoken, prefix); -  return lock; +  foreach(prefix_locks; string prefix; mapping(mixed:DAVLock) sub_locks) { +  if (has_prefix(prefix, rsc)) { +  TRACE_ENTER(sprintf("Unlocking %d locks for path %O...", +  sizeof(sub_locks), prefix), this); +  m_delete(prefix_locks, prefix); +  TRACE_LEAVE("");    } -  if (!shared) -  // If we've found a shared lock then we won't find an -  // exclusive one anywhere else. -  foreach(locks;; DAVLock lock) { -  if (lock->lockscope == "DAV:exclusive") { -  TRACE_LEAVE(sprintf("Found other user's exclusive lock %O.", -  lock->locktoken)); -  return LOCK_EXCL_AT; +     } -  shared = LOCK_SHARED_AT; -  break; -  } -  } -  } +     -  if (!recursive) { -  SIMPLE_TRACE_LEAVE("Returning %O.", shared); -  return shared; +  TRACE_LEAVE("Done.");   }    -  int(0..1) locked_by_auth_user; -  -  // We want to know if there are any locks with @[path] as prefix -  // that apply to us. -  LOOP_OVER_BOTH (string prefix, mapping(mixed:DAVLock) locks, { -  if (has_prefix(prefix, path)) { -  if (locks[auth_user]) -  locked_by_auth_user = 1; -  else -  foreach(locks;; DAVLock lock) { -  if (lock->lockscope == "DAV:exclusive") { -  TRACE_LEAVE(sprintf("Found other user's exclusive lock %O.", -  lock->locktoken)); -  return LOCK_EXCL_BELOW; -  } -  if (!shared) shared = LOCK_SHARED_BELOW; -  break; -  } -  } -  }); -  -  SIMPLE_TRACE_LEAVE("Returning %O.", locked_by_auth_user ? LOCK_OWN_BELOW : shared); -  return locked_by_auth_user ? LOCK_OWN_BELOW : shared; - } -  +    //! Register @[lock] on the path @[path] under the assumption that - //! there is no other lock already that conflicts with this one, i.e. - //! that @expr{check_locks(path,lock->recursive,id)@} would return - //! @expr{LOCK_NONE@} if @expr{lock->lockscope@} is - //! @expr{"DAV:exclusive"@}, or @expr{< LOCK_OWN_BELOW@} if - //! @expr{lock->lockscope@} is @expr{"DAV:shared"@}. + //! there is no other lock already that conflicts with this one.   //!   //! This function is only provided as a helper to call from   //! @[lock_file] if the default lock implementation is to be used.   //!   //! @param path   //! Normalized path (below the filesystem location) that the lock   //! applies to.   //!   //! @param lock   //! The lock to register.
Roxen.git/server/base_server/module.pike:1181:    if (lock->recursive) {    if (id) {    removed_lock = m_delete(prefix_locks[path], auth_user);    } else {    foreach(prefix_locks[path]||([]); mixed user; DAVLock l) {    if (l == lock) {    removed_lock = m_delete(prefix_locks[path], user);    }    }    } -  if (!sizeof (prefix_locks[path])) m_delete (prefix_locks, path); +  if (prefix_locks[path] && !sizeof (prefix_locks[path])) { +  m_delete(prefix_locks, path);    } -  +  }    else if (file_locks[path]) {    if (id) {    removed_lock = m_delete (file_locks[path], auth_user);    } else {    foreach(file_locks[path]||([]); mixed user; DAVLock l) {    if (l == lock) {    removed_lock = m_delete(file_locks[path], user);    }    }    }
Roxen.git/server/base_server/module.pike:1204:    }    // NB: The lock may have already been removed in the !id case.    ASSERT_IF_DEBUG (!(id || removed_lock) ||    (lock /*%O*/ == removed_lock /*%O*/),    lock, removed_lock);    TRACE_LEAVE("Ok.");    return 0;   }      //! Register @[lock] on the path @[path] under the assumption that - //! there is no other lock already that conflicts with this one, i.e. - //! that @expr{check_locks(path,lock->recursive,id)@} would return - //! @expr{LOCK_NONE@} if @expr{lock->lockscope@} is - //! @expr{"DAV:exclusive"@}, or @expr{<= LOCK_SHARED_AT@} if - //! @expr{lock->lockscope@} is @expr{"DAV:shared"@}. + //! there is no other lock already that conflicts with this one.   //!   //! The implementation must at least support the @expr{"DAV:write"@}   //! lock type (RFC 2518, section 7). Briefly: An exclusive lock on a   //! file prohibits other users from changing its content. An exclusive   //! lock on a directory (aka collection) prohibits other users from   //! adding or removing files or directories in it. An exclusive lock   //! on a file or directory prohibits other users from setting or   //! deleting any of its properties. A shared lock prohibits users   //! without locks to do any of this, and it prohibits other users from   //! obtaining an exclusive lock. A resource that doesn't exist can be
Roxen.git/server/base_server/module.pike:1284:   //! @returns   //! Returns a status mapping on any error, zero otherwise.   //!   //! @note   //! To use the default lock implementation, call @[unregister_lock]   //! from this function.   mapping(string:mixed) unlock_file (string path,    DAVLock lock,    RequestID|int(0..0) id);    - //! Checks that the conditions specified by the WebDAV @expr{"If"@} - //! header are fulfilled on the given path (RFC 2518 9.4). This means - //! that locks are checked as necessary using @[check_locks]. - //! - //! WARNING: This function has some design issues and will very likely - //! get a different interface. Compatibility is NOT guaranteed. - //! - //! @param path - //! Path (below the filesystem location) that the lock applies to. - //! - //! @param recursive - //! If @expr{1@} also check write access recursively under @[path]. - //! - //! @returns - //! Returns @expr{0@} (zero) on success, a status mapping on - //! failure, or @expr{1@} if @[recursive] is set and write access is - //! allowed on this level but maybe not somewhere below. The caller - //! should in the last case do the operation on this level if - //! possible and then handle each member in the directory - //! recursively with @[write_access] etc. - mapping(string:mixed)|int(0..1) check_if_header(string relative_path, -  int(0..1) recursive, -  RequestID id) - { -  SIMPLE_TRACE_ENTER(this, "Checking \"If\" header for %O", -  relative_path); -  -  int/*LockFlag*/|DAVLock lock = check_locks(relative_path, recursive, id); -  -  int(0..1) got_sublocks; -  if (lock && intp(lock)) { -  if (lock & 1) { -  TRACE_LEAVE("Locked by other user."); -  return Roxen.http_status(Protocols.HTTP.DAV_LOCKED); -  } -  else if (recursive) -  // This is set for LOCK_OWN_BELOW too since it might be -  // necessary to come back here and check the If header for -  // those locks. -  got_sublocks = 1; -  } -  -  string path = relative_path; -  if (!has_suffix (path, "/")) path += "/"; // get_if_data always adds a "/". -  path = query_location() + path; // No need for fancy combine_path stuff here. -  -  mapping(string:array(array(array(string)))) if_data = id->get_if_data(); -  array(array(array(string))) condition; -  if (!if_data || !sizeof(condition = if_data[path] || if_data[0])) { -  if (lock) { -  TRACE_LEAVE("Locked, no if header."); -  return Roxen.http_status(Protocols.HTTP.DAV_LOCKED); -  } -  SIMPLE_TRACE_LEAVE("No lock and no if header - ok%s.", -  got_sublocks ? " (this level only)" : ""); -  return got_sublocks; // No condition and no lock -- Ok. -  } -  -  string|int(-1..0) etag; -  -  int(0..1) locked_fail = !!lock; -  next_condition: -  foreach(condition, array(array(string)) sub_cond) { -  SIMPLE_TRACE_ENTER(this, -  "Trying condition ( %{%s:%O %})...", sub_cond); -  int negate; -  DAVLock locked = lock; -  foreach(sub_cond, array(string) token) { -  switch(token[0]) { -  case "not": -  negate = !negate; -  break; -  case "etag": -  if (!etag) { -  // Get the etag for this resource (if any). -  // FIXME: We only support straight strings as etag properties. -  if (!stringp(etag = query_property(relative_path, -  "DAV:getetag", id))) { -  etag = -1; -  } -  } -  if (etag != token[1]) { -  // No etag available for this resource, or mismatch. -  if (!negate) { -  TRACE_LEAVE("Etag mismatch."); -  continue next_condition; -  } -  } else if (negate) { -  // Etag match with negated expression. -  TRACE_LEAVE("Matched negated etag."); -  continue next_condition; -  } -  negate = 0; -  break; -  case "key": -  // The user has specified a key, so don't fail with DAV_LOCKED. -  locked_fail = 0; -  if (negate) { -  if (lock && lock->locktoken == token[1]) { -  TRACE_LEAVE("Matched negated lock."); -  continue next_condition; // Fail. -  } -  } else if (!lock || lock->locktoken != token[1]) { -  // Lock mismatch. -  TRACE_LEAVE("Lock mismatch."); -  continue next_condition; // Fail. -  } else { -  locked = 0; -  } -  negate = 0; -  break; -  } -  } -  if (!locked) { -  TRACE_LEAVE("Found match."); -  SIMPLE_TRACE_LEAVE("Ok%s.", -  got_sublocks ? " (this level only)" : ""); -  return got_sublocks; // Found matching sub-condition. -  } -  SIMPLE_TRACE_LEAVE("Conditional ok, but still locked (locktoken: %O).", -  lock->locktoken); -  locked_fail = 1; -  } -  -  if (locked_fail) { -  TRACE_LEAVE("Failed (locked)."); -  } else { -  TRACE_LEAVE("Precondition failed."); -  } -  return Roxen.http_status(locked_fail ? -  Protocols.HTTP.DAV_LOCKED : -  Protocols.HTTP.HTTP_PRECOND_FAILED); - } -  +    //! Used by some default implementations to check if we may perform a   //! write access to @[path]. It should at least call - //! @[check_if_header] to check DAV locks. It takes the same arguments - //! and has the same return value as that function. + //! @[Configuration::check_locks()] to check DAV locks. It takes the + //! same arguments and has the same return value as that function.   //!   //! WARNING: This function has some design issues and will very likely   //! get a different interface. Compatibility is NOT guaranteed.   //!   //! A filesystem module should typically put all needed write access   //! checks here and then use this from @[find_file()],   //! @[delete_file()] etc.   //!   //! @returns   //! Returns @expr{0@} (zero) on success, a status mapping on   //! failure, or @expr{1@} if @[recursive] is set and write access is   //! allowed on this level but maybe not somewhere below. The caller   //! should in the last case do the operation on this level if   //! possible and then handle each member in the directory   //! recursively with @[write_access] etc.   protected mapping(string:mixed)|int(0..1) write_access(string relative_path,    int(0..1) recursive,    RequestID id)   { -  return check_if_header (relative_path, recursive, id); +  string path = query_location() + relative_path; +  return id->conf->check_locks(path, recursive, id);   }      //!   protected variant mapping(string:mixed)|int(0..1) write_access(array(string) paths,    int(0..1) recursive,    RequestID id)   {    mapping(string:mixed)|int(0..1) ret; -  int got_ok; +     foreach(paths, string path) {    ret = write_access(path, recursive, id); -  if (!ret) { -  got_ok = 1; -  continue; -  } -  if (ret == 1) { -  continue; -  } -  if (ret->error == Protocols.HTTP.HTTP_PRECOND_FAILED) { -  continue; -  } +  if (mappingp(ret)) {    return ret;    } -  +  }    -  if (got_ok) { -  // The if headers are valid for at least one of the paths, -  // and none of the other paths are locked. +  // None of the paths are locked.    return 0;   }    -  // HTTP_PRECOND_FAILED for all of the paths. -  return ret; - } -  +    mapping(string:mixed)|int(-1..0)|Stdio.File find_file(string path,    RequestID id);      //! Used by the default @[recurse_delete_files] implementation to   //! delete a file or an empty directory.   //!   //! @returns   //! Returns a 2xx series status mapping on success (typically 204 No   //! Content). Returns 0 if the file doesn't exist. Returns an   //! appropriate status mapping for any other error.
Roxen.git/server/base_server/module.pike:1527:    if (!stat)    stat = id->get_multi_status()->prefix (id->url_base() +    query_location()[1..]);       Stat st = stat_file(path, id);    if (!st) {    SIMPLE_TRACE_LEAVE ("No such file or directory");    return 0;    }    +  mapping(string:mixed) ret = write_access(path, 1, id); +  if (ret) { +  SIMPLE_TRACE_LEAVE("Write access denied: %O", ret); +  return ret; +  } +     mapping(string:mixed) recurse (string path, Stat st)    {    // Note: Already got an extra TRACE_ENTER level on entry here.       if (st->isdir) {    // RFC 2518 8.6.2    // The DELETE operation on a collection MUST act as if a    // "Depth: infinity" header was used on it.    int fail;    if (!has_suffix(path, "/")) path += "/";    foreach(find_dir(path, id) || ({}), string fname) {    fname = path + fname;    if (Stat sub_stat = stat_file (fname, id)) {    SIMPLE_TRACE_ENTER (this, "Deleting %O", fname);    if (mapping(string:mixed) sub_res = recurse(fname, sub_stat)) {    // RFC 2518 8.6.2    // Additionally 204 (No Content) errors SHOULD NOT be returned    // in the 207 (Multi-Status). The reason for this prohibition    // is that 204 (No Content) is the default success code.    if (sizeof (sub_res) && sub_res->error != 204) { -  stat->add_status(fname, sub_res->error, sub_res->rettext); +  stat->add_status(fname, sub_res);    }    if (!sizeof (sub_res) || sub_res->error >= 300) fail = 1;    }    }    }    if (fail) {    SIMPLE_TRACE_LEAVE ("Partial failure");    return ([]);    }    }
Roxen.git/server/base_server/module.pike:1720:    //    // 412 (Precondition Failed) - /.../ the Overwrite header    // is "F" and the state of the destination resource is    // non-null.    //    // That clearly doesn't include this case. Also, common sense    // says that the error from the failed delete is more useful    // to the client.   #if 0    return Roxen.http_status(Protocols.HTTP.HTTP_PRECOND_FAILED); - #else + #elif 0    if (sizeof (res)) {    // RFC 2518 8.8.3:    // If an error in executing the COPY method occurs with a    // resource other than the resource identified in the    // Request-URI then the response MUST be a 207    // (Multi-Status).    //    // So if the failure was on the root destination resource we    // have to convert it to a multi-status.    result->add_status (destination, res->error, res->rettext);    }    return ([]); -  + #else +  // RFC 4918 9.8.5: +  // 423 (Locked) - The destination resource, or resource +  // within the destination collection, was locked. This +  // response SHOULD contain the 'lock-token-submitted' +  // precondition element. +  return res;   #endif    }    TRACE_LEAVE("Deletion ok.");    break;    case NEVER_OVERWRITE:    TRACE_LEAVE("Destination already exists.");    return Roxen.http_status(Protocols.HTTP.HTTP_PRECOND_FAILED);    case MAYBE_OVERWRITE:    // No overwrite header.    // Be nice, and fail only if we don't already have a collection.    if (st->isdir) {    TRACE_LEAVE("Destination exists and is a directory.");    return copy_properties(source, destination, behavior, id);    }    TRACE_LEAVE("Destination exists and is not a directory."); -  return Roxen.http_status(Protocols.HTTP.HTTP_PRECOND_FAILED); +  return Roxen.http_status(Protocols.HTTP.HTTP_CONFLICT);    }    }    // Create the new collection.    TRACE_LEAVE("Make a new collection.");    mapping(string:mixed) res = make_collection(destination, id);    if (res && res->error >= 300) return res; -  return copy_properties(source, destination, behavior, id) || res; +  res = copy_properties(source, destination, behavior, id) || res; +  if (res && st && (res->error == Protocols.HTTP.HTTP_CREATED)) { +  // RFC 4918 9.8.5: +  // 204 (No Content) - The source resource was successfully +  // copied to a preexisting destination resource. +  res->error = Protocols.HTTP.HTTP_NO_CONTENT;    } -  +  return res; + }      //! Used by the default @[recurse_copy_files] to copy a single file   //! along with its properties.   //!   //! @param source   //! Source path below the filesystem location.   //!   //! @param destination   //! Destination path below the filesystem location.   //!
Roxen.git/server/base_server/module.pike:1806:   //! Destination path below the filesystem location.   //!   //! @param behavior   //! Specifies how to copy properties. See the @[PropertyBehavior]   //! type for details.   //!   //! @param overwrite   //! Specifies how to handle the situation if the destination already   //! exists. See the @[Overwrite] type for details.   //! + //! @param one_level + //! Indicates whether recursion is to be inhibited. + //!   //! @returns   //! Returns a 2xx series status mapping on success (typically 201   //! Created if the destination didn't exist before, or 204 No   //! Content otherwise). Returns 0 if the source doesn't exist.   //! Returns an appropriate status mapping for any other error. That   //! includes an empty mapping in case there's a failure on some   //! subpart or at the destination, to signify a 207 Multi-Status   //! response using the info in @[id->get_multi_status()].   mapping(string:mixed) recurse_copy_files(string source, string destination,    PropertyBehavior behavior, -  Overwrite overwrite, RequestID id) +  Overwrite overwrite, RequestID id, +  int|void one_level)   {    SIMPLE_TRACE_ENTER(this, "Recursive copy from %O to %O (%s)",    source, destination,    overwrite == DO_OVERWRITE ? "replace" :    overwrite == NEVER_OVERWRITE ? "no overwrite" :    "overlay");    string src_tmp = has_suffix(source, "/")?source:(source+"/");    string dst_tmp = has_suffix(destination, "/")?destination:(destination+"/");    if ((src_tmp == dst_tmp) ||    has_prefix(src_tmp, dst_tmp) ||    has_prefix(dst_tmp, src_tmp)) {    TRACE_LEAVE("Source and destination overlap.");    return Roxen.http_status(403, "Source and destination overlap.");    }       string prefix = map(query_location()[1..]/"/", Roxen.http_encode_url)*"/";    MultiStatus.Prefixed result =    id->get_multi_status()->prefix (id->url_base() + prefix);    -  mapping(string:mixed) recurse(string source, string destination) { +  mapping(string:mixed) recurse(string source, string destination, +  int|void one_level) {    // Note: Already got an extra TRACE_ENTER level on entry here.       Stat st = stat_file(source, id);    if (!st) {    TRACE_LEAVE("Source not found.");    return 0;    } -  // FIXME: Check destination? +  // NB: No need to check the destination here, as it is done by +  // copy_collection() and copy_file().    if (st->isdir) {    mapping(string:mixed) res =    copy_collection(source, destination, behavior, overwrite, result, id);    if (res && (!sizeof (res) || res->error >= 300)) {    // RFC 2518 8.8.3 and 8.8.8 (error minimization).    TRACE_LEAVE("Copy of collection failed.");    return res;    } -  +  if (!one_level) {    foreach(find_dir(source, id), string filename) {    string subsrc = combine_path_unix(source, filename);    string subdst = combine_path_unix(destination, filename);    SIMPLE_TRACE_ENTER(this, "Copy from %O to %O\n", subsrc, subdst);    mapping(string:mixed) sub_res = recurse(subsrc, subdst);    if (sub_res && !(<0, 201, 204>)[sub_res->error]) {    result->add_status(subdst, sub_res->error, sub_res->rettext);    }    } -  +  }    TRACE_LEAVE("");    return res;    } else {    TRACE_LEAVE("");    return copy_file(source, destination, behavior, overwrite, id);    }    };       int start_ms_size = id->multi_status_size(); -  mapping(string:mixed) res = recurse (source, destination); +  mapping(string:mixed) res = recurse (source, destination, one_level);    if (res && res->error != 204 && res->error != 201)    return res;    else if (id->multi_status_size() != start_ms_size)    return ([]);    else    return res;   }      //! Used by the default @[recurse_move_files] to move a file (and not   //! a directory) from @[source] to @[destination].
Roxen.git/server/base_server/module.pike:1978:    Overwrite overwrite, RequestID id)   {    // Fall back to find_file().    RequestID tmp_id = id->clone_me();    tmp_id->not_query = query_location() + source;    tmp_id->misc["new-uri"] = query_location() + destination;    tmp_id->request_headers->destination =    id->url_base() + query_location()[1..] + destination;    tmp_id->method = "MOVE";    mapping(string:mixed) res = find_file(source, tmp_id); -  if (!res || res->error != 501) return res; +  if (!res || res->error != 501) { +  if (res && !sizeof(res)) { +  foreach(tmp_id->get_multi_status()->get_responses_by_prefix(""); +  string href; MultiStatusNode status) { +  id->set_status_for_url(href, status); +  } +  } +  return res; +  }    // Not implemented. Fall back to COPY + DELETE.    string prefix = map(query_location()[1..]/"/", Roxen.http_encode_url)*"/";    MultiStatus.Prefixed result =    id->get_multi_status()->prefix (id->url_base() + prefix);    res = copy_collection(source, destination, behavior, overwrite, result, id);    if (res && (res->error >= 300 || !sizeof(res))) {    // Copy failed.    return res;    }    int fail;