--- parser3/src/classes/file.C 2024/12/23 18:30:55 1.295 +++ parser3/src/classes/file.C 2026/10/07 19:37:27 1.307 @@ -1,12 +1,10 @@ /** @file Parser: @b file parser class. - Copyright (c) 2001-2024 Art. Lebedev Studio (http://www.artlebedev.com) + Copyright (c) 2001-2026 Art. Lebedev Studio (https://www.artlebedev.com) Authors: Konstantin Morshnev , Alexandr Petrosian */ -#include "pa_config_includes.h" - #include "classes.h" #include "pa_vmethod_frame.h" #include "pa_base64.h" @@ -27,7 +25,7 @@ #include "pa_vregex.h" #include "pa_version.h" -volatile const char * IDENT_FILE_C="$Id: file.C,v 1.295 2024/12/23 18:30:55 moko Exp $"; +volatile const char * IDENT_FILE_C="$Id: file.C,v 1.307 2026/10/07 19:37:27 moko Exp $"; // defines @@ -35,6 +33,7 @@ volatile const char * IDENT_FILE_C="$Id: #define CHARSET_EXEC_PARAM_NAME "charset" #define NAME_NAME "name" +#define MODE_APPEND "append" #define KEEP_EMPTY_DIRS_NAME "keep-empty-dirs" #define SUPPRESS_EXCEPTION_NAME "exception" @@ -133,6 +132,7 @@ static const String::Body cdate_name("cd // methods static void _save(Request& r, MethodParams& params) { + bool do_append=false; bool is_text=VFile::is_text_mode(params.as_string(0, MODE_MUST_BE_STRING)); const String& file_name=params.as_file_name(1); @@ -144,12 +144,16 @@ static void _save(Request& r, MethodPara asked_charset=&pa_charsets.get(vcharset_name->as_string()); valid_options++; } + if(Value* vappend=options->get(MODE_APPEND)){ + do_append=vappend->as_bool(); + valid_options++; + } if(valid_options != options->count()) throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION); } // save - GET_SELF(r, VFile).save(r.charsets, r.full_disk_path(file_name), is_text, asked_charset); + GET_SELF(r, VFile).save(r.charsets, r.full_disk_path(file_name), is_text, do_append, asked_charset); } static void _delete(Request& r, MethodParams& params) { @@ -173,7 +177,9 @@ static void _delete(Request& r, MethodPa } // unlink - file_delete(r.full_disk_path(file_name), fail_on_problem, keep_empty_dirs); + const String* path=fail_on_problem ? &r.full_disk_path(file_name) : r.real_disk_path(file_name); + if(path) + file_delete(*path, fail_on_problem, keep_empty_dirs); } static void _move(Request& r, MethodParams& params) { @@ -226,7 +232,7 @@ static void _copy(Request& r, MethodPara if(params.count()>2) if(HashStringValue* options=params.as_hash(2)){ int valid_options=0; - if(Value* vappend=options->get("append")){ + if(Value* vappend=options->get(MODE_APPEND)){ append=r.process(*vappend).as_bool(); valid_options++; } @@ -255,7 +261,7 @@ static void _load_pass_param( static void _load(Request& r, MethodParams& params) { bool as_text=VFile::is_text_mode(params.as_string(0, MODE_MUST_BE_STRING)); - const String& lfile_name=r.full_disk_path(params.as_file_name(1)); + const String& lfile_name=r.full_disk_path(params.as_file_name(1), PA_ALLOW_HTTP); size_t param_index=params.count()-1; Value* param_value=param_index>1?¶ms.as_no_junction(param_index, "file name or options must not be code"):0; @@ -488,14 +494,14 @@ static void _exec_cgi(Request& r, Method const String& script_name=r.full_disk_path(params.as_file_name(param_index++)); HashStringString env; - #define ECSTR(name, value_cstr) if(value_cstr) env.put(#name, value_cstr); + #define ECSTR(name, value_cstr) if(value_cstr) env.put(#name, String::Body(value_cstr)); // passing environment for(SAPI::Env::Iterator i(r.sapi_info); i; i.next() ) - env.put(i.key(), i.value() ); + env.put(i.key(), String::Body(i.value()) ); // const ECSTR(GATEWAY_INTERFACE, "CGI/1.1"); - ECSTR(PARSER_VERSION, PARSER_VERSION); + ECSTR(PARSER_VERSION, PARSER_VERSION_WITH_FEATURES); // from Request.info ECSTR(DOCUMENT_ROOT, r.request_info.document_root); ECSTR(PATH_TRANSLATED, r.request_info.path_translated); @@ -699,6 +705,7 @@ static void _list(Request& r, MethodPara Value& relative_path=params.as_no_junction(0, "path must not be code"); bool stat=false; + bool fail_on_problem=false; VRegex* vregex=0; VRegexCleaner vrcleaner; @@ -716,6 +723,10 @@ static void _list(Request& r, MethodPara vfilter=value; valid_options++; } + if(Value* vexception=options->get(SUPPRESS_EXCEPTION_NAME)) { + fail_on_problem=r.process(*vexception).as_bool(); + valid_options++; + } if(valid_options!=options->count()) throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION); } else { @@ -737,11 +748,17 @@ static void _list(Request& r, MethodPara } } - const char* absolute_path_cstr=r.full_disk_path(relative_path.as_string()).taint_cstr(String::L_FILE_SPEC); - Table::Action_options table_options; Table& table=*new Table(file_list_table_template(), table_options); + // no exception for a uri or an invalid or tainted file:// + const String* path=fail_on_problem ? &r.full_disk_path(relative_path.as_string()) : r.real_disk_path(relative_path.as_string()); + if(!path) { + r.write(*new VTable(&table)); + return; + } + const char* absolute_path_cstr=path->taint_cstr(String::L_FILE_SPEC); + const int ovector_size=(1/*match*/)*3; int ovector[ovector_size]; @@ -761,6 +778,10 @@ static void _list(Request& r, MethodPara } table+=row; } + , + if(fail_on_problem) + throw Exception(errno==EACCES ? "file.access" : (errno==ENOENT || errno==ENOTDIR) ? "file.missing" : 0, + path, "list failed: %s (%d)%s", strerror(errno), errno, actual_filename(*path, absolute_path_cstr)); ); // write out result @@ -804,41 +825,65 @@ static size_t afterlastslash(const Strin return pos!=STRING_NOT_FOUND?pos+1:0; } +// absolute web path or null when outside of document_root +static const String* web_path(Request& r, const String& file_spec, bool fail_on_problem) { + if(file_spec.first_char()=='/') // already absolute, just changing backslashes + return &backslashes_to_slashes(file_spec); + + // /some/page.html: ^file:fullpath[a.gif] => /some/a.gif + const String* disk_path=fail_on_problem ? &r.full_disk_path(file_spec) : r.real_disk_path(file_spec); + if(!disk_path) // a uri or an invalid file:// one + return 0; + + const char* document_root=r.request_info.document_root_path; + size_t document_root_length=strlen(document_root); + + const String& result=disk_path->mid(document_root_length, disk_path->length()); + + // checking the result is inside document root; + // first checking the segment boundary: /root must not match /root-but-longer + return result.first_char()=='/' && !path_cmp(disk_path->cstr(), document_root, document_root_length) ? &result : 0; +} + static void _find(Request& r, MethodParams& params) { const String& file_name=params.as_string(0, FILE_NAME_MUST_BE_STRING); Value* not_found_code=(params.count()==2)?¶ms.as_junction(1, "not-found param must be code"):0; const String* file_spec; - if(file_name.first_char()=='/') - file_spec=&file_name; - else - file_spec=&r.relative(r.request_info.uri, file_name); - - // easy way - if(file_exist(r.full_disk_path(*file_spec))) { - r.write(*file_spec); - return; + if(path_scheme(file_name) || is_os_absolute_path(file_name)) + file_spec=web_path(r, file_name, false); // do not throw + else { + const String& name=backslashes_to_slashes(file_name); + file_spec=name.first_char()=='/' ? &name : &r.relative(r.request_info.uri, name); } - // monkey way - size_t last_slash=file_spec->strrpbrk("/\\"); - const String& dirname=file_spec->mid(0, last_slash!=STRING_NOT_FOUND?last_slash:0); - const String& basename=file_spec->mid(last_slash!=STRING_NOT_FOUND?last_slash+1:0, file_spec->length()); - - size_t rpos=dirname.is_empty()?0:dirname.length()-1; - while((rpos=dirname.rskipchars("/\\", 0, rpos))!=STRING_NOT_FOUND){ - size_t slash=dirname.strrpbrk("/\\", 0, rpos); - if(slash==STRING_NOT_FOUND) - break; - String test_name; - test_name << dirname.mid(0, slash+1); - test_name << basename; - if(file_exist(r.full_disk_path(test_name))) { - r.write(test_name); + if(file_spec) { + // easy way + if(file_exist(r.full_disk_path(*file_spec))) { + r.write(*file_spec); return; } - rpos=slash; + + // monkey way + size_t last_slash=file_spec->strrchr('/'); + const String& dirname=file_spec->mid(0, last_slash!=STRING_NOT_FOUND?last_slash:0); + const String& basename=file_spec->mid(last_slash!=STRING_NOT_FOUND?last_slash+1:0, file_spec->length()); + + size_t rpos=dirname.is_empty()?0:dirname.length()-1; + while((rpos=dirname.rskipchars("/", 0, rpos))!=STRING_NOT_FOUND){ + size_t slash=dirname.strrchr('/', rpos); + if(slash==STRING_NOT_FOUND) + break; + String test_name; + test_name << dirname.mid(0, slash+1); + test_name << basename; + if(file_exist(r.full_disk_path(test_name))) { + r.write(test_name); + return; + } + rpos=slash; + } } // no way, not found @@ -928,22 +973,14 @@ static void _justext(Request& r, MethodP static void _fullpath(Request& r, MethodParams& params) { const String& file_spec=params.as_file_spec(0); - const String* result; - if(file_spec.first_char()=='/') - result=&file_spec; - else { - // /some/page.html: ^file:fullpath[a.gif] => /some/a.gif - const String& full_disk_path=r.full_disk_path(file_spec); - size_t document_root_length=strlen(r.request_info.document_root); - - if(document_root_length>0) { - char last_char=r.request_info.document_root[document_root_length-1]; - if(last_char == '/' || last_char == '\\') - --document_root_length; - } - result=&full_disk_path.mid(document_root_length, full_disk_path.length()); - } - r.write(*result); + if(const String* result=web_path(r, file_spec, true)) + r.write(*result); + else + throw Exception(PARSER_RUNTIME, &file_spec, "is outside of $request:document-root [%s]", r.request_info.document_root); +} + +static void _diskpath(Request& r, MethodParams& params) { + r.write(r.full_disk_path(params.as_file_spec(0))); } static void _sql_string(Request& r, MethodParams&) { @@ -1064,28 +1101,21 @@ extern Base64Options base64_encode_optio Base64Options base64_decode_options(Request& r, HashStringValue* options, VString** vcontent_type) { Base64Options result; if(options) { - int valid_options=0; for(HashStringValue::Iterator i(*options); i; i.next() ) { String::Body key=i.key(); Value* value=i.value(); if(key == "pad") { result.pad=r.process(*value).as_bool(); - valid_options++; } else if(key == "strict") { result.strict=r.process(*value).as_bool(); - valid_options++; } else if(key == CONTENT_TYPE_NAME) { *vcontent_type=new VString(value->as_string()); - valid_options++; } else if(key == "url-safe") { if(r.process(*value).as_bool()) result.set_url_safe_abc(); - valid_options++; - } + } else + throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION); } - - if(valid_options != options->count()) - throw Exception(PARSER_RUNTIME, 0, CALLED_WITH_INVALID_OPTION); } return result; } @@ -1227,7 +1257,7 @@ MFile::MFile(): Methoded("file") { add_native_method("create", Method::CT_DYNAMIC, _create, 1, 4); // ^file.save[mode;file-name] - // ^file.save[mode;file-name;$.charset[...]] + // ^file.save[mode;file-name;$.charset[...]$.append(false)] add_native_method("save", Method::CT_DYNAMIC, _save, 2, 3); // ^file:delete[file-name] @@ -1280,6 +1310,8 @@ 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); + // /some/page.html: ^file:diskpath[a.gif] => /document/root/some/a.gif, file:// and C:\ resolved as well + add_native_method("diskpath", Method::CT_STATIC, _diskpath, 1, 1); // ^file.sql-string[] add_native_method("sql-string", Method::CT_DYNAMIC, _sql_string, 0, 0);