diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..60e996b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,15 @@ +{ + "name": "colopl_timeshifter", + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cpptools-extension-pack", + "maelvalais.autoconf" + ] + } + }, + "dockerComposeFile": "../compose.yaml", + "service": "dev", + "workspaceFolder": "/usr/src/php/ext/extension" +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6b8710a --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.git diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..c905fdd --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + day: "saturday" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + day: "saturday" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "saturday" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..369759a --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,42 @@ +name: Build +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' +jobs: + ubuntu_2204_php81_origin_deb: + runs-on: ubuntu-22.04 + timeout-minutes: 60 + strategy: + matrix: + arch: ["arm64v8", "amd64"] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + - name: Setup Buildx + uses: docker/setup-buildx-action@v3 + - name: Build Container + uses: docker/build-push-action@v6 + with: + build-args: ARCH=${{ matrix.arch }} + cache-from: type=gha + cache-to: type=gha,mode=max + context: . + file: ./build/ubuntu2204/Dockerfile + load: true + tags: "pskel-build-ubuntu2204-${{ matrix.arch }}" + - name: Build Extension with Container + run: | + mkdir "artifacts" + docker run --env VERSION="${{ github.ref_name }}" --rm -v"$(pwd)/artifacts:/tmp/artifacts" -i "pskel-build-ubuntu2204-${{ matrix.arch }}" + - name: Upload deb Packages + uses: actions/upload-artifact@v4 + with: + name: ubuntu_2204_debs-${{ matrix.arch }} + path: artifacts/ diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..810182a --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,49 @@ +name: CI +on: [push, pull_request] +jobs: + CI: + runs-on: ubuntu-22.04 + timeout-minutes: 60 + strategy: + matrix: + arch: ["amd64", "arm64v8", "s390x"] + version: ["8.1", "8.2", "8.3"] + type: ["cli", "zts"] + distro: ["bookworm", "alpine"] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: "arm64,s390x" + - name: Setup buildx + uses: docker/setup-buildx-action@v3 + - name: Build container + run: | + docker compose build --pull --no-cache --build-arg IMAGE=${{ matrix.arch }}/php --build-arg TAG=${{ matrix.version }}-${{ matrix.type }}-${{ matrix.distro }} --build-arg PSKEL_SKIP_DEBUG=${{ matrix.arch != 'amd64' && '1' || '' }} + - name: Run tests + run: | + docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION=1 dev + - name: Test extension with PHP Debug Build + if: matrix.arch == 'amd64' + run: | + docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION_DEBUG=1 dev + - name: Test extension with Valgrind + if: matrix.arch == 'amd64' + run: | + docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION_VALGRIND=1 dev + - name: Test extension with LLVM Sanitizer (MemorySanitizer) + if: matrix.arch == 'amd64' && matrix.distro != 'alpine' + run: | + docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION_MSAN=1 dev + - name: Test extension with LLVM Sanitizer (AddressSanitizer) + if: matrix.arch == 'amd64' && matrix.distro != 'alpine' + run: | + docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION_ASAN=1 dev + - name: Test extension with LLVM Sanitizer (UndefinedBehaviorSanitizer) + if: matrix.arch == 'amd64' && matrix.distro != 'alpine' + run: | + docker compose run --rm --entrypoint=/usr/bin/ci --env TEST_EXTENSION_UBSAN=1 dev diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3c4903 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea/ +/vendor/ +composer.lock diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..22c4d49 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "ext/third_party/timelib"] + path = ext/third_party/timelib + url = https://github.com/derickr/timelib.git +[submodule "derickr/timelib"] + path = ext/third_party/timelib + url = https://github.com/derickr/timelib.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a1f3757 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,129 @@ +ARG IMAGE=php +ARG TAG=8.3-cli + +FROM ${IMAGE}:${TAG} + +ARG PSKEL_SKIP_DEBUG="" +ARG PSKEL_EXTRA_CONFIGURE_OPTIONS="" + +ENV USE_ZEND_ALLOC=0 +ENV ZEND_DONT_UNLOAD_MODULES=1 +ENV PSKEL_SKIP_DEBUG=${PSKEL_SKIP_DEBUG} +ENV PSKEL_EXTRA_CONFIGURE_OPTIONS=${PSKEL_EXTRA_CONFIGURE_OPTIONS} + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +RUN if test -f "/etc/debian_version"; then \ + apt-get update && \ + DEBIAN_FRONTEND="noninteractive" apt-get install -y \ + "build-essential" "bison" "valgrind" "llvm" "clang" "zlib1g-dev" "libsqlite3-dev" "git" "unzip" && \ + if test "${PSKEL_SKIP_DEBUG}" = ""; then \ + docker-php-source extract && \ + cd "/usr/src/php" && \ + CFLAGS="-fpic -fpie -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-pie" ./configure --disable-all \ + --includedir="/usr/local/include/gcc-valgrind-php" --program-prefix="gcc-valgrind-" \ + --disable-cgi --disable-fpm --enable-cli \ + --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \ + --enable-debug --without-pcre-jit "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \ + --with-valgrind \ + ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \ + --enable-option-checking=fatal && \ + make -j$(nproc) && \ + make install && \ + cd - && \ + docker-php-source delete && \ + docker-php-source extract && \ + cd "/usr/src/php" && \ + CC=clang CXX=clang++ CFLAGS="-fsanitize=memory -fno-sanitize-recover -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-fsanitize=memory" ./configure \ + --includedir="/usr/local/include/clang-msan-php" --program-prefix="clang-msan-" \ + --disable-cgi --disable-all --disable-fpm --enable-cli \ + --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \ + --enable-debug --without-pcre-jit "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \ + --enable-memory-sanitizer \ + ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \ + --enable-option-checking=fatal && \ + make -j$(nproc) && \ + make install && \ + cd - && \ + docker-php-source delete && \ + docker-php-source extract && \ + cd "/usr/src/php" && \ + CC=clang CXX=clang++ CFLAGS="-fsanitize=address -fno-sanitize-recover -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-fsanitize=address" ./configure \ + --includedir="/usr/local/include/clang-asan-php" --program-prefix="clang-asan-" \ + --disable-cgi --disable-all --disable-fpm --enable-cli \ + --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \ + --enable-debug --without-pcre-jit "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \ + --enable-address-sanitizer \ + ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \ + --enable-option-checking=fatal && \ + make -j$(nproc) && \ + make install && \ + cd - && \ + docker-php-source delete && \ + docker-php-source extract && \ + cd "/usr/src/php" && \ + CC=clang CXX=clang++ CFLAGS="-fsanitize=undefined -fno-sanitize-recover -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-fsanitize=undefined" ./configure \ + --includedir="/usr/local/include/clang-ubsan-php" --program-prefix="clang-ubsan-" \ + --disable-cgi --disable-all --disable-fpm --enable-cli \ + --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \ + --enable-debug --without-pcre-jit "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \ + --enable-undefined-sanitizer \ + ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \ + --enable-option-checking=fatal && \ + make -j$(nproc) && \ + make install && \ + cd - && \ + docker-php-source delete && \ + docker-php-source extract && \ + cd "/usr/src/php" && \ + CFLAGS="-fpic -fpie -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-pie" ./configure --disable-all \ + --includedir="/usr/local/include/debug-php" --program-prefix="debug-" \ + --disable-cgi --disable-fpm --enable-cli \ + --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \ + --enable-debug "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \ + ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \ + --enable-option-checking=fatal && \ + make -j$(nproc) && \ + make install && \ + cd - && \ + docker-php-source delete; \ + fi; \ + elif test -f "/etc/alpine-release"; then \ + apk add --no-cache ${PHPIZE_DEPS} "bison" "valgrind" "valgrind-dev" "zlib-dev" "sqlite-dev" "git" "unzip" && \ + if test "${PSKEL_SKIP_DEBUG}" = ""; then \ + docker-php-source extract && \ + cd "/usr/src/php" && \ + CFLAGS="-fpic -fpie -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-pie" ./configure --disable-all \ + --includedir="/usr/local/include/gcc-valgrind-php" --program-prefix="gcc-valgrind-" \ + --disable-cgi --disable-fpm --enable-cli \ + --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \ + --enable-debug --without-pcre-jit "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \ + --with-valgrind \ + ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \ + --enable-option-checking=fatal && \ + make -j$(nproc) && \ + make install && \ + cd - && \ + docker-php-source delete && \ + docker-php-source extract && \ + cd "/usr/src/php" && \ + CFLAGS="-fpic -fpie -DZEND_TRACK_ARENA_ALLOC" LDFLAGS="-pie" ./configure --disable-all \ + --includedir="/usr/local/include/debug-php" --program-prefix="debug-" \ + --disable-cgi --disable-fpm --enable-cli \ + --enable-mysqlnd --enable-pdo --with-pdo-mysql --with-pdo-sqlite \ + --enable-debug "$(php -r "echo PHP_ZTS === 1 ? '--enable-zts' : '';")" \ + ${PSKEL_EXTRA_CONFIGURE_OPTIONS} \ + --enable-option-checking=fatal && \ + make -j$(nproc) && \ + make install && \ + cd - && \ + docker-php-source delete; \ + fi; \ + fi && \ + docker-php-source extract + +WORKDIR "/usr/src/php" + +COPY ./ext /ext + +COPY ./ci.sh /usr/bin/ci diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4076fe9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,68 @@ +-------------------------------------------------------------------- + The PHP License, version 3.01 +Copyright (c) 1999 - 2019 The PHP Group. All rights reserved. +-------------------------------------------------------------------- + +Redistribution and use in source and binary forms, with or without +modification, is permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + 3. The name "PHP" must not be used to endorse or promote products + derived from this software without prior written permission. For + written permission, please contact group@php.net. + + 4. Products derived from this software may not be called "PHP", nor + may "PHP" appear in their name, without prior written permission + from group@php.net. You may indicate that your software works in + conjunction with PHP by saying "Foo for PHP" instead of calling + it "PHP Foo" or "phpfoo" + + 5. The PHP Group may publish revised and/or new versions of the + license from time to time. Each version will be given a + distinguishing version number. + Once covered code has been published under a particular version + of the license, you may always continue to use it under the terms + of that version. You may also choose to use such covered code + under the terms of any subsequent version of the license + published by the PHP Group. No one other than the PHP Group has + the right to modify the terms applicable to covered code created + under this License. + + 6. Redistributions of any form whatsoever must retain the following + acknowledgment: + "This product includes PHP software, freely available from + ". + +THIS SOFTWARE IS PROVIDED BY THE PHP DEVELOPMENT TEAM ``AS IS'' AND +ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE PHP +DEVELOPMENT TEAM OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------- + +This software consists of voluntary contributions made by many +individuals on behalf of the PHP Group. + +The PHP Group can be contacted via Email at group@php.net. + +For more information on the PHP Group and the PHP project, +please see . + +PHP includes the Zend Engine, freely available at +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a33675 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# colopl_timeshifter + +This extension changes the current time in PHP to a specified modified value. + +> [!WARNING] +> **DO NOT USE THIS EXTENSION IN ANY PRODUCTION ENVIRONMENT!!!** + +At present, this extension is effective for the following functions: + +- Any built-in PHP processing that handles the current time (`ext-date`) +- `NOW()` and many statements in MySQL or compatible DBMS via PDO +- Server environment variables for request time (e.g. `S_SERVER['REQUEST_TIME']`) + +## Setup + +```bash +$ git clone --recursive "https://github.com/colopl/php-colopl_timesifter.git" "colopl_timeshifter" +$ cd "colopl_timeshifter/ext" +$ phpize +$ ./configure --with-php-config="$(which php-config)" +$ make -j$(nproc) +$ TEST_PHP_ARGS="-q --show-diff" make test +$ sudo make install +``` + +And enable extension. + +``` +$ sudo echo "extension=colopl_timesfhiter" > "$(php-config --ini-dir)/99-colopl_timeshifter.ini" +$ php -m | grep colopl_timeshifter +colopl_timeshifter +``` + +### PHP Library (recommended) + +```bash +$ composer require --dev "colopl/colopl_timeshifter" +``` + +And use `Colopl\ColoplTimeShifter\Manager` class. + +## INI directives + +#### `colopl_timeshifter.is_hook_pdo_mysql` + +Type: `bool` +Default: `true` +Run-time switchable: **No** (`PHP_INI_SYSTEM`) + +Enables or disables the hook into `\PDO::__construct` to swap the current time in MySQL function and keywords (e.g. `NOW()`, `CURRENT_TIMESTAMP`) + +#### `colopl_timeshifter.is_hook_request_time` + +Type: `bool` +Default: `true` +Run-time switchable: **No** (`PHP_INI_SYSTEM`) + +Selects whether to hook the $_SERVER superglobals `REQUEST_TIME` and `REQUEST_TIME_FLOAT`. + +#### `colopl_timeshifter.usleep_sec` + +Type: `int` (`int<1, max>`) +Defalt: `1` +Run-time switchable: **Yes** (`PHP_INI_ALL`) + +For a string representing time, set the number of wait microseconds to check whether it is absolute or relative time. + +#### `colopl_timeshifter.is_restore_per_request` + +Type: `bool` +Default: `false` +Run-time switchable: **Yes** (`PHP_INI_ALL`) + +Sets whether or not to unhook at the end of the request. + +## Functions + +> [!TIP] +> Install `colopl/colopl_timeshifter` **Composer** package and use `Colopl\ColoplTimeShifter\Manager` support class instead. + +#### `\Colopl\ColoplTimeShifter\register_hook(\DateInterval $interval): bool` + +Sets the time difference to be subtracted from the current time. + +If the hook succeeds, it returns `true`; otherwise, it returns `false`. + +#### `\Colopl\ColoplTimeShifter\unregister_hook(): void` + +Breaks the hook. + +#### `\Colopl\ColoplTimeShifter\is_hooked(): bool` + +Check to see if the hook is done. Returns `true` if the hook is done, `false` otherwise. + +## License + +PHP License 3.01 diff --git a/build/ubuntu2204/Dockerfile b/build/ubuntu2204/Dockerfile new file mode 100644 index 0000000..1fd8930 --- /dev/null +++ b/build/ubuntu2204/Dockerfile @@ -0,0 +1,12 @@ +ARG ARCH=amd64 + +FROM ${ARCH}/ubuntu:22.04 + +RUN apt-get update && \ + DEBIAN_FRONTEND="noninteractive" apt-get install -y "php" "php-dev" "checkinstall" + +COPY ./ext /tmp/ext + +COPY ./build/ubuntu2204/build.sh /usr/bin/build + +ENTRYPOINT ["/usr/bin/build"] diff --git a/build/ubuntu2204/build.sh b/build/ubuntu2204/build.sh new file mode 100755 index 0000000..b3e9f50 --- /dev/null +++ b/build/ubuntu2204/build.sh @@ -0,0 +1,18 @@ +#!/bin/sh -eux + +cd "/tmp/ext" + echo "COLOPL PHP timeshifter extension" > "description-pak" + phpize + ./configure --with-php-config="$(which "php-config")" + make -j$(nproc) + checkinstall \ + --pkgname="php-colopl-timeshifter" \ + --pkglicense="PHP-3.01" \ + --pkgversion="${VERSION}" \ + --pkggroup="php" \ + --maintainer="g-kudo@colopl.co.jp" \ + --requires="php" \ + --stripso="yes" \ + --pakdir="/tmp/artifacts" \ + --nodoc +cd - diff --git a/ci.sh b/ci.sh new file mode 100755 index 0000000..f6bbc9c --- /dev/null +++ b/ci.sh @@ -0,0 +1,108 @@ +#!/bin/sh -e + +case "${1}" in + "") ;; + "test") TEST_EXTENSION=1;; + "debug") TEST_EXTENSION_DEBUG=1;; + "valgrind") TEST_EXTENSION_VALGRIND=1;; + "msan") TEST_EXTENSION_MSAN=1;; + "asan") TEST_EXTENSION_ASAN=1;; + "ubsan") TEST_EXTENSION_UBSAN=1;; + *) printf "Pskel CI\nusage:\n\t\t%s\t\t: %s\n\t\t%s\t\t: %s\n\t\t%s\t: %s\n\t\t%s\t\t: %s\n\t\t%s\t\t: %s\n\t\t%s\t\t: %s\n" "test" "Test extension with pre-installed PHP binary. [bin: $(which "php")]" "debug" "Test extension with Debug Build (GCC) binary. [bin: $(which "debug-php")]" "valgrind" "Test extension with GCC binary with Valgrind. [bin: $(which "gcc-valgrind-php")]" "msan" "Test extension with Clang binary with MemorySanitizer. [bin: $(which "clang-msan-php")]" "asan" "Test extension with Clang binary with AddressSanitizer. [bin: $(which "clang-asan-php")]" "ubsan" "Test extension with Clang binary with UndefinedBehaviorSanitizer. [bin: $(which "clang-ubsan-php")]"; exit 0;; +esac + +echo "[Pskel CI] BEGIN TEST" + +if test "${TEST_EXTENSION}" != ""; then + cd "/ext" + phpize + ./configure --with-php-config="$(which php-config)" + make clean + make -j"$(nproc)" + TEST_PHP_ARGS="--show-diff -q" make test + make install + docker-php-ext-enable "colopl_timeshifter" + cd "/work" + composer install + composer exec -- phpunit "tests/" +else + echo "[Pskel CI] skip: TEST_EXTENSION is not set" +fi + +if test "${TEST_EXTENSION_DEBUG}" != ""; then + cd "/ext" + debug-phpize + ./configure --with-php-config="$(which debug-php-config)" + make clean + make -j"$(nproc)" + TEST_PHP_ARGS="--show-diff -q" make test +else + echo "[Pskel CI] skip: TEST_EXTENSION_DEBUG is not set" +fi + +if test "${TEST_EXTENSION_VALGRIND}" != ""; then + if type "gcc-valgrind-php" > /dev/null 2>&1; then + cd "/ext" + gcc-valgrind-phpize + ./configure --with-php-config="$(which gcc-valgrind-php-config)" + make clean + make -j"$(nproc)" + TEST_PHP_ARGS="--show-diff -q -m" make test + else + echo "[Pskel CI] missing gcc-valgrind-php" + exit 1 + fi +else + echo "[Pskel CI] skip: TEST_EXTENSION_VALGRIND is not set" +fi + +if test "${TEST_EXTENSION_MSAN}" != ""; then + if type "clang-msan-php" > /dev/null 2>&1; then + cd "/ext" + clang-msan-phpize + CC="clang" CXX="clang++" CFLAGS="-fsanitize=memory -DZEND_TRACK_ARENA_ALLOC" CPPFLAGS="-fsanitize=memory -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=memory" ./configure --with-php-config="$(which clang-msan-php-config)" + make clean + CFLAGS="-fsanitize=memory -DZEND_TRACK_ARENA_ALLOC ${CFLAGS}" CPPFLAGS="-fsanitize=memory -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=memory" make -j"$(nproc)" + TEST_PHP_ARGS="--show-diff -q --msan" make test + else + echo "[Pskel CI] missing clang-msan-php" + exit 1 + fi +else + echo "[Pskel CI] skip: TEST_EXTENSION_MSAN is not set" +fi + +if test "${TEST_EXTENSION_ASAN}" != ""; then + if type "clang-asan-php" > /dev/null 2>&1; then + cd "/ext" + clang-asan-phpize + CC="clang" CXX="clang++" CFLAGS="-fsanitize=address -DZEND_TRACK_ARENA_ALLOC" CPPFLAGS="-fsanitize=address -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=address" ./configure --with-php-config="$(which clang-asan-php-config)" + make clean + CFLAGS="-fsanitize=address -DZEND_TRACK_ARENA_ALLOC ${CFLAGS}" CPPFLAGS="-fsanitize=address -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=address" make -j"$(nproc)" + TEST_PHP_ARGS="--show-diff -q --asan" make test + else + echo "[Pskel CI] missing clang-asan-php" + exit 1 + fi +else + echo "[Pskel CI] skip: TEST_EXTENSION_ASAN is not set" +fi + +if test "${TEST_EXTENSION_UBSAN}" != ""; then + if type "clang-ubsan-php" > /dev/null 2>&1; then + cd "/ext" + clang-ubsan-phpize + CC="clang" CXX="clang++" CFLAGS="-fsanitize=undefined -DZEND_TRACK_ARENA_ALLOC" CPPFLAGS="-fsanitize=undefined -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=undefined" ./configure --with-php-config="$(which clang-ubsan-php-config)" + make clean + CFLAGS="-fsanitize=undefined -DZEND_TRACK_ARENA_ALLOC ${CFLAGS}" CPPFLAGS="-fsanitize=undefined -DZEND_TRACK_ARENA_ALLOC ${CPPFLAGS}" LDFLAGS="-fsanitize=undefined" make -j"$(nproc)" + TEST_PHP_ARGS="--show-diff -q" make test + else + echo "[Pskel CI] missing clang-ubsan-php" + exit 1 + fi +else + echo "[Pskel CI] skip: TEST_EXTENSION_UBSAN is not set" +fi + +echo "[Pskel CI] END TEST" +exit 0 diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..1628d28 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,30 @@ +services: + dev: + build: + context: ./ + dockerfile: ./Dockerfile + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + privileged: true + volumes: + - ./ext:/usr/src/php/ext/extension:cached + - ./:/work:cached + tty: true + depends_on: + - mysql + - tidb + command: ["sleep", "infinity"] + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: testing + MYSQL_DATABASE: testing + MYSQL_USER: testing + MYSQL_PASSWORD: testing + volumes: + - ./docker/mysql/etc/mysql/conf.d/my.cnf:/etc/mysql/conf.d/my.cnf + tidb: + image: pingcap/tidb:v7.1.1 + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8fb741b --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "colopl/colopl_timeshifter", + "description": "Current time modification extension wrapper library.", + "type": "library", + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "ext-colopl_timeshifter": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "vimeo/psalm": "^5", + "phpunit/phpunit": "^10", + "phpstan/phpstan-phpunit": "^1", + "psalm/plugin-phpunit": "^0.19" + }, + "license": "PHP-3.01", + "autoload": { + "psr-4": { + "Colopl\\ColoplTimeShifter\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Colopl\\ColoplTimeShifter\\Tests\\": "tests/" + } + }, + "authors": [ + { + "name": "Go Kudo", + "email": "g-kudo@colopl.co.jp" + } + ], + "minimum-stability": "stable" +} diff --git a/docker/mysql/etc/mysql/conf.d/my.cnf b/docker/mysql/etc/mysql/conf.d/my.cnf new file mode 100644 index 0000000..e356587 --- /dev/null +++ b/docker/mysql/etc/mysql/conf.d/my.cnf @@ -0,0 +1,2 @@ +[mysqld] +default-authentication-plugin=mysql_native_password diff --git a/ext/.gitignore b/ext/.gitignore new file mode 100644 index 0000000..f5e11c8 --- /dev/null +++ b/ext/.gitignore @@ -0,0 +1,44 @@ +*.lo +*.la +*.dep +.libs +acinclude.m4 +aclocal.m4 +autom4te.cache +build +config.guess +config.h +config.h.in +config.h.in~ +config.log +config.nice +config.status +config.sub +configure~ +configure +configure.ac +configure.in +include +install-sh +libtool +ltmain.sh +Makefile +Makefile.fragments +Makefile.global +Makefile.objects +missing +mkinstalldirs +modules +php_test_results_*.txt +phpt.* +run-test-info.php +run-tests.php +tests/**/*.diff +tests/**/*.out +tests/**/*.php +tests/**/*.exp +tests/**/*.log +tests/**/*.sh +tests/**/*.db +tests/**/*.mem +tmp-php.ini diff --git a/ext/.vscode/c_cpp_properties.json b/ext/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..abf07c8 --- /dev/null +++ b/ext/.vscode/c_cpp_properties.json @@ -0,0 +1,21 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**", + "/usr/local/include/php", + "/usr/local/include/php/TSRM", + "/usr/local/include/php/Zend", + "/usr/local/include/php/ext", + "/usr/local/include/php/include", + "/usr/local/include/php/main", + "/usr/local/include/php/sapi" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc", + "cStandard": "c99" + } + ], + "version": 4 +} diff --git a/ext/.vscode/settings.json b/ext/.vscode/settings.json new file mode 100644 index 0000000..6ef144f --- /dev/null +++ b/ext/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "files.associations": { + "*.phpt": "php", + "colopl_timeshifter_arginfo.h": "c", + "timelib.h": "c", + "php_date.h": "c", + "php_colopl_timeshifter.h": "c", + "info.h": "c", + "typeinfo": "cpp", + "hook.h": "c", + "mysqlnd_libmysql_compat.h": "c", + "system_error": "c", + "array": "c", + "functional": "c", + "tuple": "c", + "type_traits": "c", + "utility": "c", + "string_view": "c", + "initializer_list": "c", + "zend_max_execution_timer.h": "c" + } +} diff --git a/ext/colopl_timeshifter.c b/ext/colopl_timeshifter.c new file mode 100644 index 0000000..64fbac7 --- /dev/null +++ b/ext/colopl_timeshifter.c @@ -0,0 +1,212 @@ +/* + +----------------------------------------------------------------------+ + | COLOPL PHP TimeShifter. | + +----------------------------------------------------------------------+ + | Copyright (c) COLOPL, Inc. | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | info@colopl.co.jp so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "php.h" +#include "ext/date/php_date.h" +#include "ext/standard/info.h" +#include "php_colopl_timeshifter.h" +#include "colopl_timeshifter_arginfo.h" + +#include "shared_memory.h" +#include "hook.h" + +/* True global */ +typedef struct { + bool is_hooked; + timelib_rel_time shift_interval; +} timeshifter_global_t; +sm_t timeshifter_global; + +/* Module global */ +ZEND_DECLARE_MODULE_GLOBALS(colopl_timeshifter); + +PHP_INI_BEGIN() + STD_PHP_INI_ENTRY("colopl_timeshifter.is_hook_pdo_mysql", "1", PHP_INI_SYSTEM, OnUpdateBool, is_hook_pdo_mysql, zend_colopl_timeshifter_globals, colopl_timeshifter_globals) + STD_PHP_INI_ENTRY("colopl_timeshifter.is_hook_request_time", "1", PHP_INI_SYSTEM, OnUpdateBool, is_hook_request_time, zend_colopl_timeshifter_globals, colopl_timeshifter_globals) + STD_PHP_INI_ENTRY("colopl_timeshifter.usleep_sec", "1", PHP_INI_ALL, OnUpdateLong, usleep_sec, zend_colopl_timeshifter_globals, colopl_timeshifter_globals) + STD_PHP_INI_ENTRY("colopl_timeshifter.is_restore_per_request", "0", PHP_INI_ALL, OnUpdateBool, is_restore_per_request, zend_colopl_timeshifter_globals, colopl_timeshifter_globals) +PHP_INI_END() + +void get_shift_interval(timelib_rel_time *time) { + timeshifter_global_t tg; + + sm_read(×hifter_global, &tg); + if (tg.is_hooked) { + memcpy(time, &tg.shift_interval, sizeof(timelib_rel_time)); + } +} + +void set_is_hooked(bool flag) { + timeshifter_global_t tg; + + sm_read(×hifter_global, &tg); + if (tg.is_hooked != flag) { + tg.is_hooked = flag; + sm_write(×hifter_global, &tg); + } +} + +bool get_is_hooked() { + timeshifter_global_t tg; + + sm_read(×hifter_global, &tg); + return tg.is_hooked; +} + +ZEND_FUNCTION(Colopl_ColoplTimeShifter_register_hook) +{ + zval *intern; + timeshifter_global_t tg; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(intern, php_date_get_interval_ce()) + ZEND_PARSE_PARAMETERS_END(); + + /* Copy interval. */ + sm_read(×hifter_global, &tg); + memcpy(&tg.shift_interval, Z_PHPINTERVAL_P(intern)->diff, sizeof(timelib_rel_time)); + tg.is_hooked = true; + if (!sm_write(×hifter_global, &tg)) { + RETURN_FALSE; + } + + if (COLOPL_TS_G(is_hook_request_time)) { + apply_request_time_hook(); + } + + RETURN_TRUE; +} + +ZEND_FUNCTION(Colopl_ColoplTimeShifter_unregister_hook) +{ + set_is_hooked(false); +} + +ZEND_FUNCTION(Colopl_ColoplTimeShifter_is_hooked) +{ + RETURN_BOOL(get_is_hooked()); +} + +PHP_MINIT_FUNCTION(colopl_timeshifter) +{ + REGISTER_INI_ENTRIES(); + + if (COLOPL_TS_G(is_hook_pdo_mysql) == true) { + register_pdo_hook(); + } + + if (!register_hooks()) { + return FAILURE; + } + + if (get_is_hooked() && COLOPL_TS_G(is_hook_request_time)) { + apply_request_time_hook(); + } + + COLOPL_TS_G(pdo_mysql_orig_methods) = NULL; + + return SUCCESS; +} + +PHP_MSHUTDOWN_FUNCTION(colopl_timeshifter) +{ + UNREGISTER_INI_ENTRIES(); + + if (!unregister_hooks()) { + return FAILURE; + } + + return SUCCESS; +} + +PHP_RINIT_FUNCTION(colopl_timeshifter) +{ +# if defined(ZTS) && defined(COMPILE_DL_COLOPL_TIMESHIFTER) + ZEND_TSRMLS_CACHE_UPDATE(); +# endif + + COLOPL_TS_G(orig_request_time) = 0; + COLOPL_TS_G(orig_request_time_float) = 0.0; + + return SUCCESS; +} + +PHP_RSHUTDOWN_FUNCTION(colopl_timeshifter) +{ + if (COLOPL_TS_G(is_restore_per_request) && get_is_hooked()) { + set_is_hooked(false); + if (!unregister_hooks()) { + return FAILURE; + } + } + + return SUCCESS; +} + +PHP_MINFO_FUNCTION(colopl_timeshifter) +{ + php_info_print_table_start(); + php_info_print_table_header(2, "colopl_timeshifter support", "enabled"); + php_info_print_table_end(); +} + +PHP_GINIT_FUNCTION(colopl_timeshifter) +{ + timeshifter_global_t tg; + +# if defined(ZTS) && defined(COMPILE_DL_COLOPL_TIMESHIFTER) + ZEND_TSRMLS_CACHE_UPDATE(); +# endif + + sm_init(×hifter_global, sizeof(timeshifter_global_t)); + sm_read(×hifter_global, &tg); + tg.is_hooked = false; + sm_write(×hifter_global, &tg); +} + +PHP_GSHUTDOWN_FUNCTION(colopl_timeshifter) +{ + sm_free(×hifter_global); +} + +zend_module_entry colopl_timeshifter_module_entry = { + STANDARD_MODULE_HEADER, + "colopl_timeshifter", + ext_functions, + PHP_MINIT(colopl_timeshifter), + PHP_MSHUTDOWN(colopl_timeshifter), + PHP_RINIT(colopl_timeshifter), + PHP_RSHUTDOWN(colopl_timeshifter), + PHP_MINFO(colopl_timeshifter), + PHP_COLOPL_TIMESHIFTER_VERSION, + PHP_MODULE_GLOBALS(colopl_timeshifter), + PHP_GINIT(colopl_timeshifter), + PHP_GSHUTDOWN(colopl_timeshifter), + NULL, + STANDARD_MODULE_PROPERTIES_EX +}; + +#ifdef COMPILE_DL_COLOPL_TIMESHIFTER +# ifdef ZTS +ZEND_TSRMLS_CACHE_DEFINE() +# endif +ZEND_GET_MODULE(colopl_timeshifter) +#endif diff --git a/ext/colopl_timeshifter.stub.php b/ext/colopl_timeshifter.stub.php new file mode 100644 index 0000000..8043483 --- /dev/null +++ b/ext/colopl_timeshifter.stub.php @@ -0,0 +1,9 @@ + $ext_builddir/third_party/timelib/timelib_config.h < +#endif +#include +#include + +#include "zend.h" + +#define timelib_malloc emalloc +#define timelib_realloc erealloc +#define timelib_calloc ecalloc +#define timelib_strdup estrdup +#define timelib_strndup estrndup +#define timelib_free efree +EOF diff --git a/ext/hook.c b/ext/hook.c new file mode 100644 index 0000000..531fc3d --- /dev/null +++ b/ext/hook.c @@ -0,0 +1,829 @@ +/* + +----------------------------------------------------------------------+ + | COLOPL PHP TimeShifter. | + +----------------------------------------------------------------------+ + | Copyright (c) COLOPL, Inc. | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | info@colopl.co.jp so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#include "hook.h" + +#include "php.h" +#include "php_colopl_timeshifter.h" +#include "ext/date/php_date.h" +#include "ext/pdo/php_pdo.h" +#include "ext/pdo/php_pdo_driver.h" + +#include "third_party/timelib/timelib.h" + +#ifdef PHP_WIN32 +# include "win32/time.h" +#else +# include +#endif + +static inline void apply_interval(timelib_time **time, timelib_rel_time *interval) +{ + timelib_time *new_time = timelib_sub(*time, interval); + timelib_time_dtor(*time); + *time = new_time; +} + +#define CALL_ORIGINAL_FUNCTION_WITH_PARAMS(_name, _params, _param_count) \ + do { \ + zend_fcall_info *fci = ecalloc(1, sizeof(zend_fcall_info)); \ + zend_fcall_info_cache *fcc = ecalloc(1, sizeof(zend_fcall_info_cache)); \ + fci->size = sizeof(zend_fcall_info); \ + fci->object = NULL; \ + fci->retval = return_value; \ + fci->param_count = _param_count; \ + fci->params = _params; \ + fci->named_params = NULL; \ + fcc->function_handler = zend_hash_str_find_ptr(CG(function_table), #_name, strlen(#_name)); \ + fcc->function_handler->internal_function.handler = COLOPL_TS_G(orig_##_name); \ + fcc->called_scope = NULL; \ + fcc->object = NULL; \ + zend_call_function(fci, fcc); \ + efree(fci); \ + efree(fcc); \ + } while (0); + +#define CALL_ORIGINAL_FUNCTION(name) \ + do { \ + COLOPL_TS_G(orig_##name)(INTERNAL_FUNCTION_PARAM_PASSTHRU); \ + } while (0); + +#define CHECK_STATE(name) \ + do { \ + if (!get_is_hooked()) { \ + CALL_ORIGINAL_FUNCTION(name); \ + return; \ + } \ + } while (0); + +#define DEFINE_DT_HOOK_CONSTRUCTOR(name) \ + static void hook_##name##_con(INTERNAL_FUNCTION_PARAMETERS) \ + { \ + CHECK_STATE(name##_con); \ + \ + CALL_ORIGINAL_FUNCTION(name##_con); \ + \ + zend_string *datetime = NULL; \ + zval *timezone = NULL; \ + php_date_obj *date = NULL; \ + timelib_rel_time interval; \ + \ + ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 0, 2) \ + Z_PARAM_OPTIONAL; \ + Z_PARAM_STR_OR_NULL(datetime); \ + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(timezone, php_date_get_timezone_ce()); \ + ZEND_PARSE_PARAMETERS_END(); \ + \ + date = Z_PHPDATE_P(ZEND_THIS); \ + \ + /* Early return if construction failed. */ \ + if (!date || !date->time) { \ + return; \ + } \ + \ + if (datetime && is_fixed_time_str(datetime, timezone)) { \ + return; \ + } \ + \ + get_shift_interval(&interval); \ + apply_interval(&date->time, &interval); \ + } + +#define DEFINE_CREATE_FROM_FORMAT_EX(fname, name) \ + static void hook_##fname(INTERNAL_FUNCTION_PARAMETERS) { \ + CHECK_STATE(name); \ + \ + php_date_obj *date; \ + timelib_time *time = NULL; \ + zval *params, orig_return_value; \ + uint32_t param_count = 0; \ + timelib_time *current_time = get_current_timelib_time(); \ + timelib_time *shifted_time = get_shifted_timelib_time(); \ + \ + CALL_ORIGINAL_FUNCTION(name); \ + \ + if (EG(exception) || Z_TYPE_P(return_value) == IS_FALSE) { \ + RETURN_FALSE; \ + } \ + \ + date = Z_PHPDATE_P(return_value); \ + time = date->time; \ + \ + zend_parse_parameters(ZEND_NUM_ARGS(), "+", ¶ms, ¶m_count); \ + \ + if (memchr(Z_STRVAL(params[0]), '!', Z_STRLEN(params[0])) != NULL) { \ + /* fixed (unix epoch) */ \ + return; \ + } \ + \ + /* Fixed check */ \ + if (current_time->y == time->y) { \ + time->y = shifted_time->y; \ + } \ + if (current_time->m == time->m) { \ + time->m = shifted_time->m; \ + } \ + if (current_time->d == time->d) { \ + time->d = shifted_time->d; \ + } \ + if (current_time->h == time->h) { \ + time->h = shifted_time->h; \ + } \ + if (current_time->i == time->i) { \ + time->i = shifted_time->i; \ + } \ + /* Maybe sometimes mistake, but not bothered. */ \ + if (llabs(current_time->s - time->s) <= 3) { \ + time->s = shifted_time->s; \ + } \ + if (llabs(current_time->us - time->us) <= 10) { \ + time->us = shifted_time->us; \ + } \ + \ + /* Apply changes */ \ + timelib_update_ts(time, NULL); \ + \ + /* Clean up */ \ + timelib_time_dtor(current_time); \ + timelib_time_dtor(shifted_time); \ + } + +#define DEFINE_CREATE_FROM_FORMAT(name) \ + DEFINE_CREATE_FROM_FORMAT_EX(name, name); + +#define HOOK_CONSTRUCTOR(ce, name) \ + do { \ + COLOPL_TS_G(orig_##name##_con) = ce->constructor->internal_function.handler; \ + ce->constructor->internal_function.handler = hook_##name##_con; \ + } while (0); + +#define HOOK_METHOD(ce, name, method) \ + do { \ + zend_function *php_function_entry = zend_hash_str_find_ptr(&ce->function_table, #method, strlen(#method)); \ + ZEND_ASSERT(php_function_entry); \ + COLOPL_TS_G(orig_##name##_##method) = php_function_entry->internal_function.handler; \ + php_function_entry->internal_function.handler = hook_##name##_##method; \ + } while (0); + +#define HOOK_FUNCTION(name) \ + do { \ + zend_function *php_function_entry = zend_hash_str_find_ptr(CG(function_table), #name, strlen(#name)); \ + ZEND_ASSERT(php_function_entry); \ + COLOPL_TS_G(orig_##name) = php_function_entry->internal_function.handler; \ + php_function_entry->internal_function.handler = hook_##name; \ + } while (0); + +#define RESTORE_CONSTRUCTOR(ce, name) \ + do { \ + ZEND_ASSERT(COLOPL_TS_G(orig_##name##_con)); \ + ce->constructor->internal_function.handler = COLOPL_TS_G(orig_##name##_con); \ + COLOPL_TS_G(orig_##name##_con) = NULL; \ + } while (0); + +#define RESTORE_METHOD(ce, name, method) \ + do { \ + zend_function *php_function_entry = zend_hash_str_find_ptr(&ce->function_table, #method, strlen(#method)); \ + ZEND_ASSERT(php_function_entry); \ + ZEND_ASSERT(COLOPL_TS_G(orig_##name##_##method)); \ + php_function_entry->internal_function.handler = COLOPL_TS_G(orig_##name##_##method); \ + COLOPL_TS_G(orig_##name##_##method) = NULL; \ + } while (0); + +#define RESTORE_FUNCTION(name) \ + do { \ + zend_function *php_function_entry = zend_hash_str_find_ptr(CG(function_table), #name, strlen(#name)); \ + ZEND_ASSERT(php_function_entry); \ + ZEND_ASSERT(COLOPL_TS_G(orig_##name)); \ + php_function_entry->internal_function.handler = COLOPL_TS_G(orig_##name); \ + COLOPL_TS_G(orig_##name) = NULL; \ + } while (0); + +static inline bool is_fixed_time_str(zend_string *datetime, zval *timezone) +{ + zval before_zv, after_zv; + php_date_obj *before, *after; + zend_class_entry *ce = php_date_get_immutable_ce(); + bool is_fixed_time_str; + + php_date_instantiate(ce, &before_zv); + before = Z_PHPDATE_P(&before_zv); + php_date_initialize(before, ZSTR_VAL(datetime), ZSTR_LEN(datetime), NULL, timezone, 0); + + /* + * Check format is absolute. + * FIXME: Need more instead method. + */ + usleep(((uint32_t) COLOPL_TS_G(usleep_sec)) > 0 ? (uint32_t) COLOPL_TS_G(usleep_sec) : 1); + + php_date_instantiate(ce, &after_zv); + after = Z_PHPDATE_P(&after_zv); + php_date_initialize(after, ZSTR_VAL(datetime), ZSTR_LEN(datetime), NULL, timezone, 0); + + is_fixed_time_str = before->time->y == after->time->y + && before->time->m == after->time->m + && before->time->d == after->time->d + && before->time->h == after->time->h + && before->time->i == after->time->i + && before->time->s == after->time->s + && before->time->us == after->time->us + ; + + zval_ptr_dtor(&before_zv); + zval_ptr_dtor(&after_zv); + + return is_fixed_time_str; +} + +static inline timelib_time *get_current_timelib_time() +{ + timelib_time *t = timelib_time_ctor(); + + timelib_unixtime2gmt(t, php_time()); + + return t; +} + +static inline timelib_time *get_shifted_timelib_time() +{ + timelib_time *t = get_current_timelib_time(); + timelib_rel_time interval; + + get_shift_interval(&interval); + apply_interval(&t, &interval); + + return t; +} + +static inline time_t get_shifted_time() +{ + time_t timestamp; + timelib_time *t = get_shifted_timelib_time(); + + timestamp = t->sse; + + timelib_time_dtor(t); + + return timestamp; +} + +static inline bool pdo_time_apply(pdo_dbh_t *dbh) +{ + zend_string *sql; + char buf[1024]; + + if (!COLOPL_TS_G(pdo_mysql_orig_methods) || !COLOPL_TS_G(pdo_mysql_orig_methods)->doer) { + return false; + } + + zend_sprintf(buf, "SET @@session.timestamp = %ld;", get_shifted_time()); + sql = zend_string_init_fast(buf, strlen(buf)); + COLOPL_TS_G(pdo_mysql_orig_methods)->doer(dbh, sql); + zend_string_release(sql); + + return true; +} + +static bool hook_pdo_driver_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *stmt, zval *driver_options) +{ + bool retval; + + if (get_is_hooked()) { + pdo_time_apply(dbh); + } + + retval = COLOPL_TS_G(pdo_mysql_orig_methods)->preparer(dbh, sql, stmt, driver_options); + + return retval; +} + +static zend_long hook_pdo_driver_doer(pdo_dbh_t *dbh, const zend_string *sql) +{ + if (get_is_hooked()) { + pdo_time_apply(dbh); + } + + return COLOPL_TS_G(pdo_mysql_orig_methods)->doer(dbh, sql); +} + +static void hook_pdo_con(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(pdo_con); + + pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS); + + CALL_ORIGINAL_FUNCTION(pdo_con); + + if (!dbh->driver || + strncmp(dbh->driver->driver_name, "mysql", 5) == 0 || + dbh->methods != &COLOPL_TS_G(hooked_mysql_driver_methods) + ) { + if (!COLOPL_TS_G(pdo_mysql_orig_methods)) { + /* Check pdo_mysql driver. */ + if (!dbh->methods) { + return; + } + + /* Copy original methods struct. */ + COLOPL_TS_G(pdo_mysql_orig_methods) = dbh->methods; + memcpy(&COLOPL_TS_G(hooked_mysql_driver_methods), dbh->methods, sizeof(struct pdo_dbh_methods)); + + /* Override function pointer. */ + COLOPL_TS_G(hooked_mysql_driver_methods).preparer = hook_pdo_driver_preparer; + COLOPL_TS_G(hooked_mysql_driver_methods).doer = hook_pdo_driver_doer; + } + + /* Override MySQL specific driver methods pointer. */ + dbh->methods = &COLOPL_TS_G(hooked_mysql_driver_methods); + } +} + +static inline void mktime_common(INTERNAL_FUNCTION_PARAMETERS, zend_long timestamp) +{ + zend_long hou, min, sec, mon, day, yea; + bool min_is_null = true, sec_is_null = true, mon_is_null = true, day_is_null = true, yea_is_null = true; + timelib_time *t = timelib_time_ctor(); + timelib_rel_time interval; + + timelib_unixtime2gmt(t, timestamp); + get_shift_interval(&interval); + apply_interval(&t, &interval); + + ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 1, 6) + Z_PARAM_LONG(hou) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(min, min_is_null) + Z_PARAM_LONG_OR_NULL(sec, sec_is_null) + Z_PARAM_LONG_OR_NULL(mon, mon_is_null) + Z_PARAM_LONG_OR_NULL(day, day_is_null) + Z_PARAM_LONG_OR_NULL(yea, yea_is_null) + ZEND_PARSE_PARAMETERS_END(); + + if (!min_is_null) { + t->i = min; + } + + if (!sec_is_null) { + t->s = sec; + } + + if (!mon_is_null) { + t->m = mon; + } + + if (!day_is_null) { + t->d = day; + } + + if (!yea_is_null) { + if (yea >= 0 && yea < 70) { + yea += 2000; + } else if (yea >= 70 && yea <= 100) { + yea += 1900; + } + t->y = yea; + } + + RETVAL_LONG(t->sse); + timelib_time_dtor(t); +} + +static inline void date_common(INTERNAL_FUNCTION_PARAMETERS, int localtime) +{ + zend_string *format; + zend_long ts; + bool ts_is_null = true; + + ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 1, 2) + Z_PARAM_STR(format) + Z_PARAM_OPTIONAL; + Z_PARAM_LONG_OR_NULL(ts, ts_is_null) + ZEND_PARSE_PARAMETERS_END(); + + if (ts_is_null) { + ts = get_shifted_time(); + } + + RETVAL_STR(php_format_date(ZSTR_VAL(format), ZSTR_LEN(format), ts, localtime)); +} + +static inline void date_create_common(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry *ce) +{ + zval *timezone_object = NULL; + zend_string *time_str = NULL; + php_date_obj *date = NULL; + timelib_rel_time interval; + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL; + Z_PARAM_STR(time_str) + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(timezone_object, php_date_get_timezone_ce()) + ZEND_PARSE_PARAMETERS_END(); + + php_date_instantiate(ce, return_value); + if (!php_date_initialize( + Z_PHPDATE_P(return_value), + (!time_str ? NULL : ZSTR_VAL(time_str)), + (!time_str ? 0 : ZSTR_LEN(time_str)), + NULL, + timezone_object, + 0 + )) { + zval_ptr_dtor(return_value); + RETVAL_FALSE; + } + + if (time_str && is_fixed_time_str(time_str, timezone_object)) { + return; + } + + get_shift_interval(&interval); + apply_interval(&Z_PHPDATE_P(return_value)->time, &interval); +} + +DEFINE_DT_HOOK_CONSTRUCTOR(dt); + +DEFINE_DT_HOOK_CONSTRUCTOR(dti); + +static void hook_time(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(time); + + CALL_ORIGINAL_FUNCTION(time); + RETURN_LONG(get_shifted_time()); +} + +static void hook_mktime(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(mktime); + + CALL_ORIGINAL_FUNCTION(mktime); + mktime_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, Z_LVAL_P(return_value)); +} + +static void hook_gmmktime(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(gmmktime); + + CALL_ORIGINAL_FUNCTION(gmmktime); + mktime_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, Z_LVAL_P(return_value)); +} + +static void hook_date_create(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(date_create); + + date_create_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_date_get_date_ce()); +} + +static void hook_date_create_immutable(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(date_create_immutable); + + date_create_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_date_get_immutable_ce()); +} + +DEFINE_CREATE_FROM_FORMAT(date_create_from_format); + +DEFINE_CREATE_FROM_FORMAT(date_create_immutable_from_format); + +DEFINE_CREATE_FROM_FORMAT_EX(dt_createfromformat, date_create_from_format); + +DEFINE_CREATE_FROM_FORMAT_EX(dti_createfromformat, date_create_immutable_from_format); + +static void hook_date(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(date); + + date_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1); +} + +static void hook_gmdate(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(gmdate); + + date_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0); +} + +static void hook_idate(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(idate); + + zend_string *format; + zend_long ts; + bool ts_is_null = 1; + + if (Z_TYPE_P(return_value) == IS_FALSE) { + return; + } + + ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET,1, 2) + Z_PARAM_STR(format) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(ts, ts_is_null) + ZEND_PARSE_PARAMETERS_END(); + + if (ts_is_null) { + ts = get_shifted_time(); + } + + RETURN_LONG(php_idate(ZSTR_VAL(format)[0], ts, 0)); +} + +static void hook_getdate(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(getdate); + + zend_long timestamp; + bool timestamp_is_null = true; + + ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(timestamp, timestamp_is_null) + ZEND_PARSE_PARAMETERS_END(); + + if (!timestamp_is_null) { + return; + } + + /* Call original function with timestamp params. */ + zval params[1]; + ZVAL_LONG(¶ms[0], get_shifted_time()); + CALL_ORIGINAL_FUNCTION_WITH_PARAMS(getdate, params, 1); +} + +static void hook_localtime(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(localtime); + + zend_long timestamp; + bool timestamp_is_null = true, associative = false; + + ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 0, 2) + Z_PARAM_OPTIONAL; + Z_PARAM_LONG_OR_NULL(timestamp, timestamp_is_null); + Z_PARAM_BOOL(associative); + ZEND_PARSE_PARAMETERS_END(); + + /* Call original function with params. */ + zval params[2]; + ZVAL_LONG(¶ms[0], get_shifted_time()); + ZVAL_BOOL(¶ms[1], associative); + CALL_ORIGINAL_FUNCTION_WITH_PARAMS(localtime, params, 2); +} + +static void hook_strtotime(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(strtotime); + + zend_string *times, *times_lower; + zend_long preset_ts; + bool preset_ts_is_null = true; + + ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 1, 2) + Z_PARAM_STR(times); + Z_PARAM_OPTIONAL; + Z_PARAM_LONG_OR_NULL(preset_ts, preset_ts_is_null); + ZEND_PARSE_PARAMETERS_END(); + + /* "now" special case */ + times_lower = zend_string_tolower(times); + if (strncmp(ZSTR_VAL(times_lower), "now", 3) == 0) { + zend_string_release(times_lower); + RETURN_LONG((zend_long) get_shifted_time()); + } + zend_string_release(times_lower); + + if (!preset_ts_is_null || is_fixed_time_str(times, NULL) ) { + CALL_ORIGINAL_FUNCTION(strtotime); + return; + } + + /* Call original function with params. */ + zval *params = NULL; + uint32_t param_count = 0; + zend_parse_parameters(ZEND_NUM_ARGS(), "+", ¶ms, ¶m_count); + ZVAL_LONG(¶ms[1], get_shifted_time()); + CALL_ORIGINAL_FUNCTION_WITH_PARAMS(strtotime, params, param_count); + + /* Apply interval. */ + timelib_time *t = timelib_time_ctor(); + timelib_rel_time interval; + timelib_unixtime2gmt(t, Z_LVAL_P(return_value)); + get_shift_interval(&interval); + apply_interval(&t, &interval); + RETVAL_LONG(timelib_date_to_int(t, NULL)); + timelib_time_dtor(t); +} + +#if HAVE_GETTIMEOFDAY +static inline void gettimeofday_common(INTERNAL_FUNCTION_PARAMETERS, int mode) +{ + bool get_as_float = false; + struct timeval tp = {0}; + timelib_time *tm = timelib_time_ctor(); + timelib_rel_time interval; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL; + Z_PARAM_BOOL(get_as_float); + ZEND_PARSE_PARAMETERS_END(); + + if (gettimeofday(&tp, NULL)) { + ZEND_ASSERT(0 && "gettimeofday() can't fail"); + } + + timelib_unixtime2gmt(tm, tp.tv_sec); + tm->us = tp.tv_usec; + get_shift_interval(&interval); + apply_interval(&tm, &interval); + + if (get_as_float) { + RETVAL_DOUBLE((double)(tm->sse + tm->us / 1000000.00)); + } else { + if (mode) { + timelib_time_offset *offset; + + offset = timelib_get_time_zone_info(tm->sse, get_timezone_info()); + + array_init(return_value); + add_assoc_long(return_value, "sec", tm->sse); + add_assoc_long(return_value, "usec", tm->us); + + add_assoc_long(return_value, "minuteswest", -offset->offset / 60); + add_assoc_long(return_value, "dsttime", -offset->is_dst); + + timelib_time_offset_dtor(offset); + } else { + RETVAL_NEW_STR(zend_strpprintf(0, "%.8F %ld", tm->us / 1000000.00, (long) tm->sse)); + } + } + + timelib_time_dtor(tm); +} + +static void hook_microtime(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(microtime); + + gettimeofday_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0); +} + +static void hook_gettimeofday(INTERNAL_FUNCTION_PARAMETERS) +{ + CHECK_STATE(gettimeofday); + + gettimeofday_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1); +} +#endif + +bool register_hooks() +{ + /* \DateTime::__construct */ + HOOK_CONSTRUCTOR(php_date_get_date_ce(), dt); + + /* \DateTimeImmutabel::__construct */ + HOOK_CONSTRUCTOR(php_date_get_immutable_ce(), dti); + + /* \DateTime::createFromFormat */ + HOOK_METHOD(php_date_get_date_ce(), dt, createfromformat); + + /* \DateTimeImmutable::createFromFormat */ + HOOK_METHOD(php_date_get_immutable_ce(), dti, createfromformat); + + HOOK_FUNCTION(time); + HOOK_FUNCTION(mktime); + HOOK_FUNCTION(gmmktime); + HOOK_FUNCTION(date_create); + HOOK_FUNCTION(date_create_immutable); + HOOK_FUNCTION(date_create_from_format); + HOOK_FUNCTION(date_create_immutable_from_format); + HOOK_FUNCTION(date); + HOOK_FUNCTION(gmdate); + HOOK_FUNCTION(idate); + HOOK_FUNCTION(getdate); + HOOK_FUNCTION(localtime); + HOOK_FUNCTION(strtotime); + +#if HAVE_GETTIMEOFDAY + HOOK_FUNCTION(microtime); + HOOK_FUNCTION(gettimeofday); +#endif + + return true; +} + +void register_pdo_hook() +{ + /* \PDO::__construct */ + HOOK_CONSTRUCTOR(php_pdo_get_dbh_ce(), pdo); +} + +bool unregister_hooks() +{ + /* \DateTime::__construct */ + RESTORE_CONSTRUCTOR(php_date_get_date_ce(), dt); + + /* \DateTimeImmutabel::__construct */ + RESTORE_CONSTRUCTOR(php_date_get_immutable_ce(), dti); + + /* \DateTime::createFromFormat */ + RESTORE_METHOD(php_date_get_date_ce(), dt, createfromformat); + + /* \DateTimeImmutable::createFromFormat */ + RESTORE_METHOD(php_date_get_immutable_ce(), dti, createfromformat); + + RESTORE_FUNCTION(time); + RESTORE_FUNCTION(mktime); + RESTORE_FUNCTION(gmmktime); + RESTORE_FUNCTION(date_create); + RESTORE_FUNCTION(date_create_immutable); + RESTORE_FUNCTION(date_create_from_format); + RESTORE_FUNCTION(date_create_immutable_from_format); + RESTORE_FUNCTION(date); + RESTORE_FUNCTION(gmdate); + RESTORE_FUNCTION(idate); + RESTORE_FUNCTION(getdate); + RESTORE_FUNCTION(localtime); + RESTORE_FUNCTION(strtotime); + +#if HAVE_GETTIMEOFDAY + RESTORE_FUNCTION(microtime); + RESTORE_FUNCTION(gettimeofday); +#endif + + return true; +} + +void apply_request_time_hook() +{ + zval *globals_server, *request_time, *request_time_float; + timelib_time *t; + timelib_rel_time interval; + + globals_server = zend_hash_str_find(&EG(symbol_table), "_SERVER", strlen("_SERVER")); + + if (!globals_server || Z_TYPE_P(globals_server) != IS_ARRAY) { + /* $_SERVER not defined */ + return; + } + + request_time = zend_hash_str_find(Z_ARR_P(globals_server), "REQUEST_TIME", strlen("REQUEST_TIME")); + request_time_float = zend_hash_str_find(Z_ARR_P(globals_server), "REQUEST_TIME_FLOAT", strlen("REQUEST_TIME_FLOAT")); + + /* Get original request time at once */ + if (COLOPL_TS_G(orig_request_time) == 0 && COLOPL_TS_G(orig_request_time_float) == 0) { + if (request_time_float) { + COLOPL_TS_G(orig_request_time_float) = Z_DVAL_P(request_time_float); + } else if (request_time) { + COLOPL_TS_G(orig_request_time) = Z_LVAL_P(request_time); + } else { + /* Missing REQUEST_TIME or REQUEST_TIME_FLOAT */ + return; + } + } + + if (COLOPL_TS_G(orig_request_time_float) != 0) { + timelib_sll ts = (timelib_sll) COLOPL_TS_G(orig_request_time_float); + timelib_sll tus = (timelib_sll) ((COLOPL_TS_G(orig_request_time_float) - ts) * 1e6); + + t = timelib_time_ctor(); + timelib_unixtime2gmt(t, ts); + t->us = tus; + timelib_update_ts(t, NULL); + } else if (COLOPL_TS_G(orig_request_time) != 0) { + t = timelib_time_ctor(); + timelib_unixtime2gmt(t, (timelib_sll) COLOPL_TS_G(orig_request_time)); + } else { + /* REQUEST_TIME or REQUEST_TIME_FLOAT not found */ + return; + } + + /* Apply interval. */ + get_shift_interval(&interval); + apply_interval(&t, &interval); + + if (request_time) { + ZVAL_LONG(request_time, (zend_long) t->sse); + } + + if (request_time_float) { + ZVAL_DOUBLE(request_time_float, ((double) t->sse + ((double) t->us / 1000000.0))); + } + + timelib_time_dtor(t); +} diff --git a/ext/hook.h b/ext/hook.h new file mode 100644 index 0000000..8054716 --- /dev/null +++ b/ext/hook.h @@ -0,0 +1,29 @@ +/* + +----------------------------------------------------------------------+ + | COLOPL PHP TimeShifter. | + +----------------------------------------------------------------------+ + | Copyright (c) COLOPL, Inc. | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | info@colopl.co.jp so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ +#ifndef HOOK_H +# define HOOK_H + +# include "php.h" +# include "shared_memory.h" + +bool register_hooks(); +void register_pdo_hook(); +bool unregister_hooks(); +void apply_request_time_hook(); + +#endif /* HOOK_H */ diff --git a/ext/php_colopl_timeshifter.h b/ext/php_colopl_timeshifter.h new file mode 100644 index 0000000..3f953b2 --- /dev/null +++ b/ext/php_colopl_timeshifter.h @@ -0,0 +1,82 @@ +/* + +----------------------------------------------------------------------+ + | COLOPL PHP TimeShifter. | + +----------------------------------------------------------------------+ + | Copyright (c) COLOPL, Inc. | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | info@colopl.co.jp so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ +#ifndef PHP_COLOPL_TIMESHIFTER_H +# define PHP_COLOPL_TIMESHIFTER_H + +# include "ext/date/php_date.h" +# include "ext/pdo/php_pdo_driver.h" + +# include "shared_memory.h" + +void get_shift_interval(timelib_rel_time *time); +bool get_is_hooked(); + +extern zend_module_entry colopl_timeshifter_module_entry; +# define phpext_colopl_timeshifter_ptr &colopl_timeshifter_module_entry + +# define PHP_COLOPL_TIMESHIFTER_VERSION "1.0.0" + +ZEND_BEGIN_MODULE_GLOBALS(colopl_timeshifter) + struct pdo_dbh_methods hooked_mysql_driver_methods; + const struct pdo_dbh_methods *pdo_mysql_orig_methods; + zif_handler orig_pdo_con; /* \PDO::__construct */ + zif_handler orig_dt_con; /* \DateTime::__construct() */ + zif_handler orig_dt_createfromformat; /* \DateTime::createFromFormat() */ + zif_handler orig_dti_con; /* \DateTimeImmutable::__construct() */ + zif_handler orig_dti_createfromformat; /* \DateTimeImmutable::createFromFormat() */ + zif_handler orig_time; + zif_handler orig_mktime; + zif_handler orig_gmmktime; + zif_handler orig_date_create; + zif_handler orig_date_create_immutable; + zif_handler orig_date_create_from_format; + zif_handler orig_date_create_immutable_from_format; + zif_handler orig_date; + zif_handler orig_gmdate; + zif_handler orig_idate; + zif_handler orig_getdate; + zif_handler orig_localtime; + zif_handler orig_strtotime; +# if HAVE_GETTIMEOFDAY + zif_handler orig_microtime; + zif_handler orig_gettimeofday; +# endif + zend_long orig_request_time; + double orig_request_time_float; + zend_long usleep_sec; + bool is_restore_per_request; + bool is_hook_pdo_mysql; + bool is_hook_request_time; +ZEND_END_MODULE_GLOBALS(colopl_timeshifter) + +ZEND_EXTERN_MODULE_GLOBALS(colopl_timeshifter) + +# define COLOPL_TS_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(colopl_timeshifter, v) + +PHP_MINIT_FUNCTION(colopl_timeshifter); +PHP_MSHUTDOWN_FUNCTION(colopl_timeshifter); +PHP_RINIT_FUNCTION(colopl_timeshifter); +PHP_RSHUTDOWN_FUNCTION(colopl_timeshifter); +PHP_MINFO_FUNCTION(colopl_timeshifter); +/* PHP_GINIT_FUNCTION(colopl_timeshifter); */ + +# if defined(ZTS) && defined(COMPILE_DL_COLOPL_TIMESHIFTER) +ZEND_TSRMLS_CACHE_EXTERN() +# endif + +#endif /* PHP_COLOPL_TIMESHIFTER_H */ diff --git a/ext/shared_memory.c b/ext/shared_memory.c new file mode 100644 index 0000000..9eade0b --- /dev/null +++ b/ext/shared_memory.c @@ -0,0 +1,72 @@ +/* + +----------------------------------------------------------------------+ + | COLOPL PHP TimeShifter. | + +----------------------------------------------------------------------+ + | Copyright (c) COLOPL, Inc. | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | info@colopl.co.jp so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "shared_memory.h" + +bool sm_init(sm_t *sm, size_t size) { + sm->size = size; + + if ((sm->data = mmap(NULL, sm->size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)) == MAP_FAILED) { + return false; + } + + if (sem_init(&sm->semaphore, 1, 1) != 0) { + return false; + } + + return sm; +} + +void sm_read(sm_t *sm, void *dest) { + memcpy(dest, sm->data, sm->size); +} + +bool sm_write(sm_t *sm, void *src) { + if (!sm->data) { + return false; + } + + if (sem_wait(&sm->semaphore) != 0) { + return false; + } + + memcpy(sm->data, src, sm->size); + + if (sem_post(&sm->semaphore) != 0) { + return false; + } + + return true; +} + +bool sm_free(sm_t *sm) { + if (!sm->data) { + return false; + } + + if (munmap(sm->data, sm->size) != 0) { + return false; + } + + if (sem_destroy(&sm->semaphore) != 0) { + return false; + } + + return true; +} diff --git a/ext/shared_memory.h b/ext/shared_memory.h new file mode 100644 index 0000000..653ac4e --- /dev/null +++ b/ext/shared_memory.h @@ -0,0 +1,36 @@ +/* + +----------------------------------------------------------------------+ + | COLOPL PHP TimeShifter. | + +----------------------------------------------------------------------+ + | Copyright (c) COLOPL, Inc. | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | info@colopl.co.jp so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ +#ifndef SHARED_MEMORY_H +# define SHARED_MEMORY_H + +# include "php.h" +# include +# include + +typedef struct { + void *data; + size_t size; + sem_t semaphore; +} sm_t; + +bool sm_init(sm_t *sm, size_t size); +void sm_read(sm_t *sm, void *dest); +bool sm_write(sm_t *sm, void *src); +bool sm_free(sm_t *sm); + +#endif /* SHARED_MEMORY_H */ diff --git a/ext/tests/classes/datetime_construct.phpt b/ext/tests/classes/datetime_construct.phpt new file mode 100644 index 0000000..c82840b --- /dev/null +++ b/ext/tests/classes/datetime_construct.phpt @@ -0,0 +1,25 @@ +--TEST-- +Check DateTime::__construct() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- += $before_now || $before_static != $after_static) { + die('failure'); +} + +die('success'); +?> +--EXPECT-- +success diff --git a/ext/tests/classes/datetime_create_from_format.phpt b/ext/tests/classes/datetime_create_from_format.phpt new file mode 100644 index 0000000..88d5ebc --- /dev/null +++ b/ext/tests/classes/datetime_create_from_format.phpt @@ -0,0 +1,33 @@ +--TEST-- +Check DateTime::createFromFormat() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff($before_now); + +if (!$before_now instanceof \DateTime || !$before_static instanceof \DateTime || !$after_now instanceof \DateTime || !$after_static instanceof \DateTime) { + die('failed'); +} + +if ($after_now != $before_now && $interval->y === 3 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/classes/datetime_immutable_construct.phpt b/ext/tests/classes/datetime_immutable_construct.phpt new file mode 100644 index 0000000..0abf1dc --- /dev/null +++ b/ext/tests/classes/datetime_immutable_construct.phpt @@ -0,0 +1,25 @@ +--TEST-- +Check DateTimeImmutable::__construct() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- += $before_now || $before_static != $after_static) { + die('failure'); +} + +die('success'); +?> +--EXPECT-- +success diff --git a/ext/tests/classes/datetime_immutable_create_from_format.phpt b/ext/tests/classes/datetime_immutable_create_from_format.phpt new file mode 100644 index 0000000..6a8106a --- /dev/null +++ b/ext/tests/classes/datetime_immutable_create_from_format.phpt @@ -0,0 +1,33 @@ +--TEST-- +Check DateTimeImmutable::createFromFormat() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff($before_now); + +if (!$before_now instanceof \DateTimeImmutable || !$before_static instanceof \DateTimeImmutable || !$after_now instanceof \DateTimeImmutable || !$after_static instanceof \DateTimeImmutable) { + die('failed'); +} + +if ($after_now != $before_now && $interval->y === 3 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/classes/pdo/mysql/exec_doer.phpt b/ext/tests/classes/pdo/mysql/exec_doer.phpt new file mode 100644 index 0000000..0ebc945 --- /dev/null +++ b/ext/tests/classes/pdo/mysql/exec_doer.phpt @@ -0,0 +1,41 @@ +--TEST-- +Check PDO MySQL (doer) +--EXTENSIONS-- +colopl_timeshifter +pdo +pdo_mysql +mysqlnd +--INI-- +colopl_timeshifter.is_hook_pdo_mysql=1 +--FILE-- +exec('DROP TABLE IF EXISTS testing;'); +$pdo->exec('CREATE TABLE IF NOT EXISTS testing ( + id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, + date DATETIME NOT NULL +);'); +$pdo->exec('INSERT INTO testing (date) VALUES (NOW());'); + +$after = new \DateTimeImmutable( + $pdo->query('SELECT date FROM testing ORDER BY id DESC LIMIT 1;')->fetch(\PDO::FETCH_NUM)[0] +); + +$interval = $after->diff($before); + +if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/classes/pdo/mysql/exec_doer_tidb.phpt b/ext/tests/classes/pdo/mysql/exec_doer_tidb.phpt new file mode 100644 index 0000000..6d3cb68 --- /dev/null +++ b/ext/tests/classes/pdo/mysql/exec_doer_tidb.phpt @@ -0,0 +1,41 @@ +--TEST-- +Check PDO MySQL (doer, TiDB) +--EXTENSIONS-- +colopl_timeshifter +pdo +pdo_mysql +mysqlnd +--FILE-- +exec('CREATE DATABASE IF NOT EXISTS testing;'); +$pdo->exec('USE testing;'); +$pdo->exec('DROP TABLE IF EXISTS testing;'); +$pdo->exec('CREATE TABLE IF NOT EXISTS testing ( + id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, + date DATETIME NOT NULL +);'); +$pdo->exec('INSERT INTO testing (date) VALUES (NOW());'); + +$after = new \DateTimeImmutable( + $pdo->query('SELECT date FROM testing ORDER BY id DESC LIMIT 1;')->fetch(\PDO::FETCH_NUM)[0] +); + +$interval = $after->diff($before); + +if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/classes/pdo/mysql/query_preparer.phpt b/ext/tests/classes/pdo/mysql/query_preparer.phpt new file mode 100644 index 0000000..a6db882 --- /dev/null +++ b/ext/tests/classes/pdo/mysql/query_preparer.phpt @@ -0,0 +1,53 @@ +--TEST-- +Check PDO MySQL (preparer) +--EXTENSIONS-- +colopl_timeshifter +pdo +pdo_mysql +mysqlnd +--INI-- +colopl_timeshifter.is_hook_pdo_mysql=1 +--FILE-- +query(' + SELECT NOW() AS "now", + CURRENT_TIMESTAMP AS "current_timestamp", + CURRENT_TIMESTAMP() AS "current_timestamp_fun", + UTC_TIMESTAMP() AS "utc_timestamp"; +', \PDO::FETCH_ASSOC)->fetch() as $result) { + $after = new \DateTimeImmutable($result); + + if ($after->getTimestamp() >= $before->getTimestamp()) { + die('failure'); + } + + $interval = $after->diff($before); + if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { + } else { + die('failure'); + } +} + +/* UNIX_TIMESTAMP() */ +$after = new \DateTimeImmutable('@' . $pdo->query('SELECT UNIX_TIMESTAMP() AS "unix_timestamp";')->fetch()[0]); +$interval = $after->diff($before); +if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { +} else { + die('failure'); +} + +die('success'); + +?> +--EXPECT-- +success diff --git a/ext/tests/classes/pdo/mysql/query_preparer_tidb.phpt b/ext/tests/classes/pdo/mysql/query_preparer_tidb.phpt new file mode 100644 index 0000000..9923f79 --- /dev/null +++ b/ext/tests/classes/pdo/mysql/query_preparer_tidb.phpt @@ -0,0 +1,53 @@ +--TEST-- +Check PDO MySQL (preparer, TiDB) +--EXTENSIONS-- +colopl_timeshifter +pdo +pdo_mysql +mysqlnd +--FILE-- +exec('CREATE DATABASE IF NOT EXISTS testing;'); +$pdo->exec('USE testing;'); + +/* NOW(), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP(), UTC_TIMESTAMP() */ +foreach ($pdo->query(' + SELECT NOW() AS "now", + CURRENT_TIMESTAMP AS "current_timestamp", + CURRENT_TIMESTAMP() AS "current_timestamp_fun", + UTC_TIMESTAMP() AS "utc_timestamp"; +', \PDO::FETCH_ASSOC)->fetch() as $result) { + $after = new \DateTimeImmutable($result); + + if ($after->getTimestamp() >= $before->getTimestamp()) { + die('failure'); + } + + $interval = $after->diff($before); + if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { + } else { + die('failure'); + } +} + +/* UNIX_TIMESTAMP() */ +$after = new \DateTimeImmutable('@' . $pdo->query('SELECT UNIX_TIMESTAMP() AS "unix_timestamp";')->fetch()[0]); +$interval = $after->diff($before); +if ($after < $before && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { +} else { + die('failure'); +} + +die('success'); + +?> +--EXPECT-- +success diff --git a/ext/tests/extension.phpt b/ext/tests/extension.phpt new file mode 100644 index 0000000..b9b926b --- /dev/null +++ b/ext/tests/extension.phpt @@ -0,0 +1,47 @@ +--TEST-- +Check if colopl_timeshifter is loaded +--FILE-- += $after || $hooked >= $before || $seconde_hooked >= $before) { + die('failure behavior'); +} + +die('success'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/date.phpt b/ext/tests/functions/date.phpt new file mode 100644 index 0000000..1b95298 --- /dev/null +++ b/ext/tests/functions/date.phpt @@ -0,0 +1,25 @@ +--TEST-- +Check date() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff(new \DateTime("{$before}")); + +if (4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/date_create.phpt b/ext/tests/functions/date_create.phpt new file mode 100644 index 0000000..ddacd09 --- /dev/null +++ b/ext/tests/functions/date_create.phpt @@ -0,0 +1,32 @@ +--TEST-- +Check date_create() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- += $before_now || $before_static != $after_static) { + die('failure'); +} + +if ( + !$before_now instanceof \DateTime || !$before_static instanceof \DateTime || + !$after_now instanceof \DateTime || !$after_static instanceof \DateTime +) { + die('failure'); +} + +die('success'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/date_create_from_format.phpt b/ext/tests/functions/date_create_from_format.phpt new file mode 100644 index 0000000..ab8b832 --- /dev/null +++ b/ext/tests/functions/date_create_from_format.phpt @@ -0,0 +1,33 @@ +--TEST-- +Check date_create_from_format() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff($before_now); + +if (!$before_now instanceof \DateTime || !$before_static instanceof \DateTime || !$after_now instanceof \DateTime || !$after_static instanceof \DateTime) { + die('failed'); +} + +if ($after_now != $before_now && $interval->y === 3 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/date_create_immutable.phpt b/ext/tests/functions/date_create_immutable.phpt new file mode 100644 index 0000000..ec4eda6 --- /dev/null +++ b/ext/tests/functions/date_create_immutable.phpt @@ -0,0 +1,32 @@ +--TEST-- +Check date_create_immutable() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- += $before_now || $before_static != $after_static) { + die('failure'); +} + +if ( + !$before_now instanceof \DateTimeImmutable || !$before_static instanceof \DateTimeImmutable || + !$after_now instanceof \DateTimeImmutable || !$after_static instanceof \DateTimeImmutable +) { + die('failure'); +} + +die('success'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/date_create_immutable_from_format.phpt b/ext/tests/functions/date_create_immutable_from_format.phpt new file mode 100644 index 0000000..8c6ebc4 --- /dev/null +++ b/ext/tests/functions/date_create_immutable_from_format.phpt @@ -0,0 +1,33 @@ +--TEST-- +Check date_create_immutable_from_format() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff($before_now); + +if (!$before_now instanceof \DateTimeImmutable || !$before_static instanceof \DateTimeImmutable || !$after_now instanceof \DateTimeImmutable || !$after_static instanceof \DateTimeImmutable) { + die('failed'); +} + +if ($after_now != $before_now && $interval->y === 3 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/getdate.phpt b/ext/tests/functions/getdate.phpt new file mode 100644 index 0000000..b38467f --- /dev/null +++ b/ext/tests/functions/getdate.phpt @@ -0,0 +1,25 @@ +--TEST-- +Check getdate() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff(new \DateTime("@{$before[0]}")); + +if (4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/gettimeofday.phpt b/ext/tests/functions/gettimeofday.phpt new file mode 100644 index 0000000..af07008 --- /dev/null +++ b/ext/tests/functions/gettimeofday.phpt @@ -0,0 +1,32 @@ +--TEST-- +Check gettimeofday() +--EXTENSIONS-- +colopl_timeshifter +--SKIPIF-- + +--FILE-- +diff(new \DateTime("@{$before1['sec']}")); +$interval2 = (new \DateTime("@{$after2}"))->diff(new \DateTime("@{$before2}")); + +if (4 > $interval1->days && $interval1->days >= 2 && $interval1->invert === 0 && + 4 > $interval2->days && $interval2->days >= 2 && $interval2->invert === 0 +) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/gmdate.phpt b/ext/tests/functions/gmdate.phpt new file mode 100644 index 0000000..bd9ee03 --- /dev/null +++ b/ext/tests/functions/gmdate.phpt @@ -0,0 +1,25 @@ +--TEST-- +Check gmdate() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff(new \DateTime("{$before}")); + +if (4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/gmmktime.phpt b/ext/tests/functions/gmmktime.phpt new file mode 100644 index 0000000..0f3be18 --- /dev/null +++ b/ext/tests/functions/gmmktime.phpt @@ -0,0 +1,26 @@ +--TEST-- +Check gmmktime() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff(new \DateTime("@{$before}")); + +if ($interval->i >= 29 && 32 > $interval->i && $interval->invert === 0) { + die('success'); +} + +die('failed'); + +?> +--EXPECT-- +success diff --git a/ext/tests/functions/idate.phpt b/ext/tests/functions/idate.phpt new file mode 100644 index 0000000..c7d0de3 --- /dev/null +++ b/ext/tests/functions/idate.phpt @@ -0,0 +1,25 @@ +--TEST-- +Check idate() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff(new \DateTime("@{$before}")); + +if (4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/localtime.phpt b/ext/tests/functions/localtime.phpt new file mode 100644 index 0000000..bd55a2b --- /dev/null +++ b/ext/tests/functions/localtime.phpt @@ -0,0 +1,28 @@ +--TEST-- +Check localtime() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff(new \DateTime("{$before1[5]}-{$before1[4]}-{$before1[3]} {$before1[2]}:{$before1[1]}:{$before1[0]}")); +$interval2 = (new \DateTime("{$after2['tm_year']}-{$after2['tm_mon']}-{$after2['tm_mday']} {$after2['tm_hour']}:{$after2['tm_min']}:{$after2['tm_sec']}"))->diff(new \DateTime("{$before2['tm_year']}-{$before2['tm_mon']}-{$before2['tm_mday']} {$before2['tm_hour']}:{$before2['tm_min']}:{$before2['tm_sec']}")); + +if ($interval1->days >= 2 && $interval1->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/microtime.phpt b/ext/tests/functions/microtime.phpt new file mode 100644 index 0000000..041dadf --- /dev/null +++ b/ext/tests/functions/microtime.phpt @@ -0,0 +1,32 @@ +--TEST-- +Check microtime() +--EXTENSIONS-- +colopl_timeshifter +--SKIPIF-- + +--FILE-- +diff(new \DateTime("@{$before1[1]}")); +$interval2 = (new \DateTime("@{$after2}"))->diff(new \DateTime("@{$before2}")); + +if (4 > $interval1->days && $interval1->days >= 2 && $interval1->invert === 0 && + 4 > $interval2->days && $interval2->days >= 2 && $interval2->invert === 0 +) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/mktime.phpt b/ext/tests/functions/mktime.phpt new file mode 100644 index 0000000..bc14d04 --- /dev/null +++ b/ext/tests/functions/mktime.phpt @@ -0,0 +1,26 @@ +--TEST-- +Check mktime() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff(new \DateTime("@{$before}")); + +if ($interval->i >= 29 && 32 > $interval->i && $interval->invert === 0) { + die('success'); +} + +die('failed'); + +?> +--EXPECT-- +success diff --git a/ext/tests/functions/strtotime.phpt b/ext/tests/functions/strtotime.phpt new file mode 100644 index 0000000..081866d --- /dev/null +++ b/ext/tests/functions/strtotime.phpt @@ -0,0 +1,37 @@ +--TEST-- +Check strtotime() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff(new \DateTime("{$before}")); + +if ($before_now != $after_now && $before_fixed === $after_fixed && 4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/functions/strtotime_extra.phpt b/ext/tests/functions/strtotime_extra.phpt new file mode 100644 index 0000000..ea96580 --- /dev/null +++ b/ext/tests/functions/strtotime_extra.phpt @@ -0,0 +1,30 @@ +--TEST-- +Check strtotime() extra pattern +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff($before); + +if ($before == $after_one || $before == $after_two) { + die('failed'); +} + +/* Note: Sometime valgrind makes flaky: $interval->y !== 2 */ +if (($interval->y > 2 && $interval->y !== 0) || $interval->invert !== 0) { + die('failed'); +} + +die('success'); + +?> +--EXPECT-- +success diff --git a/ext/tests/functions/time.phpt b/ext/tests/functions/time.phpt new file mode 100644 index 0000000..49bacb9 --- /dev/null +++ b/ext/tests/functions/time.phpt @@ -0,0 +1,25 @@ +--TEST-- +Check time() +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff(new \DateTime("@{$before}")); + +if (4 > $interval->days && $interval->days >= 2 && $interval->invert === 0) { + die('success'); +} + +die('failed'); +?> +--EXPECT-- +success diff --git a/ext/tests/variables/request_time.phpt b/ext/tests/variables/request_time.phpt new file mode 100644 index 0000000..4478a5a --- /dev/null +++ b/ext/tests/variables/request_time.phpt @@ -0,0 +1,27 @@ +--TEST-- +Check $_SERVER['REQUEST_TIME'] +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff($before); + +if ($before == $after || $interval->y !== 1 || $interval->invert !== 0) { + die('failed'); +} + +die('success'); + +?> +--EXPECT-- +success diff --git a/ext/tests/variables/request_time_fail.phpt b/ext/tests/variables/request_time_fail.phpt new file mode 100644 index 0000000..164e249 --- /dev/null +++ b/ext/tests/variables/request_time_fail.phpt @@ -0,0 +1,29 @@ +--TEST-- +Check fail pattern $_SERVER['REQUEST_TIME'] +--EXTENSIONS-- +colopl_timeshifter +--INI-- +colopl_timeshifter.is_hook_request_time=0 +--FILE-- +diff($before); + +if ($before == $after || $interval->y !== 1 || $interval->invert !== 0) { + die('failed'); +} + +die('success'); + +?> +--EXPECT-- +failed diff --git a/ext/tests/variables/request_time_float.phpt b/ext/tests/variables/request_time_float.phpt new file mode 100644 index 0000000..3d92308 --- /dev/null +++ b/ext/tests/variables/request_time_float.phpt @@ -0,0 +1,31 @@ +--TEST-- +Check $_SERVER['REQUEST_TIME_FLOAT'] +--EXTENSIONS-- +colopl_timeshifter +--FILE-- +diff($before); + +if ($before == $after || $interval->y !== 1 || $interval->invert !== 0) { + die('failed'); +} + +if ($before->format('u') !== $after->format('u')) { + die('failed'); +} + +die('success'); + +?> +--EXPECT-- +success diff --git a/ext/third_party/timelib b/ext/third_party/timelib new file mode 160000 index 0000000..06dc8d1 --- /dev/null +++ b/ext/third_party/timelib @@ -0,0 +1 @@ +Subproject commit 06dc8d1bb22816c14cfd5a09c9fc914c2086dec9 diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..9f2da6d --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,10 @@ +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon +parameters: + level: max + paths: + - src + - tests + parallel: + processTimeout: 1800.0 + maximumNumberOfProcesses: 4 diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..87e9167 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/Manager.php b/src/Manager.php new file mode 100644 index 0000000..1841281 --- /dev/null +++ b/src/Manager.php @@ -0,0 +1,89 @@ +diff(new \DateTimeImmutable())); + } + + /** + * Set interval to the current time. + */ + public static function hookDateInterval(\DateInterval $dateInterval): bool + { + if (! self::isAvailable()) { + return \false; + } + + /* + * Calculate in days to ensure correct conversion of days + * when specifying a period that straddles months. + */ + $actualInterval = clone $dateInterval; + if (is_int($actualInterval->days) && $actualInterval->days > 0 && $actualInterval->days !== $actualInterval->d) { + /** @psalm-suppress InaccessibleProperty */ + $actualInterval->d = $actualInterval->days; + /** @psalm-suppress InaccessibleProperty */ + $actualInterval->y = 0; + /** @psalm-suppress InaccessibleProperty */ + $actualInterval->m = 0; + } + + return \Colopl\ColoplTimeShifter\register_hook($actualInterval); + } + + private static function checkAvailable(): void + { + self::$isAvailable = \extension_loaded('colopl_timeshifter'); + } +} diff --git a/tests/ManagerTest.php b/tests/ManagerTest.php new file mode 100644 index 0000000..b1b0b29 --- /dev/null +++ b/tests/ManagerTest.php @@ -0,0 +1,61 @@ +format('Y-m-d')); + } + + public function testUnhook(): void + { + $now = new \DateTimeImmutable(); + + Manager::hookDateTime(new \DateTimeImmutable('1994-10-26 12:00:00')); + + self::assertEquals('1994-10-26', (new \DateTimeImmutable())->format('Y-m-d')); + + Manager::unhook(); + + self::assertEquals($now->format('Y-m-d'), (new \DateTimeImmutable())->format('Y-m-d')); + } + + public function testHookInterval(): void + { + $now = new \DateTimeImmutable(); + self::assertTrue(Manager::hookDateInterval(new \DateInterval('P1D'))); + self::assertEquals($now->sub(new \DateInterval('P1D'))->format('Y-m-d'), (new \DateTimeImmutable())->format('Y-m-d')); + } + + public function testHookDateTime(): void + { + $now = new \DateTimeImmutable(); + $diff = (new \DateTimeImmutable('1994-10-26 12:00:00'))->diff($now); + + self::assertTrue(Manager::hookDateTime(new \DateTimeImmutable('1994-10-26 12:00:00'))); + + self::assertEquals($diff->format('%d'), (new \DateTimeImmutable())->diff($now)->format('%d')); + } + + protected function tearDown(): void + { + Manager::unhook(); + \Colopl\ColoplTimeShifter\unregister_hook(); + } +}