--- parser3/src/classes/file.C 2003/11/20 16:34:23 1.115 +++ parser3/src/classes/file.C 2004/07/26 10:44:21 1.126 @@ -1,11 +1,11 @@ /** @file Parser: @b file parser class. - Copyright (c) 2001-2003 ArtLebedev Group (http://www.artlebedev.com) + Copyright (c) 2001-2004 ArtLebedev Group (http://www.artlebedev.com) Author: Alexandr Petrosian (http://paf.design.ru) */ -static const char * const IDENT_FILE_C="$Date: 2003/11/20 16:34:23 $"; +static const char * const IDENT_FILE_C="$Date: 2004/07/26 10:44:21 $"; #include "pa_config_includes.h" @@ -24,10 +24,14 @@ static const char * const IDENT_FILE_C=" #include "pa_vtable.h" #include "pa_charset.h" #include "pa_charsets.h" +#include "pa_sql_connection.h" +#include "pa_vresponse.h" +#include "pa_vcookie.h" // defines #define TEXT_MODE_NAME "text" +#define BINARY_MODE_NAME "binary" #define STDIN_EXEC_PARAM_NAME "stdin" #define CHARSET_EXEC_PARAM_NAME "charset" @@ -104,13 +108,23 @@ static const String::Body cdate_name("cd // methods +static bool is_text_mode(const String& mode) { + if(mode==TEXT_MODE_NAME) + return true; + if(mode==BINARY_MODE_NAME) + return false; + throw Exception("parser.runtime", + &mode, + "is invalid mode, must be either '"TEXT_MODE_NAME"' or '"BINARY_MODE_NAME"'"); +} + static void _save(Request& r, MethodParams& params) { Value& vmode_name=params. as_no_junction(0, "mode must not be code"); Value& vfile_name=params.as_no_junction(1, "file name must not be code"); // save GET_SELF(r, VFile).save(r.absolute(vfile_name.as_string()), - vmode_name.as_string()==TEXT_MODE_NAME); + is_text_mode(vmode_name.as_string())); } static void _delete(Request& r, MethodParams& params) { @@ -147,7 +161,7 @@ static void _load(Request& r, MethodPara alt_filename_param_index++; File_read_result file=file_read(r.charsets, lfile_name, - vmode_name.as_string()==TEXT_MODE_NAME, + is_text_mode(vmode_name.as_string()), third_param_hash ); @@ -223,7 +237,7 @@ static void append_env_pair( throw Exception("parser.runtime", new String(akey, String::L_TAINTED), "not safe environment variable"); - info->env->put(akey, avalue->as_string()); + info->env->put(akey, avalue->as_string().cstr(String::L_UNSPECIFIED)); } } #ifndef DOXYGEN @@ -294,7 +308,7 @@ static void _exec_cgi(Request& r, Method if(params.count()>1) { Value& venv=params.as_no_junction(1, "env must not be code"); if(HashStringValue* user_env=venv.get_hash()) { - Append_env_pair_info info={&env}; + Append_env_pair_info info={&env, 0, 0}; user_env->for_each(append_env_pair, &info); // $.stdin if(info.vstdin) { @@ -398,7 +412,7 @@ static void _exec_cgi(Request& r, Method ArrayString rows; size_t pos_after=0; header->split(rows, pos_after, eol_marker); - Pass_cgi_header_attribute_info info={0}; + Pass_cgi_header_attribute_info info={0, 0, 0}; info.charset=&r.charsets.source(); info.fields=&self.fields(); rows.for_each(pass_cgi_header_attribute, &info); @@ -501,10 +515,11 @@ static void lock_execute_body(int , void info.r->write_assign_lang(info.r->process(*info.body_code)); }; static void _lock(Request& r, MethodParams& params) { - Lock_execute_body_info info={0}; - info.r=&r; const String& file_spec=r.absolute(params.as_string(0, "file name must be string")); - info.body_code=¶ms.as_junction(1, "body must be code"); + Lock_execute_body_info info={ + &r, + ¶ms.as_junction(1, "body must be code") + }; file_write_action_under_lock(file_spec, "lock", lock_execute_body, &info); } @@ -513,7 +528,7 @@ static int lastposafter(const String& s, size_t size=0; // just to calm down compiler if(beforelast) size=s.length(); - int at; + size_t at; while((at=s.pos(String::Body(substr, substr_size), after))!=STRING_NOT_FOUND) { size_t newafter=at+substr_size/*skip substr*/; if(beforelast && newafter==size) @@ -614,6 +629,357 @@ static void _fullpath(Request& r, Method r.write_assign_lang(*result); } +static void _sql_string(Request& r, MethodParams&) { + VFile& self=GET_SELF(r, VFile); + + const char *quoted=r.connection()->quote(self.value_ptr(), self.value_size()); + r.write_assign_lang(*new String(quoted)); +} + +#ifndef DOXYGEN +class File_sql_event_handlers: public SQL_Driver_query_event_handlers { + const String& statement_string; const char* statement_cstr; + int got_columns; + int got_cells; +public: + String::C value; + String* user_file_name; + String* user_content_type; +public: + File_sql_event_handlers( + const String& astatement_string, const char* astatement_cstr): + statement_string(astatement_string), statement_cstr(astatement_cstr), + got_columns(0), + got_cells(0), + user_file_name(0), + user_content_type(0) {} + + bool add_column(SQL_Error& error, const char* /*str*/, size_t /*length*/) { + if(got_columns++==3) { + error=SQL_Error("parser.runtime", "result must contain not more then 3 columns"); + return true; + } + return false; + } + bool before_rows(SQL_Error& /*error*/ ) { /* ignore */ return false; } + bool add_row(SQL_Error& /*error*/) { /* ignore */ return false; } + bool add_row_cell(SQL_Error& error, const char* str, size_t length) { + try { + switch(got_cells++) { + case 0: + value=String::C(str, length); + break; + case 1: + user_file_name=new String(str, length, true); + break; + case 2: + user_content_type=new String(str, length, true); + break; + default: + error=SQL_Error("parser.runtime", "result must not contain more then one row, three rows"); + return true; + } + return false; + } catch(...) { + error=SQL_Error("exception occured in File_sql_event_handlers::add_row_cell"); + return true; + } + } +}; +#endif +static void _sql(Request& r, MethodParams& params) { + const String* user_file_name=0; + if(params.get(0)->is_string()) + user_file_name=¶ms.get(0)->as_string(); + + Value& statement=params.as_junction(params.count()-1, "statement must be code"); + + Temp_lang temp_lang(r, String::L_SQL); + const String& statement_string=r.process_to_string(statement); + const char* statement_cstr= + statement_string.cstr(String::L_UNSPECIFIED, r.connection()); + File_sql_event_handlers handlers(statement_string, statement_cstr); + r.connection()->query( + statement_cstr, + 0, 0, + 0, 0, + handlers, + statement_string); + + if(!handlers.value) + throw Exception("parser.runtime", + 0, + "produced no result"); + + if(!user_file_name) +class send_attr_info +{ +public: + send_attr_info(Request *t) : r(t), add_content_type(true), add_last_modified(true), add_content_disposition(true) {} + Request *r; + bool add_content_type; + bool add_last_modified; + bool add_content_disposition; +}; + +static void send_add_header_attribute( + HashStringValue::key_type aattribute, + HashStringValue::value_type ameaning, + send_attr_info *r) +{ + const char *a = aattribute.cstr(); + SAPI::add_header_attribute(r->r->sapi_info, + a, + attributed_meaning_to_string(*ameaning, String::L_HTTP_HEADER, false). + cstr(String::L_UNSPECIFIED)); + if(strcasecmp(a, "content-type")==0) + r->add_content_type = false; + else if(strcasecmp(a, "last-modified")==0) + r->add_last_modified = false; + else if(strcasecmp(a, "content-disposition")==0) + r->add_content_disposition = false; +} + +struct RANGE +{ + size_t start; + size_t end; +}; + +static void parse_range(const String* s, Array &ar) +{ + const char *p = s->cstr(); + if(s->starts_with("bytes=")) + p += 6; + RANGE r; + while(*p){ + r.start = (size_t)-1; + r.end = (size_t)-1; + if(*p >= '0' && *p <= '9'){ + r.start = atol(p); + while(*p>='0' && *p<='9') ++p; + } + if(*p++ != '-') break; + if(*p >= '0' && *p <= '9'){ + r.end = atol(p); + while(*p>='0' && *p<='9') ++p; + } + if(*p == ',') ++p; + ar += r; + } +} + +class auto_file +{ +protected: + FILE *f; +public: + auto_file(FILE *t){ + f = t; + } + ~auto_file(){ + if(f != 0){ + fclose(f); + f = 0; + } + } + operator FILE*(){ + return f; + } +}; + +// ^file:send[filename] +// ^file:send[filename;options hash] +// ^file:send[local_filename;remote_filename] +// ^file:send[local_filename;remote_filename;options hash] +static void _send(Request& r, MethodParams& params) { + SAPI::add_header_attribute(r.sapi_info, "Accept-Ranges", "bytes"); + if(r.response.fields().get("ignore")!=0) throw Exception("parser.runtime", 0, "^file:send not allowed here"); + Value *to_file_name = 0; + Value *options = 0; + Value *from_file_name = params.get(0); + const char *c_from_file_name=0, *disposition=0; + if(!from_file_name->is("string")) throw Exception("parser.runtime", 0, "filename must be string"); + + size_t count = params.count(); + if(count > 1){ + to_file_name = params.get(1); + if(to_file_name->is("hash")){ + options = to_file_name; + to_file_name = 0; + }else if(count > 2){ + options = params.get(2); + if(!options->is("hash")) throw Exception("parser.runtime", 0, "options parameter must be hash"); + } + } + + c_from_file_name=r.absolute(from_file_name->as_string()).cstr(); + + size_t offset = 0; + size_t limit = (size_t)-1; + send_attr_info info(&r); + VDate *date = 0; + + if(options){ + HashStringValue *opts = options->get_hash(); + if(opts == 0) + throw Exception("parser.runtime", 0, "options must be hash"); + Value *v; + int valid_options = 0; + if(v = opts->get("offset")){ + ++valid_options; + offset = v->as_int(); + } + if(v = opts->get("limit")){ + ++valid_options; + limit = v->as_int(); + } + if(v = opts->get("headers")){ + ++valid_options; + HashStringValue *headers = v->get_hash(); + if(headers == 0) + throw Exception("parser.runtime", 0, "headers must be hash"); + headers->for_each(send_add_header_attribute, &info); + } + if(v = opts->get("mdate")){ + ++valid_options; + if(Value* vdate=v->as(VDATE_TYPE, false)) + date=static_cast(vdate); + else throw Exception("parser.runtime", 0, "mdate must be a date"); + } + if(v = opts->get("disposition")){ + ++valid_options; + if(!v->is("string")) throw Exception("parser.runtime", 0, "disposition must be a string"); + disposition = v->get_string()->cstr(); + if(strcmp(disposition, "inline") && strcmp(disposition, "attachment")) throw Exception("parser.runtime", 0, "disposition can be only 'inline' or 'attachment'"); + } + if(valid_options != opts->count()) + throw Exception("parser.runtime", 0, "invalid option passed"); + } + + auto_file f = fopen(c_from_file_name, "rb"); + if(f == 0) + throw Exception("parser.runtime", 0, "Can't open file"); + + if(fseek(f, 0, SEEK_END)!=0) + throw Exception("parser.runtime", 0, "Can't seek file"); + + size_t file_length = (size_t)ftell(f); + if(file_length == (size_t)-1) + throw Exception("parser.runtime", 0, "can't get file size"); + if(file_length <= offset) + throw Exception("parser.runtime", 0, "offset too big"); + + size_t content_length = file_length-offset; + if(limit != (size_t)-1) + content_length = limit ar; + parse_range(new String(range), ar); + size_t count = ar.count(); + if(count == 1){ + RANGE &rg = ar.get_ref(0); + if(rg.start == (size_t)-1 && rg.end == (size_t)-1){ + SAPI::add_header_attribute(r.sapi_info, "status", "416 Requested Range Not Satisfiable"); + return; + } + if(rg.start == (size_t)-1 && rg.end != (size_t)-1){ + rg.start = content_length - rg.end; + rg.end = content_length; + offset += rg.start; + part_length = rg.end-rg.start; + }else if(rg.start != (size_t)-1 && rg.end == (size_t)-1){ + rg.end = content_length-1; + offset += rg.start; + part_length -= rg.start; + } + if(part_length == 0){ + SAPI::add_header_attribute(r.sapi_info, "status", "204 No Content"); + return; + } + SAPI::add_header_attribute(r.sapi_info, "status", "206 Partial Content"); + snprintf((char*)buf, BUFSIZE, "bytes %u-%u/%u", rg.start, rg.end, content_length); + SAPI::add_header_attribute(r.sapi_info, "Content-Range", (char*)buf); + }else if(count != 0){ + SAPI::add_header_attribute(r.sapi_info, "status", "501 Not Implemented"); + return; + } + } + + fseek(f, offset, SEEK_SET); + snprintf((char*)buf, BUFSIZE, "%u", part_length); + SAPI::add_header_attribute(r.sapi_info, "Content-Length", (char*)buf); + + if(info.add_content_disposition && disposition){ + const char *fname = 0; + if(to_file_name){ + fname = to_file_name->as_string().cstr(); + }else{ + const char *fname = c_from_file_name; + const char *p1 = strrchr(fname, '/'); + const char *p2 = strrchr(fname, '\\'); + if(p1 || p2) + fname = max(p1, p2)+1; + } + + snprintf((char*)buf, BUFSIZE, "%s; filename=\"%s\"", disposition, fname); + SAPI::add_header_attribute(r.sapi_info, "Content-Disposition", (char*)buf); + } + if(info.add_content_type) + SAPI::add_header_attribute(r.sapi_info, "Content-Type", r.mime_type_of(c_from_file_name).cstr()); + if(info.add_last_modified){ + if(date == 0){ + struct stat st; + if(stat(c_from_file_name, &st)!=0) throw Exception("parser.runtime", 0, "can't get file stat"); + date = new VDate(st.st_mtime); + } + const String &s = attributed_meaning_to_string(*date, String::L_AS_IS, true); + SAPI::add_header_attribute(r.sapi_info, "Last-Modified", s.cstr()); + } + r.cookie.output_result(r.sapi_info); + SAPI::send_header(r.sapi_info); + + const char* request_method=getenv("REQUEST_METHOD"); + bool header_only=request_method && strcasecmp(request_method, "HEAD")==0; + size_t sent = 0; + if(!header_only){ + size_t to_read = 0; + size_t size = 0; + do{ + to_read = part_lengthcstr(): 0; + + VString* vcontent_type=handlers.user_content_type? + new VString(*handlers.user_content_type) + : user_file_name_cstr? + new VString(r.mime_type_of(user_file_name_cstr)) + : 0; + VFile& self=GET_SELF(r, VFile); + self.set(true/*tainted*/, handlers.value.str, handlers.value.length, user_file_name_cstr, vcontent_type); +} // constructor @@ -659,6 +1025,12 @@ MFile::MFile(): Methoded("file") { // ^file:dirname[/a/b/]=/a add_native_method("dirname", Method::CT_STATIC, _dirname, 1, 1); // ^file:basename[/a/some.tar.gz]=some.tar.gz + + // ^file:send[filename] + // ^file:send[filename;options hash] + // ^file:send[filename;new_filename] + // ^file:send[filename;new_filename;options hash] + add_native_method("send", Method::CT_STATIC, _send, 1, 3); add_native_method("basename", Method::CT_STATIC, _basename, 1, 1); // ^file:justname[/a/some.tar.gz]=some.tar add_native_method("justname", Method::CT_STATIC, _justname, 1, 1); @@ -666,4 +1038,10 @@ MFile::MFile(): Methoded("file") { add_native_method("justext", Method::CT_STATIC, _justext, 1, 1); // /some/page.html: ^file:fullpath[a.gif] => /some/a.gif add_native_method("fullpath", Method::CT_STATIC, _fullpath, 1, 1); + + // ^file.sql-string[] + add_native_method("sql-string", Method::CT_DYNAMIC, _sql_string, 0, 0); + + // ^file::sql[[alt_name]]{} + add_native_method("sql", Method::CT_DYNAMIC, _sql, 1, 2); }