diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index 71ee5631b5..c559ff1c4d 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -23,23 +23,39 @@ jobs: cpp: 17 asan: off ubsan: off + light_runtime: off - os: focal compiler: clang++ cpp: 17 asan: off ubsan: on + light_runtime: off - os: focal compiler: g++-10 cpp: 20 asan: on ubsan: off + light_runtime: off - os: jammy compiler: g++ cpp: 20 asan: on ubsan: off - - name: "${{matrix.os}}/${{matrix.compiler}}/c++${{matrix.cpp}}/asan=${{matrix.asan}}/ubsan=${{matrix.ubsan}}" + light_runtime: off + - os: focal + compiler: g++-11 + cpp: 20 + asan: off + ubsan: off + light_runtime: on + - os: focal + compiler: clang++-18 + cpp: 20 + asan: off + ubsan: off + light_runtime: on + + name: "${{matrix.os}}/${{matrix.compiler}}/c++${{matrix.cpp}}/asan=${{matrix.asan}}/ubsan=${{matrix.ubsan}}/light_runtime=${{matrix.light_runtime}}" steps: - uses: actions/checkout@v3 @@ -54,8 +70,8 @@ jobs: uses: actions/cache@v3 id: docker-image-cache with: - path: kphp-build-env-${{matrix.os}}.tar - key: docker-image-cache-${{matrix.os}}-${{ hashFiles('.github/workflows/Dockerfile.*', 'tests/python/requirements.txt') }} + path: /tmp/docker-save-${{matrix.os}} + key: docker-save-${{matrix.os}}-${{ hashFiles('.github/workflows/Dockerfile.*', 'tests/python/requirements.txt') }} - name: Build and save docker image if: steps.docker-image-cache.outputs.cache-hit != 'true' @@ -68,7 +84,7 @@ jobs: - name: Load docker image from cache if: steps.docker-image-cache.outputs.cache-hit == 'true' - run: docker load --input kphp-build-env-${{matrix.os}}.tar + run: docker load --input /tmp/docker-save-${{matrix.os}}/kphp-build-env-${{matrix.os}}.tar - name: Start docker container run: | @@ -79,41 +95,53 @@ jobs: run: docker exec kphp-build-container-${{matrix.os}} bash -c "git config --global --add safe.directory ${{env.kphp_root_dir}}" + - name: Check formatting in light runtime folder + if: ${{ matrix.os == 'focal' && matrix.light_runtime == 'on' }} + run: docker exec kphp-build-container-${{matrix.os}} bash -c + "find ${{env.kphp_root_dir}}/runtime-light/ -iname '*.h' -o -iname '*.cpp' | xargs clang-format-18 --dry-run -Werror" + - name: Build all run: docker exec kphp-build-container-${{matrix.os}} bash -c - "cmake -DCMAKE_CXX_COMPILER=${{matrix.compiler}} -DCMAKE_CXX_STANDARD=${{matrix.cpp}} -DADDRESS_SANITIZER=${{matrix.asan}} -DUNDEFINED_SANITIZER=${{matrix.ubsan}} -DPDO_DRIVER_MYSQL=ON -DPDO_DRIVER_PGSQL=ON -DPDO_LIBS_STATIC_LINKING=ON -S ${{env.kphp_root_dir}} -B ${{env.kphp_build_dir}} && make -C ${{env.kphp_build_dir}} -j$(nproc) all" + "cmake -DCMAKE_CXX_COMPILER=${{matrix.compiler}} -DCMAKE_CXX_STANDARD=${{matrix.cpp}} -DCOMPILE_RUNTIME_LIGHT=${{matrix.light_runtime}} -DADDRESS_SANITIZER=${{matrix.asan}} -DUNDEFINED_SANITIZER=${{matrix.ubsan}} -DPDO_DRIVER_MYSQL=ON -DPDO_DRIVER_PGSQL=ON -DPDO_LIBS_STATIC_LINKING=ON -S ${{env.kphp_root_dir}} -B ${{env.kphp_build_dir}} && make -C ${{env.kphp_build_dir}} -j$(nproc) all" - name: Run unit tests run: docker exec kphp-build-container-${{matrix.os}} bash -c "make -C ${{env.kphp_build_dir}} -j$(nproc) test" - name: Compile dummy PHP script + if: matrix.light_runtime == 'off' run: docker exec kphp-build-container-${{matrix.os}} bash -c "cd ${{env.kphp_build_dir}} && echo 'hello world' > demo.php && ${{env.kphp_root_dir}}/objs/bin/kphp2cpp --cxx ${{matrix.compiler}} demo.php && kphp_out/server -o --user kitten" + - name: Compile dummy PHP script + if: matrix.light_runtime == 'on' + run: docker exec kphp-build-container-${{matrix.os}} bash -c + "cd ${{env.kphp_build_dir}} && echo "${{matrix.light_runtime}}" && echo 'hello world' > demo.php && ${{env.kphp_root_dir}}/objs/bin/kphp2cpp --mode k2-component --cxx ${{matrix.compiler}} demo.php" + - name: Polyfills composer install run: docker exec kphp-build-container-${{matrix.os}} bash -c "composer install -d ${{env.kphp_polyfills_dir}}" - name: Run python tests + if: matrix.light_runtime == 'off' id: python_tests continue-on-error: true run: docker exec kphp-build-container-${{matrix.os}} bash -c "chown -R kitten /home && su kitten -c 'GITHUB_ACTIONS=1 KPHP_TESTS_POLYFILLS_REPO=${{env.kphp_polyfills_dir}} KPHP_CXX=${{matrix.compiler}} python3.7 -m pytest --tb=native -n$(nproc) ${{env.kphp_root_dir}}/tests/python/'" - name: Prepare python tests artifacts - if: steps.python_tests.outcome == 'failure' + if: ${{ (steps.python_tests.outcome == 'failure') && matrix.light_runtime == 'off' }} run: docker cp kphp-build-container-${{matrix.os}}:${{env.kphp_root_dir}}/tests/python/_tmp/ ${{runner.temp}} && rm -rf ${{runner.temp}}/_tmp/*/working_dir - name: Upload python tests artifacts uses: actions/upload-artifact@v3 - if: steps.python_tests.outcome == 'failure' + if: ${{ (steps.python_tests.outcome == 'failure') && matrix.light_runtime == 'off' }} with: path: ${{runner.temp}}/_tmp/ - name: Fail pipeline if python tests failed - if: steps.python_tests.outcome == 'failure' + if: ${{ (steps.python_tests.outcome == 'failure') && matrix.light_runtime == 'off' }} run: exit 1 - name: Remove docker container diff --git a/.github/workflows/Dockerfile.focal b/.github/workflows/Dockerfile.focal index 255e51a82d..8df13e0327 100644 --- a/.github/workflows/Dockerfile.focal +++ b/.github/workflows/Dockerfile.focal @@ -6,13 +6,17 @@ COPY tests/python/requirements.txt /tmp/ RUN apt-get update && \ apt-get install -y --no-install-recommends apt-utils ca-certificates gnupg wget pkg-config software-properties-common && \ wget -qO /etc/apt/trusted.gpg.d/vkpartner.asc https://artifactory-external.vkpartner.ru/artifactory/api/gpg/key/public && \ + wget -qO - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - && \ + add-apt-repository ppa:ubuntu-toolchain-r/test && \ + echo "deb http://apt.llvm.org/focal/ llvm-toolchain-focal-18 main" >> /etc/apt/sources.list && \ + echo "deb-src http://apt.llvm.org/focal/ llvm-toolchain-focal-18 main" >> /etc/apt/sources.list && \ echo "deb https://artifactory-external.vkpartner.ru/artifactory/kphp focal main" >> /etc/apt/sources.list && \ echo "deb http://apt.postgresql.org/pub/repos/apt focal-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ add-apt-repository ppa:deadsnakes/ppa && \ apt-get update && \ apt-get install -y --no-install-recommends \ - git cmake make clang g++ g++-10 gperf netcat \ + git cmake make clang g++ g++-10 g++-11 clang-18 libclang-rt-18-dev clang-format-18 gperf netcat \ python3.7 python3-pip python3.7-distutils python3.7-dev libpython3.7-dev python3-jsonschema python3-setuptools mysql-server libmysqlclient-dev && \ python3.7 -m pip install pip && python3.7 -m pip install -r /tmp/requirements.txt && \ apt-get install -y --no-install-recommends curl-kphp-vk kphp-timelib libuber-h3-dev libfmt-dev libgtest-dev libgmock-dev libre2-dev libpcre3-dev \ diff --git a/builtin-functions/kphp-full/_functions.txt b/builtin-functions/kphp-full/_functions.txt index 48b2d84f79..0e355b2f62 100644 --- a/builtin-functions/kphp-full/_functions.txt +++ b/builtin-functions/kphp-full/_functions.txt @@ -124,6 +124,8 @@ global $_COOKIE; global $_REQUEST; /** @var mixed $_ENV */ global $_ENV; +/** @var mixed $_SESSION */ +global $_SESSION; /** @var mixed $argc */ global $argc; /** @var mixed $argv */ @@ -775,6 +777,21 @@ function mb_strtolower ($str ::: string, $encoding ::: string = "cp1251") ::: st function mb_strtoupper ($str ::: string, $encoding ::: string = "cp1251") ::: string; function mb_substr ($str ::: string, $start ::: int, $length ::: mixed = PHP_INT_MAX, $encoding ::: string = "cp1251") ::: string; +function session_start($options ::: mixed = array()) ::: bool; +function session_abort() ::: bool; +function session_commit() ::: bool; +function session_write_close() ::: bool; +function session_gc() ::: bool; +function session_status() ::: int; +function session_id($id ::: ?string = null) ::: string | false; +function session_encode() ::: string | false; +function session_decode($data ::: string) ::: bool; +function session_get_cookie_params() ::: array; +function session_reset() ::: bool; +function session_save_path($path ::: ?string = null) ::: string | false; +function session_unset() ::: bool; +// function session_destroy() ::: bool; + define('PHP_ROUND_HALF_UP', 123423141); define('PHP_ROUND_HALF_DOWN', 123423144); define('PHP_ROUND_HALF_EVEN', 123423145); diff --git a/cmake/init-compilation-flags.cmake b/cmake/init-compilation-flags.cmake index b78f922259..5b94fda501 100644 --- a/cmake/init-compilation-flags.cmake +++ b/cmake/init-compilation-flags.cmake @@ -10,7 +10,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES Clang) set(COMPILER_CLANG True) elseif(CMAKE_CXX_COMPILER_ID MATCHES GNU) if (COMPILE_RUNTIME_LIGHT) - check_compiler_version(gcc 10.1.0) + check_compiler_version(gcc 11.4.0) else() check_compiler_version(gcc 8.3.0) endif() @@ -139,6 +139,7 @@ if(COMPILE_RUNTIME_LIGHT) "${PROJECT_BINARY_DIR}/tmp" "${PROJECT_BINARY_DIR}/check_coroutine_include.cpp" COMPILE_DEFINITIONS "${TRY_COMPILE_COMPILE_OPTIONS}" + CXX_STANDARD 20 ) if(NOT HAS_COROUTINE) message(FATAL_ERROR "Compiler or libstdc++ does not support coroutines") diff --git a/common/dl-utils-lite.cpp b/common/dl-utils-lite.cpp index 8648274d6c..6bf9d35218 100644 --- a/common/dl-utils-lite.cpp +++ b/common/dl-utils-lite.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -49,9 +50,9 @@ double dl_time() { } void dl_print_backtrace(void **trace, int trace_size) { - write (2, "\n------- Stack Backtrace -------\n", 33); + std::ignore = write (2, "\n------- Stack Backtrace -------\n", 33); backtrace_symbols_fd (trace, trace_size, 2); - write (2, "-------------------------------\n", 32); + std::ignore = write (2, "-------------------------------\n", 32); } void dl_print_backtrace() { @@ -71,7 +72,7 @@ void dl_print_backtrace_gdb() { name_buf[res] = 0; int child_pid = fork(); if (child_pid < 0) { - write (2, "Can't fork() to run gdb\n", 24); + std::ignore = write (2, "Can't fork() to run gdb\n", 24); _exit (0); } if (!child_pid) { @@ -83,7 +84,7 @@ void dl_print_backtrace_gdb() { waitpid (child_pid, nullptr, 0); } } else { - write (2, "can't get name of executable file to pass to gdb\n", 49); + std::ignore = write (2, "can't get name of executable file to pass to gdb\n", 49); } } diff --git a/compiler/compiler-settings.cpp b/compiler/compiler-settings.cpp index cca200a018..87a8c2a7ce 100644 --- a/compiler/compiler-settings.cpp +++ b/compiler/compiler-settings.cpp @@ -314,6 +314,7 @@ void CompilerSettings::init() { ss << " -std=c++17"; #elif __cplusplus <= 202002L ss << " -std=c++20"; + ss << " -Wno-type-limits -Wno-attributes -Wno-ignored-attributes"; #else #error unsupported __cplusplus value #endif diff --git a/compiler/data/var-data.cpp b/compiler/data/var-data.cpp index 6c7dc36915..5474e420b5 100644 --- a/compiler/data/var-data.cpp +++ b/compiler/data/var-data.cpp @@ -35,7 +35,8 @@ const ClassMemberInstanceField *VarData::as_class_instance_field() const { bool VarData::does_name_eq_any_language_superglobal(const std::string &name) { // these vars are 'superglobals' in PHP language: they are available in all scopes static const std::unordered_set superglobal_names = { - "_SERVER", "_GET", "_POST", "_ENV", "_FILES", "_COOKIE", "_REQUEST", + "_SERVER", "_GET", "_POST", "_ENV", "_FILES", "_COOKIE", "_REQUEST", + "_SESSION", "_KPHPSESSARR" }; return superglobal_names.find(name) != superglobal_names.end(); } @@ -44,7 +45,7 @@ bool VarData::does_name_eq_any_builtin_runtime(const std::string &name) { // these vars are runtime built-ins, see PhpScriptBuiltInSuperGlobals static const std::unordered_set runtime_names = { "_SERVER", "_GET", "_POST", "_ENV", "_FILES", "_COOKIE", "_REQUEST", - "argc", "argv", "d$PHP_SAPI" + "_SESSION", "_KPHPSESSARR", "argc", "argv", "d$PHP_SAPI" }; return runtime_names.find(name) != runtime_names.end(); } diff --git a/runtime-light/core/globals/php-script-globals.h b/runtime-light/core/globals/php-script-globals.h index a1a45b08d4..6a62197581 100644 --- a/runtime-light/core/globals/php-script-globals.h +++ b/runtime-light/core/globals/php-script-globals.h @@ -17,6 +17,8 @@ struct PhpScriptBuiltInSuperGlobals { mixed v$_FILES; mixed v$_COOKIE; mixed v$_REQUEST; + mixed v$_SESSION; + mixed v$_KPHPSESSARR; // variables below are not superglobals of the PHP language, but since they are set by runtime, // the compiler is also aware about them diff --git a/runtime-light/header.h b/runtime-light/header.h index 1d498ad26f..c5a7a38600 100644 --- a/runtime-light/header.h +++ b/runtime-light/header.h @@ -286,6 +286,7 @@ enum PollStatus vk_k2_poll(const struct ImageState *image_state, const struct Pl * for now, returning nullptr will indicate error */ struct ComponentState *vk_k2_create_component_state(const struct ImageState *image_state, const struct PlatformCtx *pt_ctx); + struct ImageState *vk_k2_create_image_state(const struct PlatformCtx *pt_ctx); const struct ImageInfo *vk_k2_describe(); diff --git a/runtime-light/utils/json-functions.cpp b/runtime-light/utils/json-functions.cpp index 5fde8e0723..58d88c05a2 100644 --- a/runtime-light/utils/json-functions.cpp +++ b/runtime-light/utils/json-functions.cpp @@ -7,13 +7,13 @@ #include "common/algorithms/find.h" #include "runtime-light/component/component.h" // -//#include "runtime/string_functions.h" +// #include "runtime/string_functions.h" // note: json-functions.cpp is used for non-typed json implementation: for json_encode() and json_decode() // for classes, e.g. `JsonEncoder::encode(new A)`, see json-writer.cpp and from/to visitors namespace { -void json_append_one_char(unsigned int c, string_buffer & sb) noexcept { +void json_append_one_char(unsigned int c, string_buffer &sb) noexcept { sb.append_char('\\'); sb.append_char('u'); sb.append_char("0123456789abcdef"[c >> 12]); @@ -22,7 +22,7 @@ void json_append_one_char(unsigned int c, string_buffer & sb) noexcept { sb.append_char("0123456789abcdef"[c & 15]); } -bool json_append_char(unsigned int c, string_buffer & sb) noexcept { +bool json_append_char(unsigned int c, string_buffer &sb) noexcept { if (c < 0x10000) { if (0xD7FF < c && c < 0xE000) { return false; @@ -39,8 +39,7 @@ bool json_append_char(unsigned int c, string_buffer & sb) noexcept { return false; } - -bool do_json_encode_string_php(const JsonPath &json_path, const char *s, int len, int64_t options, string_buffer & sb) noexcept { +bool do_json_encode_string_php(const JsonPath &json_path, const char *s, int len, int64_t options, string_buffer &sb) noexcept { int begin_pos = sb.size(); if (options & JSON_UNESCAPED_UNICODE) { sb.reserve(2 * len + 2); @@ -178,7 +177,7 @@ string JsonPath::to_string() const { } unsigned num_parts = std::clamp(depth, 0U, static_cast(arr.size())); string result; - result.reserve_at_least((num_parts+1) * 8); + result.reserve_at_least((num_parts + 1) * 8); result.push_back('/'); for (unsigned i = 0; i < num_parts; i++) { const char *key = arr[i]; @@ -200,13 +199,12 @@ string JsonPath::to_string() const { namespace impl_ { -JsonEncoder::JsonEncoder(int64_t options, bool simple_encode, const char *json_obj_magic_key) noexcept: - options_(options), - simple_encode_(simple_encode), - json_obj_magic_key_(json_obj_magic_key) { -} +JsonEncoder::JsonEncoder(int64_t options, bool simple_encode, const char *json_obj_magic_key) noexcept + : options_(options) + , simple_encode_(simple_encode) + , json_obj_magic_key_(json_obj_magic_key) {} -bool JsonEncoder::encode(bool b, string_buffer & sb) noexcept { +bool JsonEncoder::encode(bool b, string_buffer &sb) noexcept { if (b) { sb.append("true", 4); } else { @@ -215,17 +213,17 @@ bool JsonEncoder::encode(bool b, string_buffer & sb) noexcept { return true; } -bool JsonEncoder::encode_null(string_buffer & sb) const noexcept { +bool JsonEncoder::encode_null(string_buffer &sb) const noexcept { sb.append("null", 4); return true; } -bool JsonEncoder::encode(int64_t i, string_buffer & sb) noexcept { +bool JsonEncoder::encode(int64_t i, string_buffer &sb) noexcept { sb << i; return true; } -bool JsonEncoder::encode(double d, string_buffer & sb) noexcept { +bool JsonEncoder::encode(double d, string_buffer &sb) noexcept { if (vk::any_of_equal(std::fpclassify(d), FP_INFINITE, FP_NAN)) { php_warning("%s: strange double %lf in function json_encode", json_path_.to_string().c_str(), d); if (options_ & JSON_PARTIAL_OUTPUT_ON_ERROR) { @@ -234,17 +232,17 @@ bool JsonEncoder::encode(double d, string_buffer & sb) noexcept { return false; } } else { - //todo:k2 implement f$number_format - sb << /*(simple_encode_ ? f$number_format(d, 6, string{"."}, string{}) : */ string{d}/*)*/; + // todo:k2 implement f$number_format + sb << /*(simple_encode_ ? f$number_format(d, 6, string{"."}, string{}) : */ string{d} /*)*/; } return true; } -bool JsonEncoder::encode(const string &s, string_buffer & sb) noexcept { +bool JsonEncoder::encode(const string &s, string_buffer &sb) noexcept { return do_json_encode_string_php(json_path_, s.c_str(), s.size(), options_, sb); } -bool JsonEncoder::encode(const mixed &v, string_buffer & sb) noexcept { +bool JsonEncoder::encode(const mixed &v, string_buffer &sb) noexcept { switch (v.get_type()) { case mixed::type::NUL: return encode_null(sb); @@ -280,29 +278,22 @@ bool do_json_decode(const char *s, int s_len, int &i, mixed &v, const char *json json_skip_blanks(s, i); switch (s[i]) { case 'n': - if (s[i + 1] == 'u' && - s[i + 2] == 'l' && - s[i + 3] == 'l') { + if (s[i + 1] == 'u' && s[i + 2] == 'l' && s[i + 3] == 'l') { i += 4; return true; } break; case 't': - if (s[i + 1] == 'r' && - s[i + 2] == 'u' && - s[i + 3] == 'e') { + if (s[i + 1] == 'r' && s[i + 2] == 'u' && s[i + 3] == 'e') { i += 4; - new(&v) mixed(true); + new (&v) mixed(true); return true; } break; case 'f': - if (s[i + 1] == 'a' && - s[i + 2] == 'l' && - s[i + 3] == 's' && - s[i + 4] == 'e') { + if (s[i + 1] == 'a' && s[i + 2] == 'l' && s[i + 3] == 's' && s[i + 4] == 'e') { i += 5; - new(&v) mixed(false); + new (&v) mixed(false); return true; } break; @@ -364,8 +355,7 @@ bool do_json_decode(const char *s, int s_len, int &i, mixed &v, const char *json } if (0xD7FF < num && num < 0xE000) { - if (s[i + 1] == '\\' && s[i + 2] == 'u' && - isxdigit(s[i + 3]) && isxdigit(s[i + 4]) && isxdigit(s[i + 5]) && isxdigit(s[i + 6])) { + if (s[i + 1] == '\\' && s[i + 2] == 'u' && isxdigit(s[i + 3]) && isxdigit(s[i + 4]) && isxdigit(s[i + 5]) && isxdigit(s[i + 6])) { i += 2; int u = 0; for (int t = 0; t < 4; t++) { @@ -419,7 +409,7 @@ bool do_json_decode(const char *s, int s_len, int &i, mixed &v, const char *json } value.shrink(l); - new(&v) mixed(value); + new (&v) mixed(value); i++; return true; } @@ -446,7 +436,7 @@ bool do_json_decode(const char *s, int s_len, int &i, mixed &v, const char *json i++; } - new(&v) mixed(res); + new (&v) mixed(res); return true; } case '{': { @@ -483,7 +473,7 @@ bool do_json_decode(const char *s, int s_len, int &i, mixed &v, const char *json res[string{json_obj_magic_key}] = true; } - new(&v) mixed(res); + new (&v) mixed(res); return true; } default: { @@ -495,7 +485,7 @@ bool do_json_decode(const char *s, int s_len, int &i, mixed &v, const char *json int64_t intval = 0; if (php_try_to_int(s + i, j - i, &intval)) { i = j; - new(&v) mixed(intval); + new (&v) mixed(intval); return true; } @@ -503,7 +493,7 @@ bool do_json_decode(const char *s, int s_len, int &i, mixed &v, const char *json double floatval = strtod(s + i, &end_ptr); if (end_ptr == s + j) { i = j; - new(&v) mixed(floatval); + new (&v) mixed(floatval); return true; } } diff --git a/runtime-light/utils/json-functions.h b/runtime-light/utils/json-functions.h index 9f3dc26782..e3496122fc 100644 --- a/runtime-light/utils/json-functions.h +++ b/runtime-light/utils/json-functions.h @@ -9,7 +9,6 @@ #include "common/mixin/not_copyable.h" #include "runtime-core/runtime-core.h" - constexpr int64_t JSON_UNESCAPED_UNICODE = 1; constexpr int64_t JSON_FORCE_OBJECT = 16; constexpr int64_t JSON_PRETTY_PRINT = 128; // TODO: add actual support to untyped @@ -22,7 +21,7 @@ constexpr int64_t JSON_AVAILABLE_FLAGS_TYPED = JSON_PRETTY_PRINT | JSON_PRESERVE struct JsonPath { constexpr static int MAX_DEPTH = 8; - std::array arr; + std::array arr; unsigned depth = 0; void enter(const char *key) noexcept { @@ -47,31 +46,31 @@ class JsonEncoder : vk::not_copyable { public: JsonEncoder(int64_t options, bool simple_encode, const char *json_obj_magic_key = nullptr) noexcept; - //todo:k2 change static_SB everywhere to string_buffer arg - bool encode(bool b, string_buffer & sb) noexcept; - bool encode(int64_t i, string_buffer & sb) noexcept; - bool encode(const string &s, string_buffer & sb) noexcept; - bool encode(double d, string_buffer & sb) noexcept; - bool encode(const mixed &v, string_buffer & sb) noexcept; + // todo:k2 change static_SB everywhere to string_buffer arg + bool encode(bool b, string_buffer &sb) noexcept; + bool encode(int64_t i, string_buffer &sb) noexcept; + bool encode(const string &s, string_buffer &sb) noexcept; + bool encode(double d, string_buffer &sb) noexcept; + bool encode(const mixed &v, string_buffer &sb) noexcept; template - bool encode(const array &arr, string_buffer & sb) noexcept; + bool encode(const array &arr, string_buffer &sb) noexcept; template - bool encode(const Optional &opt, string_buffer & sb) noexcept; + bool encode(const Optional &opt, string_buffer &sb) noexcept; private: - bool encode_null(string_buffer & sb) const noexcept; + bool encode_null(string_buffer &sb) const noexcept; JsonPath json_path_; const int64_t options_{0}; - //todo:k2 use simple_encode + // todo:k2 use simple_encode [[maybe_unused]] const bool simple_encode_{false}; const char *json_obj_magic_key_{nullptr}; }; template -bool JsonEncoder::encode(const array &arr, string_buffer & sb) noexcept { +bool JsonEncoder::encode(const array &arr, string_buffer &sb) noexcept { bool is_vector = arr.is_vector(); const bool force_object = static_cast(JSON_FORCE_OBJECT & options_); if (!force_object && !is_vector && arr.is_pseudo_vector()) { @@ -142,7 +141,7 @@ bool JsonEncoder::encode(const array &arr, string_buffer & sb) noexcept { } template -bool JsonEncoder::encode(const Optional &opt, string_buffer & sb) noexcept { +bool JsonEncoder::encode(const Optional &opt, string_buffer &sb) noexcept { switch (opt.value_state()) { case OptionalState::has_value: return encode(opt.val(), sb); @@ -170,7 +169,7 @@ Optional f$json_encode(const T &v, int64_t options = 0, bool simple_enco return sb.c_str(); } -//todo:k2 implement string f$vk_json_encode_safe(const T &v, bool simple_encode = true) noexcept +// todo:k2 implement string f$vk_json_encode_safe(const T &v, bool simple_encode = true) noexcept template inline Optional f$vk_json_encode(const T &v) noexcept { diff --git a/runtime-light/utils/panic.h b/runtime-light/utils/panic.h new file mode 100644 index 0000000000..7dfe5e956a --- /dev/null +++ b/runtime-light/utils/panic.h @@ -0,0 +1,24 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2024 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include + +#include "context.h" +#include "runtime-light/component/component.h" +#include "runtime-light/utils/logs.h" + +inline void critical_error_handler() { + constexpr const char *message = "script panic"; + const PlatformCtx &ptx = *get_platform_context(); + ComponentState &ctx = *get_component_context(); + ptx.log(Debug, strlen(message), message); + + if (ctx.not_finished()) { + ctx.poll_status = PollStatus::PollFinishedError; + } + ptx.abort(); + exit(1); +} diff --git a/runtime-light/utils/php_assert.h b/runtime-light/utils/php_assert.h new file mode 100644 index 0000000000..81d534e5dd --- /dev/null +++ b/runtime-light/utils/php_assert.h @@ -0,0 +1,13 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2020 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include + +#include "common/mixin/not_copyable.h" +#include "common/wrappers/likely.h" + +#include "runtime-core/utils/kphp-assert-core.h" diff --git a/runtime-light/utils/to-array-processor.h b/runtime-light/utils/to-array-processor.h index 1824313c0b..5d2dfca891 100644 --- a/runtime-light/utils/to-array-processor.h +++ b/runtime-light/utils/to-array-processor.h @@ -31,4 +31,4 @@ class ShapeKeyDemangle : vk::not_copyable { bool inited_{false}; std::unordered_map shape_keys_storage_; -}; +}; \ No newline at end of file diff --git a/runtime/interface.cpp b/runtime/interface.cpp index 76f75e837c..6568d7b605 100644 --- a/runtime/interface.cpp +++ b/runtime/interface.cpp @@ -372,7 +372,7 @@ void f$send_http_103_early_hints(const array & headers) { http_send_immediate_response(header.c_str(), header.size(), "\r\n", 2); } -void f$setrawcookie(const string &name, const string &value, int64_t expire, const string &path, const string &domain, bool secure, bool http_only) { +void setrawcookie_impl(const string &name, const string &value, int64_t expire, const string &path, const string &domain, bool secure, bool http_only, const string &samesite) { string date = f$gmdate(HTTP_DATE, expire); kphp_runtime_context.static_SB_spare.clean() << "Set-Cookie: " << name << '='; @@ -397,9 +397,45 @@ void f$setrawcookie(const string &name, const string &value, int64_t expire, con if (http_only) { kphp_runtime_context.static_SB_spare << "; HttpOnly"; } + if (!samesite.empty()) { + kphp_runtime_context.static_SB_spare << "; SameSite=" << samesite; + } header(kphp_runtime_context.static_SB_spare.c_str(), (int)kphp_runtime_context.static_SB_spare.size(), false); } +void setrawcookie_array(const string &name, const string &value, const array &options) { + int64_t expire = 0; + string path = string(); + string domain = string(); + string samesite = string(); + bool secure = false, http_only = false; + + for (auto it = options.begin(); it != options.end(); ++it) { + string key = it.get_key().to_string(); + if (key == string("expires")) { + expire = it.get_value().to_int(); + } else if (key == string("path")) { + path = it.get_value().to_string(); + } else if (key == string("domain")) { + domain = it.get_value().to_string(); + } else if (key == string("samesite")) { + samesite = it.get_value().to_string(); + } else if (key == string("secure")) { + secure = it.get_value().to_bool(); + } else if (key == string("httponly")) { + http_only = it.get_value().to_bool(); + } else { + php_warning("setcookie(): option \"%s\" is invalid", it.get_key().to_string().c_str()); + return; + } + } + setrawcookie_impl(name, value, expire, path, domain, secure, http_only, samesite); +} + +void f$setrawcookie(const string &name, const string &value, int64_t expire, const string &path, const string &domain, bool secure, bool http_only) { + setrawcookie_impl(name, value, expire, path, domain, secure, http_only, string("")); +} + void f$setcookie(const string &name, const string &value, int64_t expire, const string &path, const string &domain, bool secure, bool http_only) { f$setrawcookie(name, f$urlencode(value), expire, path, domain, secure, http_only); } @@ -892,7 +928,6 @@ string f$php_sapi_name() { return PhpScriptMutableGlobals::current().get_superglobals().v$d$PHP_SAPI; } - static std::aligned_storage_t), alignof(array)> uploaded_files_storage; static array *uploaded_files = reinterpret_cast *> (&uploaded_files_storage); static long long uploaded_files_last_query_num = -1; @@ -1459,6 +1494,8 @@ static void reset_superglobals(PhpScriptBuiltInSuperGlobals &superglobals) { hard_reset_var(superglobals.v$_FILES, array()); hard_reset_var(superglobals.v$_COOKIE, array()); hard_reset_var(superglobals.v$_REQUEST, array()); + hard_reset_var(superglobals.v$_SESSION, array()); + hard_reset_var(superglobals.v$_KPHPSESSARR, array()); dl::leave_critical_section(); } diff --git a/runtime/interface.h b/runtime/interface.h index b1a05f8e55..51b9d3b811 100644 --- a/runtime/interface.h +++ b/runtime/interface.h @@ -56,6 +56,8 @@ void f$setcookie(const string &name, const string &value, int64_t expire = 0, co void f$setrawcookie(const string &name, const string &value, int64_t expire = 0, const string &path = string(), const string &domain = string(), bool secure = false, bool http_only = false); +void setrawcookie_array(const string &name, const string &value, const array &options = array()); + int64_t f$ignore_user_abort(Optional enable = Optional()); enum class ShutdownType { diff --git a/runtime/php-script-globals.h b/runtime/php-script-globals.h index a81bd03d64..b85e164b3d 100644 --- a/runtime/php-script-globals.h +++ b/runtime/php-script-globals.h @@ -17,6 +17,8 @@ struct PhpScriptBuiltInSuperGlobals { mixed v$_FILES; mixed v$_COOKIE; mixed v$_REQUEST; + mixed v$_SESSION; + mixed v$_KPHPSESSARR; // variables below are not superglobals of the PHP language, but since they are set by runtime, // the compiler is also aware about them diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 0739061987..b107b0d15a 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -129,7 +129,8 @@ prepend(KPHP_RUNTIME_SOURCES ${BASE_DIR}/runtime/ vkext_stats.cpp ffi.cpp zlib.cpp - zstd.cpp) + zstd.cpp + sessions.cpp) set_source_files_properties( ${BASE_DIR}/server/php-engine.cpp diff --git a/runtime/sessions.cpp b/runtime/sessions.cpp new file mode 100644 index 0000000000..92d77d3c90 --- /dev/null +++ b/runtime/sessions.cpp @@ -0,0 +1,623 @@ +#include +#include +#include +#include + +#include "runtime/sessions.h" +#include "runtime/interface.h" +#include "runtime/php-script-globals.h" +#include "runtime/files.h" +#include "runtime/serialize-functions.h" +#include "runtime/misc.h" +#include "common/wrappers/to_array.h" +#include "runtime/url.h" +#include "runtime/math_functions.h" +#include "runtime/string_functions.h" + +namespace sessions { + +static mixed get_sparam(const char *key) noexcept; +static mixed get_sparam(const string &key) noexcept; +static void set_sparam(const char *key, const mixed &value) noexcept; +static void set_sparam(const string &key, const mixed &value) noexcept; +static void reset_sparams() noexcept; +static void initialize_sparams(const array &options) noexcept; + +static bool session_start(); +static bool session_initialize(); +static bool session_generate_id(); +static bool session_valid_id(const string &id); +static bool session_abort(); +static bool session_reset_id(); +static bool session_send_cookie(); +static int session_gc(const bool &immediate); +static bool session_flush(); +static string session_encode(); +static bool session_decode(const string &data); + +static bool session_open(); +static bool session_read(); +static bool session_write(); +static void session_close(); + +constexpr static auto S_READ_CLOSE = "read_and_close"; +constexpr static auto S_ID = "session_id"; +constexpr static auto S_FD = "handler"; +constexpr static auto S_STATUS = "session_status"; +constexpr static auto S_DIR = "save_path"; +constexpr static auto S_PATH = "session_path"; +constexpr static auto S_NAME = "name"; +constexpr static auto S_CTIME = "session_ctime"; +constexpr static auto S_LIFETIME = "gc_maxlifetime"; +constexpr static auto S_PROBABILITY = "gc_probability"; +constexpr static auto S_DIVISOR = "gc_divisor"; +constexpr static auto S_SEND_COOKIE = "send_cookie"; +constexpr static auto S_STRICT_MODE = "use_strict_mode"; +constexpr static auto S_ID_LENGTH = "sid_length"; +constexpr static auto S_LAZY_WRITE = "lazy_write"; +constexpr static auto S_VARS = "session_buffered_variables"; +constexpr static auto C_PATH = "cookie_path"; +constexpr static auto C_LIFETIME = "cookie_lifetime"; +constexpr static auto C_DOMAIN = "cookie_domain"; +constexpr static auto C_SECURE = "cookie_secure"; +constexpr static auto C_HTTPONLY = "cookie_httponly"; +constexpr static auto C_SAMESITE = "cookie_samesite"; + +constexpr static auto MAX_SID_LENGTH = 256; + +// TO-DO: reconsider it +const auto skeys = vk::to_array>({ + {S_READ_CLOSE, false}, + {S_DIR, string((getenv("TMPDIR") != NULL) ? getenv("TMPDIR") : "/tmp/").append("sessions/")}, + {S_NAME, string("PHPSESSID")}, + {S_LIFETIME, 1440}, + {S_PROBABILITY, 1}, + {S_DIVISOR, 100}, + {S_STRICT_MODE, false}, + {S_ID_LENGTH, 32}, + {S_LAZY_WRITE, true}, + {C_PATH, string("/")}, + {C_LIFETIME, 0}, + {C_DOMAIN, string("")}, + {C_SECURE, false}, + {C_HTTPONLY, false}, + {C_SAMESITE, string("")} +}); + +static int set_tag(const char *path, const char *name, void *value, const size_t size) { +#if defined(__APPLE__) + return setxattr(path, name, value, size, 0, 0); +#else + return setxattr(path, name, value, size, 0); +#endif +} + +static int get_tag(const char *path, const char *name, void *value, const size_t size) { +#if defined(__APPLE__) + return getxattr(path, name, value, size, 0, 0); +#else + return getxattr(path, name, value, size); +#endif +} + +static void initialize_sparams(const array &options) noexcept { + for (const auto& it : skeys) { + if (options.isset(string(it.first))) { + set_sparam(it.first, options.get_value(string(it.first))); + continue; + } + set_sparam(it.first, mixed(it.second)); + } + + if (get_sparam(S_ID_LENGTH).to_int() < 22 || get_sparam(S_ID_LENGTH).to_int() > MAX_SID_LENGTH) { + set_sparam(S_ID_LENGTH, 32); + php_warning("session.configuration \"session.sid_length\" must be between 22 and 256. The default value is set"); + } +} + +static array session_get_cookie_params() { + array result; + if (PhpScriptMutableGlobals::current().get_superglobals().v$_KPHPSESSARR.as_array().empty()) { + php_warning("Session cookie params cannot be received when there is no active session. Returned the default params"); + result.emplace_value(string(C_PATH), skeys[9].second); + result.emplace_value(string(C_LIFETIME), skeys[10].second); + result.emplace_value(string(C_DOMAIN), skeys[11].second); + result.emplace_value(string(C_SECURE), skeys[12].second); + result.emplace_value(string(C_HTTPONLY), skeys[13].second); + result.emplace_value(string(C_SAMESITE), skeys[14].second); + } else { + result.emplace_value(string(C_PATH), get_sparam(C_PATH)); + result.emplace_value(string(C_LIFETIME), get_sparam(C_LIFETIME)); + result.emplace_value(string(C_DOMAIN), get_sparam(C_DOMAIN)); + result.emplace_value(string(C_SECURE), get_sparam(C_SECURE)); + result.emplace_value(string(C_HTTPONLY), get_sparam(C_HTTPONLY)); + result.emplace_value(string(C_SAMESITE), get_sparam(C_SAMESITE)); + } + return result; +} + +static void reset_sparams() noexcept { + PhpScriptMutableGlobals::current().get_superglobals().v$_KPHPSESSARR.as_array().clear(); +} + +static mixed get_sparam(const char *key) noexcept { + return get_sparam(string(key)); +} + +static mixed get_sparam(const string &key) noexcept { + PhpScriptMutableGlobals &php_globals = PhpScriptMutableGlobals::current(); + if (!php_globals.get_superglobals().v$_KPHPSESSARR.as_array().isset(key)) { + return false; + } + return php_globals.get_superglobals().v$_KPHPSESSARR.as_array().get_value(key); +} + +static void set_sparam(const char *key, const mixed &value) noexcept { + set_sparam(string(key), value); +} + +static void set_sparam(const string &key, const mixed &value) noexcept { + PhpScriptMutableGlobals::current().get_superglobals().v$_KPHPSESSARR.as_array().emplace_value(key, value); +} + +static bool session_valid_id(const string &id) { + if (id.empty()) { + return false; + } + + bool result = true; + for (auto i = 0; i < id.size(); ++i) { + if (!((id[i] >= 'a' && id[i] <= 'z') + || (id[i] >= 'A' && id[i] <= 'Z') + || (id[i] >= '0' && id[i] <= '9') + || (id[i] == ',') + || (id[i] == '-'))) { + result = false; + break; + } + } + + return result; +} + +static bool session_generate_id() { + Optional id = f$random_bytes(get_sparam(S_ID_LENGTH).to_int() / 2); + for (uint i = 0; i < 3 && (id.is_false() || !session_valid_id(f$bin2hex(id.val()))); ++i) { + id = f$random_bytes(get_sparam(S_ID_LENGTH).to_int() / 2); + } + + if (id.is_false() || !session_valid_id(f$bin2hex(id.val()))) { + php_warning("Failed to create new ID\n"); + return false; + } + set_sparam(S_ID, f$bin2hex(id.val())); + return true; +} + +static bool session_abort() { + if (get_sparam(S_STATUS).to_bool()) { + session_close(); + return true; + } + return false; +} + +static string session_encode() { + return f$serialize(PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION.as_array()); +} + +static bool session_decode(const string &data) { + mixed buf = f$unserialize(data); + if (buf.is_bool()) { + return false; + } + PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION = buf.as_array(); + return true; +} + +static bool session_open() { + if (get_sparam(S_FD).to_bool()) { + lseek(get_sparam(S_FD).to_int(), 0, SEEK_SET); + return true; + } + + if (!f$file_exists(get_sparam(S_DIR).to_string()) && !f$mkdir(get_sparam(S_DIR).to_string())) { + php_warning("Failed to create session directory on path %s", get_sparam(S_DIR).to_string().c_str()); + return false; + } + + set_sparam(S_PATH, string(get_sparam(S_DIR).to_string()).append(get_sparam(S_ID).to_string())); + bool is_new = (!f$file_exists(get_sparam(S_PATH).to_string())) ? 1 : 0; + + set_sparam(S_FD, open_safe(get_sparam(S_PATH).to_string().c_str(), O_RDWR | O_CREAT, 0666)); + + if (get_sparam(S_FD).to_int() < 0) { + php_warning("Failed to open the file %s", get_sparam(S_PATH).to_string().c_str()); + return false; + } + + lockf(get_sparam(S_FD).to_int(), F_LOCK, 0); + + // set new metadata to the file + int ret_ctime = get_tag(get_sparam(S_PATH).to_string().c_str(), S_CTIME, NULL, 0); + int ret_gc_lifetime = get_tag(get_sparam(S_PATH).to_string().c_str(), S_LIFETIME, NULL, 0); + if (is_new or ret_ctime < 0) { // add the creation data to metadata of file + int is_session = 1; // to filter sessions from other files in session_gc() + set_tag(get_sparam(S_PATH).to_string().c_str(), "is_session", &is_session, sizeof(int)); + + int ctime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + set_tag(get_sparam(S_PATH).to_string().c_str(), S_CTIME, &ctime, sizeof(int)); + } + if (ret_gc_lifetime < 0) { + int gc_maxlifetime = get_sparam(S_LIFETIME).to_int(); + set_tag(get_sparam(S_PATH).to_string().c_str(), S_LIFETIME, &gc_maxlifetime, sizeof(int)); + } + + return true; +} + +static void session_close() { + if (get_sparam(S_FD).to_bool()) { + lockf(get_sparam(S_FD).to_int(), F_ULOCK, 0); + close_safe(get_sparam(S_FD).to_int()); + } + set_sparam(S_STATUS, false); + reset_sparams(); +} + +static bool session_read() { + session_open(); + struct stat buf; + if (fstat(get_sparam(S_FD).to_int(), &buf) < 0) { + php_warning("Failed to read session data on path %s", get_sparam(S_PATH).to_string().c_str()); + return false; + } + + if (buf.st_size == 0) { + PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION.as_array().clear(); + return true; + } + + char result[buf.st_size]; + ssize_t n = read_safe(get_sparam(S_FD).to_int(), result, buf.st_size, get_sparam(S_PATH).to_string()); + if (n < buf.st_size) { + if (n == -1) { + php_warning("Read failed"); + } else { + php_warning("Read returned less bytes than requested"); + } + return false; + } + + string read_data(result, n); + if (get_sparam(S_LAZY_WRITE).to_bool()) { + set_sparam(S_VARS, read_data); + } + + if (!session_decode(read_data)) { + php_warning("Failed to unzerialize the data"); + return false; + } + return true; +} + +static bool session_write() { + // rewind the fd of the session file + session_open(); + + string data = f$serialize(PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION.as_array()); + if (get_sparam(S_LAZY_WRITE).to_bool() && get_sparam(S_VARS).to_string() == data) { + return true; + } + + ssize_t n = write_safe(get_sparam(S_FD).to_int(), data.c_str(), data.size(), get_sparam(S_PATH).to_string()); + if (n < data.size()) { + if (n == -1) { + php_warning("Write failed"); + } else { + php_warning("Write wrote less bytes than requested"); + } + return false; + } + return true; +} + +static bool session_send_cookie() { + if (strpbrk(get_sparam(S_NAME).to_string().c_str(), "=,;.[ \t\r\n\013\014") != NULL) { + php_warning("session.name cannot contain any of the following '=,;.[ \\t\\r\\n\\013\\014'"); + return false; + } + + int expire = get_sparam(C_LIFETIME).to_int(); + if (expire > 0) { + expire += std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + } + + array cookie_options; + cookie_options.emplace_value(string("expires"), expire); + cookie_options.emplace_value(string("path"), get_sparam(C_PATH).to_string()); + cookie_options.emplace_value(string("domain"), get_sparam(C_DOMAIN).to_string()); + cookie_options.emplace_value(string("secure"), get_sparam(C_SECURE).to_bool()); + cookie_options.emplace_value(string("httponly"), get_sparam(C_HTTPONLY).to_bool()); + cookie_options.emplace_value(string("samesite"), get_sparam(S_NAME).to_string()); + + string name = get_sparam(S_NAME).to_string(); + string sid = f$urlencode(get_sparam(S_ID).to_string()); + + setrawcookie_array(name, sid, cookie_options); + return true; +} + +static bool session_reset_id() { + if (!get_sparam(S_ID).to_bool()) { + php_warning("Cannot set session ID - session ID is not initialized"); + return false; + } + + if (get_sparam(S_SEND_COOKIE).to_bool()) { + session_send_cookie(); + set_sparam(S_SEND_COOKIE, false); + } + return true; +} + +static bool session_expired(const string &path) { + int ctime, lifetime; + int ret_ctime = get_tag(path.c_str(), S_CTIME, &ctime, sizeof(int)); + int ret_lifetime = get_tag(path.c_str(), S_LIFETIME, &lifetime, sizeof(int)); + if (ret_ctime < 0 or ret_lifetime < 0) { + php_warning("Failed to get metadata of the file on path: %s", path.c_str()); + return false; + } + + int now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + if (ctime + lifetime <= now) { + return true; + } + return false; +} + +static int session_gc(const bool &immediate = false) { + double prob = f$lcg_value() * get_sparam(S_DIVISOR).to_float(); + double s_prob = get_sparam(S_PROBABILITY).to_float(); + if ((!immediate) && ((s_prob <= 0) or (prob >= s_prob))) { + return -1; + } + + mixed s_list = f$scandir(get_sparam(S_DIR).to_string()); + if (!s_list.to_bool()) { + php_warning("Failed to scan the session directory on the save_path: %s", get_sparam(S_DIR).to_string().c_str()); + return -1; + } + + // reset the fd before changing the session directory + close_safe(get_sparam(S_FD).to_int()); + + int result = 0; + for (auto s = s_list.as_array().begin(); s != s_list.as_array().end(); ++s) { + string path = s.get_value().to_string(); + if (path[0] == '.') { + continue; + } + path = string(get_sparam(S_DIR).to_string()).append(path); + if (path == get_sparam(S_PATH).to_string()) { + continue; + } + + { // filter session files from others + int is_session, ret_is_session = get_tag(path.c_str(), "is_session", &is_session, sizeof(int)); + if (ret_is_session < 0) { + continue; + } + } + + int fd; + if ((fd = open_safe(path.c_str(), O_RDWR, 0666)) < 0) { + php_warning("Failed to open file on path: %s", path.c_str()); + continue; + } + + if (lockf(fd, F_TEST, 0) < 0) { + close_safe(fd); + continue; + } + + close_safe(fd); + if (session_expired(path)) { + f$unlink(path); + ++result; + } + } + + set_sparam(S_FD, open_safe(get_sparam(S_PATH).to_string().c_str(), O_RDWR, 0666)); + if (get_sparam(S_FD).to_int() < 0) { + php_warning("Failed to reopen the file %s after session_gc()", get_sparam(S_PATH).to_string().c_str()); + session_abort(); + } else { + lockf(get_sparam(S_FD).to_int(), F_LOCK, 0); + } + + return result; +} + +static bool session_initialize() { + set_sparam(S_STATUS, true); + + if (!get_sparam(S_ID).to_bool() + || (get_sparam(S_STRICT_MODE).to_bool() && !session_valid_id(get_sparam(S_ID).to_string()))) { + if (!session_generate_id()) { + php_warning( + "Failed to create session ID: %s (path: %s)", + get_sparam(S_NAME).to_string().c_str(), + get_sparam(S_PATH).to_string().c_str() + ); + session_abort(); + return false; + } + set_sparam(S_SEND_COOKIE, true); + } + + if (!session_open() || !session_reset_id() || !session_read()) { + session_abort(); + return false; + } + + session_gc(0); + + return true; +} + +static bool session_start() { + if (get_sparam(S_STATUS).to_bool()) { + php_warning("Ignoring session_start() because a session is already active"); + return false; + } + + set_sparam(S_SEND_COOKIE, true); + + if (!get_sparam(S_ID).to_bool()) { // Check session id in cookie values + mixed id = false; + PhpScriptMutableGlobals &php_globals = PhpScriptMutableGlobals::current(); + if (php_globals.get_superglobals().v$_COOKIE.as_array().isset(get_sparam(S_NAME).to_string())) { + id = php_globals.get_superglobals().v$_COOKIE.as_array().get_value(get_sparam(S_NAME).to_string()).to_string(); + set_sparam(S_ID, id.to_string()); + } + } + + if (get_sparam(S_ID).to_bool() && (get_sparam(S_ID).to_string().empty() || strpbrk(get_sparam(S_ID).to_string().c_str(), "\r\n\t <>'\"\\"))) { + set_sparam(S_ID, false); + } + + if (!session_initialize()) { + set_sparam(S_STATUS, false); + set_sparam(S_ID, false); + return false; + } + + return true; +} + +static bool session_flush() { + if (!get_sparam(S_STATUS).to_bool()) { + return false; + } + + session_write(); + session_close(); + return true; +} + +} // namespace sessions + +bool f$session_start(const array &options) { + if (sessions::get_sparam(sessions::S_STATUS).to_bool()) { + php_warning("Ignoring session_start() because a session is already active"); + return false; + } + + sessions::initialize_sparams(options); + sessions::session_start(); + + if (sessions::get_sparam(sessions::S_READ_CLOSE).to_bool()) { + sessions::session_close(); + } + return true; +} + +bool f$session_abort() { + if (!sessions::get_sparam(sessions::S_STATUS).to_bool()) { + return false; + } + sessions::session_abort(); + return true; +} + +Optional f$session_gc() { + if (!sessions::get_sparam(sessions::S_STATUS).to_bool()) { + php_warning("Session cannot be garbage collected when there is no active session"); + return false; + } + int result = sessions::session_gc(1); + return (result <= 0) ? Optional{false} : Optional{result}; +} + +bool f$session_write_close() { + if (!sessions::get_sparam(sessions::S_STATUS).to_bool()) { + return false; + } + sessions::session_flush(); + return true; +} + +bool f$session_commit() { + return f$session_write_close(); +} + +int64_t f$session_status() { + return sessions::get_sparam(sessions::S_STATUS).to_int() + 1; +} + +Optional f$session_encode() { + return Optional{sessions::session_encode()}; +} + +bool f$session_decode(const string &data) { + if (!sessions::get_sparam(sessions::S_STATUS).to_bool()) { + php_warning("Session data cannot be decoded when there is no active session"); + return false; + } + return sessions::session_decode(data); +} + +array f$session_get_cookie_params() { + return sessions::session_get_cookie_params(); +} + +Optional f$session_id(const Optional &id) { + if (id.has_value() && sessions::get_sparam(sessions::S_STATUS).to_bool()) { + php_warning("Session ID cannot be changed when a session is active"); + return Optional{false}; + } + mixed prev_id = sessions::get_sparam(sessions::S_ID); + if (id.has_value()) { + sessions::set_sparam(sessions::S_ID, id.val()); + } + return (prev_id.is_bool()) ? Optional{false} : Optional(prev_id.as_string()); +} + +bool f$session_reset() { + return (sessions::get_sparam(sessions::S_STATUS).to_bool() && sessions::session_initialize()); +} + +Optional f$session_save_path(const Optional &path) { + if (path.has_value() && sessions::get_sparam(sessions::S_STATUS).to_bool()) { + php_warning("Session save path cannot be changed when a session is active"); + return Optional{false}; + } + + Optional prev_path(sessions::get_sparam(sessions::S_DIR).to_string()); + if (path.has_value() && !path.val().to_string().empty()) { + sessions::set_sparam(sessions::S_DIR, path.val()); + } + return prev_path; +} + +bool f$session_unset() { + if (!sessions::get_sparam(sessions::S_STATUS).to_bool()) { + return false; + } + + PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION.as_array().clear(); + return true; +} + +/* TO-DO: +bool f$session_destroy() { + if (!sessions::get_sparam(sessions::S_STATUS).to_bool()) { + php_warning("Trying to destroy uninitialized session"); + return false; + } + sessions::session_close(); + return true; +} +*/ diff --git a/runtime/sessions.h b/runtime/sessions.h new file mode 100644 index 0000000000..91b5a91cf1 --- /dev/null +++ b/runtime/sessions.h @@ -0,0 +1,25 @@ +#pragma once + +#include "runtime-core/runtime-core.h" + +bool f$session_start(const array &options = array()); +bool f$session_abort(); +bool f$session_commit(); +bool f$session_write_close(); +Optional f$session_gc(); +int64_t f$session_status(); +Optional f$session_encode(); +bool f$session_decode(const string &data); +array f$session_get_cookie_params(); +Optional f$session_id(const Optional &id = Optional()); +bool f$session_reset(); +Optional f$session_save_path(const Optional &path = Optional()); +bool f$session_unset(); + +// TO-DO: +// bool f$session_destroy(); +// bool f$set_cookie_params(); +// bool f$session_set_cookie_params(); +// void f$session_register_shutdown(); +// bool f$session_regenerate_id(); +// bool f$session_create_id(); \ No newline at end of file diff --git a/tests/cpp/runtime/runtime-tests.cmake b/tests/cpp/runtime/runtime-tests.cmake index 731da98fe6..1466b1a33d 100644 --- a/tests/cpp/runtime/runtime-tests.cmake +++ b/tests/cpp/runtime/runtime-tests.cmake @@ -21,7 +21,8 @@ prepend(RUNTIME_TESTS_SOURCES ${BASE_DIR}/tests/cpp/runtime/ memory_resource/unsynchronized_pool_resource-test.cpp string-list-test.cpp string-test.cpp - zstd-test.cpp) + zstd-test.cpp + sessions-test.cpp) allow_deprecated_declarations_for_apple(${BASE_DIR}/tests/cpp/runtime/inter-process-mutex-test.cpp) vk_add_unittest(runtime "${RUNTIME_LIBS};${RUNTIME_LINK_TEST_LIBS}" ${RUNTIME_TESTS_SOURCES}) diff --git a/tests/cpp/runtime/sessions-test.cpp b/tests/cpp/runtime/sessions-test.cpp new file mode 100644 index 0000000000..2cf6145c8f --- /dev/null +++ b/tests/cpp/runtime/sessions-test.cpp @@ -0,0 +1,212 @@ +#include +#include + +#include "runtime/files.h" +#include "runtime/sessions.h" +#include "runtime/php-script-globals.h" +#include "runtime/serialize-functions.h" +#include "runtime/exec.h" + +constexpr int SESSION_NONE = 1; +constexpr int SESSION_ACTIVE = 2; + +const auto tmpdir_path = string(getenv("TMPDIR") != nullptr ? getenv("TMPDIR") : "/tmp/"); +const auto session_dir_path = string(tmpdir_path).append("sessions/"); +const auto example_dir_path = string(tmpdir_path).append("example/"); + +TEST(sessions_test, test_session_id_with_invalid_id) { + const string id = string("\t12345678\\\r"); + ASSERT_FALSE(f$session_id(id).has_value()); + ASSERT_TRUE(f$session_start()); + ASSERT_NE(f$session_id().val(), id); + + const string full_path = string(session_dir_path).append(f$session_id().val()); + ASSERT_TRUE(f$file_exists(full_path)); + ASSERT_TRUE(f$session_abort()); + + f$exec(string("rm -rf ").append(session_dir_path)); +} + +TEST(sessions_test, test_session_id_with_valid_id) { + const string id = string("sess_668d4f818ca3b"); + ASSERT_FALSE(f$session_id(id).has_value()); + ASSERT_TRUE(f$session_start()); + ASSERT_TRUE(f$session_id().has_value() && !f$session_id().val().empty()); + ASSERT_EQ(f$session_id().val(), id); + + const string full_path = string(session_dir_path).append(id); + ASSERT_TRUE(f$file_exists(full_path)); + ASSERT_TRUE(f$session_abort()); + + f$exec(string("rm -rf ").append(session_dir_path)); +} + +TEST(sessions_test, test_session_start) { + ASSERT_TRUE(f$session_start()); + ASSERT_FALSE(f$session_start()); + ASSERT_TRUE(f$session_abort()); + ASSERT_TRUE(f$session_start()); + ASSERT_FALSE(f$session_start()); + f$exec(string("rm -rf ").append(session_dir_path)); +} + +TEST(sessions_test, test_session_start_with_params) { + array predefined_consts = array(); + + predefined_consts.emplace_value(string("save_path"), example_dir_path); + ASSERT_TRUE(f$session_start(predefined_consts)); + ASSERT_TRUE(f$session_id().has_value() && !f$session_id().val().empty()); + ASSERT_TRUE(f$file_exists(string(example_dir_path).append(f$session_id().val()))); + ASSERT_TRUE(f$session_abort()); + + f$exec(string("rm -rf ").append(example_dir_path)); +} + +TEST(sessions_test, test_session_status) { + ASSERT_EQ(f$session_status(), SESSION_NONE); + ASSERT_TRUE(f$session_start()); + ASSERT_EQ(f$session_status(), SESSION_ACTIVE); + ASSERT_TRUE(f$session_abort()); + ASSERT_EQ(f$session_status(), SESSION_NONE); + + f$exec(string("rm -rf ").append(session_dir_path)); +} + +TEST(sessions_test, test_session_save_path) { + f$session_start(); + ASSERT_TRUE(f$session_save_path().has_value() && !f$session_save_path().val().empty()); + ASSERT_EQ(f$session_save_path().val(), session_dir_path); + f$session_abort(); + + array predefined_consts; + predefined_consts.emplace_value(string("save_path"), example_dir_path); + ASSERT_TRUE(f$session_start(predefined_consts)); + ASSERT_TRUE(f$session_save_path().has_value() && !f$session_save_path().val().empty()); + ASSERT_EQ(f$session_save_path().val(), example_dir_path); + f$session_abort(); + + f$exec(string("rm -rf ").append(session_dir_path)); + f$exec(string("rm -rf ").append(example_dir_path)); +} + +TEST(sessions_test, test_session_commit) { + f$session_start(); + const string id = f$session_id().val(); + const string file_path = string(session_dir_path).append(id); + + PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION.as_array().emplace_value(string("message"), string("hello")); + ASSERT_TRUE(f$session_commit()); + + struct stat buf; + int fd = open_safe(file_path.c_str(), O_RDWR, 0666); + fstat(fd, &buf); + char result[buf.st_size]; + ssize_t n = read_safe(fd, result, buf.st_size, file_path); + string read_data(result, n); + close_safe(fd); + + array session_array; + session_array.emplace_value(string("message"), string("hello")); + string write_data = f$serialize(session_array); + + ASSERT_EQ(read_data, write_data); + + f$session_id(id); + f$session_start(); + ASSERT_TRUE(f$session_id().has_value() && !f$session_id().val().empty()); + ASSERT_EQ(f$session_id().val(), id); + PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION.as_array().emplace_value(string("message"), string("hello")); + f$session_abort(); + ASSERT_FALSE(f$session_commit()); + + fd = open_safe(file_path.c_str(), O_RDWR, 0666); + fstat(fd, &buf); + n = read_safe(fd, result, buf.st_size, file_path); + read_data = string(result, n); + ASSERT_EQ(read_data, write_data); + + f$exec(string("rm -rf ").append(session_dir_path)); +} + +TEST(sessions_test, test_session_decode) { + f$session_start(); + array data; + data.emplace_value(string("first"), string("hello world")); + data.emplace_value(string("second"), 60); + data.emplace_value(3, string("smth")); + + string to_write = f$serialize(data); + ASSERT_TRUE(f$session_decode(to_write)); + + string result = f$serialize(PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION.as_array()); + ASSERT_EQ(to_write, result); + + to_write = f$serialize(array()); + ASSERT_TRUE(f$session_decode(to_write)); + result = f$serialize(PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION.as_array()); + ASSERT_EQ(result, to_write); + ASSERT_TRUE(f$session_abort()); + + f$exec(string("rm -rf ").append(session_dir_path)); +} + +TEST(sessions_test, test_session_encode) { + f$session_start(); + array data; + data.emplace_value(string("first"), string("hello world")); + data.emplace_value(string("second"), 60); + data.emplace_value(3, string("smth")); + + string to_read = f$serialize(data); + PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION = data; + ASSERT_TRUE(f$session_encode().has_value() && !f$session_encode().val().empty()); + ASSERT_EQ(f$session_encode().val(), to_read); + ASSERT_TRUE(f$session_abort()); + + f$session_start(); + ASSERT_TRUE(f$session_encode().has_value() && !f$session_encode().val().empty()); + ASSERT_EQ(f$session_encode().val(), f$serialize(array())); + ASSERT_TRUE(f$session_abort()); + + f$exec(string("rm -rf ").append(session_dir_path)); +} + +TEST(sessions_test, test_session_reset) { + f$session_start(); + ASSERT_TRUE(f$session_id().has_value() && !f$session_id().val().empty()); + string id = f$session_id().val(); + array data; + data.emplace_value(string("first"), string("hello world")); + data.emplace_value(string("second"), 60); + data.emplace_value(3, string("smth")); + PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION = data; + ASSERT_TRUE(f$session_commit()); + + f$session_id(id); + f$session_start(); + ASSERT_TRUE(f$session_id().has_value() && !f$session_id().val().empty()); + ASSERT_EQ(f$session_id().val(), id); + array new_data; + new_data.emplace_value(string("first"), string("buy")); + new_data.emplace_value(string("second"), 1000); + PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION = new_data; + ASSERT_TRUE(f$session_reset()); + new_data = PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION.as_array(); + ASSERT_EQ(f$serialize(data), f$serialize(new_data)); + ASSERT_TRUE(f$session_abort()); + + f$exec(string("rm -rf ").append(session_dir_path)); +} + +TEST(sessions_test, test_session_unset) { + f$session_start(); + array data; + data.emplace_value(string("first"), string("hello world")); + PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION = data; + ASSERT_TRUE(f$session_unset()); + data = PhpScriptMutableGlobals::current().get_superglobals().v$_SESSION.as_array(); + ASSERT_EQ(f$serialize(data), f$serialize(array())); + ASSERT_TRUE(f$session_abort()); + + f$exec(string("rm -rf ").append(session_dir_path)); +}