Roxen.git / server / base_server / module.pike

version» Context lines:

Roxen.git/server/base_server/module.pike:1:   // This file is part of Roxen WebServer. - // Copyright © 1996 - 2001, Roxen IS. - // $Id: module.pike,v 1.128 2001/08/23 23:34:41 mast Exp $ + // Copyright © 1996 - 2009, Roxen IS. + // $Id$      #include <module_constants.h>   #include <module.h>   #include <request_trace.h>      constant __pragma_save_parent__ = 1;    -  + // Tell Pike.count_memory this is global. + constant pike_cycle_depth = 0; +    inherit "basic_defvar";   mapping(string:array(int)) error_log=([]);      constant is_module = 1;   // constant module_type = MODULE_ZERO;   // constant module_name = "Unnamed module";   // constant module_doc = "Undocumented";   constant module_unique = 1;    -  + //! Specifies that the module is opaque when it comes WebDAV + //! requests. Normally, recursive WebDAV requests will iterate through + //! all matching location modules even after a successful result has + //! been returned by some module. With this flag set, iteration will + //! stop after the call to this module. Useful if the module wants to + //! handle all requests for the specified location itself with no + //! fallback to other modules. + constant webdav_opaque = 0;      private Configuration _my_configuration;   private string _module_local_identifier;   private string _module_identifier =    lambda() {    mixed init_info = roxen->bootstrap_info->get();    if (arrayp (init_info)) {    [_my_configuration, _module_local_identifier] = init_info;    return _my_configuration->name + "/" + _module_local_identifier;    } -  + #ifdef DEBUG +  else +  error ("Got invalid bootstrap info for module: %O\n", init_info); + #endif    }(); - static mapping _api_functions = ([]); + protected mapping _api_functions = ([]);      string|array(string) module_creator;   string module_url;   RXML.TagSet module_tag_set;    - /* These functions exists in here because otherwise the messages in -  * the event log does not always end up in the correct -  * module/configuration. And the reason for that is that if the -  * messages are logged from subclasses in the module, the DWIM in -  * roxenlib.pike cannot see that they are logged from a module. This -  * solution is not really all that beautiful, but it works. :-) + /* These functions exist in here because otherwise the messages in the +  * event log do not always end up in the correct module/configuration. +  * And the reason for that is that if the messages are logged from +  * subclasses in the module, the DWIM in roxenlib.pike cannot see that +  * they are logged from a module. This solution is not really all that +  * beautiful, but it works. :-)    */ - void report_fatal( mixed ... args ) { predef::report_fatal( @args ); } - void report_error( mixed ... args ) { predef::report_error( @args ); } - void report_notice( mixed ... args ) { predef::report_notice( @args ); } - void report_debug( mixed ... args ) { predef::report_debug( @args ); } + void report_fatal(sprintf_format fmt, sprintf_args ... args) +  { predef::report_fatal(fmt, @args); } + void report_error(sprintf_format fmt, sprintf_args ... args) +  { predef::report_error(fmt, @args); } + void report_warning(sprintf_format fmt, sprintf_args ... args) +  { predef::report_warning(fmt, @args); } + void report_notice(sprintf_format fmt, sprintf_args ... args) +  { predef::report_notice(fmt, @args); } + void report_debug(sprintf_format fmt, sprintf_args ... args) +  { predef::report_debug(fmt, @args); }    -  + void log_event (string facility, string action, string resource, +  void|mapping(string:mixed) info) + //! Log an event. See @[Configuration.log_event] for details. + //! + //! @[facility] may be zero. The local module identifier as returned + //! by @[module_local_id] is used as facility in that case. + { +  _my_configuration->log_event (facility || _module_local_identifier, +  action, resource, info); + }      string module_identifier()   //! Returns a string that uniquely identifies this module instance   //! within the server. The identifier is the same as   //! @[Roxen.get_module] and @[Roxen.get_modname] handles.   { - #if 1 +     return _module_identifier; - #else -  if (!_module_identifier) { -  string|mapping name = this_object()->register_module()[1]; -  if (mappingp (name)) name = name->standard; -  string cname = sprintf ("%O", my_configuration()); -  if (sscanf (cname, "Configuration(%s", cname) == 1 && -  sizeof (cname) && cname[-1] == ')') -  cname = cname[..sizeof (cname) - 2]; -  _module_identifier = sprintf ("%s,%s", -  name||this_object()->module_name, cname); +    } -  return _module_identifier; - #endif - } +       string module_local_id()   //! Returns a string that uniquely identifies this module instance   //! within the configuration. The returned string is the same as the   //! part after the first '/' in the one returned from   //! @[module_identifier].   {    return _module_local_identifier;   }      RoxenModule this_module()   {    return this_object(); // To be used from subclasses.   }    -  + //! @ignore + DECLARE_OBJ_COUNT; + //! @endignore +    string _sprintf()   { -  return sprintf ("RoxenModule(%s)", _module_identifier || "?"); +  return sprintf ("RoxenModule(%s)" + OBJ_COUNT, _module_identifier || "?");   }      array register_module()   {    return ({    this_object()->module_type,    this_object()->module_name,    this_object()->module_doc,    0,    module_unique, -  +  this_object()->module_locked, +  this_object()->module_counter,    });   }      string fix_cvs(string from)   {    from = replace(from, ({ "$", "Id: "," Exp $" }), ({"","",""}));    sscanf(from, "%*s,v %s", from);    return replace(from,"/","-");   }      int module_dependencies(Configuration configuration,    array (string) modules,    int|void now) - //! If your module depends on other modules present in the server, - //! calling <pi>module_dependencies()</pi>, supplying an array of - //! module identifiers. A module identifier is either the filename - //! minus extension, or a string on the form that Roxen.get_modname - //! returns. In the latter case, the <config name> and <copy> parts - //! are ignored. + //! If your module depends on other modules, call this function to + //! ensure that those modules get loaded. + //! + //! @param configuration + //! The configuration for the modules. Use zero for the same as this + //! module. + //! + //! @param modules + //! An array of module identifiers. A module identifier is either + //! the name of the pike file minus extension, or a string on the + //! form that @[Roxen.get_modname] returns. In the latter case, the + //! @tt{<config name>@} and @tt{<copy>@} parts are ignored. + //! + //! @param now + //! If this flag is nonzero then any modules that aren't already + //! loaded get loaded (and started) right away, so that the code + //! following the @[module_dependencies] call can start using them. + //! + //! Otherwise the function only ensures that the modules exist in + //! the configuration and they will be loaded sometime during the + //! startup of it. + //! + //! @note + //! This function is only intended to be called from @[start]. + //! + //! @note + //! The @[now] flag does not affect calls to + //! @[ready_to_receive_requests] in the listed modules. I.e. even if + //! you have declared a dependency on a module and have @[now] set, + //! you cannot assume its @[ready_to_receive_requests] function has + //! run (in fact, you can almost safely assume it hasn't). You can + //! however assume that its @[start] function has been called.   {    modules = map (modules,    lambda (string modname) {    sscanf ((modname / "/")[-1], "%[^#]", modname);    return modname;    });    Configuration conf = configuration || my_configuration();    if (!conf) -  report_warning ("Configuration not resolved; module(s) %s that %s " -  "depend on weren't added.", String.implode_nicely (modules), -  module_identifier()); +  report_warning ("Configuration not resolved; module(s) %s that %O " +  "depend on weren't added.\n", String.implode_nicely (modules), +  module_identifier() || +  master()->describe_program(this_program) || +  "unknown module");    else    conf->add_modules( modules, now );    return 1;   }      string file_name_and_stuff()   {    return ("<b>Loaded from:</b> "+(roxen->filename(this_object()))+"<br>"+    (this_object()->cvs_version? -  "<b>CVS Version: </b>"+ +  "<b>CVS Version:</b> "+    fix_cvs(this_object()->cvs_version)+"\n":""));   }         Configuration my_configuration()   //! Returns the Configuration object of the virtual server the module   //! belongs to.   {    return _my_configuration;   }    - nomask void set_configuration(Configuration c) + final void set_configuration(Configuration c)   {    if(_my_configuration && _my_configuration != c)    error("set_configuration() called twice.\n");    _my_configuration = c;   }      void set_module_creator(string|array(string) c)   //! Set the name and optionally email address of the author of the   //! module. Names on the format "author name <author_email>" will   //! end up as links on the module's information page in the admin
Roxen.git/server/base_server/module.pike:168:   //! A common way of referring to a location where you maintain   //! information about your module or similar. The URL will turn up   //! on the module's information page in the admin interface,   //! referred to as the module's home page.   {    module_url = to;   }      void free_some_sockets_please(){}    - void start(void|int num, void|Configuration conf) {} + // These functions have bodies to make module inheritance easier: An + // inheriting module can always assume that the module it inherits + // have these functions defined and properly proxy the calls to them. + // Thus an inherited module should be free to define them later on + // even if it didn't have them initially. (Modules need never proxy + // calls to the default implementations here.) + void start(int variable_save, Configuration conf, void|int newly_added) {} + void stop() {} + void ready_to_receive_requests (Configuration conf) {}      string status() {}      string info(Configuration conf)   {    return (this_object()->register_module()[2]);   }      string sname( )   {
Roxen.git/server/base_server/module.pike:200:   {    my_configuration()->save_one( this_object() );    my_configuration()->module_changed( my_moduleinfo(), this_object() );   }      void save() { save_me(); }   string comment() { return ""; }      string query_internal_location()   //! Returns the internal mountpoint, where <ref>find_internal()</ref> - //! is mounted. + //! is mounted. It always ends with a '/'.   {    if(!_my_configuration)    error("Please do not call this function from create()!\n");    return _my_configuration->query_internal_location(this_object());   }      string query_absolute_internal_location(RequestID id) - //! Returns the internal mountpoint as an absolute path. + //! Returns the internal mountpoint as an absolute path. It always + //! ends with a '/'.   {    return (id->misc->site_prefix_path || "") + query_internal_location();   }      string query_location()   //! Returns the mountpoint as an absolute path. The default   //! implementation uses the "location" configuration variable in the   //! module.   {    string s;
Roxen.git/server/base_server/module.pike:233:   array(string) location_urls()   //! Returns an array of all locations where the module is mounted.   {    string loc = query_location();    if (!loc) return ({});    if(!_my_configuration)    error("Please do not call this function from create()!\n");    array(string) urls = copy_value(_my_configuration->query("URLs"));    string hostname;    if (string world_url = _my_configuration->query ("MyWorldLocation")) -  sscanf (world_url, "%*s://%s%*[:/]", hostname); +  if (sizeof(world_url)) { +  Standards.URI uri = Standards.URI(world_url); +  hostname = uri->host; +  }    if (!hostname) hostname = gethostname();    for (int i = 0; i < sizeof (urls); i++) -  +  { +  urls[i] = (urls[i]/"#")[0];    if (sizeof (urls[i]/"*") == 2)    urls[i] = replace(urls[i], "*", hostname); -  +  }    return map (urls, `+, loc[1..]);   }    -  + string location_url() + //! Returns an http or https url including the modules mountpoint. The + //! ip-number for the corresponding port will be added to the fragment + //! of the url. An http url will be prioritized over an https url. + { +  string short_array(array a) +  { +  return "({ " + (map(a, lambda(object o) { +  return sprintf("%O", o); +  })*", ") + " })"; +  }; +  string loc = query_location(); +  if(!loc) return 0; +  if(!_my_configuration) +  error("Please do not call this function from create()!\n"); +  string hostname; +  string world_url = _my_configuration->query("MyWorldLocation"); +  if (world_url && sizeof(world_url)) { +  Standards.URI uri = Standards.URI(world_url); +  hostname = uri->host; +  } +  if(!hostname) +  hostname = gethostname(); + #ifdef LOCATION_URL_DEBUG +  werror(" Hostname: %O\n", hostname); + #endif +  Standards.URI candidate_uri; +  array(string) urls = +  filter(_my_configuration->registered_urls, has_prefix, "http:") + +  filter(_my_configuration->registered_urls, has_prefix, "https:"); +  foreach(urls, string url) +  { + #ifdef LOCATION_URL_DEBUG +  werror(" URL: %s\n", url); + #endif +  mapping url_info = roxen.urls[url]; +  if(!url_info || !url_info->port || url_info->conf != _my_configuration) +  continue; +  Protocol p = url_info->port; + #ifdef LOCATION_URL_DEBUG +  werror(" Protocol: %s\n", p); + #endif +  Standards.URI uri = Standards.URI(url); +  string ip = p->ip || "127.0.0.1"; +  if (ip == "::") +  ip = "::1"; +  uri->fragment = "ip=" + ip; +  if(has_value(uri->host, "*") || has_value(uri->host, "?")) +  if(glob(uri->host, hostname)) +  uri->host = hostname; +  else { +  if(!candidate_uri) { +  candidate_uri = uri; +  candidate_uri->host = hostname; +  } +  continue; +  } +  return (string)uri + loc[1..]; +  } +  if(candidate_uri) { +  report_warning("Warning: Could not find any suitable ports, continuing anyway. " +  "Please make sure that your Primary Server URL matches " +  "at least one port. Primary Server URL: %O, URLs: %s.\n", +  world_url, short_array(urls)); +  return (string)candidate_uri + loc[1..]; +  } +  return 0; + } +    /* By default, provide nothing. */ - string query_provides() { return 0; } + multiset(string) query_provides() { return 0; }         function(RequestID:int|mapping) query_seclevels()   {    if(catch(query("_seclevels")) || (query("_seclevels") == 0))    return 0;    return roxen.compile_security_pattern(query("_seclevels"),this_object());   }    -  + void set_status_for_path (string path, RequestID id, int status_code, +  string|void message, mixed... args) + //! Register a status to be included in the response that applies only + //! for the given path. This is used for recursive operations that can + //! yield different results for different encountered files or + //! directories. + //! + //! The status is stored in the @[MultiStatus] object returned by + //! @[id->get_multi_status]. The server will use it to make a 207 + //! Multi-Status response iff the module returns an empty mapping as + //! response. + //! + //! @param path + //! Path (below the filesystem location) to which the status applies. + //! + //! @param status_code + //! The HTTP status code. + //! + //! @param message + //! If given, it's a message to include in the response. The + //! message may contain line feeds ('\n') and ISO-8859-1 + //! characters in the ranges 32..126 and 128..255. Line feeds are + //! converted to spaces if the response format doesn't allow them. + //! + //! @param args + //! If there are more arguments after @[message] then @[message] + //! is taken as an @[sprintf] style format string which is used to + //! format @[args]. + //! + //! @note + //! This function is just a wrapper for @[id->set_status_for_path] + //! that corrects for the filesystem location. + //! + //! @seealso + //! @[RequestID.set_status_for_path], @[Roxen.http_status] + { +  if (sizeof (args)) message = sprintf (message, @args); +  id->set_status_for_path (query_location() + path, status_code, message); + } +    Stat stat_file(string f, RequestID id){}   array(string) find_dir(string f, RequestID id){}   mapping(string:Stat) find_dir_stat(string f, RequestID id)   { -  TRACE_ENTER("find_dir_stat(): \""+f+"\"", 0); +  SIMPLE_TRACE_ENTER(this, "find_dir_stat(): %O", f);       array(string) files = find_dir(f, id);    mapping(string:Stat) res = ([]);       foreach(files || ({}), string fname) { -  TRACE_ENTER("stat()'ing "+ f + "/" + fname, 0); +  SIMPLE_TRACE_ENTER(this, "stat()'ing %O", f + "/" + fname);    Stat st = stat_file(replace(f + "/" + fname, "//", "/"), id);    if (st) {    res[fname] = st;    TRACE_LEAVE("OK");    } else {    TRACE_LEAVE("No stat info");    }    }       TRACE_LEAVE("");    return(res);   }    -  + class DefaultPropertySet + { +  inherit PropertySet; +  +  protected Stat stat; +  +  protected void create (string path, string abs_path, RequestID id, Stat stat) +  { +  ::create (path, abs_path, id); +  this_program::stat = stat; +  } +  +  Stat get_stat() {return stat;} +  +  protected mapping(string:string) response_headers; +  +  mapping(string:string) get_response_headers() +  { +  if (!response_headers) { +  // Old kludge inherited from configuration.try_get_file. +  if (!id->misc->common) +  id->misc->common = ([]); +  +  RequestID sub_id = id->clone_me(); +  sub_id->misc->common = id->misc->common; +  +  sub_id->not_query = query_location() + path; +  sub_id->raw_url = replace (id->raw_url, id->not_query, sub_id->not_query); +  sub_id->method = "HEAD"; +  +  mapping(string:mixed)|int(-1..0)|object res = find_file (path, sub_id); +  if (res == -1) res = ([]); +  else if (objectp (res)) { +  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]); +  } +  response_headers = sub_id->make_response_headers (res); +  destruct (sub_id); +  } +  +  return response_headers; +  } + } +  + //! Return the set of properties for @[path]. + //! + //! @returns + //! Returns @tt{0@} (zero) if @[path] does not exist. + //! + //! Returns an error mapping if there's some other error accessing + //! the properties. + //! + //! Otherwise returns a @[PropertySet] object. + //! + //! @seealso + //! @[query_property()] + PropertySet|mapping(string:mixed) query_property_set(string path, RequestID id) + { +  SIMPLE_TRACE_ENTER (this, "Querying properties on %O", path); +  Stat st = stat_file(path, id); +  +  if (!st) { +  SIMPLE_TRACE_LEAVE ("No such file or dir"); +  return 0; +  } +  +  PropertySet res = DefaultPropertySet(path, query_location()+path, id, st); +  SIMPLE_TRACE_LEAVE (""); +  return res; + } +  + //! Returns the value of the specified property, or an error code + //! mapping. + //! + //! @note + //! Returning a string is shorthand for returning an array + //! with a single text node. + //! + //! @seealso + //! @[query_property_set()] + string|array(Parser.XML.Tree.SimpleNode)|mapping(string:mixed) +  query_property(string path, string prop_name, RequestID id) + { +  mapping(string:mixed)|PropertySet properties = query_property_set(path, id); +  if (!properties) { +  return Roxen.http_status(Protocols.HTTP.HTTP_NOT_FOUND, +  "No such file or directory."); +  } +  if (mappingp (properties)) +  return properties; +  return properties->query_property(prop_name) || +  Roxen.http_status(Protocols.HTTP.HTTP_NOT_FOUND, "No such property."); + } +  + //! RFC 2518 PROPFIND implementation with recursion according to + //! @[depth]. See @[PropertySet()->find_properties()] for details. + //! + //! @seealso + //! @[query_property_set()] + mapping(string:mixed) recurse_find_properties(string path, string mode, +  int depth, RequestID id, +  multiset(string)|void filt) + { +  MultiStatus.Prefixed result = +  id->get_multi_status()->prefix (id->url_base() + query_location()[1..]); +  +  mapping(string:mixed) recurse (string path, int depth) { +  SIMPLE_TRACE_ENTER (this, "%s for %O, depth %d", +  mode == "DAV:propname" ? "Listing property names" : +  mode == "DAV:allprop" ? "Retrieving all properties" : +  mode == "DAV:prop" ? "Retrieving specific properties" : +  "Finding properties with mode " + mode, +  path, depth); +  mapping(string:mixed)|PropertySet properties = query_property_set(path, id); +  +  if (!properties) { +  SIMPLE_TRACE_LEAVE ("No such file or dir"); +  return 0; +  } +  +  { +  mapping(string:mixed) ret = mappingp (properties) ? +  properties : properties->find_properties(mode, result, filt); +  +  if (ret) { +  SIMPLE_TRACE_LEAVE ("Got status %d: %O", ret->error, ret->rettext); +  return ret; +  } +  } +  +  if (properties->get_stat()->isdir) { +  if (depth <= 0) { +  SIMPLE_TRACE_LEAVE ("Not recursing due to depth limit"); +  return ([]); +  } +  depth--; +  foreach(find_dir(path, id) || ({}), string filename) { +  filename = combine_path_unix(path, filename); +  if (mapping(string:mixed) sub_res = recurse(filename, depth)) +  if (sizeof (sub_res)) +  result->add_status (filename, sub_res->error, sub_res->rettext); +  } +  } +  +  SIMPLE_TRACE_LEAVE (""); +  return ([]); +  }; +  +  return recurse (path, depth); + } +  + mapping(string:mixed) patch_properties(string path, +  array(PatchPropertyCommand) instructions, +  RequestID id) + { +  SIMPLE_TRACE_ENTER (this, "Patching properties for %O", path); +  mapping(string:mixed)|PropertySet properties = query_property_set(path, id); +  +  if (!properties) { +  SIMPLE_TRACE_LEAVE ("No such file or dir"); +  return 0; +  } +  if (mappingp (properties)) { +  SIMPLE_TRACE_LEAVE ("Got error %d from query_property_set: %O", +  properties->error, properties->rettext); +  return properties; +  } +  +  mapping(string:mixed) errcode; +  +  if (errcode = write_access(path, 0, id)) { +  SIMPLE_TRACE_LEAVE("Patching denied by write_access()."); +  return errcode; +  } +  +  if (errcode = properties->start()) { +  SIMPLE_TRACE_LEAVE ("Got error %d from PropertySet.start: %O", +  errcode->error, errcode->rettext); +  return errcode; +  } +  +  array(mapping(string:mixed)) results; +  +  mixed err = catch { +  results = instructions->execute(properties); +  }; +  if (err) { +  properties->unroll(); +  throw (err); +  } else { +  MultiStatus.Prefixed result = +  id->get_multi_status()->prefix (id->url_base() + query_location()[1..] + path); +  int any_failed; +  foreach(results, mapping(string:mixed) answer) { +  if (any_failed = (answer && (answer->error >= 300))) { +  break; +  } +  } +  if (any_failed) { +  // Unroll and fail any succeeded items. +  int i; +  mapping(string:mixed) answer = +  Roxen.http_status (Protocols.HTTP.DAV_FAILED_DEP); +  for(i = 0; i < sizeof(results); i++) { +  if (!results[i] || results[i]->error < 300) { +  result->add_property("", instructions[i]->property_name, +  answer); +  } else { +  result->add_property("", instructions[i]->property_name, +  results[i]); +  } +  } +  properties->unroll(); +  } else { +  int i; +  for(i = 0; i < sizeof(results); i++) { +  result->add_property("", instructions[i]->property_name, +  results[i]); +  } +  properties->commit(); +  } +  } +  +  SIMPLE_TRACE_LEAVE (""); +  return 0; + } +  + //! Convenience variant of @[patch_properties()] that sets a single + //! property. + //! + //! @returns + //! Returns a mapping on any error, zero otherwise. + mapping(string:mixed) set_property (string path, string prop_name, +  string|array(Parser.XML.Tree.SimpleNode) value, +  RequestID id) + { +  mapping(string:mixed)|PropertySet properties = query_property_set(path, id); +  if (!properties) return Roxen.http_status(Protocols.HTTP.HTTP_NOT_FOUND, +  "File not found."); +  if (mappingp (properties)) return properties; +  +  mapping(string:mixed) result = properties->start(); +  if (result) return result; +  +  result = properties->set_property(prop_name, value); +  if (result && result->error >= 300) { +  properties->unroll(); +  return result; +  } +  +  properties->commit(); +  return 0; + } +  + //! Convenience variant of @[patch_properties()] that removes a single + //! property. + //! + //! @returns + //! Returns a mapping on any error, zero otherwise. + mapping(string:mixed) remove_property (string path, string prop_name, +  RequestID id) + { +  mapping(string:mixed)|PropertySet properties = query_property_set(path, id); +  if (!properties) return Roxen.http_status(Protocols.HTTP.HTTP_NOT_FOUND, +  "File not found."); +  if (mappingp (properties)) return properties; +  +  mapping(string:mixed) result = properties->start(); +  if (result) return result; +  +  result = properties->remove_property(prop_name); +  if (result && result->error >= 300) { +  properties->unroll(); +  return result; +  } +  +  properties->commit(); +  return 0; + } +  + string resource_id (string path, RequestID|int(0..0) id) + //! Return a string that within the filesystem uniquely identifies the + //! resource on @[path] in the given request. This is commonly @[path] + //! itself but can be extended with e.g. language, user or some form + //! variable if the path is mapped to different files according to + //! those fields. + //! + //! The important criteria here is that every unique returned string + //! corresponds to a resource that can be changed independently of + //! every other. Thus e.g. dynamic pages that evaluate to different + //! results depending on variables or cookies etc should _not_ be + //! mapped to more than one string by this function. It also means + //! that if files are stored in a filesystem which is case insensitive + //! then this function should normalize case differences. + //! + //! This function is used e.g by the default lock implementation to + //! convert paths to resources that can be locked independently of + //! each other. There's also a notion of recursive locks there, which + //! means that a recursive lock on a certain resource identifier also + //! locks every resource whose identifier it is a prefix of. Therefore + //! it's typically necessary to ensure that every identifier ends with + //! "/" so that a recursive lock on e.g. "doc/foo" doesn't lock + //! "doc/foobar". + //! + //! @param path + //! The requested path below the filesystem location. It has been + //! normalized with @[VFS.normalize_path]. + //! + //! @param id + //! The request id may have the value @expr{0@} (zero) if called + //! by @[Configuration()->expire_locks()]. + { +  return has_suffix (path, "/") ? path : path + "/"; + } +  + string|int authenticated_user_id (string path, RequestID id) + //! Return a value that uniquely identifies the user that the given + //! request is authenticated as. + //! + //! This function is e.g. used by the default lock implementation to + //! tell different users holding locks apart. WARNING: Due to some + //! design issues in the lock system, it's likely that it will change + //! to not use this function in the future. + //! + //! @param path + //! The requested path below the filesystem location. It has been + //! normalized with @[VFS.normalize_path]. + { +  // Leave this to the standard auth system by default. +  User uid = my_configuration()->authenticate (id); +  return uid && uid->name(); + } +  + // Mapping from resource id to a mapping from user id to the lock + // that apply to the resource. + // + // Only used internally by the default lock implementation. + protected mapping(string:mapping(mixed:DAVLock)) file_locks = ([]); +  + // Mapping from resource id to a mapping from user id to the lock + // that apply recursively to the resource and all other resources + // it's a prefix of. + // + // Only used internally by the default lock implementation. + protected mapping(string:mapping(mixed:DAVLock)) prefix_locks = ([]); +  + #define LOOP_OVER_BOTH(PATH, LOCKS, CODE) \ +  do { \ +  foreach (file_locks; PATH; LOCKS) {CODE;} \ +  foreach (prefix_locks; PATH; LOCKS) {CODE;} \ +  } while (0) +  + //! Find some or all locks that apply to @[path]. + //! + //! @param path + //! Normalized path below the filesystem location. + //! + //! @param recursive + //! If @expr{1@} also return locks anywhere below @[path]. + //! + //! @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 + //! 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, +  int(0..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 = (<>); +  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; +  }; +  } +  else +  add_locks = lambda (mapping(mixed:DAVLock) sub_locks) { +  locks |= mkmultiset (values (sub_locks)); +  }; +  +  if (file_locks[rsc]) { +  add_locks (file_locks[rsc]); +  } +  +  foreach(prefix_locks; +  string prefix; mapping(mixed:DAVLock) sub_locks) { +  if (has_prefix(rsc, prefix)) { +  add_locks (sub_locks); +  break; +  } +  } +  +  if (recursive) { +  LOOP_OVER_BOTH (string prefix, mapping(mixed:DAVLock) sub_locks, { +  if (has_prefix(prefix, rsc)) { +  add_locks (sub_locks); +  } +  }); +  } +  +  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. + //! + //! WARNING: This function has some design issues and will very likely + //! get a different interface. Compatibility is NOT guaranteed. + //! + //! @param path + //! Normalized path below the filesystem location. + //! + //! @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) + { +  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; +  } +  +  mixed auth_user = authenticated_user_id (path, id); +  path = resource_id (path, id); +  +  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; +  } +  +  LockFlag shared; +  +  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; +  } +  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; +  } +  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; +  } +  +  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"@}. + //! + //! 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. + //! + //! @note + //! The default implementation only handles the @expr{"DAV:write"@} + //! lock type. It uses @[resource_id] to map paths to unique resources + //! and @[authenticated_user_id] to tell users apart. + protected void register_lock(string path, DAVLock lock, RequestID id) + { +  TRACE_ENTER(sprintf("register_lock(%O, lock(%O), X).", path, lock->locktoken), +  this); +  ASSERT_IF_DEBUG (lock->locktype == "DAV:write"); +  mixed auth_user = authenticated_user_id (path, id); +  path = resource_id (path, id); +  if (lock->recursive) { +  if (prefix_locks[path]) { +  prefix_locks[path][auth_user] = lock; +  } else { +  prefix_locks[path] = ([ auth_user:lock ]); +  } +  } else { +  if (file_locks[path]) { +  file_locks[path][auth_user] = lock; +  } else { +  file_locks[path] = ([ auth_user:lock ]); +  } +  } +  TRACE_LEAVE("Ok."); + } +  + //! Unregister @[lock] that currently is locking the resource at + //! @[path]. It's assumed that the lock is registered for exactly that + //! path. + //! + //! This function is only provided as a helper to call from + //! @[unlock_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 unregister. (It must not be changed or destructed.) + //! + //! @param id + //! The request id may have the value @expr{0@} (zero) if called + //! by @[Configuration()->expire_locks()]. + protected void unregister_lock (string path, DAVLock lock, +  RequestID|int(0..0) id) + { +  TRACE_ENTER(sprintf("unregister_lock(%O, lock(%O), X).", path, lock->locktoken), +  this); +  mixed auth_user = id && authenticated_user_id (path, id); +  path = resource_id (path, id); +  DAVLock removed_lock; +  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); +  } +  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); +  } +  } +  } +  if (!sizeof (file_locks[path])) m_delete (file_locks, path); +  } +  ASSERT_IF_DEBUG (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"@}. + //! + //! 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 + //! locked, provided the directory it would be in exists (relaxed in + //! RFC 2518Bis (working draft)). The default implementation fulfills + //! these criteria. + //! + //! It's up to @[find_file] et al to actually check that the necessary + //! locks are held. It can preferably use @[write_access] for that, + //! which has a default implementation for checking + //! @expr{"DAV:write"@} locks. + //! + //! @param path + //! Normalized path (below the filesystem location) that the lock + //! applies to. + //! + //! @param lock + //! The lock to register. + //! + //! @returns + //! Returns @expr{0@} if the lock is successfully installed or if + //! locking isn't used. Returns a status mapping if an error + //! occurred. + //! + //! @note + //! To use the default lock implementation, call @[register_lock] + //! from this function. + mapping(string:mixed) lock_file(string path, +  DAVLock lock, +  RequestID id) + { +  return 0; + } +  + //! Remove @[lock] that currently is locking the resource at @[path]. + //! It's assumed that the lock is registered for exactly that path. + //! + //! @param path + //! Normalized path (below the filesystem location) that the lock + //! applies to. + //! + //! @param lock + //! The lock to unregister. (It must not be changed or destructed.) + //! + //! @param id + //! @mixed + //! @type RequestID + //! The request that attempted to unlock the lock. The function + //! should do the normal access checks before unlocking the lock + //! in this case. + //! + //! @type int(0..0) + //! The lock is unlocked internally by the server (typically due + //! to a timeout) and should be carried out without any access + //! checks. The function must succeed and return zero in this + //! case. + //! @endmixed + //! + //! @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; +  } +  +  TRACE_LEAVE("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. + //! + //! 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. + 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); + } +  + 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. + //! + //! @note + //! The default implementation falls back to @[find_file()]. + protected mapping(string:mixed) delete_file(string path, RequestID id) + { +  // Fall back to find_file(). +  RequestID tmp_id = id->clone_me(); +  tmp_id->not_query = query_location() + path; +  tmp_id->method = "DELETE"; +  // FIXME: Logging? +  return find_file(path, tmp_id) || +  tmp_id->misc->error_code && Roxen.http_status (tmp_id->misc->error_code); + } +  + //! Delete @[path] recursively. + //! + //! The default implementation handles the recursion and calls + //! @[delete_file] for each file and 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. That includes an + //! empty mapping in case some subparts couldn't be deleted, to + //! signify a 207 Multi-Status response using the info in + //! @[id->get_multi_status()]. + mapping(string:mixed) recurse_delete_files(string path, +  RequestID id, +  void|MultiStatus.Prefixed stat) + { +  SIMPLE_TRACE_ENTER (this, "Deleting %O recursively", path); +  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) 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); +  } +  if (!sizeof (sub_res) || sub_res->error >= 300) fail = 1; +  } +  } +  } +  if (fail) { +  SIMPLE_TRACE_LEAVE ("Partial failure"); +  return ([]); +  } +  } +  +  SIMPLE_TRACE_LEAVE (""); +  return delete_file (path, id); +  }; +  +  return recurse(path, st) || Roxen.http_status(204); + } +  + //! Make a new collection (aka directory) at @[path]. + //! + //! @returns + //! Returns a 2xx series status on success (typically 201 Created). + //! Returns @expr{0@} (zero) if there's no directory to create the + //! new one in. Returns other result mappings on failure. + mapping(string:mixed) make_collection(string path, RequestID id) + { +  // Fall back to find_file(). +  RequestID tmp_id = id->clone_me(); +  tmp_id->not_query = query_location() + path; +  tmp_id->method = "MKCOL"; +  // FIXME: Logging? +  return find_file(path, tmp_id); + } +  + //! Used by the default @[copy_collection] implementation to copy all + //! properties at @[source] to @[destination]. + //! + //! @param source + //! Source path below the filesystem location. + //! + //! @param destination + //! Destination path below the filesystem location. + //! + //! @param behavior + //! Specifies how to copy properties. See the @[PropertyBehavior] + //! type for details. + //! + //! @returns + //! @expr{0@} (zero) on success or an appropriate status mapping for + //! any error. + protected mapping(string:mixed) copy_properties( +  string source, string destination, PropertyBehavior behavior, RequestID id) + { +  SIMPLE_TRACE_ENTER(this, "copy_properties(%O, %O, %O, %O)", +  source, destination, behavior, id); +  PropertySet source_properties = query_property_set(source, id); +  PropertySet destination_properties = query_property_set(destination, id); +  +  multiset(string) property_set = source_properties->query_all_properties(); +  mapping(string:mixed) res; +  foreach(property_set; string property_name;) { +  SIMPLE_TRACE_ENTER(this, "Copying the property %O.", property_name); +  string|array(Parser.XML.Tree.SimpleNode)|mapping(string:mixed) source_val = +  source_properties->query_property(property_name); +  if (mappingp(source_val)) { +  TRACE_LEAVE("Reading of property failed. Skipped."); +  continue; +  } +  string|array(Parser.XML.Tree.SimpleNode)|mapping(string:mixed) dest_val = +  destination_properties->query_property(property_name); +  if (dest_val == source_val) { +  TRACE_LEAVE("Destination already has the correct value."); +  continue; +  } +  mapping(string:mixed) subres = +  destination_properties->set_property(property_name, source_val); +  if (!behavior) { +  TRACE_LEAVE("Omit verify."); +  continue; +  } +  if ((intp(behavior) || behavior[property_name]) && (subres->error < 300)) { +  // FIXME: Check that if the property was live in source, +  // it is still live in destination. +  // This is likely already so, since we're in the same module. +  } +  if ((subres->error < 300) || +  (subres->error == Protocols.HTTP.HTTP_CONFLICT)) { +  // Ok, or read-only property. +  TRACE_LEAVE("Copy ok or read-only property."); +  continue; +  } +  if (!subres) { +  // Copy failed, but attempt to copy the rest. +  res = Roxen.http_status(Protocols.HTTP.HTTP_PRECOND_FAILED); +  } +  TRACE_LEAVE("Copy failed."); +  } +  TRACE_LEAVE(res?"Failed.":"Ok."); +  return res; + } +  + //! Used by the default @[recurse_copy_files] and @[move_collection] + //! to copy a collection (aka directory) and its properties but not + //! its contents. + //! + //! @param source + //! Source path below the filesystem location. + //! + //! @param destination + //! 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 result + //! A @[MultiStatus.Prefixed] to collect status mappings if some + //! subparts fail. It's prefixed with the URL to the filesystem + //! location. + //! + //! @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()]. + protected mapping(string:mixed) copy_collection( +  string source, string destination, PropertyBehavior behavior, +  Overwrite overwrite, MultiStatus.Prefixed result, RequestID id) + { +  SIMPLE_TRACE_ENTER(this, "copy_collection(%O, %O, %O, %O, %O, %O).", +  source, destination, behavior, overwrite, result, id); +  Stat st = stat_file(destination, id); +  if (st) { +  // Destination exists. Check the overwrite header. +  switch(overwrite) { +  case DO_OVERWRITE: +  // RFC 2518 8.8.4 +  // If a resource exists at the destination, and the Overwrite +  // header is "T" then prior to performing the copy the server +  // MUST perform a DELETE with "Depth: infinity" on the +  // destination resource. +  TRACE_ENTER("Destination exists and overwrite is on.", this); +  mapping(string:mixed) res = recurse_delete_files(destination, id, result); +  if (res && (!sizeof (res) || res->error >= 300)) { +  // Failed to delete something. +  TRACE_LEAVE("Deletion failed."); +  TRACE_LEAVE("Copy collection failed."); +  // RFC 2518 9.6 says: +  // +  // If a COPY or MOVE is not performed due to the value of +  // the Overwrite header, the method MUST fail with a 412 +  // (Precondition Failed) status code. +  // +  // That can perhaps be interpreted as that we should return +  // 412 here. But otoh, in RFC 2518 8.8.5 COPY status codes: +  // +  // 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 +  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 ([]); + #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); +  } +  } +  // 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; + } +  + //! 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. + //! + //! @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. + //! + //! @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. + protected mapping(string:mixed) copy_file(string source, string destination, +  PropertyBehavior behavior, +  Overwrite overwrite, RequestID id) + { +  SIMPLE_TRACE_ENTER(this, "copy_file(%O, %O, %O, %O, %O)\n", +  source, destination, behavior, overwrite, id); +  TRACE_LEAVE("Not implemented."); +  return Roxen.http_status (Protocols.HTTP.HTTP_NOT_IMPL); + } +  + //! Copy a resource recursively from @[source] to @[destination]. + //! + //! @param source + //! Source path below the filesystem location. + //! + //! @param destination + //! 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. + //! + //! @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) + { +  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 loc = query_location(); +  MultiStatus.Prefixed result = +  id->get_multi_status()->prefix (id->url_base() + loc[1..]); +  +  mapping(string:mixed) recurse(string source, string destination) { +  // 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? +  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; +  } +  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); +  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]. + //! + //! The default implementation tries to call @[find_file] with the + //! MOVE method. If that returns 501 Not Implemented then it copies + //! the file and deletes the source afterwards. + //! + //! @param source + //! Source path below the filesystem location. + //! + //! @param destination + //! Destination path below the filesystem location. + //! + //! @param behavior + //! Specifies how to move 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. + //! + //! @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()]. + protected mapping(string:mixed) move_file(string source, string destination, +  PropertyBehavior behavior, +  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; +  // Not implemented. Fall back to COPY + DELETE. +  res = copy_file(source, destination, behavior, overwrite, id); +  if (res && res->error >= 300) { +  // Copy failed. +  return res; +  } +  return delete_file(source, id); + } +  + //! Used by the default @[recurse_move_files] to move a collection + //! (aka directory) and all its content from @[source] to + //! @[destination]. + //! + //! The default implementation tries to call @[find_file] with the + //! MOVE method. If that returns 501 Not Implemented then it copies + //! the collection, moves each directory entry recursively, and + //! deletes the source collection afterwards. + //! + //! @param source + //! Source path below the filesystem location. + //! + //! @param destination + //! Destination path below the filesystem location. + //! + //! @param behavior + //! Specifies how to move 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. + //! + //! @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()]. + //! + //! @note + //! The function must be prepared to recurse to check DAV locks + //! properly. + protected mapping(string:mixed) move_collection( +  string source, string destination, PropertyBehavior behavior, +  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; +  // Not implemented. Fall back to COPY + DELETE. +  MultiStatus.Prefixed result = +  id->get_multi_status()->prefix (id->url_base() + query_location()[1..]); +  res = copy_collection(source, destination, behavior, overwrite, result, id); +  if (res && (res->error >= 300 || !sizeof(res))) { +  // Copy failed. +  return res; +  } +  int fail; +  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, "Recursive move from %O to %O\n", +  subsrc, subdst); +  if (mapping(string:mixed) sub_res = +  recurse_move_files(subsrc, subdst, behavior, overwrite, id)) { +  if (!(<0, 201, 204>)[sub_res->error]) { +  result->add_status(subdst, sub_res->error, sub_res->rettext); +  } +  if (!sizeof (sub_res) || sub_res->error >= 300) { +  // Failed to move some content. +  fail = 1; +  } +  } +  } +  if (fail) return ([]); +  return delete_file(source, id); + } +  + //! Move a resource from @[source] to @[destination]. + //! + //! @param source + //! Source path below the filesystem location. + //! + //! @param destination + //! Destination path below the filesystem location. + //! + //! @param behavior + //! Specifies how to move 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. + //! + //! @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_move_files(string source, string destination, +  PropertyBehavior behavior, +  Overwrite overwrite, RequestID id) + { +  Stat st = stat_file(source, id); +  if (!st) return 0; +  +  if (st->isdir) { +  return move_collection(source, destination, behavior, overwrite, id); +  } +  return move_file(source, destination, behavior, overwrite, id); + } +    string real_file(string f, RequestID id){}      void add_api_function( string name, function f, void|array(string) types)   {    _api_functions[name] = ({ f, types });   }      mapping api_functions()   {    return _api_functions;
Roxen.git/server/base_server/module.pike:338:    RXML.FLAG_PROC_INSTR, this_object()[q]});    return m;   }      RXML.TagSet query_tag_set()   {    if (!module_tag_set) {    array(function|program|object) tags =    filter (rows (this_object(),    glob ("Tag*", indices (this_object()))), -  functionp); +  lambda(mixed x) { return functionp(x)||programp(x); });    for (int i = 0; i < sizeof (tags); i++)    if (programp (tags[i]))    if (!tags[i]->is_RXML_Tag) tags[i] = 0;    else tags[i] = tags[i]();    else {    tags[i] = tags[i]();    // Bogosity: The check is really a little too late here..    if (!tags[i]->is_RXML_Tag) tags[i] = 0;    }    tags -= ({0});    module_tag_set =    (this_object()->ModuleTagSet || RXML.TagSet) (this_object(), "", tags);    }    return module_tag_set;   }      mixed get_value_from_file(string path, string index, void|string pre)   {    Stdio.File file=Stdio.File();    if(!file->open(path,"r")) return 0; -  if(index[sizeof(index)-2..sizeof(index)-1]=="()") { -  return compile_string((pre||"")+file->read())[index[..sizeof(index)-3]](); +  if(has_suffix(index, "()")) +  index = index[..sizeof(index) - 3]; +  +  // Pass path to original file so that include statements for local files +  // work correctly. +  return compile_string((pre || "") + file->read(), path)[index];   } -  return compile_string((pre||"")+file->read())[index]; - } +     - static private mapping __my_tables = ([]); + private mapping __my_tables = ([]);      array(mapping(string:mixed)) sql_query( string query, mixed ... args )   //! Do a SQL-query using @[get_my_sql], the table names in the query   //! should be written as &table; instead of table. As an example, if   //! the tables 'meta' and 'data' have been created with create_tables   //! or get_my_table, this query will work:   //!   //! SELECT &meta;.id AS id, &data;.data as DATA   //! FROM &data;, &meta; WHERE &my.meta;.xsize=200   //!   {    return get_my_sql()->query( replace( query, __my_tables ), @args );   }      object sql_big_query( string query, mixed ... args ) - //! Identical to @[sql_query], but the @[Sql.sql()->big_query] method - //! will be used instead of the @[Sql.sql()->query] method. + //! Identical to @[sql_query], but the @[Sql.Sql()->big_query] method + //! will be used instead of the @[Sql.Sql()->query] method.   {    return get_my_sql()->big_query( replace( query, __my_tables ), @args );   }      array(mapping(string:mixed)) sql_query_ro( string query, mixed ... args )   //! Do a read-only SQL-query using @[get_my_sql], the table names in the query   //! should be written as &table; instead of table. As an example, if   //! the tables 'meta' and 'data' have been created with create_tables   //! or get_my_table, this query will work:   //!   //! SELECT &meta;.id AS id, &data;.data as DATA   //! FROM &data;, &meta; WHERE &my.meta;.xsize=200   //!   {    return get_my_sql(1)->query( replace( query, __my_tables ), @args );   }      object sql_big_query_ro( string query, mixed ... args ) - //! Identical to @[sql_query_ro], but the @[Sql.sql()->big_query] method - //! will be used instead of the @[Sql.sql()->query] method. + //! Identical to @[sql_query_ro], but the @[Sql.Sql()->big_query] method + //! will be used instead of the @[Sql.Sql()->query] method.   {    return get_my_sql(1)->big_query( replace( query, __my_tables ), @args );   }    - static int create_sql_tables( mapping(string:array(string)) defenitions, -  string|void comment, -  int|void no_unique_names ) + protected int create_sql_tables( mapping(string:array(string)) definitions, +  string|void comment, int|void no_unique_names )   //! Create multiple tables in one go. See @[get_my_table]   //! Returns the number of tables that were actually created.   {    int ddc;    if( !no_unique_names ) -  foreach( indices( defenitions ), string t ) -  ddc+=get_my_table( t, defenitions[t], comment, 1 ); +  foreach( indices( definitions ), string t ) +  ddc+=get_my_table( t, definitions[t], comment, 1 );    else    {    Sql.Sql sql = get_my_sql(); -  foreach( indices( defenitions ), string t ) +  foreach( indices( definitions ), string t )    {    if( !catch { -  sql->query("CREATE TABLE "+t+" ("+defenitions[t]*","+")" ); +  sql->query("CREATE TABLE "+t+" ("+definitions[t]*","+")" );    } )    ddc++;    DBManager.is_module_table( this_object(), my_db, t, comment );    }    }    return ddc;   }    - static string sql_table_exists( string name ) + protected string sql_table_exists( string name )   //! Return the real name of the table 'name' if it exists.   {    if(strlen(name))    name = "_"+name;       string res = hash(_my_configuration->name)->digits(36) -  + "_" + replace(sname(),"#","_") + name; +  + "_" + replace(sname(), ({ "#","-" }), ({ "_","_" })) + name;       return catch(get_my_sql()->query( "SELECT * FROM "+res+" LIMIT 1" ))?0:res;   }       - static string|int get_my_table( string|array(string) name, -  void|array(string)|string defenition, -  string|void comment, -  int|void flag ) + protected string|int get_my_table( string|array(string) name, +  void|array(string)|string definition, +  string|void comment, int|void flag )   //! @decl string get_my_table( string name, array(string) types ) - //! @decl string get_my_table( string name, string defenition ) - //! @decl string get_my_table( string defenition ) - //! @decl string get_my_table( array(string) defenition ) + //! @decl string get_my_table( string name, string definition ) + //! @decl string get_my_table( string definition ) + //! @decl string get_my_table( array(string) definition )   //!   //! Returns the name of a table in the 'shared' database that is   //! unique for this module. It is possible to select another database   //! by using @[set_my_db] before calling this function.   //!   //! You can use @[create_sql_tables] instead of this function if you want   //! to create more than one table in one go.   //!   //! If @[flag] is true, return 1 if a table was created, and 0 otherwise.   //!   //! In the first form, @[name] is the (postfix of) the name of the - //! table, and @[types] is an array of defenitions, as an example: + //! table, and @[types] is an array of definitions, as an example:   //!   //! - //! @code{ + //! @code   //! cache_table = get_my_table( "cache", ({   //! "id INT UNSIGNED AUTO_INCREMENT",   //! "data BLOB",   //! }) ); - //! @} + //! @endcode   //! - //! In the second form, the whole table defenition is instead sent as + //! In the second form, the whole table definition is instead sent as   //! a string. The cases where the name is not included (the third and   //! fourth form) is equivalent to the first two cases with the name ""   //!   //! If the table does not exist in the datbase, it is created.   //!   //! @note   //! This function may not be called from create   // - // If it exists, but it's defenition is different, the table will be - // altered with a ALTER TABLE call to conform to the defenition. This + // If it exists, but it's definition is different, the table will be + // altered with a ALTER TABLE call to conform to the definition. This   // might not work if the database the table resides in is not a MySQL   // database (normally it is, but it's possible, using @[set_my_db],   // to change this).   {    string oname;    int ddc; -  if( !defenition ) +  if( !definition )    { -  defenition = name; +  definition = name;    oname = name = "";    }    else if(strlen(name))    name = "_"+(oname = name);       Sql.Sql sql = get_my_sql();       string res = hash(_my_configuration->name)->digits(36) -  + "_" + replace(sname(),"#","_") + name; +  + "_" + replace(sname(),({ "#","-" }), ({ "_","_" })) + name;       if( !sql )    {    report_error("Failed to get SQL handle, permission denied for "+my_db+"\n");    return 0;    } -  if( arrayp( defenition ) ) -  defenition *= ", "; +  if( arrayp( definition ) ) +  definition *= ", ";       if( catch(sql->query( "SELECT * FROM "+res+" LIMIT 1" )) )    {    ddc++;    mixed error =    catch    { -  get_my_sql()->query( "CREATE TABLE "+res+" ("+defenition+")" ); +  get_my_sql()->query( "CREATE TABLE "+res+" ("+definition+")" );    DBManager.is_module_table( this_object(), my_db, res,    oname+"\0"+comment );    };    if( error )    {    if( strlen( name ) )    name = " "+name;    report_error( "Failed to create table"+name+": "+    describe_error( error ) );    return 0;    }    if( flag )    {    __my_tables[ "&"+oname+";" ] = res;    return ddc;    }    return __my_tables[ "&"+oname+";" ] = res;    } - // // Update defenition if it has changed. + // // Update definition if it has changed.   // mixed error =   // catch   // { - // get_my_sql()->query( "ALTER TABLE "+res+" ("+defenition+")" ); + // get_my_sql()->query( "ALTER TABLE "+res+" ("+definition+")" );   // };   // if( error )   // {   // if( strlen( name ) )   // name = " for "+name; - // report_notice( "Failed to update table defenition"+name+": "+ + // report_notice( "Failed to update table definition"+name+": "+   // describe_error( error ) );   // }    if( flag )    {    __my_tables[ "&"+oname+";" ] = res;    return ddc;    }    return __my_tables[ "&"+oname+";" ] = res;   }    - static string my_db = "shared"; + protected string my_db = "local";    - static void set_my_db( string to ) + protected void set_my_db( string to )   //! Select the database in which tables will be created with   //! get_my_table, and also the one that will be returned by   //! @[get_my_sql]   {    my_db = to;   }    - Sql.Sql get_my_sql( int|void read_only ) + Sql.Sql get_my_sql( int|void read_only, void|string charset )   //! Return a SQL-object for the database set with @[set_my_db], - //! defaulting to the 'shared' database. If read_only is specified, - //! the database will be opened in read_only mode. + //! defaulting to the 'shared' database. If @[read_only] is specified, + //! the database will be opened in read_only mode. @[charset] may be + //! used to specify a charset for the connection if the database + //! supports it.   //!   //! See also @[DBManager.get]   { -  return DBManager.cached_get( my_db, _my_configuration, read_only ); +  return DBManager.cached_get( my_db, _my_configuration, read_only, charset );   }