4cf1151999-04-24Henrik Grubbström (Grubba) /* $Id: module.pike,v 1.39 1999/04/24 17:55:02 grubba Exp $ */
44f12a1997-06-01Henrik Grubbström (Grubba) 
b1fca01996-11-12Per Hedbor #include <module.h>
b275871998-05-23Henrik Grubbström (Grubba) #define TRACE_ENTER(A,B) do{if(id->misc->trace_enter)id->misc->trace_enter((A),(B));}while(0) #define TRACE_LEAVE(A) do{if(id->misc->trace_leave)id->misc->trace_leave((A));}while(0)
b1fca01996-11-12Per Hedbor mapping (string:mixed *) variables=([]);
2a2a5b1996-12-01Per Hedbor object this = this_object();
dde9651996-12-08David Hedbor int module_type;
2a2a5b1996-12-01Per Hedbor string fix_cvs(string from) {
fd0b6f1996-12-02Per Hedbor  from = replace(from, ({ "$", "Id: "," Exp $" }), ({"","",""}));
2a2a5b1996-12-01Per Hedbor  sscanf(from, "%*s,v %s", from); return from; }
edb5061997-08-25Peter Bortas int module_dependencies(object configuration, array (string) modules) { if(configuration) { foreach (modules, string module) { if(!configuration->modules[module] || (!configuration->modules[module]->copies && !configuration->modules[module]->master)) configuration->enable_module(module+"#0"); } if(roxen->root) roxen->configuration_interface()->build_root(roxen->root); } _do_call_outs(); return 1; }
2a2a5b1996-12-01Per Hedbor string file_name_and_stuff() { return ("<b>Loaded from:</b> "+(roxen->filename(this))+"<br>"+ (this->cvs_version?"<b>CVS Version: </b>"+fix_cvs(this->cvs_version)+"<nr>\n":"")); }
5839c31999-01-22Marcus Comstedt static private object _my_configuration;
b1fca01996-11-12Per Hedbor object my_configuration() {
5839c31999-01-22Marcus Comstedt  if(_my_configuration) return _my_configuration;
b1fca01996-11-12Per Hedbor  object conf; foreach(roxen->configurations, conf) if(conf->otomod[this]) return conf; return 0; }
5839c31999-01-22Marcus Comstedt nomask void set_configuration(object c) { if(_my_configuration && _my_configuration != c) error("set_configuration() called twice.\n"); _my_configuration = c; }
b1fca01996-11-12Per Hedbor string module_creator; string module_url; void set_module_creator(string c) { module_creator = c; } void set_module_url(string to) { module_url = to; } int killvar(string var) {
2a2a5b1996-12-01Per Hedbor  if(!variables[var]) error("Killing undefined variable.\n");
b1fca01996-11-12Per Hedbor  m_delete(variables, var); return 1; } void free_some_sockets_please(){}
08df121997-08-19Henrik Grubbström (Grubba) void start(void|int num, void|object conf) {}
b1fca01996-11-12Per Hedbor string status() {}
08df121997-08-19Henrik Grubbström (Grubba) string info(object conf)
b1fca01996-11-12Per Hedbor {
08df121997-08-19Henrik Grubbström (Grubba)  return (this->register_module(conf)[2]);
b1fca01996-11-12Per Hedbor }
e416431998-07-07Henrik Grubbström (Grubba) static class ConfigurableWrapper { int mode; function f; int check() { if ((mode & VAR_EXPERT) && (!roxen->configuration_interface()->expert_mode)) { return 1; } if ((mode & VAR_MORE) && (!roxen->configuration_interface()->more_mode)) { return 1; } return(f()); } void create(int mode_, function f_) { mode = mode_; f = f_; } };
b1fca01996-11-12Per Hedbor // Define a variable, with more than a little error checking...
c856841998-01-21Henrik Grubbström (Grubba) void defvar(string|void var, mixed|void value, string|void name, int|void type, string|void doc_str, mixed|void misc, int|function|void not_in_config)
b1fca01996-11-12Per Hedbor { if(!strlen(var)) error("No name for variable!\n");
5e4ede1996-11-12Per Hedbor // if(var[0]=='_' && previous_object() != roxen) // error("Variable names beginning with '_' are reserved for" // " internal usage.\n");
b1fca01996-11-12Per Hedbor  if (!stringp(name)) name = var;
f6d62d1997-03-26Per Hedbor  if((search(name, "\"") != -1)) error("Please do not use \" in variable names");
b1fca01996-11-12Per Hedbor  if (!stringp(doc_str)) doc_str = "No documentation";
df6cd11998-10-13Per Hedbor 
b1fca01996-11-12Per Hedbor  switch (type & VAR_TYPE_MASK) {
be377f1997-06-12Henrik Grubbström (Grubba)  case TYPE_NODE: if(!arrayp(value)) error("TYPE_NODE variables should contain a list of variables " "to use as subnodes.\n"); break; case TYPE_CUSTOM: if(!misc && arrayp(misc) && (sizeof(misc)>=3) && functionp(misc[0]) && functionp(misc[1]) && functionp(misc[2]))
0f8c871997-06-01Henrik Grubbström (Grubba)  error("When defining a TYPE_CUSTOM variable, the MISC " "field must be an array of functionpointers: \n" "({describe,describe_form,set_from_form})\n");
be377f1997-06-12Henrik Grubbström (Grubba)  break; case TYPE_TEXT_FIELD: case TYPE_FILE: case TYPE_STRING: case TYPE_LOCATION: case TYPE_PASSWORD: if(value && !stringp(value)) { report_error(sprintf("%s:\nPassing illegal value (%t:%O) " "to string type variable.\n", roxen->filename(this), value, value)); } break;
b1fca01996-11-12Per Hedbor 
be377f1997-06-12Henrik Grubbström (Grubba)  case TYPE_FLOAT: if(!floatp(value)) report_error(sprintf("%s:\nPassing illegal value (%t:%O) " "(not float) to floating point " "decimal number variable.\n", roxen->filename(this), value, value)); break; case TYPE_INT: if(!intp(value)) report_error(sprintf("%s:\nPassing illegal value (%t:%O) " "(not int) to integer number variable.\n", roxen->filename(this), value, value)); break; case TYPE_MODULE_LIST: value = ({}); break;
b1fca01996-11-12Per Hedbor 
be377f1997-06-12Henrik Grubbström (Grubba)  case TYPE_MODULE: /* No default possible */ value = 0; break; case TYPE_DIR_LIST: int i; if(!arrayp(value)) { report_error(sprintf("%s:\nIllegal type %t to TYPE_DIR_LIST, " "must be array.\n", roxen->filename(this), value)); value = ({ "./" }); } else { for(i=0; i<sizeof(value); i++) { if(strlen(value[i])) { if(value[i][-1] != '/') value[i] += "/";
0f8c871997-06-01Henrik Grubbström (Grubba)  } else { value[i]="./"; }
be377f1997-06-12Henrik Grubbström (Grubba)  } } break; case TYPE_DIR: if(value && !stringp(value)) report_error(sprintf("%s:\nPassing illegal value (%t:%O) (not string) " "to directory variable.\n", roxen->filename(this), value, value));
b1fca01996-11-12Per Hedbor 
be377f1997-06-12Henrik Grubbström (Grubba)  if(value && strlen(value) && ((string)value)[-1] != '/') value+="/"; break; case TYPE_INT_LIST: case TYPE_STRING_LIST: if(!misc && value && !arrayp(value)) { report_error(sprintf("%s:\nPassing illegal misc (%t:%O) (not array) " "to multiple choice variable.\n", roxen->filename(this), value, value)); } else { if(misc && !arrayp(misc)) { report_error(sprintf("%s:\nPassing illegal misc (%t:%O) (not array) " "to multiple choice variable.\n", roxen->filename(this), misc, misc)); }
afd9501998-07-24Martin Stjernholm  if(misc && value && search(misc, value)==-1) {
9b9f701997-08-12Per Hedbor  roxen_perror(sprintf("%s:\nPassing value (%t:%O) not present "
be377f1997-06-12Henrik Grubbström (Grubba)  "in the misc array.\n", roxen->filename(this), value, value)); } }
b1fca01996-11-12Per Hedbor  break;
0f8c871997-06-01Henrik Grubbström (Grubba)  case TYPE_FLAG:
b1fca01996-11-12Per Hedbor  value=!!value; break;
0f8c871997-06-01Henrik Grubbström (Grubba)  case TYPE_ERROR:
b1fca01996-11-12Per Hedbor  break;
0f8c871997-06-01Henrik Grubbström (Grubba)  case TYPE_COLOR:
b1fca01996-11-12Per Hedbor  if (!intp(value))
be377f1997-06-12Henrik Grubbström (Grubba)  report_error(sprintf("%s:\nPassing illegal value (%t:%O) (not int) " "to color variable.\n", roxen->filename(this), value, value));
b1fca01996-11-12Per Hedbor  break;
be377f1997-06-12Henrik Grubbström (Grubba)  case TYPE_FILE_LIST: case TYPE_PORTS: case TYPE_FONT: // FIXME: Add checks for these. break;
0f8c871997-06-01Henrik Grubbström (Grubba)  default:
be377f1997-06-12Henrik Grubbström (Grubba)  report_error(sprintf("%s:\nIllegal type (%s) in defvar.\n", roxen->filename(this), type));
0f8c871997-06-01Henrik Grubbström (Grubba)  break;
b1fca01996-11-12Per Hedbor  }
df6cd11998-10-13Per Hedbor  // Locale stuff! // Här blir vi farliga... Locale.Roxen.standard ->register_module_doc( this_object(), var, name, doc_str );
b1fca01996-11-12Per Hedbor  variables[var]=allocate( VAR_SIZE ); if(!variables[var]) error("Out of memory in defvar.\n"); variables[var][ VAR_VALUE ]=value; variables[var][ VAR_TYPE ]=type&VAR_TYPE_MASK; variables[var][ VAR_DOC_STR ]=doc_str; variables[var][ VAR_NAME ]=name;
e416431998-07-07Henrik Grubbström (Grubba)  type &= ~VAR_TYPE_MASK; // Probably not needed, but... type &= (VAR_EXPERT | VAR_MORE); if (functionp(not_in_config)) { if (type) { variables[var][ VAR_CONFIGURABLE ] = ConfigurableWrapper(type, not_in_config)->check; } else { variables[var][ VAR_CONFIGURABLE ] = not_in_config; } } else if (type) { variables[var][ VAR_CONFIGURABLE ] = type; } else if(intp(not_in_config)) { variables[var][ VAR_CONFIGURABLE ] = !not_in_config; }
b1fca01996-11-12Per Hedbor  variables[var][ VAR_MISC ]=misc; variables[var][ VAR_SHORTNAME ]= var; }
df6cd11998-10-13Per Hedbor void deflocaledoc( string locale, string variable,
b796b51998-11-18Per Hedbor  string name, string doc, mapping|void translate )
df6cd11998-10-13Per Hedbor { if(!Locale.Roxen[locale]) report_debug("Invalid locale: "+locale+". Ignoring.\n"); else Locale.Roxen[locale]
b796b51998-11-18Per Hedbor  ->register_module_doc( this_object(), variable, name, doc, translate );
df6cd11998-10-13Per Hedbor }
b1fca01996-11-12Per Hedbor 
4cf1151999-04-24Henrik Grubbström (Grubba) // Convenience function, define an invisible variable, this variable // will be saved, but it won't be visible in the configuration interface.
0f8c871997-06-01Henrik Grubbström (Grubba) void definvisvar(string name, int value, int type, array|void misc)
b1fca01996-11-12Per Hedbor {
0f8c871997-06-01Henrik Grubbström (Grubba)  defvar(name, value, "", type, "", misc, 1);
b1fca01996-11-12Per Hedbor } string check_variable( string s, mixed value ) { // Check if `value' is O.K. to store in the variable `s'. If so, // return 0, otherwise return a string, describing the error. return 0; }
c856841998-01-21Henrik Grubbström (Grubba) mixed query(string|void var, int|void ok)
b1fca01996-11-12Per Hedbor {
c856841998-01-21Henrik Grubbström (Grubba)  if(var) {
b1fca01996-11-12Per Hedbor  if(variables[var]) return variables[var][VAR_VALUE];
c6fa431998-11-22Per Hedbor  else if(!ok && var[0] != '_')
b1fca01996-11-12Per Hedbor  error("Querying undefined variable.\n");
c6fa431998-11-22Per Hedbor  return 0;
c856841998-01-21Henrik Grubbström (Grubba)  }
b1fca01996-11-12Per Hedbor  return variables; } void set_module_list(string var, string what, object to) { int p; p = search(variables[var][VAR_VALUE], what); if(p == -1) { #ifdef MODULE_DEBUG perror("The variable '"+var+"': '"+what+"' found by hook.\n"); perror("Not found in variable!\n"); #endif } else variables[var][VAR_VALUE][p]=to; } void set(string var, mixed value) { if(!variables[var]) error( "Setting undefined variable.\n" ); else if(variables[var][VAR_TYPE] == TYPE_MODULE && stringp(value))
f6d62d1997-03-26Per Hedbor  roxenp()->register_module_load_hook( value, set, var );
b1fca01996-11-12Per Hedbor  else if(variables[var][VAR_TYPE] == TYPE_MODULE_LIST) { variables[var][VAR_VALUE]=value; if(arrayp(value)) foreach(value, value) if(stringp(value))
f6d62d1997-03-26Per Hedbor  roxenp()->register_module_load_hook(value,set_module_list,var,value);
b1fca01996-11-12Per Hedbor  } else variables[var][VAR_VALUE]=value; } int setvars( mapping (string:mixed) vars ) { string v; int err; foreach( indices( vars ), v ) if(variables[v]) set( v, vars[v] ); return !err; } string comment() { return ""; }
5839c31999-01-22Marcus Comstedt string query_internal_location() { if(!_my_configuration) error("Please do not call this function from create()!\n"); return _my_configuration->query_internal_location(this_object()); }
b7c45e1997-01-27Per Hedbor /* Per default, return the value of the module variable 'location' */ string query_location() { string s; catch{s = query("location");}; return s; }
ae32d01998-03-23David Hedbor /* By default, provide nothing. */ string query_provides() { return 0; }
b7c45e1997-01-27Per Hedbor 
b1fca01996-11-12Per Hedbor /* * Parse and return a parsed version of the security levels for this module * */
17d1731997-08-13Henrik Grubbström (Grubba) class IP_with_mask { int net; int mask; static private int ip_to_int(string ip) { int res;
eac26d1997-09-27Henrik Grubbström (Grubba)  foreach(((ip/".") + ({ "0", "0", "0" }))[..3], string num) {
17d1731997-08-13Henrik Grubbström (Grubba)  res = res*256 + (int)num; } return(res); }
4117501997-08-13Henrik Grubbström (Grubba)  void create(string _ip, string|int _mask)
17d1731997-08-13Henrik Grubbström (Grubba)  { net = ip_to_int(_ip);
4117501997-08-13Henrik Grubbström (Grubba)  if (intp(_mask)) { if (_mask > 32) { report_error(sprintf("Bad netmask: %s/%d\n" "Using %s/32\n", _ip, _mask, _ip)); _mask = 32; } mask = ~0<<(32-_mask); } else { mask = ip_to_int(_mask); }
17d1731997-08-13Henrik Grubbström (Grubba)  if (net & ~mask) {
4117501997-08-13Henrik Grubbström (Grubba)  report_error(sprintf("Bad netmask: %s for network %s\n" "Ignoring node-specific bits\n", _ip, _mask)); net &= mask;
17d1731997-08-13Henrik Grubbström (Grubba)  } } int `()(string ip) { return((ip_to_int(ip) & mask) == net); } };
b7c45e1997-01-27Per Hedbor 
b1fca01996-11-12Per Hedbor array query_seclevels() { array patterns=({ });
de493f1997-04-28Henrik Grubbström (Grubba)  if(catch(query("_seclevels"))) {
b1fca01996-11-12Per Hedbor  return patterns;
de493f1997-04-28Henrik Grubbström (Grubba)  }
b1fca01996-11-12Per Hedbor 
4117501997-08-13Henrik Grubbström (Grubba)  foreach(replace(query("_seclevels"), ({" ","\t","\\\n"}), ({"","",""}))/"\n", string sl) {
b1fca01996-11-12Per Hedbor  if(!strlen(sl) || sl[0]=='#') continue;
de493f1997-04-28Henrik Grubbström (Grubba) 
b1fca01996-11-12Per Hedbor  string type, value; if(sscanf(sl, "%s=%s", type, value)==2) {
de493f1997-04-28Henrik Grubbström (Grubba)  switch(lower_case(type))
b1fca01996-11-12Per Hedbor  { case "allowip":
4117501997-08-13Henrik Grubbström (Grubba)  array(string|int) arr; if (sizeof(arr = (value/"/")) == 2) { // IP/bits arr[1] = (int)arr[1]; patterns += ({ ({ MOD_ALLOW, IP_with_mask(@arr) }) }); } else if ((sizeof(arr = (value/":")) == 2) ||
8919711998-02-06Gerald Schupfner  (sizeof(arr = (value/",")) > 1)) {
4117501997-08-13Henrik Grubbström (Grubba)  // IP:mask or IP,mask patterns += ({ ({ MOD_ALLOW, IP_with_mask(@arr) }) }); } else { // Pattern
17d1731997-08-13Henrik Grubbström (Grubba)  value = replace(value, ({ "?", ".", "*" }), ({ ".", "\\.", ".*" })); patterns += ({ ({ MOD_ALLOW, Regexp(value)->match, }) }); }
b1fca01996-11-12Per Hedbor  break;
9579781998-06-29Henrik Grubbström (Grubba)  case "acceptip": // Short-circuit version of allow ip. array(string|int) arr; if (sizeof(arr = (value/"/")) == 2) { // IP/bits arr[1] = (int)arr[1]; patterns += ({ ({ MOD_ACCEPT, IP_with_mask(@arr) }) }); } else if ((sizeof(arr = (value/":")) == 2) || (sizeof(arr = (value/",")) > 1)) { // IP:mask or IP,mask patterns += ({ ({ MOD_ACCEPT, IP_with_mask(@arr) }) }); } else { // Pattern value = replace(value, ({ "?", ".", "*" }), ({ ".", "\\.", ".*" })); patterns += ({ ({ MOD_ACCEPT, Regexp(value)->match, }) }); } break;
b1fca01996-11-12Per Hedbor  case "denyip":
4117501997-08-13Henrik Grubbström (Grubba)  array(string|int) arr; if (sizeof(arr = (value/"/")) == 2) { // IP/bits arr[1] = (int)arr[1]; patterns += ({ ({ MOD_DENY, IP_with_mask(@arr) }) }); } else if ((sizeof(arr = (value/":")) == 2) ||
8919711998-02-06Gerald Schupfner  (sizeof(arr = (value/",")) > 1)) {
4117501997-08-13Henrik Grubbström (Grubba)  // IP:mask or IP,mask patterns += ({ ({ MOD_DENY, IP_with_mask(@arr) }) }); } else { // Pattern
17d1731997-08-13Henrik Grubbström (Grubba)  value = replace(value, ({ "?", ".", "*" }), ({ ".", "\\.", ".*" })); patterns += ({ ({ MOD_DENY, Regexp(value)->match, }) }); }
b1fca01996-11-12Per Hedbor  break;
f6d62d1997-03-26Per Hedbor  case "allowuser":
17d1731997-08-13Henrik Grubbström (Grubba)  value = replace(value, ({ "?", ".", "*" }), ({ ".", "\\.", ".*" }));
f6d62d1997-03-26Per Hedbor  array(string) users = (value/"," - ({""})); int i; for(i=0; i < sizeof(users); i++) {
de493f1997-04-28Henrik Grubbström (Grubba)  if (lower_case(users[i]) == "any") {
f6d62d1997-03-26Per Hedbor  if(this->register_module()[0] & MODULE_PROXY) patterns += ({ ({ MOD_PROXY_USER, lambda(){ return 1; } }) }); else patterns += ({ ({ MOD_USER, lambda(){ return 1; } }) }); break; } else { users[i & 0x0f] = "(^"+users[i]+"$)"; } if ((i & 0x0f) == 0x0f) { value = users[0..0x0f]*"|"; if(this->register_module()[0] & MODULE_PROXY) { patterns += ({ ({ MOD_PROXY_USER, Regexp(value)->match, }) }); } else { patterns += ({ ({ MOD_USER, Regexp(value)->match, }) }); } } } if (i & 0x0f) { value = users[0..(i-1)&0x0f]*"|"; if(this->register_module()[0] & MODULE_PROXY) { patterns += ({ ({ MOD_PROXY_USER, Regexp(value)->match, }) }); } else { patterns += ({ ({ MOD_USER, Regexp(value)->match, }) }); }
a397fe1996-12-13David Hedbor  }
b1fca01996-11-12Per Hedbor  break;
9579781998-06-29Henrik Grubbström (Grubba)  case "acceptuser": // Short-circuit version of allow user. // NOTE: MOD_PROXY_USER is already short-circuit. value = replace(value, ({ "?", ".", "*" }), ({ ".", "\\.", ".*" })); array(string) users = (value/"," - ({""})); int i; for(i=0; i < sizeof(users); i++) { if (lower_case(users[i]) == "any") { if(this->register_module()[0] & MODULE_PROXY) patterns += ({ ({ MOD_PROXY_USER, lambda(){ return 1; } }) }); else patterns += ({ ({ MOD_ACCEPT_USER, lambda(){ return 1; } }) }); break; } else { users[i & 0x0f] = "(^"+users[i]+"$)"; } if ((i & 0x0f) == 0x0f) { value = users[0..0x0f]*"|"; if(this->register_module()[0] & MODULE_PROXY) { patterns += ({ ({ MOD_PROXY_USER, Regexp(value)->match, }) }); } else { patterns += ({ ({ MOD_ACCEPT_USER, Regexp(value)->match, }) }); } } } if (i & 0x0f) { value = users[0..(i-1)&0x0f]*"|"; if(this->register_module()[0] & MODULE_PROXY) { patterns += ({ ({ MOD_PROXY_USER, Regexp(value)->match, }) }); } else { patterns += ({ ({ MOD_ACCEPT_USER, Regexp(value)->match, }) }); } } break;
de493f1997-04-28Henrik Grubbström (Grubba)  default:
4117501997-08-13Henrik Grubbström (Grubba)  report_error(sprintf("Unknown Security:Patterns directive: " "type=\"%s\"\n", type));
de493f1997-04-28Henrik Grubbström (Grubba)  break;
b1fca01996-11-12Per Hedbor  }
de493f1997-04-28Henrik Grubbström (Grubba)  } else {
4117501997-08-13Henrik Grubbström (Grubba)  report_error(sprintf("Syntax error in Security:Patterns directive: " "line=\"%s\"\n", sl));
b1fca01996-11-12Per Hedbor  } } return patterns; } mixed stat_file(string f, object id){} mixed find_dir(string f, object id){}
a476711997-10-20Henrik Grubbström (Grubba) mapping(string:array(mixed)) find_dir_stat(string f, object id) {
b275871998-05-23Henrik Grubbström (Grubba)  TRACE_ENTER("find_dir_stat(): \""+f+"\"", 0);
a476711997-10-20Henrik Grubbström (Grubba)  array(string) files = find_dir(f, id); mapping(string:array(mixed)) res = ([]);
0c8b9a1997-10-22Henrik Grubbström (Grubba)  foreach(files || ({}), string fname) {
b275871998-05-23Henrik Grubbström (Grubba)  TRACE_ENTER("stat()'ing "+ f + "/" + fname, 0);
0c8b9a1997-10-22Henrik Grubbström (Grubba)  array(mixed) st = stat_file(f + "/" + fname, id);
a476711997-10-20Henrik Grubbström (Grubba)  if (st) {
0c8b9a1997-10-22Henrik Grubbström (Grubba)  res[fname] = st;
b275871998-05-23Henrik Grubbström (Grubba)  TRACE_LEAVE("OK"); } else { TRACE_LEAVE("No stat info");
a476711997-10-20Henrik Grubbström (Grubba)  } }
b275871998-05-23Henrik Grubbström (Grubba)  TRACE_LEAVE("");
a476711997-10-20Henrik Grubbström (Grubba)  return(res); }
b1fca01996-11-12Per Hedbor mixed real_file(string f, object id){}
4f4bc11998-02-04Per Hedbor mapping _api_functions = ([]); void add_api_function( string name, function f, void|array(string) types) { _api_functions[name] = ({ f, types }); } mapping api_functions() { return _api_functions; }
e493e81997-07-11Per Hedbor object get_font_from_var(string base) { int weight, slant; switch(query(base+"_weight")) { case "light": weight=-1; break; default: weight=0; break; case "bold": weight=1; break; case "black": weight=2; break; } switch(query(base+"_slant")) { case "obligue": slant=-1; break; default: slant=0; break; case "italic": slant=1; break; } return get_font(query(base+"_font"), 32, weight, slant, "left", 0, 0); }