diff --git a/.appveyor.yml b/.appveyor.yml index 51d965a250..1c40068b72 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -30,7 +30,7 @@ install: - echo extension=php_mbstring.dll >> php.ini - echo extension=php_openssl.dll >> php.ini - echo extension=php_pdo_sqlite.dll >> php.ini - - IF NOT EXIST C:\tools\composer.phar (cd C:\tools && appveyor DownloadFile https://getcomposer.org/download/2.5.8/composer.phar) + - IF NOT EXIST C:\tools\composer.phar (cd C:\tools && appveyor DownloadFile https://getcomposer.org/download/2.6.3/composer.phar) before_test: - cd C:\projects\yii2 diff --git a/.editorconfig b/.editorconfig index 257221d23d..2f151b766c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,7 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false + +[*.yml] +indent_style = tab +indent_size = 2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad632e4c7c..8abe95eb24 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,15 +13,15 @@ concurrency: jobs: phpunit: - name: PHP ${{ matrix.php }} on ${{ matrix.os }} + name: PHP ${{ matrix.php }} - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: os: [ubuntu-latest] - php: [8.1, 8.2, 8.3] + php: [8.1, 8.2, 8.3, 8.4] steps: - name: Generate french locale. @@ -55,6 +55,7 @@ jobs: - name: Upload coverage to Codecov. if: matrix.php == '8.1' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/ci-mariadb.yml b/.github/workflows/ci-mariadb.yml new file mode 100644 index 0000000000..d09208c78c --- /dev/null +++ b/.github/workflows/ci-mariadb.yml @@ -0,0 +1,71 @@ +on: + - pull_request + - push + +name: ci-mariadb + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: PHP ${{ matrix.php }}-mariadb-${{ matrix.mariadb }} + env: + extensions: curl, intl, pdo, pdo_mysql + XDEBUG_MODE: coverage, develop + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + php: [8.1, 8.2, 8.3, 8.4] + mariadb: + - mariadb:10.4 + - mariadb:latest + + services: + mysql: + image: ${{ matrix.mariadb }} + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: yiitest + ports: + - 3306:3306 + options: --health-cmd="mariadb-admin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - name: Checkout. + uses: actions/checkout@v4 + + - name: Install PHP with extensions. + uses: shivammathur/setup-php@v2 + with: + coverage: xdebug + extensions: ${{ env.EXTENSIONS }} + ini-values: date.timezone='UTC' + php-version: ${{ matrix.php }} + tools: composer:v2, pecl + + - name: Update composer. + run: composer self-update + + - name: Install dependencies with composer. + run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi + + - name: Run MariaDB tests with PHPUnit and generate coverage. + if: matrix.php == '8.1' + run: vendor/bin/phpunit --group mysql --coverage-clover=coverage.xml --colors=always --verbose + + - name: Run MariaDB tests with PHPUnit. + if: matrix.php != '8.1' + run: vendor/bin/phpunit --group mysql --colors=always --verbose + + - name: Upload coverage to Codecov. + if: matrix.php == '8.1' + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/ci-mssql.yml b/.github/workflows/ci-mssql.yml index 74a46ade0a..ea4d2f351b 100644 --- a/.github/workflows/ci-mssql.yml +++ b/.github/workflows/ci-mssql.yml @@ -13,42 +13,45 @@ jobs: name: PHP ${{ matrix.php }}-mssql-${{ matrix.mssql }} env: - EXTENSIONS: pdo, pdo_sqlsrv-5.10.1 + EXTENSIONS: pdo, pdo_sqlsrv runs-on: ubuntu-latest strategy: - matrix: - os: - - ubuntu-latest + matrix: + os: + - ubuntu-latest - php: - - 8.1 - - 8.2 - - 8.3 + php: + - 8.1 + - 8.2 + - 8.3 + - 8.4 - mssql: - - server:2017-latest - - server:2019-latest - - server:2022-latest + mssql: + - server:2019-latest + - server:2022-latest services: mssql: - image: mcr.microsoft.com/mssql/${{ matrix.mssql }} - env: - SA_PASSWORD: YourStrong!Passw0rd - ACCEPT_EULA: Y - MSSQL_PID: Developer - ports: - - 1433:1433 - options: --name=mssql --health-cmd="/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1'" --health-interval=10s --health-timeout=5s --health-retries=3 + image: mcr.microsoft.com/mssql/${{ matrix.mssql }} + env: + SA_PASSWORD: YourStrong!Passw0rd + ACCEPT_EULA: Y + MSSQL_PID: Developer + ports: + - 1433:1433 + options: --name=mssql --health-cmd="/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1'" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout uses: actions/checkout@v4 + - name: Install ODBC driver + run: sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 + - name: Create MS SQL Database. - run: docker exec -i mssql /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'CREATE DATABASE yiitest' + run: docker exec -i mssql /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'CREATE DATABASE yiitest' - name: Install PHP with extensions. uses: shivammathur/setup-php@v2 @@ -75,6 +78,7 @@ jobs: - name: Upload coverage to Codecov. if: matrix.php == '8.1' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/ci-mysql.yml b/.github/workflows/ci-mysql.yml index 034293fed7..1671dcf528 100644 --- a/.github/workflows/ci-mysql.yml +++ b/.github/workflows/ci-mysql.yml @@ -14,17 +14,16 @@ jobs: env: EXTENSIONS: curl, intl, pdo, pdo_mysql - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - os: - - ubuntu-latest - php: - 8.1 - 8.2 - 8.3 + - 8.4 mysql: - 5.7 @@ -66,7 +65,7 @@ jobs: - name: Upload coverage to Codecov. if: matrix.php == '8.1' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml - + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/ci-node.yml b/.github/workflows/ci-node.yml index 3f49942b84..345ef1b6e7 100644 --- a/.github/workflows/ci-node.yml +++ b/.github/workflows/ci-node.yml @@ -11,7 +11,7 @@ concurrency: jobs: test: - name: NPM 6 on ubuntu-latest + name: NPM 10 on ubuntu-latest runs-on: ubuntu-latest @@ -26,9 +26,9 @@ jobs: run: composer require "bower-asset/jquery:3.6.*@stable" - name: Install node.js. - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: - node-version: 6 + node-version: 20 - name: Tests. run: | diff --git a/.github/workflows/ci-oracle.yml b/.github/workflows/ci-oracle.yml index c471108c6a..607148e241 100644 --- a/.github/workflows/ci-oracle.yml +++ b/.github/workflows/ci-oracle.yml @@ -55,6 +55,7 @@ jobs: run: vendor/bin/phpunit --group oci --coverage-clover=coverage.xml --colors=always --verbose - name: Upload coverage to Codecov. - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/ci-pgsql.yml b/.github/workflows/ci-pgsql.yml index 7b9506007e..e0e96f2131 100644 --- a/.github/workflows/ci-pgsql.yml +++ b/.github/workflows/ci-pgsql.yml @@ -14,17 +14,16 @@ jobs: env: EXTENSIONS: curl, intl, pdo, pdo_pgsql - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - os: - - ubuntu-latest - php: - 8.1 - 8.2 - 8.3 + - 8.4 pgsql: - 10 @@ -71,6 +70,7 @@ jobs: - name: Upload coverage to Codecov. if: matrix.php == '8.1' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/ci-sqlite.yml b/.github/workflows/ci-sqlite.yml index b7857e8524..915616856f 100644 --- a/.github/workflows/ci-sqlite.yml +++ b/.github/workflows/ci-sqlite.yml @@ -18,14 +18,13 @@ jobs: runs-on: ubuntu-latest strategy: - matrix: - os: - - ubuntu-latest - - php: - - 8.1 - - 8.2 - - 8.3 + fail-fast: false + matrix: + php: + - 8.1 + - 8.2 + - 8.3 + - 8.4 steps: - name: Checkout. @@ -57,6 +56,7 @@ jobs: - name: Upload coverage to Codecov. if: matrix.php == '8.1' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.well-known/funding-manifest-urls b/.well-known/funding-manifest-urls new file mode 100644 index 0000000000..c323cd4126 --- /dev/null +++ b/.well-known/funding-manifest-urls @@ -0,0 +1 @@ +https://www.yiiframework.com/funding.json diff --git a/README.md b/README.md index ae8fd4ae47..b4b8631bdb 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The framework is easy to adjust to meet your needs, because Yii has been designe Installation ------------ -- The minimum required PHP version of Yii is PHP 5.4. +- The minimum required PHP version of Yii is PHP 7.3. - It works best with PHP 8. - [Follow the Definitive Guide](https://www.yiiframework.com/doc-2.0/guide-start-installation.html) in order to get step by step instructions. @@ -34,6 +34,11 @@ and a [Definitive Guide Mirror](http://stuff.cebe.cc/yii2docs/) which is updated - For Yii 1.1 users, there is [Upgrading from Yii 1.1](https://www.yiiframework.com/doc/guide/2.0/en/intro-upgrade-from-v1) to get an idea of what has changed in 2.0. +Versions & PHP compatibility +---------------------------- + +See ["Release Cycle" at the website](https://www.yiiframework.com/release-cycle). + Community --------- diff --git a/build/controllers/PhpDocController.php b/build/controllers/PhpDocController.php index 2e38e1c89d..6d14d2d3a5 100644 --- a/build/controllers/PhpDocController.php +++ b/build/controllers/PhpDocController.php @@ -233,6 +233,7 @@ class PhpDocController extends Controller 'requirements/', 'gii/generators/', 'vendor/', + '_support/', ]), ]; diff --git a/build/controllers/TranslationController.php b/build/controllers/TranslationController.php index 3ca6166762..550b74975d 100644 --- a/build/controllers/TranslationController.php +++ b/build/controllers/TranslationController.php @@ -8,7 +8,6 @@ namespace yii\build\controllers; use DirectoryIterator; -use Yii; use yii\console\Controller; use yii\helpers\Html; diff --git a/composer.json b/composer.json index 37a21f8ce9..7c9f043fbb 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,7 @@ "ext-ctype": "*", "lib-pcre": "*", "yiisoft/yii2-composer": "~2.0.4", - "ezyang/htmlpurifier": "^4.6", + "ezyang/htmlpurifier": "^4.17", "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", "bower-asset/inputmask": "^5.0.8 ", @@ -81,10 +81,10 @@ "bower-asset/yii2-pjax": "~2.0.1" }, "require-dev": { - "dms/phpunit-arraysubset-asserts": "^0.5", - "phpunit/phpunit": "^9.6", "cebe/indent": "~1.0.2", "dealerdirect/phpcodesniffer-composer-installer": "*", + "dms/phpunit-arraysubset-asserts": "^0.5", + "phpunit/phpunit": "9.6", "yiisoft/yii2-coding-standards": "^3.0" }, "repositories": [ diff --git a/composer.lock b/composer.lock index e5d5f186df..112a5d9aa0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7269257cc9e1f2b37d4e30c637bb226b", + "content-hash": "67f4619312f2cbd6fe508df0d6219a2f", "packages": [ { "name": "bower-asset/inputmask", - "version": "5.0.8", + "version": "5.0.9", "source": { "type": "git", - "url": "git@github.com:RobinHerbots/Inputmask.git", - "reference": "e0f39e0c93569c6b494c3a57edef2c59313a6b64" + "url": "https://github.com/RobinHerbots/Inputmask.git", + "reference": "310a33557e2944daf86d5946a5e8c82b9118f8f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/RobinHerbots/Inputmask/zipball/e0f39e0c93569c6b494c3a57edef2c59313a6b64", - "reference": "e0f39e0c93569c6b494c3a57edef2c59313a6b64" + "url": "https://api.github.com/repos/RobinHerbots/Inputmask/zipball/310a33557e2944daf86d5946a5e8c82b9118f8f7", + "reference": "310a33557e2944daf86d5946a5e8c82b9118f8f7" }, "require": { "bower-asset/jquery": ">=1.7" @@ -147,20 +147,20 @@ }, { "name": "ezyang/htmlpurifier", - "version": "v4.17.0", + "version": "v4.18.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c" + "reference": "cb56001e54359df7ae76dc522d08845dc741621b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c", - "reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b", + "reference": "cb56001e54359df7ae76dc522d08845dc741621b", "shasum": "" }, "require": { - "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { "cerdic/css-tidy": "^1.7 || ^2.0", @@ -202,22 +202,22 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0" }, - "time": "2023-11-17T15:01:25+00:00" + "time": "2024-11-01T03:51:45+00:00" }, { "name": "yiisoft/yii2-composer", - "version": "2.0.10", + "version": "2.0.11", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-composer.git", - "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510" + "reference": "b684b01ecb119c8287721def726a0e24fec2fef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/94bb3f66e779e2774f8776d6e1bdeab402940510", - "reference": "94bb3f66e779e2774f8776d6e1bdeab402940510", + "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/b684b01ecb119c8287721def726a0e24fec2fef2", + "reference": "b684b01ecb119c8287721def726a0e24fec2fef2", "shasum": "" }, "require": { @@ -260,11 +260,11 @@ "yii2" ], "support": { - "forum": "http://www.yiiframework.com/forum/", - "irc": "irc://irc.freenode.net/yii", + "forum": "https://www.yiiframework.com/forum/", + "irc": "ircs://irc.libera.chat:6697/yii", "issues": "https://github.com/yiisoft/yii2-composer/issues", "source": "https://github.com/yiisoft/yii2-composer", - "wiki": "http://www.yiiframework.com/wiki/" + "wiki": "https://www.yiiframework.com/wiki/" }, "funding": [ { @@ -280,7 +280,7 @@ "type": "tidelift" } ], - "time": "2020-06-24T00:04:01+00:00" + "time": "2025-02-13T20:59:36+00:00" } ], "packages-dev": [ @@ -515,16 +515,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", "shasum": "" }, "require": { @@ -532,11 +532,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -562,7 +563,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" }, "funding": [ { @@ -570,20 +571,20 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2025-02-12T12:17:51+00:00" }, { "name": "nikic/php-parser", - "version": "v5.0.2", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { @@ -594,7 +595,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -626,9 +627,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-03-05T20:51:40+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "phar-io/manifest", @@ -750,35 +751,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -787,7 +788,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -816,7 +817,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -824,7 +825,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1069,16 +1070,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.17", + "version": "9.6.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd" + "reference": "70fc8be1d0b9fad56a199a4df5f9cfabfc246f84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1a156980d78a6666721b7e8e8502fe210b587fcd", - "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/70fc8be1d0b9fad56a199a4df5f9cfabfc246f84", + "reference": "70fc8be1d0b9fad56a199a4df5f9cfabfc246f84", "shasum": "" }, "require": { @@ -1093,7 +1094,7 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", + "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -1111,8 +1112,8 @@ "sebastian/version": "^3.0.2" }, "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "ext-soap": "*", + "ext-xdebug": "*" }, "bin": [ "phpunit" @@ -1151,8 +1152,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.17" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.0" }, "funding": [ { @@ -1168,7 +1168,7 @@ "type": "tidelift" } ], - "time": "2024-02-23T13:14:51+00:00" + "time": "2023-02-03T07:32:24+00:00" }, { "name": "sebastian/cli-parser", @@ -2135,16 +2135,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.0", + "version": "3.12.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" + "reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa", + "reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa", "shasum": "" }, "require": { @@ -2209,9 +2209,13 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-02-16T15:06:51+00:00" + "time": "2025-04-13T04:10:18+00:00" }, { "name": "theseer/tokenizer", @@ -2265,16 +2269,16 @@ }, { "name": "yiisoft/yii2-coding-standards", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-coding-standards.git", - "reference": "8bc39acaae848aec1ad52b2af4cf380e3f0b104e" + "reference": "842ffdf6c31f46bb6f4b3f3c7dda4f570321ace7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-coding-standards/zipball/8bc39acaae848aec1ad52b2af4cf380e3f0b104e", - "reference": "8bc39acaae848aec1ad52b2af4cf380e3f0b104e", + "url": "https://api.github.com/repos/yiisoft/yii2-coding-standards/zipball/842ffdf6c31f46bb6f4b3f3c7dda4f570321ace7", + "reference": "842ffdf6c31f46bb6f4b3f3c7dda4f570321ace7", "shasum": "" }, "require": { @@ -2347,7 +2351,7 @@ "type": "open_collective" } ], - "time": "2024-03-15T12:57:48+00:00" + "time": "2024-06-12T13:50:40+00:00" } ], "aliases": [], @@ -2363,6 +2367,6 @@ "ext-ctype": "*", "lib-pcre": "*" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/docs/guide-ar/intro-yii.md b/docs/guide-ar/intro-yii.md index bff073a5c4..ae284244e2 100644 --- a/docs/guide-ar/intro-yii.md +++ b/docs/guide-ar/intro-yii.md @@ -45,7 +45,7 @@ Yii هو إطار عام لبرمجة الويب ، مما يعني أنه يم #
المتطلبات الأساسية للعمل على إطار ال Yii
diff --git a/docs/guide-es/caching-data.md b/docs/guide-es/caching-data.md index 065903d853..3adc25f5c8 100644 --- a/docs/guide-es/caching-data.md +++ b/docs/guide-es/caching-data.md @@ -189,6 +189,7 @@ Aquí abajo se muestra un sumario de las dependencias disponibles: - [[yii\caching\ChainedDependency]]: la dependencia cambia si cualquiera de las dependencias en la cadena cambia. - [[yii\caching\DbDependency]]: la dependencia cambia si el resultado de la consulta de la sentencia SQL especificada cambia. - [[yii\caching\ExpressionDependency]]: la dependencia cambia si el resultado de la expresión de PHP especificada cambia. +- [[yii\caching\CallbackDependency]]: la dipendenza viene modificata se il risultato della callback PHP specificata cambia. - [[yii\caching\FileDependency]]: la dependencia cambia si se modifica la última fecha de modificación del archivo. - [[yii\caching\TagDependency]]: marca un elemento de datos en caché con un nombre de grupo. Puedes invalidar los elementos de datos almacenados en caché con el mismo nombre del grupo a la vez llamando a [[yii\caching\TagDependency::invalidate()]]. diff --git a/docs/guide-es/intro-yii.md b/docs/guide-es/intro-yii.md index 5c57a80a2c..28f6e31182 100644 --- a/docs/guide-es/intro-yii.md +++ b/docs/guide-es/intro-yii.md @@ -51,7 +51,7 @@ Esta guía está basada principalmente en la versión 2.0. del framework. Requisitos y Prerequisitos -------------------------- -Yii 2.0 requiere PHP 5.4.0 o una versión posterior y corre de mejor manera en la última versión de PHP. Se pueden encontrar requisitos más detallados de características individuales +Yii 2.0 requiere PHP 7.3.0 o una versión posterior y corre de mejor manera en la última versión de PHP. Se pueden encontrar requisitos más detallados de características individuales ejecutando el script de comprobación incluido en cada lanzamiento de Yii. Para utilizar Yii se requieren conocimientos básicos de programación orientada a objetos (POO), porque el framework Yii se basa íntegramente en esta tecnología. diff --git a/docs/guide-es/security-authentication.md b/docs/guide-es/security-authentication.md new file mode 100644 index 0000000000..1a75e50578 --- /dev/null +++ b/docs/guide-es/security-authentication.md @@ -0,0 +1,167 @@ +Authentication +============== + +La autenticación es el proceso de verificar la identidad de un usuario. Usualmente se usa un identificador (ej. un `username` o una dirección de correo electrónico) y una token secreto (ej. una contraseña o un token de acceso) para juzgar si el usuario es quien dice ser. La autenticación es la base de la función de inicio de sesión. + +Yii proporciona un marco de autenticación que conecta varios componentes para soportar el inicio de sesión. Para utilizar este marco, usted necesita principalmente hacer el siguiente trabajo: + +* Configurar el componente de la aplicación [[yii\web\User|user]]; +* Crear una clase que implemente la interfaz [[yii\web\IdentityInterface]]. + +## Configurando [[yii\web\User]] + +El componente [[yii\web\User|user]] gestiona el estado de autenticación del usuario. Requiere que especifiques una [[yii\web\User::identityClass|clase de identidad]] la cual contiene la lógica de autenticación. En la siguiente configuración de la aplicación, la [[yii\web\User::identityClass|clase identity]] para [[yii\web\User|user]] está configurada para ser `app\models\User` cuya implementación se explica en la siguiente subsección: + +```php +return [ + 'components' => [ + 'user' => [ + 'identityClass' => 'app\models\User', + ], + ], +]; +``` + +## Implementando [[yii\web\IdentityInterface]] +La [[yii\web\User::identityClass|clase identity]] debe implementar la [[yii\web\IdentityInterface]] que contiene los siguientes métodos: +* [[yii\web\IdentityInterface::findIdentity()|findIdentity()]]: busca una instancia de la clase identidad usando el ID de usuario especificado. Este método se utiliza cuando se necesita mantener el estado de inicio de sesión (login) a través de la sesión. + +* [[yii\web\IdentityInterface::findIdentityByAccessToken()|findIdentityByAccessToken()]]: busca una instancia de la clase de identidad usando el token de acceso especificado. Este método se utiliza cuando se necesita autenticar a un usuario mediante un único token secreto (ej. en una aplicación RESTful sin estado). +* [[yii\web\IdentityInterface::getId()|getId()]]: devuelve el ID del usuario representado por esta instancia de identidad. +* [[yii\web\IdentityInterface::getAuthKey()|getAuthKey()]]: devuelve una clave utilizada para validar la sesión y el auto-login en caso de que esté habilitado. +* [[yii\web\IdentityInterface::validateAuthKey()|validateAuthKey()]]: implementa la lógica para verificar la clave de autenticación. + +Si no se necesita un método en particular, se podría implementar con un cuerpo vacío, Por ejemplo, Si un método en particular no es necesario, puedes implementarlo con un cuerpo vacío. Por ejemplo, si tu aplicación es una aplicación RESTful pura sin estado, sólo necesitarás implementar [[yii\web\IdentityInterface::findIdentityByAccessToken()|findIdentityByAccessToken()]] y [[yii\web\IdentityInterface::getId()|getId()]] dejando el resto de métodos con un cuerpo vacío. O si tu aplicación utiliza autenticación sólo de sesión, necesitarías implementar todos los métodos excepto findIdentityByAccessToken(). + +En el siguiente ejemplo, una clase [[yii\web\User::identityClass|identity]] es implementada como una clase [Active Record](db-active-record.md) asociada con la tabla de base de datos `user`. + +```php + $token]); + } + + /** + * @return int|string ID del usuario actual + */ + public function getId() + { + return $this->id; + } + + /** + * @return string|null llave de autenticación del usuario actual + */ + public function getAuthKey() + { + return $this->auth_key; + } + + /** + * @param string $authKey + * @return bool|null si la llave de autenticación es válida para el usuario actual + */ + public function validateAuthKey($authKey) + { + return $this->getAuthKey() === $authKey; + } +} +``` + +Puede utilizar el siguiente código para generar una clave de autenticación para cada usuario y almacenarla en la tabla `user`: + +```php +class User extends ActiveRecord implements IdentityInterface +{ + ...... + + public function beforeSave($insert) + { + if (parent::beforeSave($insert)) { + if ($this->isNewRecord) { + $this->auth_key = \Yii::$app->security->generateRandomString(); + } + return true; + } + return false; + } +} +``` + +> Nota: No confundas la clase de identidad `User` con [[yii\web\User]]. La primera es la clase que implementa la lógica de autenticación. Suele implementarse como una clase [Active Record](db-active-record.md) asociada a algún almacenamiento persistente para guardar la información de las credenciales del usuario. Esta última es una clase de componente de aplicación responsable de gestionar el estado de autenticación del usuario. + +## Usando [[yii\web\User]] +Principalmente se usa [[yii\web\User]] en términos del componente de aplicación `user`. + +Puede detectar la identidad del usuario actual usando la expresión `Yii::$app->user->identity`. Devuelve una instancia de la clase [[yii\web\User::identityClass|identity]] que representa al usuario actualmente conectado, o `null` si el usuario actual no está autenticado (es decir, es un invitado). El siguiente código muestra como recuperar otra información relacionada con la autenticación desde [[yii\web\User]]: + +```php +// El usuario actual identificado. `null` si el usuario no esta autenticado. +$identity = Yii::$app->user->identity; + +// El ID del usuario actual. `null` si el usuario no esta autenticado. +$id = Yii::$app->user->id; + +// si el usuario actual es un invitado (No autenticado) +$isGuest = Yii::$app->user->isGuest; +``` + +Para acceder a un usuario, puede utilizar el siguiente código: + +```php +// encontrar una identidad de usuario con el nombre de usuario especificado. +// tenga en cuenta que es posible que desee comprobar la contraseña si es necesario +$identity = User::findOne(['username' => $username]); + +// inicia la sesión del usuario. +Yii::$app->user->login($identity); +``` + +El método [[yii\web\User::login()]] establece la identidad del usuario actual a [[yii\web\User]]. Si la sesión es [[yii\web\User::enableSession|habilitada]], mantendrá la identidad en la sesión para que el estado de autenticación del usuario se mantenga durante toda la sesión. Si el login basado en cookies (es decir, inicio de sesión "recordarme") está [[yii\web\User::enableAutoLogin|habilitado]], también guardará la identidad en una cookie para que el estado de autenticación del usuario pueda ser recuperado desde la cookie mientras la cookie permanezca válida. + +Para habilitar el login basado en cookies, necesita configurar [[yii\web\User::enableAutoLogin]] como `true` en la configuración de la aplicación. También necesita proporcionar un parámetro de tiempo de duración cuando llame al método [[yii\web\User::login()]]. + +Para cerrar la sesión de un usuario, basta con llamar a: + +```php +Yii::$app->user->logout(); +``` + +Tenga en cuenta que cerrar la sesión de un usuario sólo tiene sentido cuando la sesión está activada. El método limpiará el estado de autenticación del usuario tanto de la memoria como de la sesión. Y por defecto, también destruirá *todos* los datos de sesión del usuario. Si desea mantener los datos de sesión, debe llamar a `Yii::$app->user->logout(false)`, en su lugar. + +## Eventos de Autenticación +La clase [[yii\web\User]] genera algunos eventos durante los procesos de inicio y cierre de sesión. +* [[yii\web\User::EVENT_BEFORE_LOGIN|EVENT_BEFORE_LOGIN]]: levantado al comienzo de [[yii\web\User::login()]]. Si el manejador del evento establece la propiedad [[yii\web\UserEvent::isValid|isValid]] del objeto evento a `false`, el proceso de inicio de sesión será cancelado. +* [[yii\web\User::EVENT_AFTER_LOGIN|EVENT_AFTER_LOGIN]]: se produce después de un inicio de sesión exitoso. +* [[yii\web\User::EVENT_BEFORE_LOGOUT|EVENT_BEFORE_LOGOUT]]: levantado al comienzo de [[yii\web\User::logout()]]. Si el manejador del evento establece la propiedad [[yii\web\UserEvent::isValid|isValid]] del objeto evento a `false`, el proceso de cierre de sesión será cancelado. +* [[yii\web\User::EVENT_AFTER_LOGOUT|EVENT_AFTER_LOGOUT]]: se produce después de un cierre de sesión exitoso. +Usted puede responder a estos eventos para implementar características como auditoria de inicio de sesión, estadísticas de usuarios en línea. Por ejemplo, en el manejador para [[yii\web\User::EVENT_AFTER_LOGIN|EVENT_AFTER_LOGIN]], puede registrar la hora de inicio de sesión y la dirección IP en la tabla `user`. diff --git a/docs/guide-fr/caching-data.md b/docs/guide-fr/caching-data.md index d68571834d..13fcd05177 100644 --- a/docs/guide-fr/caching-data.md +++ b/docs/guide-fr/caching-data.md @@ -213,6 +213,7 @@ Ci-dessous nous présentons un résumé des dépendances de mise en cache dispon - [[yii\caching\ChainedDependency]]: la dépendance est modifiée si l'une des dépendances de la chaîne est modifiée. - [[yii\caching\DbDependency]]: la dépendance est modifiée si le résultat de le requête de l'instruction SQL spécifiée est modifié. - [[yii\caching\ExpressionDependency]]: la dépendance est modifiée si le résultat de l'expression PHP spécifiée est modifié. +- [[yii\caching\CallbackDependency]]: la dépendance est modifiée si le résultat du rappel PHP spécifié est modifié. - [[yii\caching\FileDependency]]: la dépendance est modifiée si la date de dernière modification du fichier est modifiée. - [[yii\caching\TagDependency]]: associe une donnée mise en cache à une ou plusieurs balises. Vous pouvez invalider la donnée mise en cache associée à la balise spécifiée en appelant [[yii\caching\TagDependency::invalidate()]]. @@ -338,4 +339,3 @@ $result = $db->cache(function ($db) { La mise en cache de requêtes ne fonctionne pas avec des résultats de requêtes qui contiennent des gestionnaires de ressources. Par exemple, lorsque vous utilisez de type de colonne `BLOB` dans certains systèmes de gestion de bases de données (DBMS), la requête retourne un gestionnaire de ressources pour la donnée de la colonne. Quelques supports de stockage pour cache sont limités en taille. Par exemple, avec memcache, chaque entrée est limitée en taille à 1 MO. En conséquence, si le résultat d'une requête dépasse cette taille, la mise en cache échoue. - diff --git a/docs/guide-fr/intro-yii.md b/docs/guide-fr/intro-yii.md index b378e36605..508d760837 100644 --- a/docs/guide-fr/intro-yii.md +++ b/docs/guide-fr/intro-yii.md @@ -47,7 +47,7 @@ Ce guide est principalement pour la version 2.0. Configuration nécessaire ------------------------ -Yii 2.0 nécessite PHP 5.4.0 ou plus. Vous pouvez trouver plus de détails sur la configuration requise pour chaque fonctionnalité +Yii 2.0 nécessite PHP 7.3.0 ou plus. Vous pouvez trouver plus de détails sur la configuration requise pour chaque fonctionnalité en utilisant le script de test de la configuration inclus dans chaque distribution de Yii. Utiliser Yii requiert des connaissances de base sur la programmation objet (OOP), en effet Yii est un framework basé sur ce type de programmation. diff --git a/docs/guide-id/intro-yii.md b/docs/guide-id/intro-yii.md index 2b1ae4c91c..56620691ad 100644 --- a/docs/guide-id/intro-yii.md +++ b/docs/guide-id/intro-yii.md @@ -38,7 +38,7 @@ Panduan ini terutama tentang versi 2.0. ## Persyaratan dan Prasyarat -Yii 2.0 memerlukan PHP 5.4.0 atau versi lebih tinggi. Anda dapat menemukan persyaratan yang lebih rinci untuk setiap fitur +Yii 2.0 memerlukan PHP 7.3.0 atau versi lebih tinggi. Anda dapat menemukan persyaratan yang lebih rinci untuk setiap fitur dengan menjalankan pengecek persyaratan yang diikutsertakan dalam setiap rilis Yii. Menggunakan Yii memerlukan pengetahuan dasar tentang pemrograman berorientasi objek (OOP), mengingat Yii adalah framework berbasis OOP murni. diff --git a/docs/guide-it/intro-yii.md b/docs/guide-it/intro-yii.md index 056143be68..f3cf8e7e4f 100644 --- a/docs/guide-it/intro-yii.md +++ b/docs/guide-it/intro-yii.md @@ -50,7 +50,7 @@ Questa guida è focalizzata principalmente sulla versione 2.0. Richieste e requisiti di sistema --------------------------------- -Yii 2.0 richiede PHP 5.4.0 o successivo. Puoi trovare maggiori dettagli sulle richieste delle singole funzionalità +Yii 2.0 richiede PHP 7.3.0 o successivo. Puoi trovare maggiori dettagli sulle richieste delle singole funzionalità eseguendo lo script di verifica requisiti incluso in ogni versione di Yii. L'uso di Yii richiede una conoscenza base della programmazione ad oggetti (OOP), dato che Yii è un framework puramente OOP. diff --git a/docs/guide-ja/caching-data.md b/docs/guide-ja/caching-data.md index 00608e000f..75bf1e3aa4 100644 --- a/docs/guide-ja/caching-data.md +++ b/docs/guide-ja/caching-data.md @@ -270,6 +270,7 @@ $data = $cache->get($key); - [[yii\caching\ChainedDependency]]: チェーン上のいずれかの依存が変更された場合に、依存が変更されます。 - [[yii\caching\DbDependency]]: 指定された SQL 文のクエリ結果が変更された場合、依存が変更されます。 - [[yii\caching\ExpressionDependency]]: 指定された PHP の式の結果が変更された場合、依存が変更されます。 +- [[yii\caching\CallbackDependency]]: 指定されたPHPコールバックの結果が変更された場合、依存関係は変更されます。 - [[yii\caching\FileDependency]]: ファイルの最終更新日時が変更された場合、依存が変更されます。 - [[yii\caching\TagDependency]]: キャッシュされるデータ・アイテムに一つまたは複数のタグを関連付けます。 [[yii\caching\TagDependency::invalidate()]] を呼び出すことによって、指定されたタグ (複数可) を持つキャッシュされたデータ・アイテムを無効にすることができます。 @@ -426,4 +427,3 @@ $result = $db->cache(function ($db) { > Info: デフォルトでは、コンソール・アプリケーションは独立した構成情報ファイルを使用します。 正しい結果を得るためには、ウェブとコンソールのアプリケーション構成で同じキャッシュ・コンポーネントを使用していることを確認してください。 - diff --git a/docs/guide-ja/db-active-record.md b/docs/guide-ja/db-active-record.md index eaa829d211..ca9562a64f 100644 --- a/docs/guide-ja/db-active-record.md +++ b/docs/guide-ja/db-active-record.md @@ -648,6 +648,16 @@ Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]); > - [[yii\db\ActiveRecord::updateCounters()]] > - [[yii\db\ActiveRecord::updateAllCounters()]] +> Note: パフォーマンスを考慮して、DI(依存注入) はデフォルトではサポートされていません。必要であれば、 +> [[Yii::createObject()]] によってクラスのインスタンス生成をするように [[yii\db\ActiveRecord::instantiate()|instantiate()]] メソッドをオーバーライドして、サポートを追加することが出来ます。 +> +> ```php +> public static function instantiate($row) +> { +> return Yii::createObject(static::class); +> } +> ``` + ### データをリフレッシュする際のライフサイクル [[yii\db\ActiveRecord::refresh()|refresh()]] を呼んでアクティブ・レコード・インスタンスをリフレッシュする際は、リフレッシュが成功してメソッドが `true` を返すと diff --git a/docs/guide-ja/intro-yii.md b/docs/guide-ja/intro-yii.md index f2995b3e76..a249a0bed0 100644 --- a/docs/guide-ja/intro-yii.md +++ b/docs/guide-ja/intro-yii.md @@ -50,7 +50,7 @@ Yii は現在、利用可能な二つのメジャー・バージョン、すな 必要条件と前提条件 ------------------ -Yii 2.0 は PHP 5.4.0 以上を必要とし、PHP の最新バージョンで最高の力を発揮します。 +Yii 2.0 は PHP 7.3.0 以上を必要とし、PHP の最新バージョンで最高の力を発揮します。 個々の機能に対する詳細な必要条件は、全ての Yii リリースに含まれている必要条件チェッカを走らせることによって知ることが出来ます。 Yii を使うためには、オブジェクト指向プログラミング (OOP) の基本的な知識が必要です。 diff --git a/docs/guide-pl/intro-yii.md b/docs/guide-pl/intro-yii.md index a70e448fcf..b80de97d93 100644 --- a/docs/guide-pl/intro-yii.md +++ b/docs/guide-pl/intro-yii.md @@ -52,7 +52,7 @@ Ten przewodnik opisuje wersję 2.0. Wymagania i zależności ---------------------- -Yii 2.0 wymaga PHP w wersji 5.4.0 lub nowszej i pracuje najwydajniej na najnowszej wersji PHP. Aby otrzymać więcej +Yii 2.0 wymaga PHP w wersji 7.3.0 lub nowszej i pracuje najwydajniej na najnowszej wersji PHP. Aby otrzymać więcej informacji na temat wymagań i indywidualnych funkcjonalności, uruchom specjalny skrypt testujący system dołączony w każdym wydaniu Yii. Używanie Yii wymaga podstawowej wiedzy o programowaniu obiektowym w PHP (OOP), ponieważ Yii diff --git a/docs/guide-pt-BR/caching-data.md b/docs/guide-pt-BR/caching-data.md index c259f286be..d39a493bfc 100644 --- a/docs/guide-pt-BR/caching-data.md +++ b/docs/guide-pt-BR/caching-data.md @@ -225,6 +225,7 @@ Abaixo um sumário das dependências de cache disponíveis: - [[yii\caching\DbDependency]]: a dependência muda caso o resultado da consulta especificada pela instrução SQL seja alterado. - [[yii\caching\ExpressionDependency]]: a dependência muda se o resultado da expressão PHP especificada for alterado. +- [[yii\caching\CallbackDependency]]: a dependência é alterada se o resultado do callback PHP especificado for alterado.. - [[yii\caching\FileDependency]]: A dependência muda se a data da última alteração do arquivo for alterada. - [[yii\caching\TagDependency]]: associa um registro em cache com uma ou múltiplas tags. Você pode invalidar os registros em cache com a tag especificada ao chamar [[yii\caching\TagDependency::invalidate()]]. @@ -348,4 +349,3 @@ O cache de consulta não funciona com resultados de consulta que contêm mani Por exemplo, ao usar o tipo de coluna `BLOB` em alguns SGBDs, o resultado da consulta retornará um manipulador de recurso (resource handler) para o registro na coluna. Alguns armazenamentos em cache têm limitações de tamanho. Por exemplo, memcache limita o uso máximo de espaço de 1MB para cada registro. Então, se o tamanho do resultado de uma consulta exceder este limite, o cache falhará. - diff --git a/docs/guide-pt-BR/intro-yii.md b/docs/guide-pt-BR/intro-yii.md index 5ba072351c..bc2126c75f 100644 --- a/docs/guide-pt-BR/intro-yii.md +++ b/docs/guide-pt-BR/intro-yii.md @@ -59,7 +59,7 @@ desenvolvimento nos próximos anos. Este guia trata principalmente da versão 2. Requisitos e Pré-requisitos --------------------------- -Yii 2.0 requer PHP 5.4.0 ou superior. Você pode encontrar requisitos mais +Yii 2.0 requer PHP 7.3.0 ou superior. Você pode encontrar requisitos mais detalhados para recursos específicos executando o verificador de requisitos (requirement checker) incluído em cada lançamento do Yii. diff --git a/docs/guide-ru/caching-data.md b/docs/guide-ru/caching-data.md index 07d3358435..1c932fcc0e 100644 --- a/docs/guide-ru/caching-data.md +++ b/docs/guide-ru/caching-data.md @@ -219,6 +219,7 @@ $data = $cache->get($key); - [[yii\caching\ChainedDependency]]: зависимость меняется, если любая зависимость в цепочке изменяется; - [[yii\caching\DbDependency]]: зависимость меняется, если результат некоторого определенного SQL запроса изменён; - [[yii\caching\ExpressionDependency]]: зависимость меняется, если результат определенного PHP выражения изменён; +- [[yii\caching\CallbackDependency]]: зависимость меняется, если результат коллбэк функции изменён; - [[yii\caching\FileDependency]]: зависимость меняется, если изменилось время последней модификации файла; - [[yii\caching\TagDependency]]: Связывает кэшированные данные элемента с одним или несколькими тегами. Вы можете аннулировать кэширование данных элементов с заданным тегом(тегами) по вызову. [[yii\caching\TagDependency::invalidate()]]; diff --git a/docs/guide-ru/concept-behaviors.md b/docs/guide-ru/concept-behaviors.md index 966a871a7c..9922c3e3f5 100644 --- a/docs/guide-ru/concept-behaviors.md +++ b/docs/guide-ru/concept-behaviors.md @@ -327,8 +327,8 @@ $user->touch('login_time'); сторонние: - [[yii\behaviors\BlameableBehavior]] - автоматически заполняет указанные атрибуты ID текущего пользователя. -- [[yii\behaviors\SluggableBehavior]] - автоматически заполняет указанные атрибут пригодным для URL текстом, получаемым - из другого атрибута. +- [[yii\behaviors\SluggableBehavior]] - автоматически заполняет указанный атрибут пригодным для URL текстом, получаемым + из 1 или нескольких других атрибутов. - [[yii\behaviors\AttributeBehavior]] - автоматически задаёт указанное значение одному или нескольким атрибутам ActiveRecord при срабатывании определённых событий. - [yii2tech\ar\softdelete\SoftDeleteBehavior](https://github.com/yii2tech/ar-softdelete) - предоставляет методы для diff --git a/docs/guide-ru/db-active-record.md b/docs/guide-ru/db-active-record.md index 243120692f..3ea60db3d8 100644 --- a/docs/guide-ru/db-active-record.md +++ b/docs/guide-ru/db-active-record.md @@ -534,7 +534,7 @@ $customer->loadDefaultValues(); Можно также использовать условия для столбцов JSON: ```php -$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar']) +$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])]) ``` Дополнительные сведения о системе построения выражений см. [Query Builder – добавление пользовательских условий и выражений](db-query-builder.md#adding-custom-conditions-and-expressions) diff --git a/docs/guide-ru/helper-array.md b/docs/guide-ru/helper-array.md index e0219c637a..94a60765c2 100644 --- a/docs/guide-ru/helper-array.md +++ b/docs/guide-ru/helper-array.md @@ -5,7 +5,7 @@ ArrayHelper ## Получение значений -Извлечение значений из массива, объекта или структуры состоящей из них обоих с помощью стандартных средств PHP является довольно скучным занятием. Сначала вам нужно проверить есть ли соответствующий ключ с помощью `isset`, и если есть – получить, если нет – подставить значение по умолчанию. +Извлечение значений из массива, объекта или структуры состоящей из них обоих с помощью стандартных средств PHP является довольно скучным занятием. Сначала вам нужно проверить, есть ли соответствующий ключ с помощью `isset`, и если есть – получить, если нет – подставить значение по умолчанию. ```php class User @@ -152,9 +152,10 @@ $result = ArrayHelper::getColumn($array, function ($element) { ## Переиндексация массивов -Чтобы проиндексировать массив в соответствии с определенным ключом, используется метод `index` . Входящий массив должен +Чтобы проиндексировать массив в соответствии с определенным ключом, используется метод `index`. Входящий массив должен быть многомерным или массивом объектов. Ключом может быть имя ключа вложенного массива, имя свойства объекта или -анонимная функция, которая будет возвращать значение ключа по переданному массиву. +анонимная функция, которая будет возвращать значение ключа по переданному элементу индексируемого массива (то есть по +вложенному массиву или объекту). Если значение ключа равно `null`, то соответствующий элемент массива будет опущен и не попадет в результат. @@ -347,3 +348,140 @@ ArrayHelper::isIn('a', new(ArrayObject['a'])); ArrayHelper::isSubset(new(ArrayObject['a', 'c']), new(ArrayObject['a', 'b', 'c']) ``` + +## Преобразование многомерных массивов + +Метод `ArrayHelper::flatten()` позволяет преобразовать многомерный массив в одномерный, объединяя ключи. + +### Основное использование + +Чтобы преобразовать вложенный массив, просто передайте массив в метод `flatten()`: + +```php +$array = [ + 'a' => [ + 'b' => [ + 'c' => 1, + 'd' => 2, + ], + 'e' => 3, + ], + 'f' => 4, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Результат: +// [ +// 'a.b.c' => 1, +// 'a.b.d' => 2, +// 'a.e' => 3, +// 'f' => 4, +// ] +``` + +### Пользовательский разделитель + +Вы можете указать пользовательский (т.е. отличный от значения по умолчанию: `.`) разделитель для объединения ключей: + +```php +$array = [ + 'a' => [ + 'b' => [ + 'c' => 1, + 'd' => 2, + ], + 'e' => 3, + ], + 'f' => 4, +]; + +$flattenedArray = ArrayHelper::flatten($array, '_'); +// Результат: +// [ +// 'a_b_c' => 1, +// 'a_b_d' => 2, +// 'a_e' => 3, +// 'f' => 4, +// ] +``` + +### Обработка специальных символов в ключах + +Метод `flatten()` может обрабатывать ключи со специальными символами: + +```php +$array = [ + 'a.b' => [ + 'c.d' => 1, + ], + 'e.f' => 2, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Результат: +// [ +// 'a.b.c.d' => 1, +// 'e.f' => 2, +// ] +``` + +### Смешанные типы данных + +Метод `flatten()` работает с массивами, содержащими различные типы данных: + +```php +$array = [ + 'a' => [ + 'b' => 'string', + 'c' => 123, + 'd' => true, + 'e' => null, + ], + 'f' => [1, 2, 3], +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Результат: +// [ +// 'a.b' => 'string', +// 'a.c' => 123, +// 'a.d' => true, +// 'a.e' => null, +// 'f.0' => 1, +// 'f.1' => 2, +// 'f.2' => 3, +// ] +``` + +### Краевые случаи + +Метод `flatten()` обрабатывает различные краевые случаи, такие как пустые массивы и значения, не являющиеся массивами: + +```php +// Пустой массив +$array = []; +$flattenedArray = ArrayHelper::flatten($array); +// Результат: [] + +// Значение, не являющееся массивом +$array = 'string'; +$flattenedArray = ArrayHelper::flatten($array); +// Результат: +// yii\base\InvalidArgumentException: Argument $array must be an array or implement Traversable +``` + +### Коллизии ключей + +Когда ключи совпадают, метод `flatten()` перезапишет предыдущее значение: + +```php +$array = [ + 'a' => [ + 'b' => 1, + ], + 'a.b' => 2, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Результат: ['a.b' => 2] +``` diff --git a/docs/guide-ru/helper-overview.md b/docs/guide-ru/helper-overview.md index bb2a2894b4..97456eab01 100644 --- a/docs/guide-ru/helper-overview.md +++ b/docs/guide-ru/helper-overview.md @@ -3,8 +3,8 @@ > Note: Этот раздел находиться в стадии разработки. -Yii предоставляет много классов, которые помогают упростить общие задачи программирования, такие как манипуляция со строками или массивами, генерация HTML-кода, и так далее. Все helper-классы организованы в рамках пространства имен `yii\helpers` и являются статическими методами - (это означает, что они содержат в себе только статические свойства и методы и объекты статического класса создать нельзя). +Yii предоставляет много классов, которые помогают упростить общие задачи программирования, такие как манипуляция со строками или массивами, генерация HTML-кода, и так далее. Все helper-классы организованы в рамках пространства имен `yii\helpers` и являются статическими классами + (это означает, что они содержат в себе только статические свойства и методы, и объекты статического класса создать нельзя). Вы можете использовать helper-класс с помощью вызова одного из статических методов, как показано ниже: diff --git a/docs/guide-ru/input-file-upload.md b/docs/guide-ru/input-file-upload.md index 5dd78ee8aa..194ca6d1be 100644 --- a/docs/guide-ru/input-file-upload.md +++ b/docs/guide-ru/input-file-upload.md @@ -74,7 +74,7 @@ use yii\widgets\ActiveForm; ``` -Важно помнить, что для корректной загрузки файла, необходим параметр формы `enctype`. Метод `fileInput()` +Важно помнить, что для корректной загрузки файла необходим параметр формы `enctype`. Метод `fileInput()` выведет тег ``, позволяющий пользователю выбрать файл для загрузки. > Tip: начиная с версии 2.0.8, [[yii\widgets\ActiveField::fileInput|fileInput]] автоматически добавляет diff --git a/docs/guide-ru/intro-yii.md b/docs/guide-ru/intro-yii.md index 353ecaeac0..4ec5d66b1c 100644 --- a/docs/guide-ru/intro-yii.md +++ b/docs/guide-ru/intro-yii.md @@ -42,7 +42,7 @@ Yii — не проект одного человека. Он поддержив Требования к ПО и знаниям ------------------------- -Yii 2.0 требует PHP 5.4.0 и выше и наилучшим образом работает на последней версии PHP. Чтобы узнать требования для отдельных возможностей, вы можете запустить скрипт проверки +Yii 2.0 требует PHP 7.3.0 и выше и наилучшим образом работает на последней версии PHP. Чтобы узнать требования для отдельных возможностей, вы можете запустить скрипт проверки требований, который поставляется с каждым релизом фреймворка. Для разработки на Yii потребуется общее понимание ООП, так как фреймворк полностью следует этой парадигме. Также стоит diff --git a/docs/guide-ru/runtime-logging.md b/docs/guide-ru/runtime-logging.md index 2f45900e2c..e805507097 100644 --- a/docs/guide-ru/runtime-logging.md +++ b/docs/guide-ru/runtime-logging.md @@ -173,6 +173,23 @@ return [ При задании значением свойства `logVars` пустого массива, общая информация не будет выводиться. Для определения собственного алгоритма подключения общей информации, следует переопределить метод [[yii\log\Target::getContextMessage()]]. +Если некоторые из полей вашего запроса содержат конфиденциальную информацию, которую вы не хотели бы логировать (например, пароли, токены доступа), +вы можете дополнительно настроить свойство `maskVars`, которое может содержать как точные значения, так и шаблоны (без учета регистра). +По умолчанию следующие параметры запроса будут замаскированы с помощью `***`: +`$_SERVER[HTTP_AUTHORIZATION]`, `$_SERVER[PHP_AUTH_USER]`, `$_SERVER[PHP_AUTH_PW]`, но вы можете задать свои собственные. Например: + +```php +[ + 'class' => 'yii\log\FileTarget', + 'logVars' => ['_SERVER'], + 'maskVars' => [ + '_SERVER.HTTP_X_PASSWORD', + '_SERVER.*_SECRET', // соответствует всем, заканчивающимся на "_SECRET" + '_SERVER.SECRET_*', // соответствует всем, начинающимся с "SECRET_" + '_SERVER.*SECRET*', // соответствует всем содержащим "SECRET" + ] +] +``` ### Уровень отслеживания выполнения кода diff --git a/docs/guide-ru/tutorial-i18n.md b/docs/guide-ru/tutorial-i18n.md index 44f233ff19..695a6a5ca4 100644 --- a/docs/guide-ru/tutorial-i18n.md +++ b/docs/guide-ru/tutorial-i18n.md @@ -506,9 +506,9 @@ class TranslationEventHandler Откройте созданный файл и настройте параметры в соответствии со своими потребностями. Уделите особое внимание следующим параметрам: * `languages`: массив, содержащий языки, на которые ваше приложение должно быть переведено; -* `messagePath`: путь для хранений файлов сообщений, который должен соответствовать параметру `basePath`, указанному в конфигурации компонента`i18n`. +* `messagePath`: путь для хранения файлов сообщений, который должен соответствовать параметру `basePath`, указанному в конфигурации компонента `i18n`. -Вы также можете использовать команду './yii message/config', чтобы динамически сгенерировать конфигурационный файл с указанными опциями с помощью командной строки. +Вы также можете использовать команду `./yii message/config`, чтобы динамически сгенерировать конфигурационный файл с указанными опциями с помощью командной строки. Например, вы можете установить параметры `languages` и `messagePath` следующим образом: ```bash @@ -529,7 +529,7 @@ class TranslationEventHandler Также вы можете использовать параметры, чтобы динамически менять настройки извлечения. -В результате вы найдете свой файлы (если вы выбрали перевод с помощью файлов) в своей директории `messagePath`. +В результате вы найдете свои файлы (если вы выбрали перевод с помощью файлов) в своей директории `messagePath`. Представления diff --git a/docs/guide-tr/intro-yii.md b/docs/guide-tr/intro-yii.md index 069f690f91..f05107f19e 100644 --- a/docs/guide-tr/intro-yii.md +++ b/docs/guide-tr/intro-yii.md @@ -32,6 +32,6 @@ Bu kılavuz esas olarak sürüm 2.0 ile ilgilidir. Gereksinimler ve Önkoşullar ------------------------------ -Yii 2.0, PHP 5.4.0 veya üstü sürüm gerektirir ve PHP 'nin en son sürümü ile en iyi şekilde çalışır. Her bir Yii sürümünde yer alan gereksinim denetleyicisini çalıştırarak, daha ayrıntılı gereksinimleri ayrı ayrı özellikler için bulabilirsiniz. +Yii 2.0, PHP 7.3.0 veya üstü sürüm gerektirir ve PHP 'nin en son sürümü ile en iyi şekilde çalışır. Her bir Yii sürümünde yer alan gereksinim denetleyicisini çalıştırarak, daha ayrıntılı gereksinimleri ayrı ayrı özellikler için bulabilirsiniz. Yii OOP temelli bir kütüphane olduğu için Yii'yi kullanmak, nesne yönelimli programlama (OOP) hakkında temel bilgi gerektirir. Yii 2.0 ayrıca PHP'nin [namespaceler](https://www.php.net/manual/en/language.namespaces.php) ve [traitler](https://www.php.net/manual/en/language.oop5.traits.php) gibi son özelliklerinden de yararlanır. Bu kavramları anlamak, Yii 2.0'ı daha kolay anlamanıza yardımcı olacaktır. diff --git a/docs/guide-uk/intro-yii.md b/docs/guide-uk/intro-yii.md index 93b0efb999..cca32875a5 100644 --- a/docs/guide-uk/intro-yii.md +++ b/docs/guide-uk/intro-yii.md @@ -46,7 +46,7 @@ Yii — не проект однієї людини. Він підтримуєт Вимоги до ПЗ і знань -------------------- -Yii 2.0 потребує PHP 5.4.0 та вище. Щоб дізнатися вимоги для окремих можливостей ви можете запустити скрипт перевірки вимог, +Yii 2.0 потребує PHP 7.3.0 та вище. Щоб дізнатися вимоги для окремих можливостей ви можете запустити скрипт перевірки вимог, який поставляється із кожним релізом фреймворку. Для розробки на Yii необхідне загальне розуміння ООП, оскільки фреймворк повністю слідує цій парадигмі. diff --git a/docs/guide-uk/runtime-sessions-cookies.md b/docs/guide-uk/runtime-sessions-cookies.md new file mode 100644 index 0000000000..7edf8e00f8 --- /dev/null +++ b/docs/guide-uk/runtime-sessions-cookies.md @@ -0,0 +1,285 @@ +Сесії та кукі +==================== + +Сесії та кукі дозволяють зберігати користувацькі дані між запитами. При використанні чистого PHP можна отримати доступ до цих даних через глобальні змінні `$_SESSION` та `$_COOKIE`, відповідно. Yii інкапсулює сесії та кукі в об'єкти, що дає можливість звертатися до них в об'єктноорієнтованому стилі та забезпечує додаткову зручність в роботі. + + +## Сесії + +За аналогією з [запитами](runtime-requests.md) та [відповідями](runtime-responses.md), до сесій можна отримати доступ через `session` [компонент додатка](structure-application-components.md), який за замовчуванням є екземпляром [[yii\web\Session]]. + + +### Відкриття та закриття сесії + +Відкрити та закрити сесію можна наступним чином: + +```php +$session = Yii::$app->session; + +// перевіряєм що сесія вже відкрита +if ($session->isActive) ... + +// відкиваєм сесію +$session->open(); + +// закриваємо сесію +$session->close(); + +// знищуємо сесію і всі пов'язані з нею дані. +$session->destroy(); +``` + +Можна викликати [[yii\web\Session::open()|open()]] і [[yii\web\Session::close()|close()]] багаторазово без виникнення помилок; всередині компонента всі методи перевіряють сесію на те, відкрита вона чи ні. + + +### Доступ до даних сесії + +Отримати доступ до збережених в сесію даних можна наступним чином: + +```php +$session = Yii::$app->session; + +// отримання змінної з сесії. Наступні способи використання еквівалентні: +$language = $session->get('language'); +$language = $session['language']; +$language = isset($_SESSION['language']) ? $_SESSION['language'] : null; + +// запис змінної в сесію. Наступні способи використання еквівалентні: +$session->set('language', 'en-US'); +$session['language'] = 'en-US'; +$_SESSION['language'] = 'en-US'; + +// видалення змінної з сесії. Наступні способи використання еквівалентні: +$session->remove('language'); +unset($session['language']); +unset($_SESSION['language']); + +// перевірка на існування змінної в сесії. Наступні способи використання еквівалентні: +if ($session->has('language')) ... +if (isset($session['language'])) ... +if (isset($_SESSION['language'])) ... + +// обхід усіх змінних у сесії. Наступні способи використання еквівалентні: +foreach ($session as $name => $value) ... +foreach ($_SESSION as $name => $value) ... +``` + +> Info: При отриманні даних з сесії через компонент `session`, сесія буде автоматично відкрита, якщо вона не була відкрита до цього. У цьому полягає відмінність від отримання даних з глобальної змінної `$_SESSION`, що вимагає обов'язкового виклику `session_start()`. + +При роботі з сесійними даними, які є масивами, компонент `session` має обмеження, що забороняє пряму модифікацію окремих елементів масиву. Наприклад, + +```php +$session = Yii::$app->session; + +// наступний код НЕ БУДЕ працювати +$session['captcha']['number'] = 5; +$session['captcha']['lifetime'] = 3600; + +// а цей буде: +$session['captcha'] = [ + 'number' => 5, + 'lifetime' => 3600, +]; + +// цей код також буде працювати: +echo $session['captcha']['lifetime']; +``` + +Для вирішення цієї проблеми можна використовувати такі обхідні прийоми: + +```php +$session = Yii::$app->session; + +// пряме використання $_SESSION (переконайтеся, що Yii::$app->session->open() був викликаний) +$_SESSION['captcha']['number'] = 5; +$_SESSION['captcha']['lifetime'] = 3600; + +// отримайте весь масив, модифікуйте і збережіть назад у сесію +$captcha = $session['captcha']; +$captcha['number'] = 5; +$captcha['lifetime'] = 3600; +$session['captcha'] = $captcha; + +// використовуйте ArrayObject замість масиву +$session['captcha'] = new \ArrayObject; +... +$session['captcha']['number'] = 5; +$session['captcha']['lifetime'] = 3600; + +// записуйте дані з ключами, які мають однаковий префікс +$session['captcha.number'] = 5; +$session['captcha.lifetime'] = 3600; +``` + +Для покращення продуктивності та читабельності коду рекомендується використовувати останній прийом. Іншими словами, замість того, щоб зберігати масив як одну змінну сесії, ми зберігаємо кожен елемент масиву як звичайну сесійну змінну зі спільним префіксом. + + +### Користувацьке сховище для сесії + +За замовчуванням клас [[yii\web\Session]] зберігає дані сесії у вигляді файлів на сервері. Однак Yii надає ряд класів, які реалізують різні способи зберігання даних сесії: + +* [[yii\web\DbSession]]: зберігає дані сесії в базі даних. +* [[yii\web\CacheSession]]: зберігання даних сесії в попередньо сконфігурованому компоненті кешу [кеш](caching-data.md#cache-components). +* [[yii\redis\Session]]: зберігання даних сесії в [redis](https://redis.io/). +* [[yii\mongodb\Session]]: зберігання сесії в [MongoDB](https://www.mongodb.com/). + +Усі ці класи підтримують однаковий набір методів API. В результаті ви можете перемикатися між різними сховищами сесій без модифікації коду додатку. + +> Note: Якщо ви хочете отримати дані з змінної `$_SESSION` при використанні користувацького сховища, ви повинні бути впевнені, що сесія вже стартувала [[yii\web\Session::open()]], оскільки обробники зберігання користувацьких сесій реєструються в цьому методі. + +Щоб дізнатися, як налаштувати і використовувати ці компоненти, зверніться до документації по API. Нижче наведено приклад конфігурації [[yii\web\DbSession]] для використання бази даних для зберігання сесії: + +```php +return [ + 'components' => [ + 'session' => [ + 'class' => 'yii\web\DbSession', + // 'db' => 'mydb', // ID компонента для взаємодії з БД. По замовчуванню 'db'. + // 'sessionTable' => 'my_session', // назва таблиці для даних сесії. По замовчуванню 'session'. + ], + ], +]; +``` + +Також необхідно створити таблицю для зберігання даних сесії: + +```sql +CREATE TABLE session +( + id CHAR(40) NOT NULL PRIMARY KEY, + expire INTEGER, + data BLOB +) +``` + +де 'BLOB' відповідає типу даних вашої DBMS. Нижче наведені приклади відповідності типів BLOB у найбільш популярних DBMS: + +- MySQL: LONGBLOB +- PostgreSQL: BYTEA +- MSSQL: BLOB + +> Note: В залежності від налаштувань параметра `session.hash_function` у вашому php.ini, може знадобитися змінити довжину поля `id`. Наприклад, якщо `session.hash_function=sha256`, потрібно встановити довжину поля на 64 замість 40. + +### Flash-повідомлення + +Flash-повідомлення - це особливий тип даних у сесії, які встановлюються один раз під час запиту і доступні лише протягом наступного запиту, після чого вони автоматично видаляються. Такий спосіб зберігання інформації в сесії найчастіше використовується для реалізації повідомлень, які будуть відображені кінцевому користувачу один раз, наприклад, підтвердження про успішну відправку форми. + +Встановити та отримати flash-повідомлення можна через компонент програми `session`. Наприклад: + +```php +$session = Yii::$app->session; + +// Запит #1 +// встановлення flash-повідомлення з назвою "postDeleted" +$session->setFlash('postDeleted', 'Ви успішно видалили пост.'); + +// Запит #2 +// відображення flash-повідомлення "postDeleted" +echo $session->getFlash('postDeleted'); + +// Запит #3 +// змінна $result буде мати значення false, оскільки flash-повідомлення було автоматично видалено +$result = $session->hasFlash('postDeleted'); +``` + +Оскільки flash-повідомлення зберігаються в сесії як звичайні дані, в них можна записувати довільну інформацію, і вона буде доступна лише в наступному запиті. + +При виклику [[yii\web\Session::setFlash()]] відбувається перезаписування flash-повідомлень з таким же назвою. Для того, щоб додати нові дані до вже існуючого flash-повідомлення, необхідно викликати [[yii\web\Session::addFlash()]]. +Наприклад: + +```php +$session = Yii::$app->session; + +// Запит #1 +// додати нове flash-повідомлення з назвою "alerts" +$session->addFlash('alerts', 'Ви успішно видалили пост.'); +$session->addFlash('alerts', 'Ви успішно додали нового друга.'); +$session->addFlash('alerts', 'Дякуємо.'); + +// Запит #2 +// Змінна $alerts тепер містить масив flash-повідомлень з назвою "alerts" +$alerts = $session->getFlash('alerts'); +``` + +> Note: Намагайтеся не використовувати [[yii\web\Session::setFlash()]] спільно з [[yii\web\Session::addFlash()]] для flash-повідомлень з однаковою назвою. Це пов'язано з тим, що останній метод автоматично перетворює збережені дані в масив, щоб мати можливість зберігати та додавати нові дані в flash-повідомлення з тією ж назвою. В результаті, при виклику [[yii\web\Session::getFlash()]] можна виявити, що повертається масив, тоді як очікувалася строка. + +## Кукі + +Yii представляє кожну з cookie як об'єкт [[yii\web\Cookie]]. Обидва компоненти програми [[yii\web\Request]] і [[yii\web\Response]] +підтримують колекції кукі через своє властивість cookies. У першому випадку колекція кукі є їх представленням з HTTP-запиту, у другому — представляє кукі, які будуть відправлені користувачу. + +### Читання кукі + +Отримати кукі з поточного запиту можна наступним чином: + +```php +// отримання колекції кукі (yii\web\CookieCollection) з компонента "request" +$cookies = Yii::$app->request->cookies; + +// отримання кукі з назвою "language". Якщо кукі не існує, "en" буде повернуто як значення за замовчуванням. +$language = $cookies->getValue('language', 'en'); + +// альтернативний спосіб отримання кукі "language" +if (($cookie = $cookies->get('language')) !== null) { + $language = $cookie->value; +} + +// тепер змінну $cookies можна використовувати як масив +if (isset($cookies['language'])) { + $language = $cookies['language']->value; +} + +// перевірка на існування кукі "language" +if ($cookies->has('language')) ... +if (isset($cookies['language'])) ... +``` + + +### Відправка кукі + +Відправити кукі кінцевому користувачу можна наступним чином: + +```php +// отримання колекції (yii\web\CookieCollection) з компонента "response" +$cookies = Yii::$app->response->cookies; + +// додавання нової кукі в HTTP-відповідь +$cookies->add(new \yii\web\Cookie([ + 'name' => 'language', + 'value' => 'zh-CN', +])); + +// видалення кукі... +$cookies->remove('language'); +// ...що еквівалентно наступному: +unset($cookies['language']); +``` + +Крім властивостей [[yii\web\Cookie::name|name]] та [[yii\web\Cookie::value|value]], клас [[yii\web\Cookie]] також надає ряд властивостей для отримання інформації про куки: [[yii\web\Cookie::domain|domain]], [[yii\web\Cookie::expire|expire]]. Ці властивості можна сконфігурувати, а потім додати кукі в колекцію для HTTP-відповіді. + +> Note: Для більшої безпеки значення властивості [[yii\web\Cookie::httpOnly]] за замовчуванням встановлено в `true`. Це зменшує ризики доступу до захищеної кукі на клієнтській стороні (якщо браузер підтримує таку можливість). Ви можете звернутися до [httpOnly wiki](https://owasp.org/www-community/HttpOnly) для додаткової інформації. + +### Валідація кукі + +Під час запису та читання куків через компоненти `request` та `response`, як буде показано в двох наступних підрозділах, фреймворк надає автоматичну валідацію, яка забезпечує захист кукі від модифікації на стороні клієнта. Це досягається завдяки підписанню кожної кукі секретним ключем, що дозволяє додатку розпізнавати кукі, які були модифіковані на клієнтській стороні. У такому випадку кукі НЕ БУДЕ доступна через властивість [[yii\web\Request::cookies|cookie collection]] компонента `request`. + +> Note: Валідація кукі захищає тільки від їх модифікації. Якщо валідація не була пройдена, отримати доступ до кукі все ще можна через глобальну змінну `$_COOKIE`. Це пов'язано з тим, що додаткові пакети та бібліотеки можуть маніпулювати кукі без виклику валідації, яку забезпечує Yii. + + +За замовчуванням валідація кукі увімкнена. Її можна вимкнути, встановивши властивість [[yii\web\Request::enableCookieValidation]] в `false`, однак ми настійливо не рекомендуємо цього робити. + +> Note: Кукі, які безпосередньо читаються/пишуться через `$_COOKIE` та `setcookie()`, НЕ БУДУТЬ валідовуватися. + +При використанні валідації кукі необхідно вказати значення властивості [[yii\web\Request::cookieValidationKey]], яке буде використано для генерації згаданого вище секретного ключа. Це можна зробити, налаштувавши компонент `request` у конфігурації додатка: + +```php +return [ + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'fill in a secret key here', + ], + ], +]; +``` + +> Note: Властивість [[yii\web\Request::cookieValidationKey|cookieValidationKey]] є секретним значенням і повинно бути відомо лише тим, кому ви довіряєте. Не розміщуйте цю інформацію в системі контролю версій. diff --git a/docs/guide-uz/intro-yii.md b/docs/guide-uz/intro-yii.md index 4e9f1d6de5..be66bdc7cd 100644 --- a/docs/guide-uz/intro-yii.md +++ b/docs/guide-uz/intro-yii.md @@ -32,6 +32,6 @@ Ayni vaqtda Yii ning ikkita yo'nalishi mavjud: 1.1 va 2.0. 1.1 yo'nalishi avvalg DT va bilimlarga talablar ------------------------- -Yii 2.0 PHP 5.4.0 va undan yuqorisini talab qiladi. Boshqa imkoniyatlar uchun talablarni bilish uchun har bir alohida yo'lga qo'yilgan freymvork bilan birga mos o'rnatilgan talablar tekshiruv skriptini ishga tushirishingiz mumkin. +Yii 2.0 PHP 7.3.0 va undan yuqorisini talab qiladi. Boshqa imkoniyatlar uchun talablarni bilish uchun har bir alohida yo'lga qo'yilgan freymvork bilan birga mos o'rnatilgan talablar tekshiruv skriptini ishga tushirishingiz mumkin. Freymvork to'liq obektga mo'ljallangan dasturlashga (OMD) asoslanganligi bois Yii da ishlash uchun OMD ni umumiy tushunish talab etiladi. Shuningdek, PHP ning zamonaviy imkoniyatlari bo'lmish [nomlar soxasi](https://www.php.net/manual/ru/language.namespaces.php) va [treytlar](https://www.php.net/manual/ru/language.oop5.traits.php) ni o'rganish talab etiladi. diff --git a/docs/guide-vi/intro-yii.md b/docs/guide-vi/intro-yii.md index 6548c58ed2..f7e51d5211 100644 --- a/docs/guide-vi/intro-yii.md +++ b/docs/guide-vi/intro-yii.md @@ -47,7 +47,7 @@ Hướng dẫn này chủ yếu là về phiên bản 2.0. Yêu cầu hệ thống và các điều kiện cần thiết ------------------------------ -Yii 2.0 đòi hỏi phiên bản PHP 5.4.0 hoặc cao hơn. Bạn có thể chạy bất kỳ gói Yii đi kèm với các yêu cầu hệ thống. +Yii 2.0 đòi hỏi phiên bản PHP 7.3.0 hoặc cao hơn. Bạn có thể chạy bất kỳ gói Yii đi kèm với các yêu cầu hệ thống. kiểm tra xem những gì các đặc điểm cụ thể của từng cấu hình PHP. Để tìm hiểu Yii, bạn cần có kiến thức cơ bản về lập trình hướng đối tượng (OOP), vì Yii là một framework hướng đối tượng diff --git a/docs/guide-zh-CN/caching-data.md b/docs/guide-zh-CN/caching-data.md index 66bc0859d9..a537579b49 100644 --- a/docs/guide-zh-CN/caching-data.md +++ b/docs/guide-zh-CN/caching-data.md @@ -271,6 +271,7 @@ $data = $cache->get($key); - [[yii\caching\ChainedDependency]]:如果依赖链上任何一个依赖产生变化,则依赖改变。 - [[yii\caching\DbDependency]]:如果指定 SQL 语句的查询结果发生了变化,则依赖改变。 - [[yii\caching\ExpressionDependency]]:如果指定的 PHP 表达式执行结果发生变化,则依赖改变。 +- [[yii\caching\CallbackDependency]]:如果指定的PHP回调结果发生变化,依赖性将改变。 - [[yii\caching\FileDependency]]:如果文件的最后修改时间发生变化,则依赖改变。 - [[yii\caching\TagDependency]]:将缓存的数据项与一个或多个标签相关联。 您可以通过调用  [[yii\caching\TagDependency::invalidate()]] 来检查指定标签的缓存数据项是否有效。 @@ -427,4 +428,3 @@ $result = $db->cache(function ($db) { > Info: 默认情况下,控制台应用使用独立的配置文件。 所以,为了上述命令发挥作用,请确保 Web 应用和控制台应用配置相同的缓存组件。 - diff --git a/docs/guide-zh-CN/intro-yii.md b/docs/guide-zh-CN/intro-yii.md index d03e5ad9a1..af91110c2e 100644 --- a/docs/guide-zh-CN/intro-yii.md +++ b/docs/guide-zh-CN/intro-yii.md @@ -50,7 +50,7 @@ Yii 当前有两个主要版本:1.1 和 2.0。 1.1 版是上代的老版本, 系统要求和先决条件 ------------------------------ -Yii 2.0 需要 PHP 5.4.0 或以上版本支持。你可以通过运行任何 +Yii 2.0 需要 PHP 7.3.0 或以上版本支持。你可以通过运行任何 Yii 发行包中附带的系统要求检查器查看每个具体特性所需的 PHP 配置。 使用 Yii 需要对面向对象编程(OOP)有基本了解,因为 Yii 是一个纯面向对象的框架。Yii 2.0 还使用了 PHP 的最新特性, diff --git a/docs/guide/caching-data.md b/docs/guide/caching-data.md index 9f98025099..6f2d46665a 100644 --- a/docs/guide/caching-data.md +++ b/docs/guide/caching-data.md @@ -273,6 +273,7 @@ Below is a summary of the available cache dependencies: - [[yii\caching\ChainedDependency]]: the dependency is changed if any of the dependencies on the chain is changed. - [[yii\caching\DbDependency]]: the dependency is changed if the query result of the specified SQL statement is changed. - [[yii\caching\ExpressionDependency]]: the dependency is changed if the result of the specified PHP expression is changed. +- [[yii\caching\CallbackDependency]]: the dependency is changed if the result of the specified PHP callback is changed. - [[yii\caching\FileDependency]]: the dependency is changed if the file's last modification time is changed. - [[yii\caching\TagDependency]]: associates a cached data item with one or multiple tags. You may invalidate the cached data items with the specified tag(s) by calling [[yii\caching\TagDependency::invalidate()]]. @@ -429,4 +430,3 @@ You can flush the cache from the console by calling `yii cache/flush` as well. > Info: Console application uses a separate configuration file by default. Ensure, that you have the same caching components in your web and console application configs to reach the proper effect. - diff --git a/docs/guide/helper-array.md b/docs/guide/helper-array.md index 19c3c03ad5..e7b1641fff 100644 --- a/docs/guide/helper-array.md +++ b/docs/guide/helper-array.md @@ -483,3 +483,140 @@ ArrayHelper::isIn('a', new ArrayObject(['a'])); // true ArrayHelper::isSubset(new ArrayObject(['a', 'c']), new ArrayObject(['a', 'b', 'c'])); ``` + +## Flattening Arrays + +The `ArrayHelper::flatten()` method allows you to convert a multi-dimensional array into a single-dimensional array by concatenating keys. + +### Basic Usage + +To flatten a nested array, simply pass the array to the `flatten()` method: + +```php +$array = [ + 'a' => [ + 'b' => [ + 'c' => 1, + 'd' => 2, + ], + 'e' => 3, + ], + 'f' => 4, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Result: +// [ +// 'a.b.c' => 1, +// 'a.b.d' => 2, +// 'a.e' => 3, +// 'f' => 4, +// ] +``` + +### Custom Separator + +You can specify a custom separator to use when concatenating keys: + +```php +$array = [ + 'a' => [ + 'b' => [ + 'c' => 1, + 'd' => 2, + ], + 'e' => 3, + ], + 'f' => 4, +]; + +$flattenedArray = ArrayHelper::flatten($array, '_'); +// Result: +// [ +// 'a_b_c' => 1, +// 'a_b_d' => 2, +// 'a_e' => 3, +// 'f' => 4, +// ] +``` + +### Handling Special Characters in Keys + +The `flatten()` method can handle keys with special characters: + +```php +$array = [ + 'a.b' => [ + 'c.d' => 1, + ], + 'e.f' => 2, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Result: +// [ +// 'a.b.c.d' => 1, +// 'e.f' => 2, +// ] +``` + +### Mixed Data Types + +The `flatten()` method works with arrays containing different data types: + +```php +$array = [ + 'a' => [ + 'b' => 'string', + 'c' => 123, + 'd' => true, + 'e' => null, + ], + 'f' => [1, 2, 3], +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Result: +// [ +// 'a.b' => 'string', +// 'a.c' => 123, +// 'a.d' => true, +// 'a.e' => null, +// 'f.0' => 1, +// 'f.1' => 2, +// 'f.2' => 3, +// ] +``` + +### Edge Cases + +The `flatten()` method handles various edge cases, such as empty arrays and non-array values: + +```php +// Empty array +$array = []; +$flattenedArray = ArrayHelper::flatten($array); +// Result: [] + +// Non-array value +$array = 'string'; +$flattenedArray = ArrayHelper::flatten($array); +// Result: +// yii\base\InvalidArgumentException: Argument $array must be an array or implement Traversable +``` + +### Key Collisions + +When keys collide, the `flatten()` method will overwrite the previous value: + +```php +$array = [ + 'a' => [ + 'b' => 1, + ], + 'a.b' => 2, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Result: ['a.b' => 2] +``` diff --git a/docs/guide/intro-yii.md b/docs/guide/intro-yii.md index ea493cf2b8..17a3242663 100644 --- a/docs/guide/intro-yii.md +++ b/docs/guide/intro-yii.md @@ -50,7 +50,7 @@ This guide is mainly about version 2.0. Requirements and Prerequisites ------------------------------ -Yii 2.0 requires PHP 5.4.0 or above and runs best with the latest version of PHP. You can find more detailed +Yii 2.0 requires PHP 7.3.0 or above and runs best with the latest version of PHP. You can find more detailed requirements for individual features by running the requirement checker included in every Yii release. Using Yii requires basic knowledge of object-oriented programming (OOP), as Yii is a pure OOP-based framework. diff --git a/docs/guide/runtime-logging.md b/docs/guide/runtime-logging.md index 041150abd2..5185c0916e 100644 --- a/docs/guide/runtime-logging.md +++ b/docs/guide/runtime-logging.md @@ -217,14 +217,20 @@ Or if you want to implement your own way of providing context information, you m [[yii\log\Target::getContextMessage()]] method. In case some of your request fields contain sensitive information you would not like to log (e.g. passwords, access tokens), -you may additionally configure `maskVars` property. By default, the following request parameters will be masked with `***`: -`$_SERVER[HTTP_AUTHORIZATION]`, `$_SERVER[PHP_AUTH_USER]`, `$_SERVER[PHP_AUTH_PW]`, but you can set your own: +you may additionally configure `maskVars` property, which can contain both exact values and (case-insensitive) patterns. By default, +the following request parameters will be masked with `***`: +`$_SERVER[HTTP_AUTHORIZATION]`, `$_SERVER[PHP_AUTH_USER]`, `$_SERVER[PHP_AUTH_PW]`, but you can set your own. For example: ```php [ 'class' => 'yii\log\FileTarget', 'logVars' => ['_SERVER'], - 'maskVars' => ['_SERVER.HTTP_X_PASSWORD'] + 'maskVars' => [ + '_SERVER.HTTP_X_PASSWORD', + '_SERVER.*_SECRET', // matches all ending with "_SECRET" + '_SERVER.SECRET_*', // matches all starting with "SECRET_" + '_SERVER.*SECRET*', // matches all containing "SECRET" + ] ] ``` diff --git a/docs/internals/git-workflow.md b/docs/internals/git-workflow.md index 11e8a499a6..b3ede5011a 100644 --- a/docs/internals/git-workflow.md +++ b/docs/internals/git-workflow.md @@ -111,7 +111,7 @@ review your suggestion, and provide appropriate feedback along the way. ### 2. Pull the latest code from the main Yii branch ``` -git pull upstream +git pull upstream master ``` You should start at this point for every new contribution to make sure you are working on the latest code. diff --git a/framework/.meta-storm/active-record.meta-storm.xml b/framework/.meta-storm/active-record.meta-storm.xml new file mode 100644 index 0000000000..41bf8aa79a --- /dev/null +++ b/framework/.meta-storm/active-record.meta-storm.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/.meta-storm/array.meta-storm.xml b/framework/.meta-storm/array.meta-storm.xml new file mode 100644 index 0000000000..57385246fe --- /dev/null +++ b/framework/.meta-storm/array.meta-storm.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/framework/.meta-storm/controller.meta-storm.xml b/framework/.meta-storm/controller.meta-storm.xml new file mode 100644 index 0000000000..ac6c56b2c7 --- /dev/null +++ b/framework/.meta-storm/controller.meta-storm.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/.meta-storm/db.meta-storm.xml b/framework/.meta-storm/db.meta-storm.xml new file mode 100644 index 0000000000..3dcb647146 --- /dev/null +++ b/framework/.meta-storm/db.meta-storm.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/.meta-storm/html.meta-storm.xml b/framework/.meta-storm/html.meta-storm.xml new file mode 100644 index 0000000000..ff8a891849 --- /dev/null +++ b/framework/.meta-storm/html.meta-storm.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/.meta-storm/model.meta-storm.xml b/framework/.meta-storm/model.meta-storm.xml new file mode 100644 index 0000000000..99e28786b9 --- /dev/null +++ b/framework/.meta-storm/model.meta-storm.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/.meta-storm/view.meta-storm.xml b/framework/.meta-storm/view.meta-storm.xml new file mode 100644 index 0000000000..5ca678dbbb --- /dev/null +++ b/framework/.meta-storm/view.meta-storm.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/.meta-storm/widgets.meta-storm.xml b/framework/.meta-storm/widgets.meta-storm.xml new file mode 100644 index 0000000000..83b37b4b40 --- /dev/null +++ b/framework/.meta-storm/widgets.meta-storm.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/BaseYii.php b/framework/BaseYii.php index 12a5dd4268..c34360bebc 100644 --- a/framework/BaseYii.php +++ b/framework/BaseYii.php @@ -1,5 +1,4 @@ ...])` (LAV45) + + +2.0.51 July 18, 2024 +-------------------- + +- Bug #16116: Codeception: oci does not support enabling/disabling integrity check (@terabytesoftw) +- Bug #20147: Fix error handler compatibility with PHP 8.3 (samdark) +- Bug #20191: Fix `ActiveRecord::getDirtyAttributes()` for JSON columns with multi-dimensional array values (brandonkelly) +- Bug #20195: Do not set non abstract values into `ColumnSchema->type` on MSSQL version less then 2017 (axeltomasson) +- Bug #20211: Add acceptable parameters to `MaskedInput::init()` method (alxlnk) +- Bug #20226: Revert all PR for "Data providers perform unnecessary COUNT queries that negatively affect performance" (@terabytesoftw) +- Bug #20230: Fix getting ID in `\yii\filters\Cors::actions()` when attached to a module (timkelty) + + +2.0.50 May 30, 2024 +------------------- + +- Bug #13920: Fixed erroneous validation for specific cases (tim-fischer-maschinensucher) - Bug #17181: Improved `BaseUrl::isRelative($url)` performance (sammousa, bizley, rob006) - Bug #17191: Fixed `BaseUrl::isRelative($url)` method in `yii\helpers\BaseUrl` (ggh2e3) - Bug #18469: Fixed `Link::serialize(array $links)` method in `yii\web\Link` (ggh2e3) -- Bug #19691: Allow using custom class to style error summary (skepticspriggan) -- Bug #20040: Fix type `boolean` in `MSSQL` (terabytesoftw) -- Bug #20005: Fix `yii\console\controllers\ServeController` to specify the router script (terabytesoftw) - Bug #19060: Fix `yii\widgets\Menu` bug when using Closure for active item and adding additional tests in `tests\framework\widgets\MenuTest` (atrandafir) -- Bug #13920: Fixed erroneous validation for specific cases (tim-fischer-maschinensucher) +- Bug #19691: Allow using custom class to style error summary (skepticspriggan) +- Bug #19817: Add MySQL Query `addCheck()` and `dropCheck()` (@bobonov) +- Bug #19855: Fixed `yii\validators\FileValidator` to not limit some of its rules only to array attribute (bizley) - Bug #19927: Fixed `console\controllers\MessageController` when saving translations to database: fixed FK error when adding new string and language at the same time, checking/regenerating all missing messages and dropping messages for unused languages (atrandafir) - Bug #20002: Fixed superfluous query on HEAD request in serializer (xicond) +- Bug #20005: Fix `yii\console\controllers\ServeController` to specify the router script (terabytesoftw) +- Bug #20040: Fix type `boolean` in `MSSQL` (terabytesoftw) +- Bug #20055: Fix Response header X-Pagination-Total-Count is always 0 (lav45, xicond) +- Bug #20083: Fix deprecated warning implicit conversion from float (skepticspriggan) - Bug #20122: Fixed parsing of boolean keywords (e.g. used in SQLite) in `\yii\db\ColumnSchema::typecast()` (rhertogh) +- Bug #20141: Update `ezyang/htmlpurifier` dependency to version `4.17` (@terabytesoftw) +- Bug #20165: Adjust pretty name of closures for PHP 8.4 compatibility (@staabm) +- Bug: CVE-2024-32877, Fix Reflected XSS in Debug mode (Antiphishing) +- Bug: CVE-2024-4990, Fix Unsafe Reflection in base Component class (@mtangoo) - Enh #12743: Added new methods `BaseActiveRecord::loadRelations()` and `BaseActiveRecord::loadRelationsFor()` to eager load related models for existing primary model instances (PowerGamer1) - Enh #20030: Improve performance of handling `ErrorHandler::$memoryReserveSize` (antonshevelev, rob006) -- Enh #20042: Add empty array check to `ActiveQueryTrait::findWith()` (renkas) - Enh #20032: Added `yii\helpers\BaseStringHelper::mask()` method for string masking with multibyte support (salehhashemi1992) - Enh #20034: Added `yii\helpers\BaseStringHelper::findBetween()` to retrieve a substring that lies between two strings (salehhashemi1992) +- Enh #20042: Add empty array check to `ActiveQueryTrait::findWith()` (renkas) +- Enh #20087: Add custom attributes to script tags (skepticspriggan) - Enh #20121: Added `yiisoft/yii2-coding-standards` to composer `require-dev` and lint code to comply with PSR12 (razvanphp) +- Enh #20134: Raise minimum `PHP` version to `7.3` (@terabytesoftw) +- Enh #20171: Support JSON columns for MariaDB 10.4 or higher (@terabytesoftw) +- New #20137: Added `yii\caching\CallbackDependency` to allow using a callback to determine if a cache dependency is still valid (laxity7) 2.0.49.2 October 12, 2023 diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index 13d92bba72..a6fdf69f08 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -51,6 +51,125 @@ if you want to upgrade from version A to version C and there is version B between A and C, you need to follow the instructions for both A and B. +Upgrade from Yii 2.0.52 +----------------------- +* There was a bug when loading fixtures into PostgreSQL database, the table sequences were not reset. If you used a work-around or if you depended on this behavior, you are advised to review your code. + +Upgrade from Yii 2.0.51 +----------------------- + +* The function signature for `yii\web\Session::readSession()` and `yii\web\Session::gcSession()` have been changed. + They now have the same return types as `\SessionHandlerInterface::read()` and `\SessionHandlerInterface::gc()` respectively. + In case those methods have overwritten you will need to update your child classes accordingly. + +Upgrade from Yii 2.0.50 +----------------------- + +* Correcting the behavior for `JSON` column type in `MariaDb`. + +Example usage of `JSON` column type in `db`: + +```php +db; +$command = $db->createCommand(); + +// Create a table with a JSON column +$command->createTable( + 'products', + [ + 'id' => Schema::TYPE_PK, + 'details' => Schema::TYPE_JSON, + ], +)->execute(); + +// Insert a new product +$command->insert( + 'products', + [ + 'details' => [ + 'name' => 'apple', + 'price' => 100, + 'color' => 'blue', + 'size' => 'small', + ], + ], +)->execute(); + +// Read all products +$records = $db->createCommand('SELECT * FROM products')->queryAll(); +``` + +Example usage of `JSON` column type in `ActiveRecord`: + +```php +details = [ + 'name' => 'windows', + 'color' => 'red', + 'price' => 200, + 'size' => 'large', +]; + +// Save the product +$product->save(); + +// Read the first product +$product = ProductModel::findOne(1); + +// Get the product details +$details = $product->details; + +echo 'Name: ' . $details['name']; +echo 'Color: ' . $details['color']; +echo 'Size: ' . $details['size']; + +// Read all products with color red +$products = ProductModel::find() + ->where(new \yii\db\Expression('JSON_EXTRACT(details, "$.color") = :color', [':color' => 'red'])) + ->all(); + +// Loop through all products +foreach ($products as $product) { + $details = $product->details; + echo 'Name: ' . $details['name']; + echo 'Color: ' . $details['color']; + echo 'Size: ' . $details['size']; +} +``` Upgrade from Yii 2.0.48 ----------------------- diff --git a/framework/base/Action.php b/framework/base/Action.php index b5d04b80dd..bee453fc75 100644 --- a/framework/base/Action.php +++ b/framework/base/Action.php @@ -1,5 +1,4 @@ attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); + if ($value instanceof Behavior) { + $this->attachBehavior($name, $value); + } elseif ($value instanceof \Closure) { + $this->attachBehavior($name, call_user_func($value)); + } elseif (isset($value['__class']) && is_subclass_of($value['__class'], Behavior::class)) { + $this->attachBehavior($name, Yii::createObject($value)); + } elseif (!isset($value['__class']) && isset($value['class']) && is_subclass_of($value['class'], Behavior::class)) { + $this->attachBehavior($name, Yii::createObject($value)); + } elseif (is_string($value) && is_subclass_of($value, Behavior::class, true)) { + $this->attachBehavior($name, Yii::createObject($value)); + } else { + throw new InvalidConfigException('Class is not of type ' . Behavior::class . ' or its subclasses'); + } return; } @@ -605,7 +616,7 @@ class Component extends BaseObject * @param string $name the event name * @param Event|null $event the event instance. If not set, a default [[Event]] object will be created. */ - public function trigger($name, Event $event = null) + public function trigger($name, ?Event $event = null) { $this->ensureBehaviors(); diff --git a/framework/base/Configurable.php b/framework/base/Configurable.php index 3fe41fe88b..b44a082841 100644 --- a/framework/base/Configurable.php +++ b/framework/base/Configurable.php @@ -1,5 +1,4 @@ 'PHP Notice', E_PARSE => 'PHP Parse Error', E_RECOVERABLE_ERROR => 'PHP Recoverable Error', - E_STRICT => 'PHP Strict Warning', E_USER_DEPRECATED => 'PHP User Deprecated Warning', E_USER_ERROR => 'PHP User Error', E_USER_NOTICE => 'PHP User Notice', E_USER_WARNING => 'PHP User Warning', E_WARNING => 'PHP Warning', self::E_HHVM_FATAL_ERROR => 'HHVM Fatal Error', - ]; + ] + (PHP_VERSION_ID < 80400 ? [E_STRICT => 'PHP Strict Warning'] : []); - return isset($names[$this->getCode()]) ? $names[$this->getCode()] : 'Error'; + return $names[$this->getCode()] ?? 'Error'; } } diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index 83e3149cac..c31cd058bc 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -1,5 +1,4 @@ _memoryReserve); + $this->_memoryReserve = null; - if (isset($this->_workingDirectory)) { + if (!empty($this->_workingDirectory)) { // fix working directory for some Web servers e.g. Apache chdir($this->_workingDirectory); // flush memory - unset($this->_workingDirectory); + $this->_workingDirectory = null; } $error = error_get_last(); diff --git a/framework/base/Event.php b/framework/base/Event.php index db66db004e..ac2fdc4346 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -1,5 +1,4 @@ has($calledClass) && isset(Yii::$container->getDefinitions()[$calledClass]['class'])) { - $calledClass = Yii::$container->getDefinitions()[$calledClass]['class']; - } + $calledClass = self::$_resolvedClasses[get_called_class()] ?? get_called_class(); if (get_class($widget) === $calledClass) { /* @var $widget Widget */ diff --git a/framework/base/WidgetEvent.php b/framework/base/WidgetEvent.php index 933f12c96c..eaa1850b85 100644 --- a/framework/base/WidgetEvent.php +++ b/framework/base/WidgetEvent.php @@ -1,5 +1,4 @@ = 80100 && is_subclass_of($type, \BackedEnum::class)) { + if ($value instanceof $type) { + return $value; + } + return $type::from($value); + } + + throw new InvalidArgumentException("Unsupported type '{$type}'"); } return call_user_func($type, $value); diff --git a/framework/behaviors/AttributesBehavior.php b/framework/behaviors/AttributesBehavior.php index 5ab2ceea0d..529c72db1f 100644 --- a/framework/behaviors/AttributesBehavior.php +++ b/framework/behaviors/AttributesBehavior.php @@ -1,5 +1,4 @@ + * @since 2.0.50 + */ +class CallbackDependency extends Dependency +{ + /** + * @var callable the PHP callback that will be called to determine if the dependency has been changed. + */ + public $callback; + + + /** + * Generates the data needed to determine if dependency has been changed. + * This method returns the result of the callback function. + * @param CacheInterface $cache the cache component that is currently evaluating this dependency + * @return mixed the data needed to determine if dependency has been changed. + */ + protected function generateDependencyData($cache) + { + return ($this->callback)(); + } +} diff --git a/framework/caching/ChainedDependency.php b/framework/caching/ChainedDependency.php index 0efa116507..b318ea8516 100644 --- a/framework/caching/ChainedDependency.php +++ b/framework/caching/ChainedDependency.php @@ -1,5 +1,4 @@ gcRecursive($fullPath, $expiredOnly); if (!$expiredOnly) { if (!@rmdir($fullPath)) { - $error = error_get_last(); - Yii::warning("Unable to remove directory '{$fullPath}': {$error['message']}", __METHOD__); + $message = "Unable to remove directory '$fullPath'"; + if ($error = error_get_last()) { + $message .= ": {$error['message']}"; + } } } } elseif (!$expiredOnly || $expiredOnly && @filemtime($fullPath) < time()) { if (!@unlink($fullPath)) { - $error = error_get_last(); - Yii::warning("Unable to remove file '{$fullPath}': {$error['message']}", __METHOD__); + $message = "Unable to remove file '$fullPath'"; + if ($error = error_get_last()) { + $message .= ": {$error['message']}"; + } } } + $message and Yii::warning($message, __METHOD__); } closedir($handle); } diff --git a/framework/caching/FileDependency.php b/framework/caching/FileDependency.php index 61b9b61054..77052aaf1a 100644 --- a/framework/caching/FileDependency.php +++ b/framework/caching/FileDependency.php @@ -1,5 +1,4 @@ fileName); + clearstatcache(false, $fileName); return @filemtime($fileName); } diff --git a/framework/caching/MemCache.php b/framework/caching/MemCache.php index 3f560c6157..a1918c90f5 100644 --- a/framework/caching/MemCache.php +++ b/framework/caching/MemCache.php @@ -1,5 +1,4 @@ YII2_PATH . '/caching/ArrayCache.php', 'yii\caching\Cache' => YII2_PATH . '/caching/Cache.php', 'yii\caching\CacheInterface' => YII2_PATH . '/caching/CacheInterface.php', + 'yii\caching\CallbackDependency' => YII2_PATH . '/caching/CallbackDependency.php', 'yii\caching\ChainedDependency' => YII2_PATH . '/caching/ChainedDependency.php', 'yii\caching\DbCache' => YII2_PATH . '/caching/DbCache.php', 'yii\caching\DbDependency' => YII2_PATH . '/caching/DbDependency.php', @@ -384,6 +384,7 @@ return [ 'yii\web\ResponseFormatterInterface' => YII2_PATH . '/web/ResponseFormatterInterface.php', 'yii\web\ServerErrorHttpException' => YII2_PATH . '/web/ServerErrorHttpException.php', 'yii\web\Session' => YII2_PATH . '/web/Session.php', + 'yii\web\SessionHandler' => YII2_PATH . '/web/SessionHandler.php', 'yii\web\SessionIterator' => YII2_PATH . '/web/SessionIterator.php', 'yii\web\TooManyRequestsHttpException' => YII2_PATH . '/web/TooManyRequestsHttpException.php', 'yii\web\UnauthorizedHttpException' => YII2_PATH . '/web/UnauthorizedHttpException.php', diff --git a/framework/composer.json b/framework/composer.json index 4934013b66..ad247b9284 100644 --- a/framework/composer.json +++ b/framework/composer.json @@ -68,11 +68,11 @@ "ext-ctype": "*", "lib-pcre": "*", "yiisoft/yii2-composer": "~2.0.4", - "ezyang/htmlpurifier": "^4.6", + "ezyang/htmlpurifier": "^4.17", "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", "bower-asset/inputmask": "^5.0.8 ", - "bower-asset/punycode": "^2.2", + "bower-asset/punycode": "^1.4", "bower-asset/yii2-pjax": "~2.0.1" }, "autoload": { diff --git a/framework/console/Application.php b/framework/console/Application.php index 7d5a87d7b9..1ec070a55d 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -1,5 +1,4 @@ ` is a route to a controller action and the params will be populated as properties of a command. * See [[options()]] for details. * - * @property-read string $help - * @property-read string $helpSummary + * @property-read string $help The help information for this controller. + * @property-read string $helpSummary The one-line short summary describing this controller. * @property-read array $passedOptionValues The properties corresponding to the passed options. * @property-read array $passedOptions The names of the options passed during execution. * @@ -190,6 +189,7 @@ class Controller extends \yii\base\Controller $method = new \ReflectionMethod($action, 'run'); } + $paramKeys = array_keys($params); $args = []; $missing = []; $actionParams = []; @@ -204,16 +204,27 @@ class Controller extends \yii\base\Controller } if ($key !== null) { - if (PHP_VERSION_ID >= 80000) { - $isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array'; + if ($param->isVariadic()) { + for ($j = array_search($key, $paramKeys); $j < count($paramKeys); $j++) { + $jKey = $paramKeys[$j]; + if ($jKey !== $key && !is_int($jKey)) { + break; + } + $args[] = $actionParams[$key][] = $params[$jKey]; + unset($params[$jKey]); + } } else { - $isArray = $param->isArray(); + if (PHP_VERSION_ID >= 80000) { + $isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array'; + } else { + $isArray = $param->isArray(); + } + if ($isArray) { + $params[$key] = $params[$key] === '' ? [] : preg_split('/\s*,\s*/', $params[$key]); + } + $args[] = $actionParams[$key] = $params[$key]; + unset($params[$key]); } - if ($isArray) { - $params[$key] = $params[$key] === '' ? [] : preg_split('/\s*,\s*/', $params[$key]); - } - $args[] = $actionParams[$key] = $params[$key]; - unset($params[$key]); } elseif ( PHP_VERSION_ID >= 70100 && ($type = $param->getType()) !== null diff --git a/framework/console/ErrorHandler.php b/framework/console/ErrorHandler.php index cefe06c4c2..c02cbb3d01 100644 --- a/framework/console/ErrorHandler.php +++ b/framework/console/ErrorHandler.php @@ -1,5 +1,4 @@ _totalCount === null) { $this->_totalCount = $this->prepareTotalCount(); } - return $this->_totalCount; } diff --git a/framework/data/DataFilter.php b/framework/data/DataFilter.php index 2c12485153..587b7b10a5 100644 --- a/framework/data/DataFilter.php +++ b/framework/data/DataFilter.php @@ -1,5 +1,4 @@ * @author Carsten Brandt * @since 2.0 + * + * @template T of (ActiveRecord|array) */ class ActiveQuery extends Query implements ActiveQueryInterface { @@ -128,6 +129,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface * @param Connection|null $db the DB connection used to create the DB command. * If null, the DB connection returned by [[modelClass]] will be used. * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. + * @psalm-return T[] + * @phpstan-return T[] */ public function all($db = null) { @@ -296,9 +299,11 @@ class ActiveQuery extends Query implements ActiveQueryInterface * Executes query and returns a single row of result. * @param Connection|null $db the DB connection used to create the DB command. * If `null`, the DB connection returned by [[modelClass]] will be used. - * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], + * @return array|ActiveRecord|null a single row of query result. Depending on the setting of [[asArray]], * the query result may be either an array or an ActiveRecord object. `null` will be returned * if the query results in nothing. + * @psalm-return T|null + * @phpstan-return T|null */ public function one($db = null) { @@ -311,6 +316,32 @@ class ActiveQuery extends Query implements ActiveQueryInterface return null; } + /** + * {@inheritdoc} + * + * @return BatchQueryResult + * @psalm-return T[][]|BatchQueryResult + * @phpstan-return T[][]|BatchQueryResult + * @codeCoverageIgnore + */ + public function batch($batchSize = 100, $db = null) + { + return parent::batch($batchSize, $db); + } + + /** + * {@inheritdoc} + * + * @return BatchQueryResult + * @psalm-return T[]|BatchQueryResult + * @phpstan-return T[]|BatchQueryResult + * @codeCoverageIgnore + */ + public function each($batchSize = 100, $db = null) + { + return parent::each($batchSize, $db); + } + /** * Creates a DB command that can be used to execute this query. * @param Connection|null $db the DB connection used to create the DB command. @@ -785,7 +816,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface * @throws InvalidConfigException when query is not initialized properly * @see via() */ - public function viaTable($tableName, $link, callable $callable = null) + public function viaTable($tableName, $link, ?callable $callable = null) { $modelClass = $this->primaryModel ? get_class($this->primaryModel) : $this->modelClass; $relation = new self($modelClass, [ diff --git a/framework/db/ActiveQueryInterface.php b/framework/db/ActiveQueryInterface.php index ec53af2d4b..55e648cd4e 100644 --- a/framework/db/ActiveQueryInterface.php +++ b/framework/db/ActiveQueryInterface.php @@ -1,5 +1,4 @@ primaryModel->getRelation($relationName); $callableUsed = $callable !== null; diff --git a/framework/db/AfterSaveEvent.php b/framework/db/AfterSaveEvent.php index 1a8a768d4f..3efccc10ec 100644 --- a/framework/db/AfterSaveEvent.php +++ b/framework/db/AfterSaveEvent.php @@ -1,5 +1,4 @@ endCommand($time); } + /** + * Creates a SQL command for adding a check constraint to an existing table. + * @param string $name the name of the check constraint. + * The name will be properly quoted by the method. + * @param string $table the table that the check constraint will be added to. + * The name will be properly quoted by the method. + * @param string $expression the SQL of the `CHECK` constraint. + */ + public function addCheck($name, $table, $expression) + { + $time = $this->beginCommand("add check $name in table $table"); + $this->db->createCommand()->addCheck($name, $table, $expression)->execute(); + $this->endCommand($time); + } + + /** + * Creates a SQL command for dropping a check constraint. + * @param string $name the name of the check constraint to be dropped. + * The name will be properly quoted by the method. + * @param string $table the table whose check constraint is to be dropped. + * The name will be properly quoted by the method. + */ + public function dropCheck($name, $table) + { + $time = $this->beginCommand("drop check $name in table $table"); + $this->db->createCommand()->dropCheck($name, $table)->execute(); + $this->endCommand($time); + } + /** * Builds and execute a SQL statement for adding comment to column. * diff --git a/framework/db/MigrationInterface.php b/framework/db/MigrationInterface.php index b2bca254ea..220264e587 100644 --- a/framework/db/MigrationInterface.php +++ b/framework/db/MigrationInterface.php @@ -1,5 +1,4 @@ separator . $orderBy; - // http://technet.microsoft.com/en-us/library/gg699618.aspx + // https://technet.microsoft.com/en-us/library/gg699618.aspx $offset = $this->hasOffset($offset) ? $offset : '0'; $sql .= $this->separator . "OFFSET $offset ROWS"; if ($this->hasLimit($limit)) { @@ -248,9 +247,11 @@ class QueryBuilder extends \yii\db\QueryBuilder $table = $this->db->getTableSchema($tableName); if ($table !== null && $table->sequenceName !== null) { $tableName = $this->db->quoteTableName($tableName); + if ($value === null) { $key = $this->db->quoteColumnName(reset($table->primaryKey)); - $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1"; + $sql = "SELECT COALESCE(MAX({$key}), 0) + 1 FROM {$tableName}"; + $value = $this->db->createCommand($sql)->queryScalar(); } else { $value = (int) $value; } diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php index 13ea534003..a8d6045d51 100644 --- a/framework/db/mssql/Schema.php +++ b/framework/db/mssql/Schema.php @@ -1,5 +1,4 @@ type = $this->booleanTypeLegacy($column->size, $type); + if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { + $column->type = 'boolean'; + } elseif ($type === 'bit') { + if ($column->size > 32) { + $column->type = 'bigint'; + } elseif ($column->size === 32) { + $column->type = 'integer'; + } + } } } } @@ -817,27 +824,4 @@ SQL; { return Yii::createObject(ColumnSchemaBuilder::class, [$type, $length, $this->db]); } - - /** - * Assigns a type boolean for the column type bit, for legacy versions of MSSQL. - * - * @param int $size column size. - * @param string $type column type. - * - * @return string column type. - */ - private function booleanTypeLegacy($size, $type) - { - if ($size === 1 && ($type === 'tinyint' || $type === 'bit')) { - return 'boolean'; - } elseif ($type === 'bit') { - if ($size > 32) { - return 'bigint'; - } elseif ($size === 32) { - return 'integer'; - } - } - - return $type; - } } diff --git a/framework/db/mssql/SqlsrvPDO.php b/framework/db/mssql/SqlsrvPDO.php index ed8139eefa..4285e220df 100644 --- a/framework/db/mssql/SqlsrvPDO.php +++ b/framework/db/mssql/SqlsrvPDO.php @@ -1,5 +1,4 @@ dropIndex($name, $table); } - /** - * {@inheritdoc} - * @throws NotSupportedException this is not supported by MySQL. - */ - public function addCheck($name, $table, $expression) - { - throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.'); - } - - /** - * {@inheritdoc} - * @throws NotSupportedException this is not supported by MySQL. - */ - public function dropCheck($name, $table) - { - throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.'); - } - /** * Creates a SQL statement for resetting the sequence value of a table's primary key. * The sequence will be reset such that the primary key of the next new row inserted diff --git a/framework/db/mysql/Schema.php b/framework/db/mysql/Schema.php index c3e8db4f51..4c47aadeb1 100644 --- a/framework/db/mysql/Schema.php +++ b/framework/db/mysql/Schema.php @@ -1,5 +1,4 @@ db->getServerVersion(); + + // check version MySQL >= 8.0.16 + if (\stripos($version, 'MariaDb') === false && \version_compare($version, '8.0.16', '<')) { + throw new NotSupportedException('MySQL < 8.0.16 does not support check constraints.'); + } + + $checks = []; + + $sql = <<resolveTableName($tableName); + $tableRows = $this->db->createCommand($sql, [':tableName' => $resolvedName->name])->queryAll(); + + if ($tableRows === []) { + return $checks; + } + + $tableRows = $this->normalizePdoRowKeyCase($tableRows, true); + + foreach ($tableRows as $tableRow) { + $check = new CheckConstraint( + [ + 'name' => $tableRow['constraint_name'], + 'expression' => $tableRow['check_clause'], + ] + ); + $checks[] = $check; + } + + return $checks; } /** @@ -338,10 +373,19 @@ SQL; } throw $e; } + + + $jsonColumns = $this->getJsonColumns($table); + foreach ($columns as $info) { if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) !== \PDO::CASE_LOWER) { $info = array_change_key_case($info, CASE_LOWER); } + + if (\in_array($info['field'], $jsonColumns, true)) { + $info['type'] = static::TYPE_JSON; + } + $column = $this->loadColumnSchema($info); $table->columns[$column->name] = $column; if ($column->isPrimaryKey) { @@ -599,4 +643,20 @@ SQL; return $result[$returnType]; } + + private function getJsonColumns(TableSchema $table): array + { + $sql = $this->getCreateTableSql($table); + $result = []; + + $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi'; + + if (\preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $result[] = $match[1]; + } + } + + return $result; + } } diff --git a/framework/db/oci/ColumnSchemaBuilder.php b/framework/db/oci/ColumnSchemaBuilder.php index b104588c4c..dcb5ff21ef 100644 --- a/framework/db/oci/ColumnSchemaBuilder.php +++ b/framework/db/oci/ColumnSchemaBuilder.php @@ -1,5 +1,4 @@ defaultValue) { if ( in_array($column->type, [self::TYPE_TIMESTAMP, self::TYPE_DATE, self::TYPE_TIME], true) && - in_array( - strtoupper($column->defaultValue), - ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'], - true + ( + in_array( + strtoupper($column->defaultValue), + ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME'], + true + ) || + (false !== strpos($column->defaultValue, '(')) ) ) { $column->defaultValue = new Expression($column->defaultValue); diff --git a/framework/db/sqlite/ColumnSchemaBuilder.php b/framework/db/sqlite/ColumnSchemaBuilder.php index 7f3444d3d3..d9853b08ab 100644 --- a/framework/db/sqlite/ColumnSchemaBuilder.php +++ b/framework/db/sqlite/ColumnSchemaBuilder.php @@ -1,5 +1,4 @@ |array{class: class-string} $class + * @phpstan-param class-string|array{class: class-string} $class + * @psalm-return T + * @phpstan-return T */ public function get($class, $params = [], $config = []) { diff --git a/framework/di/Instance.php b/framework/di/Instance.php index 458b01f274..edecbe9be8 100644 --- a/framework/di/Instance.php +++ b/framework/di/Instance.php @@ -1,5 +1,4 @@ get($class, [], $reference); if ($type === null || $component instanceof $type) { return $component; diff --git a/framework/di/NotInstantiableException.php b/framework/di/NotInstantiableException.php index 0311aa2578..6da3e33d27 100644 --- a/framework/di/NotInstantiableException.php +++ b/framework/di/NotInstantiableException.php @@ -1,5 +1,4 @@ actions[$action->id])) { - $actionParams = $this->actions[$action->id]; + $actionId = $this->getActionId($action); + + if (isset($this->actions[$actionId])) { + $actionParams = $this->actions[$actionId]; $actionParamsKeys = array_keys($actionParams); foreach ($this->cors as $headerField => $headerValue) { if (in_array($headerField, $actionParamsKeys)) { diff --git a/framework/filters/HostControl.php b/framework/filters/HostControl.php index 242fe823a1..42ca30c44d 100644 --- a/framework/filters/HostControl.php +++ b/framework/filters/HostControl.php @@ -1,5 +1,4 @@ user; + if ($authUser != null && !$authUser instanceof \yii\web\User) { + throw new InvalidConfigException(get_class($authUser) . ' must implement yii\web\User'); + } elseif ($authUser != null) { + $user = $authUser; + } + + $authRequest = $auth->request; + if ($authRequest != null && !$authRequest instanceof \yii\web\Request) { + throw new InvalidConfigException(get_class($authRequest) . ' must implement yii\web\Request'); + } elseif ($authRequest != null) { + $request = $authRequest; + } + + $authResponse = $auth->response; + if ($authResponse != null && !$authResponse instanceof \yii\web\Response) { + throw new InvalidConfigException(get_class($authResponse) . ' must implement yii\web\Response'); + } elseif ($authResponse != null) { + $response = $authResponse; + } + $identity = $auth->authenticate($user, $request, $response); if ($identity !== null) { return $identity; diff --git a/framework/filters/auth/HttpBasicAuth.php b/framework/filters/auth/HttpBasicAuth.php index 60bbc33106..d7969a696a 100644 --- a/framework/filters/auth/HttpBasicAuth.php +++ b/framework/filters/auth/HttpBasicAuth.php @@ -1,5 +1,4 @@ [1, 2], + * 'B' => [ + * 'C' => 1, + * 'D' => 2, + * ], + * 'E' => 1, + * ]; + * $result = \yii\helpers\ArrayHelper::flatten($array); + * // $result will be: + * // [ + * // 'A.0' => 1 + * // 'A.1' => 2 + * // 'B.C' => 1 + * // 'B.D' => 2 + * // 'E' => 1 + * // ] + * ``` + * + * @param array $array the input array to be flattened in terms of name-value pairs. + * @param string $separator the separator to use between keys. Defaults to '.'. + * + * @return array the flattened array. + * @throws InvalidArgumentException if `$array` is neither traversable nor an array. + */ + public static function flatten($array, $separator = '.'): array + { + if (!static::isTraversable($array)) { + throw new InvalidArgumentException('Argument $array must be an array or implement Traversable'); + } + + $result = []; + + foreach ($array as $key => $value) { + $newKey = $key; + if (is_array($value)) { + $flattenedArray = self::flatten($value, $separator); + foreach ($flattenedArray as $subKey => $subValue) { + $result[$newKey . $separator . $subKey] = $subValue; + } + } else { + $result[$newKey] = $value; + } + } + + return $result; + } } diff --git a/framework/helpers/BaseConsole.php b/framework/helpers/BaseConsole.php index 35c9b00550..04a031f73d 100644 --- a/framework/helpers/BaseConsole.php +++ b/framework/helpers/BaseConsole.php @@ -1,5 +1,4 @@ getView(); + if ($view instanceof \yii\web\View && !empty($view->styleOptions)) { + $options = array_merge($view->styleOptions, $options); + } + return static::tag('style', $content, $options); } @@ -220,6 +224,11 @@ class BaseHtml */ public static function script($content, $options = []) { + $view = Yii::$app->getView(); + if ($view instanceof \yii\web\View && !empty($view->scriptOptions)) { + $options = array_merge($view->scriptOptions, $options); + } + return static::tag('script', $content, $options); } diff --git a/framework/helpers/BaseHtmlPurifier.php b/framework/helpers/BaseHtmlPurifier.php index f707baabaf..2397bff57a 100644 --- a/framework/helpers/BaseHtmlPurifier.php +++ b/framework/helpers/BaseHtmlPurifier.php @@ -1,5 +1,4 @@ '\\\\', - '\\\\\\*' => '[*]', - '\\\\\\?' => '[?]', - '\*' => '.*', - '\?' => '.', - '\[\!' => '[^', - '\[' => '[', - '\]' => ']', - '\-' => '-', + '\\\\\\*' => '[*]', + '\\\\\\?' => '[?]', + '\*' => '.*', + '\?' => '.', + '\[\!' => '[^', + '\[' => '[', + '\]' => ']', + '\-' => '-', ]; if (isset($options['escape']) && !$options['escape']) { @@ -484,7 +488,7 @@ class BaseStringHelper */ public static function mb_ucwords($string, $encoding = 'UTF-8') { - $string = (string) $string; + $string = (string)$string; if (empty($string)) { return $string; } @@ -507,12 +511,12 @@ class BaseStringHelper * * @param string $string The input string. * @param int $start The starting position from where to begin masking. - * This can be a positive or negative integer. - * Positive values count from the beginning, - * negative values count from the end of the string. + * This can be a positive or negative integer. + * Positive values count from the beginning, + * negative values count from the end of the string. * @param int $length The length of the section to be masked. - * The masking will start from the $start position - * and continue for $length characters. + * The masking will start from the $start position + * and continue for $length characters. * @param string $mask The character to use for masking. The default is '*'. * @return string The masked string. */ diff --git a/framework/helpers/BaseUrl.php b/framework/helpers/BaseUrl.php index 7122b9e17b..864dbc63c4 100644 --- a/framework/helpers/BaseUrl.php +++ b/framework/helpers/BaseUrl.php @@ -1,5 +1,4 @@ 'application/rtf', 'text/xml' => 'application/xml', diff --git a/framework/helpers/mimeExtensions.php b/framework/helpers/mimeExtensions.php index 8247ff2829..0bbbd94647 100644 --- a/framework/helpers/mimeExtensions.php +++ b/framework/helpers/mimeExtensions.php @@ -1,5 +1,4 @@ 'ez', 'application/applixware' => 'aw', @@ -963,6 +961,7 @@ return [ 'pjp', 'pjpeg', ], + 'image/jxl' => 'jxl', 'image/ktx' => 'ktx', 'image/png' => 'png', 'image/prs.btif' => 'btif', @@ -1043,7 +1042,6 @@ return [ 'model/vnd.dwf' => 'dwf', 'model/vnd.gdl' => 'gdl', 'model/vnd.gtw' => 'gtw', - 'model/vnd.mts' => 'mts', 'model/vnd.vtu' => 'vtu', 'model/vrml' => [ 'wrl', @@ -1167,6 +1165,12 @@ return [ 'mj2', 'mjp2', ], + 'video/mp2t' => [ + 'ts', + 'm2t', + 'm2ts', + 'mts', + ], 'video/mp4' => [ 'mp4', 'mp4v', diff --git a/framework/helpers/mimeTypes.php b/framework/helpers/mimeTypes.php index c5ddb93490..87f7e3649b 100644 --- a/framework/helpers/mimeTypes.php +++ b/framework/helpers/mimeTypes.php @@ -1,5 +1,4 @@ 'application/vnd.lotus-1-2-3', '3dml' => 'text/vnd.in3d.3dml', @@ -378,6 +376,7 @@ $mimeTypes = [ 'js' => 'text/javascript', 'json' => 'application/json', 'jsonml' => 'application/jsonml+json', + 'jxl' => 'image/jxl', 'kar' => 'audio/midi', 'karbon' => 'application/vnd.kde.karbon', 'kfo' => 'application/vnd.kde.kformula', @@ -420,6 +419,8 @@ $mimeTypes = [ 'm1v' => 'video/mpeg', 'm21' => 'application/mp21', 'm2a' => 'audio/mpeg', + 'm2t' => 'video/mp2t', + 'm2ts' => 'video/mp2t', 'm2v' => 'video/mpeg', 'm3a' => 'audio/mpeg', 'm3u' => 'audio/x-mpegurl', @@ -505,7 +506,7 @@ $mimeTypes = [ 'msi' => 'application/x-msdownload', 'msl' => 'application/vnd.mobius.msl', 'msty' => 'application/vnd.muvee.style', - 'mts' => 'model/vnd.mts', + 'mts' => 'video/mp2t', 'mus' => 'application/vnd.musician', 'musicxml' => 'application/vnd.recordare.musicxml+xml', 'mvb' => 'application/x-msmediaview', @@ -820,6 +821,7 @@ $mimeTypes = [ 'tr' => 'text/troff', 'tra' => 'application/vnd.trueapp', 'trm' => 'application/x-msterminal', + 'ts' => 'video/mp2t', 'tsd' => 'application/timestamped-data', 'tsv' => 'text/tab-separated-values', 'ttc' => 'font/collection', diff --git a/framework/i18n/DbMessageSource.php b/framework/i18n/DbMessageSource.php index f86be386c3..9630699f0b 100644 --- a/framework/i18n/DbMessageSource.php +++ b/framework/i18n/DbMessageSource.php @@ -1,5 +1,4 @@ setTimestamp(0); - $valueDateTime = (new DateTime())->setTimestamp(abs($value)); + $valueDateTime = (new DateTime())->setTimestamp(abs((int) $value)); $interval = $valueDateTime->diff($zeroDateTime); } elseif (strncmp($value, 'P-', 2) === 0) { $interval = new DateInterval('P' . substr($value, 2)); diff --git a/framework/i18n/GettextFile.php b/framework/i18n/GettextFile.php index 519d9e1571..6e320348c3 100644 --- a/framework/i18n/GettextFile.php +++ b/framework/i18n/GettextFile.php @@ -1,5 +1,4 @@ logFile})!: {$error['message']}"); + $message = "Unable to export log through file ($this->logFile)!"; + if ($error = error_get_last()) { + $message .= ": {$error['message']}"; + } + throw new LogRuntimeException($message); } $textSize = strlen($text); if ($writeResult < $textSize) { diff --git a/framework/log/LogRuntimeException.php b/framework/log/LogRuntimeException.php index 4271b976bc..609f3cde47 100644 --- a/framework/log/LogRuntimeException.php +++ b/framework/log/LogRuntimeException.php @@ -1,5 +1,4 @@ logVars); + $items = ArrayHelper::flatten($context); foreach ($this->maskVars as $var) { - if (ArrayHelper::getValue($context, $var) !== null) { - ArrayHelper::setValue($context, $var, '***'); + foreach ($items as $key => $value) { + if (StringHelper::matchWildcard($var, $key, ['caseSensitive' => false])) { + ArrayHelper::setValue($context, $key, '***'); + } } } $result = []; @@ -293,7 +301,7 @@ abstract class Target extends Component */ public function formatMessage($message) { - list($text, $level, $category, $timestamp) = $message; + [$text, $level, $category, $timestamp] = $message; $level = Logger::getLevelName($level); if (!is_string($text)) { // exceptions may not be serializable if in the call stack somewhere is a Closure diff --git a/framework/mail/BaseMailer.php b/framework/mail/BaseMailer.php index ccc3750063..e334d74aa9 100644 --- a/framework/mail/BaseMailer.php +++ b/framework/mail/BaseMailer.php @@ -1,5 +1,4 @@ mailer === null) { $mailer = Yii::$app->getMailer(); diff --git a/framework/mail/MailEvent.php b/framework/mail/MailEvent.php index 2e4dd9cc0e..641f71ae10 100644 --- a/framework/mail/MailEvent.php +++ b/framework/mail/MailEvent.php @@ -1,5 +1,4 @@ 'prije {delta, plural, =1{dan} one{# dan} few{# dana} many{# dana} other{# dana}}', '{delta, plural, =1{a minute} other{# minutes}} ago' => 'prije {delta, plural, =1{minut} one{# minut} few{# minuta} many{# minuta} other{# minuta}}', '{delta, plural, =1{a month} other{# months}} ago' => 'prije {delta, plural, =1{mjesec} one{# mjesec} few{# mjeseci} many{# mjeseci} other{# mjeseci}}', - '{delta, plural, =1{a second} other{# seconds}} ago' => 'prije {delta, plural, =1{sekundu} one{# sekundu} few{# sekundi} many{# sekundi} other{# sekundi}', + '{delta, plural, =1{a second} other{# seconds}} ago' => 'prije {delta, plural, =1{sekundu} one{# sekundu} few{# sekundi} many{# sekundi} other{# sekundi}}', '{delta, plural, =1{a year} other{# years}} ago' => 'prije {delta, plural, =1{godinu} one{# godinu} few{# godina} many{# godina} other{# godina}}', '{delta, plural, =1{an hour} other{# hours}} ago' => 'prije {delta, plural, =1{sat} one{# sat} few{# sati} many{# sati} other{# sati}}', '{nFormatted} B' => '{nFormatted} B', diff --git a/framework/messages/th/yii.php b/framework/messages/th/yii.php index 51ceec4140..68e4035b2f 100644 --- a/framework/messages/th/yii.php +++ b/framework/messages/th/yii.php @@ -24,64 +24,64 @@ * NOTE: this file must be saved in UTF-8 encoding. */ return [ - ' and ' => '', - '"{attribute}" does not support operator "{operator}".' => '', + ' and ' => ' และ ', + '"{attribute}" does not support operator "{operator}".' => '"{attribute}" ไม่สนับสนุนตัวดำเนินการ "{operator}"', '(not set)' => '(ไม่ได้ตั้ง)', - 'Action not found.' => '', - 'Aliases available: {aliases}' => '', + 'Action not found.' => 'ไม่พบแอคชั่น', + 'Aliases available: {aliases}' => 'Alias ที่ใช้ได้: {aliases}', 'An internal server error occurred.' => 'เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์', 'Are you sure you want to delete this item?' => 'คุณแน่ใจหรือไม่ที่จะลบรายการนี้?', - 'Condition for "{attribute}" should be either a value or valid operator specification.' => '', + 'Condition for "{attribute}" should be either a value or valid operator specification.' => 'เงื่อนไขสำหรับ "{attribute}" ควรเป็นค่าหรือข้อกำหนดเฉพาะของตัวดำเนินการที่ถูกต้อง', 'Delete' => 'ลบ', 'Error' => 'ผิดพลาด', - 'File upload failed.' => 'อัพโหลดไฟล์ล้มเหลว', + 'File upload failed.' => 'อัพโหลดไฟล์ไม่สำเร็จ', 'Home' => 'หน้าหลัก', - 'Invalid data received for parameter "{param}".' => 'ค่าพารามิเตอร์ "{param}" ไม่ถูกต้อง', - 'Login Required' => 'จำเป็นต้องเข้าสู่ระบบ', - 'Missing required arguments: {params}' => 'อาร์กิวเมนต์ที่จำเป็นขาดหายไป: {params}', - 'Missing required parameters: {params}' => 'พารามิเตอร์ที่จำเป็นขาดหายไป: {params}', + 'Invalid data received for parameter "{param}".' => 'ข้อมูลสำหรับค่าพารามิเตอร์ "{param}" ที่ได้รับไม่ถูกต้อง', + 'Login Required' => 'ต้องเข้าสู่ระบบก่อน', + 'Missing required arguments: {params}' => 'ขาดอาร์กิวเมนต์ที่จำเป็น: {params}', + 'Missing required parameters: {params}' => 'ขาดพารามิเตอร์ที่จำเป็น: {params}', 'No' => 'ไม่', 'No results found.' => 'ไม่พบผลลัพธ์', - 'Only files with these MIME types are allowed: {mimeTypes}.' => 'เฉพาะไฟล์ที่มีชนิด MIME ต่อไปนี้ที่ได้รับการอนุญาต: {mimeTypes}', - 'Only files with these extensions are allowed: {extensions}.' => 'เฉพาะไฟล์ที่มีนามสกุลต่อไปนี้ที่ได้รับอนุญาต: {extensions}', - 'Operator "{operator}" must be used with a search attribute.' => '', - 'Operator "{operator}" requires multiple operands.' => '', - 'Options available: {options}' => '', - 'Page not found.' => 'ไม่พบหน้า', + 'Only files with these MIME types are allowed: {mimeTypes}.' => 'อนุญาตเฉพาะไฟล์ที่มีชนิด MIME ต่อไปนี้: {mimeTypes}', + 'Only files with these extensions are allowed: {extensions}.' => 'อนุญาตเฉพาะไฟล์ที่มีนามสกุลต่อไปนี้: {extensions}', + 'Operator "{operator}" must be used with a search attribute.' => 'ตัวดำเนินการ "{operator}" ต้องใช้กับแอตทริบิวต์สำหรับค้นหา', + 'Operator "{operator}" requires multiple operands.' => 'ตัวดำเนินการ "{operator}" ต้องมีตัวถูกดำเนินการหลายตัว', + 'Options available: {options}' => 'ตัวเลือกที่ใช้ได้: {options}', + 'Page not found.' => 'ไม่พบหน้าที่ต้องการ', 'Please fix the following errors:' => 'โปรดแก้ไขข้อผิดพลาดต่อไปนี้:', 'Please upload a file.' => 'กรุณาอัพโหลดไฟล์', - 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'แสดง {begin, number} ถึง {end, number} จาก {totalCount, number} ผลลัพธ์', - 'The combination {values} of {attributes} has already been taken.' => '', - 'The file "{file}" is not an image.' => 'ไฟล์ "{file}" ไม่ใช่รูปภาพ', - 'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => 'ไฟล์ "{file}" มีขนาดใหญ่ไป ไฟล์จะต้องมีขนาดไม่เกิน {formattedLimit}', - 'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.' => 'ไฟล์ "{file}" มีขนาดเล็กเกินไป ไฟล์จะต้องมีขนาดมากกว่า {formattedLimit}', + 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'แสดง {begin, number} ถึง {end, number} จาก {totalCount, number} รายการ', + 'The combination {values} of {attributes} has already been taken.' => 'กลุ่ม {values} ของ {attributes} ถูกใช้ไปแล้ว', + 'The file "{file}" is not an image.' => 'ไฟล์ "{file}" ไม่ใช่ไฟล์รูปภาพ', + 'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => 'ไฟล์ "{file}" มีขนาดใหญ่เกินไป ไฟล์ต้องมีขนาดไม่เกิน {formattedLimit}', + 'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.' => 'ไฟล์ "{file}" มีขนาดเล็กเกินไป ไฟล์ต้องมีขนาดมากกว่า {formattedLimit}', 'The format of {attribute} is invalid.' => 'รูปแบบ {attribute} ไม่ถูกต้อง', - 'The format of {filter} is invalid.' => '', - 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'รูปภาพ "{file}" ใหญ่เกินไป ความสูงต้องน้อยกว่า {limit, number} พิกเซล', - 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'รูปภาพ "{file}" ใหญ่เกินไป ความกว้างต้องน้อยกว่า {limit, number} พิกเซล', - 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'รูปภาพ "{file}" เล็กเกินไป ความสูงต้องมากว่า {limit, number} พิกเซล', - 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'รูปภาพ "{file}" เล็กเกินไป ความกว้างต้องมากกว่า {limit, number} พิกเซล', - 'The requested view "{name}" was not found.' => 'ไม่พบ "{name}" ในการเรียกใช้', - 'The verification code is incorrect.' => 'รหัสการยืนยันไม่ถูกต้อง', - 'Total {count, number} {count, plural, one{item} other{items}}.' => 'ทั้งหมด {count, number} ผลลัพธ์', - 'Unable to verify your data submission.' => 'ไม่สามารถตรวจสอบการส่งข้อมูลของคุณ', - 'Unknown alias: -{name}' => '', - 'Unknown filter attribute "{attribute}"' => '', - 'Unknown option: --{name}' => 'ไม่รู้จักตัวเลือก: --{name}', + 'The format of {filter} is invalid.' => 'รูปแบบ {filter} ไม่ถูกต้อง', + 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'ไฟล์รูปภาพ "{file}" มีขนาดใหญ่เกินไป ความสูงของรูปภาพต้องไม่เกิน {limit, number} พิกเซล', + 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'ไฟล์รูปภาพ "{file}" มีขนาดใหญ่เกินไป ความกว้างของรูปภาพต้องไม่เกิน {limit, number} พิกเซล', + 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'ไฟล์รูปภาพ "{file}" มีขนาดเล็กเกินไป ความสูงของรูปภาพต้องมากว่า {limit, number} พิกเซล', + 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'ไฟล์รูปภาพ "{file}" มีขนาดเล็กเกินไป ความกว้างของรูปภาพต้องมากกว่า {limit, number} พิกเซล', + 'The requested view "{name}" was not found.' => 'ไม่พบวิว "{name}" ที่เรียกใช้', + 'The verification code is incorrect.' => 'รหัสยืนยันไม่ถูกต้อง', + 'Total {count, number} {count, plural, one{item} other{items}}.' => 'ทั้งหมด {count, number} รายการ', + 'Unable to verify your data submission.' => 'ไม่สามารถตรวจสอบยืนยันข้อมูลที่คุณส่งได้', + 'Unknown alias: -{name}' => 'Alias ที่ไม่รู้จัก: -{name}', + 'Unknown filter attribute "{attribute}"' => 'แอตทริบิวต์ฟิลเตอร์ที่ไม่รู้จัก: "{attribute}"', + 'Unknown option: --{name}' => 'ตัวเลือกที่ไม่รู้จัก: --{name}', 'Update' => 'ปรับปรุง', 'View' => 'ดู', 'Yes' => 'ใช่', 'You are not allowed to perform this action.' => 'คุณไม่ได้รับอนุญาตให้ดำเนินการนี้', - 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'คุณสามารถอัพโหลดมากที่สุด {limit, number} ไฟล์', - 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.' => '', + 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'คุณสามารถอัพโหลดไฟล์ได้ไม่เกิน {limit, number} ไฟล์', + 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.' => 'คุณควรอัพโหลดไฟล์อย่างน้อย {limit, number} ไฟล์', 'in {delta, plural, =1{a day} other{# days}}' => 'ใน {delta} วัน', 'in {delta, plural, =1{a minute} other{# minutes}}' => 'ใน {delta} นาที', 'in {delta, plural, =1{a month} other{# months}}' => 'ใน {delta} เดือน', 'in {delta, plural, =1{a second} other{# seconds}}' => 'ใน {delta} วินาที', 'in {delta, plural, =1{a year} other{# years}}' => 'ใน {delta} ปี', 'in {delta, plural, =1{an hour} other{# hours}}' => 'ใน {delta} ชั่วโมง', - 'just now' => 'เมื่อสักครู่นี้', - 'the input value' => 'ค่าป้อนที่เข้ามา', + 'just now' => 'เมื่อสักครู่', + 'the input value' => 'ค่าที่ป้อน', '{attribute} "{value}" has already been taken.' => '{attribute} "{value}" ถูกใช้ไปแล้ว', '{attribute} cannot be blank.' => '{attribute}ต้องไม่ว่างเปล่า', '{attribute} contains wrong subnet mask.' => '{attribute}ไม่ใช่ซับเน็ตที่ถูกต้อง', @@ -92,8 +92,8 @@ return [ '{attribute} must be "{requiredValue}".' => '{attribute}ต้องการ "{requiredValue}"', '{attribute} must be a number.' => '{attribute}ต้องเป็นตัวเลขเท่านั้น', '{attribute} must be a string.' => '{attribute}ต้องเป็นตัวอักขระเท่านั้น', - '{attribute} must be a valid IP address.' => '{attribute}ต้องเป็นที่อยู่ไอพีที่ถูกต้อง', - '{attribute} must be an IP address with specified subnet.' => '{attribute}ต้องเป็นที่อยู่ไอพีกับซับเน็ตที่ระบุ', + '{attribute} must be a valid IP address.' => '{attribute}ต้องเป็นที่อยู่ IP ที่ถูกต้อง', + '{attribute} must be an IP address with specified subnet.' => '{attribute}ต้องเป็นที่อยู่ IP ตามซับเน็ตที่ระบุ', '{attribute} must be an integer.' => '{attribute}ต้องเป็นจำนวนเต็มเท่านั้น', '{attribute} must be either "{true}" or "{false}".' => '{attribute}ต้องเป็น "{true}" หรือ "{false}"', '{attribute} must be equal to "{compareValueOrAttribute}".' => '{attribute}ต้องเหมือนกับ "{compareValueOrAttribute}"', @@ -103,14 +103,14 @@ return [ '{attribute} must be less than or equal to "{compareValueOrAttribute}".' => '{attribute}ต้องน้อยกว่าหรือเท่ากับ "{compareValueOrAttribute}"', '{attribute} must be no greater than {max}.' => '{attribute}ต้องไม่มากกว่า {max}.', '{attribute} must be no less than {min}.' => '{attribute}ต้องไม่น้อยกว่า {min}', - '{attribute} must not be a subnet.' => '{attribute}ต้องไม่ใช่ซับเน็ต', - '{attribute} must not be an IPv4 address.' => '{attribute}ต้องไม่ใช่ที่อยู่ไอพีรุ่น 4', - '{attribute} must not be an IPv6 address.' => '{attribute}ต้องไม่ใช่ที่อยู่ไอพีรุ่น 6', + '{attribute} must not be a subnet.' => '{attribute}ต้องไม่เป็นซับเน็ต', + '{attribute} must not be an IPv4 address.' => '{attribute}ต้องไม่เป็นที่อยู่ IPv4', + '{attribute} must not be an IPv6 address.' => '{attribute}ต้องไม่เป็นที่อยู่ IPv6', '{attribute} must not be equal to "{compareValueOrAttribute}".' => '{attribute}ต้องมีค่าไม่เหมือนกับ "{compareValueOrAttribute}"', - '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute}ควรประกอบด้วยอักขระอย่างน้อย {min, number} อักขระ', - '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute}ควรประกอบด้วยอักขระอย่างมาก {max, number} อักขระ', - '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute}ควรประกอบด้วย {length, number} อักขระ', - '{compareAttribute} is invalid.' => '', + '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute}ควรประกอบด้วยอักขระอย่างน้อย {min, number} ตัว', + '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute}ควรประกอบด้วยอักขระไม่เกิน {max, number} ตัว', + '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute}ควรประกอบด้วยอักขระ {length, number} ตัว', + '{compareAttribute} is invalid.' => '{compareAttribute} ไม่ถูกต้อง', '{delta, plural, =1{1 day} other{# days}}' => '{delta} วัน', '{delta, plural, =1{1 hour} other{# hours}}' => '{delta} ชั่วโมง', '{delta, plural, =1{1 minute} other{# minutes}}' => '{delta} นาที', @@ -134,15 +134,15 @@ return [ '{nFormatted} TB' => '', '{nFormatted} TiB' => '', '{nFormatted} kB' => '', - '{nFormatted} {n, plural, =1{byte} other{bytes}}' => '', - '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '', - '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '', - '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}' => '', - '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}' => '', - '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}' => '', - '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}' => '', - '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}' => '', - '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '', - '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '', - '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '', + '{nFormatted} {n, plural, =1{byte} other{bytes}}' => '{nFormatted} ไบต์', + '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '{nFormatted} จิบิไบต์', + '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '{nFormatted} จิกะไบต์', + '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}' => '{nFormatted} กิบิไบต์', + '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}' => '{nFormatted} กิโลไบต์', + '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}' => '{nFormatted} เมบิไบต์', + '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}' => '{nFormatted} เมกะไบต์', + '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}' => '{nFormatted} เพบิไบต์', + '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} เพตะไบต์', + '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} เทบิไบต์', + '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} เทระไบต์', ]; diff --git a/framework/messages/zh-TW/yii.php b/framework/messages/zh-TW/yii.php index ff206fe66e..598b2b9de7 100644 --- a/framework/messages/zh-TW/yii.php +++ b/framework/messages/zh-TW/yii.php @@ -24,7 +24,7 @@ * NOTE: this file must be saved in UTF-8 encoding. */ return [ - ' and ' => '', + ' and ' => ' 與 ', '"{attribute}" does not support operator "{operator}".' => '', '(not set)' => '(未設定)', 'Action not found.' => '', @@ -51,7 +51,7 @@ return [ 'Please fix the following errors:' => '請修正以下錯誤:', 'Please upload a file.' => '請上傳一個檔案。', 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => '第 {begin, number}-{end, number} 項,共 {totalCount, number} 項資料.', - 'The combination {values} of {attributes} has already been taken.' => '', + 'The combination {values} of {attributes} has already been taken.' => '{attribute} 的值 "{value}" 已經被佔用了。', 'The file "{file}" is not an image.' => '檔案 "{file}" 不是一個圖片檔案。', 'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => '檔案"{file}"太大了。它的大小不可以超過{formattedLimit}。', 'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.' => '文件"{file}"太小了。它的大小不可以小於{formattedLimit}。', diff --git a/framework/mutex/DbMutex.php b/framework/mutex/DbMutex.php index 3bcebb4295..a19a1adf89 100644 --- a/framework/mutex/DbMutex.php +++ b/framework/mutex/DbMutex.php @@ -1,5 +1,4 @@ 'PHP version', 'mandatory' => true, - 'condition' => version_compare(PHP_VERSION, '5.4.0', '>='), + 'condition' => version_compare(PHP_VERSION, '7.3.0', '>='), 'by' => 'Yii Framework', - 'memo' => 'PHP 5.4.0 or higher is required.', + 'memo' => 'PHP 7.3.0 or higher is required.', ), array( 'name' => 'Reflection extension', diff --git a/framework/rest/Action.php b/framework/rest/Action.php index ca9accb27c..49cc4236f6 100644 --- a/framework/rest/Action.php +++ b/framework/rest/Action.php @@ -1,5 +1,4 @@ getPagination()) !== false) { - $this->addPaginationHeaders($pagination); - } - if ($this->request->getIsHead()) { - return null; - } if ($this->preserveKeys) { $models = $dataProvider->getModels(); } else { @@ -202,7 +195,13 @@ class Serializer extends Component } $models = $this->serializeModels($models); - if ($this->collectionEnvelope === null) { + if (($pagination = $dataProvider->getPagination()) !== false) { + $this->addPaginationHeaders($pagination); + } + + if ($this->request->getIsHead()) { + return null; + } elseif ($this->collectionEnvelope === null) { return $models; } diff --git a/framework/rest/UpdateAction.php b/framework/rest/UpdateAction.php index 98c2660223..c307144231 100644 --- a/framework/rest/UpdateAction.php +++ b/framework/rest/UpdateAction.php @@ -1,5 +1,4 @@ db->schema->insert($table->fullName, $row); $this->data[$alias] = array_merge($row, $primaryKeys); } + if ($table->sequenceName !== null) { + $this->db->createCommand()->executeResetSequence($table->fullName); + } } /** diff --git a/framework/test/ArrayFixture.php b/framework/test/ArrayFixture.php index faa59c7a53..c146ab34e3 100644 --- a/framework/test/ArrayFixture.php +++ b/framework/test/ArrayFixture.php @@ -1,5 +1,4 @@ db instanceof \yii\db\Connection) { return; } + + if ($this->db->getDriverName() === 'oci') { + return; + } + foreach ($this->schemas as $schema) { $this->db->createCommand()->checkIntegrity($check, $schema)->execute(); } diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 3f170d6f1a..ab29b755d9 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -1,5 +1,4 @@ maxFiles != 1 || $this->minFiles > 1) { - $rawFiles = $model->$attribute; - if (!is_array($rawFiles)) { - $this->addError($model, $attribute, $this->uploadRequired); + $files = $this->filterFiles(is_array($model->$attribute) ? $model->$attribute : [$model->$attribute]); + $filesCount = count($files); + if ($filesCount === 0) { + $this->addError($model, $attribute, $this->uploadRequired); - return; - } + return; + } - $files = $this->filterFiles($rawFiles); - $model->$attribute = $files; + if ($this->maxFiles > 0 && $filesCount > $this->maxFiles) { + $this->addError($model, $attribute, $this->tooMany, ['limit' => $this->maxFiles]); + } + if ($this->minFiles > 0 && $this->minFiles > $filesCount) { + $this->addError($model, $attribute, $this->tooFew, ['limit' => $this->minFiles]); + } - if (empty($files)) { - $this->addError($model, $attribute, $this->uploadRequired); - - return; - } - - $filesCount = count($files); - if ($this->maxFiles && $filesCount > $this->maxFiles) { - $this->addError($model, $attribute, $this->tooMany, ['limit' => $this->maxFiles]); - } - - if ($this->minFiles && $this->minFiles > $filesCount) { - $this->addError($model, $attribute, $this->tooFew, ['limit' => $this->minFiles]); - } - - foreach ($files as $file) { - $result = $this->validateValue($file); - if (!empty($result)) { - $this->addError($model, $attribute, $result[0], $result[1]); - } - } - } else { - $result = $this->validateValue($model->$attribute); + foreach ($files as $file) { + $result = $this->validateValue($file); if (!empty($result)) { $this->addError($model, $attribute, $result[0], $result[1]); } diff --git a/framework/validators/FilterValidator.php b/framework/validators/FilterValidator.php index 2d59f60684..a08b0b51d2 100644 --- a/framework/validators/FilterValidator.php +++ b/framework/validators/FilterValidator.php @@ -1,5 +1,4 @@ - */ class extends Migration { /** diff --git a/framework/web/Application.php b/framework/web/Application.php index a79565ed0f..b05ac8b1e1 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -1,5 +1,4 @@ * @since 2.0 */ @@ -130,11 +133,7 @@ class Controller extends \yii\base\Controller $name = $param->getName(); if (array_key_exists($name, $params)) { $isValid = true; - if (PHP_VERSION_ID >= 80000) { - $isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array'; - } else { - $isArray = $param->isArray(); - } + $isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array'; if ($isArray) { $params[$name] = (array)$params[$name]; } elseif (is_array($params[$name])) { diff --git a/framework/web/Cookie.php b/framework/web/Cookie.php index 6214c83048..d249afa6cb 100644 --- a/framework/web/Cookie.php +++ b/framework/web/Cookie.php @@ -1,5 +1,4 @@ * @since 2.0 diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php index 91ebc89135..b2e48b9ef5 100644 --- a/framework/web/DbSession.php +++ b/framework/web/DbSession.php @@ -1,5 +1,4 @@ db->createCommand() + return $this->db->createCommand() ->delete($this->sessionTable, '[[expire]]<:expire', [':expire' => time()]) ->execute(); - - return true; } /** diff --git a/framework/web/ErrorAction.php b/framework/web/ErrorAction.php index 7ac9b0cbe2..e59f3ab245 100644 --- a/framework/web/ErrorAction.php +++ b/framework/web/ErrorAction.php @@ -1,5 +1,4 @@ hasMethod($method)) { $reflectionMethod = $reflection->getMethod($method); diff --git a/framework/web/ForbiddenHttpException.php b/framework/web/ForbiddenHttpException.php index e04821d711..924f09e325 100644 --- a/framework/web/ForbiddenHttpException.php +++ b/framework/web/ForbiddenHttpException.php @@ -1,5 +1,4 @@ * @since 2.0 */ diff --git a/framework/web/HeadersAlreadySentException.php b/framework/web/HeadersAlreadySentException.php index 4da7e02946..5aaac82592 100644 --- a/framework/web/HeadersAlreadySentException.php +++ b/framework/web/HeadersAlreadySentException.php @@ -1,5 +1,4 @@ validateCsrfHeaderOnly) { + return null; + } + if ($this->_csrfToken === null || $regenerate) { $token = $this->loadCsrfToken(); if ($regenerate || empty($token)) { @@ -1815,11 +1850,11 @@ class Request extends \yii\base\Request } /** - * @return string|null the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent. + * @return string|null the CSRF token sent via [[csrfHeader]] by browser. Null is returned if no such header is sent. */ public function getCsrfTokenFromHeader() { - return $this->headers->get(static::CSRF_HEADER); + return $this->headers->get($this->csrfHeader); } /** @@ -1856,8 +1891,14 @@ class Request extends \yii\base\Request public function validateCsrfToken($clientSuppliedToken = null) { $method = $this->getMethod(); - // only validate CSRF token on non-"safe" methods https://tools.ietf.org/html/rfc2616#section-9.1.1 - if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) { + + if ($this->validateCsrfHeaderOnly) { + return in_array($method, $this->csrfHeaderUnsafeMethods, true) + ? $this->headers->has($this->csrfHeader) + : true; + } + + if (!$this->enableCsrfValidation || in_array($method, $this->csrfTokenSafeMethods, true)) { return true; } diff --git a/framework/web/RequestParserInterface.php b/framework/web/RequestParserInterface.php index cbc02ba731..44a1051a6d 100644 --- a/framework/web/RequestParserInterface.php +++ b/framework/web/RequestParserInterface.php @@ -1,5 +1,4 @@ registerSessionHandler(); - $this->setCookieParamsInternal(); + if ($this->getUseCookies() !== false) { + $this->setCookieParamsInternal(); + } YII_DEBUG ? session_start() : @session_start(); @@ -174,34 +177,23 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co static::$_originalSessionModule = $sessionModuleName; } + if ($this->handler === null && $this->getUseCustomStorage()) { + $this->handler = Yii::createObject( + [ + '__class' => SessionHandler::class, + '__construct()' => [$this], + ] + ); + } + if ($this->handler !== null) { - if (!is_object($this->handler)) { + if (is_array($this->handler)) { $this->handler = Yii::createObject($this->handler); } if (!$this->handler instanceof \SessionHandlerInterface) { throw new InvalidConfigException('"' . get_class($this) . '::handler" must implement the SessionHandlerInterface.'); } YII_DEBUG ? session_set_save_handler($this->handler, false) : @session_set_save_handler($this->handler, false); - } elseif ($this->getUseCustomStorage()) { - if (YII_DEBUG) { - session_set_save_handler( - [$this, 'openSession'], - [$this, 'closeSession'], - [$this, 'readSession'], - [$this, 'writeSession'], - [$this, 'destroySession'], - [$this, 'gcSession'] - ); - } else { - @session_set_save_handler( - [$this, 'openSession'], - [$this, 'closeSession'], - [$this, 'readSession'], - [$this, 'writeSession'], - [$this, 'destroySession'], - [$this, 'gcSession'] - ); - } } elseif ( $sessionModuleName !== static::$_originalSessionModule && static::$_originalSessionModule !== null @@ -268,7 +260,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co $request = Yii::$app->getRequest(); if (!empty($_COOKIE[$name]) && ini_get('session.use_cookies')) { $this->_hasSessionId = true; - } elseif (!ini_get('session.use_only_cookies') && ini_get('session.use_trans_sid')) { + } elseif (PHP_VERSION_ID < 80400 && !ini_get('session.use_only_cookies') && ini_get('session.use_trans_sid')) { $this->_hasSessionId = $request->get($name) != ''; } else { $this->_hasSessionId = false; @@ -447,7 +439,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co { if (ini_get('session.use_cookies') === '0') { return false; - } elseif (ini_get('session.use_only_cookies') === '1') { + } elseif (PHP_VERSION_ID >= 80400 || ini_get('session.use_only_cookies') === '1') { return true; } @@ -470,13 +462,19 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co $this->freeze(); if ($value === false) { ini_set('session.use_cookies', '0'); - ini_set('session.use_only_cookies', '0'); + if (PHP_VERSION_ID < 80400) { + ini_set('session.use_only_cookies', '0'); + } } elseif ($value === true) { ini_set('session.use_cookies', '1'); - ini_set('session.use_only_cookies', '1'); + if (PHP_VERSION_ID < 80400) { + ini_set('session.use_only_cookies', '1'); + } } else { ini_set('session.use_cookies', '1'); - ini_set('session.use_only_cookies', '0'); + if (PHP_VERSION_ID < 80400) { + ini_set('session.use_only_cookies', '0'); + } } $this->unfreeze(); } @@ -511,7 +509,10 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function getUseTransparentSessionID() { - return ini_get('session.use_trans_sid') == 1; + if (PHP_VERSION_ID < 80400) { + return ini_get('session.use_trans_sid') == 1; + } + return false; } /** @@ -520,7 +521,9 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co public function setUseTransparentSessionID($value) { $this->freeze(); - ini_set('session.use_trans_sid', $value ? '1' : '0'); + if (PHP_VERSION_ID < 80400) { + ini_set('session.use_trans_sid', $value ? '1' : '0'); + } $this->unfreeze(); } @@ -609,7 +612,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co * This method should be overridden if [[useCustomStorage]] returns true. * @internal Do not call this method directly. * @param string $id session ID - * @return string the session data + * @return string|false the session data, or false on failure */ public function readSession($id) { @@ -646,11 +649,11 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co * This method should be overridden if [[useCustomStorage]] returns true. * @internal Do not call this method directly. * @param int $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. - * @return bool whether session is GCed successfully + * @return int|false the number of deleted sessions on success, or false on failure */ public function gcSession($maxLifetime) { - return true; + return 0; } /** diff --git a/framework/web/SessionHandler.php b/framework/web/SessionHandler.php new file mode 100644 index 0000000000..bf79fea0b0 --- /dev/null +++ b/framework/web/SessionHandler.php @@ -0,0 +1,80 @@ + + * @since 2.0.52 + */ +class SessionHandler implements SessionHandlerInterface +{ + /** + * @var Session + */ + private $_session; + + + public function __construct(Session $session) + { + $this->_session = $session; + } + + /** + * @inheritDoc + */ + public function close(): bool + { + return $this->_session->closeSession(); + } + + /** + * @inheritDoc + */ + public function destroy($id): bool + { + return $this->_session->destroySession($id); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function gc($max_lifetime) + { + return $this->_session->gcSession($max_lifetime); + } + + /** + * @inheritDoc + */ + public function open($path, $name): bool + { + return $this->_session->openSession($path, $name); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function read($id) + { + return $this->_session->readSession($id); + } + + /** + * @inheritDoc + */ + public function write($id, $data): bool + { + return $this->_session->writeSession($id, $data); + } +} diff --git a/framework/web/SessionIterator.php b/framework/web/SessionIterator.php index 433ebd943a..e4b4c9eb80 100644 --- a/framework/web/SessionIterator.php +++ b/framework/web/SessionIterator.php @@ -1,5 +1,4 @@ mask) && empty($this->clientOptions['alias'])) { - throw new InvalidConfigException("Either the 'mask' property or the 'clientOptions[\"alias\"]' property must be set."); + if (empty($this->mask) && empty($this->clientOptions['regex']) && empty($this->clientOptions['alias'])) { + throw new InvalidConfigException("Either the 'mask' property, 'clientOptions[\"regex\"]' or the 'clientOptions[\"alias\"]' property must be set."); } } diff --git a/framework/widgets/MaskedInputAsset.php b/framework/widgets/MaskedInputAsset.php index dc7d15ff95..473f4315ff 100644 --- a/framework/widgets/MaskedInputAsset.php +++ b/framework/widgets/MaskedInputAsset.php @@ -1,5 +1,4 @@ */ class CustomerQuery extends ActiveQuery { diff --git a/tests/data/ar/CustomerWithAlias.php b/tests/data/ar/CustomerWithAlias.php index 2f622c973e..c74618808b 100644 --- a/tests/data/ar/CustomerWithAlias.php +++ b/tests/data/ar/CustomerWithAlias.php @@ -21,12 +21,12 @@ class CustomerWithAlias extends ActiveRecord public $status2; public $sumTotal; - + public static function tableName() { return 'customer'; } - + /** * {@inheritdoc} * @return CustomerQuery diff --git a/tests/data/config.php b/tests/data/config.php index 0b2bb53680..d14f035aa6 100644 --- a/tests/data/config.php +++ b/tests/data/config.php @@ -32,7 +32,7 @@ $config = [ 'fixture' => __DIR__ . '/sqlite.sql', ], 'sqlsrv' => [ - 'dsn' => 'sqlsrv:Server=127.0.0.1,1433;Database=yiitest', + 'dsn' => 'sqlsrv:Server=127.0.0.1,1433;Database=yiitest;Encrypt=no', 'username' => 'SA', 'password' => 'YourStrong!Passw0rd', 'fixture' => __DIR__ . '/mssql.sql', diff --git a/tests/data/validators/models/FakedValidationTypedModel.php b/tests/data/validators/models/FakedValidationTypedModel.php new file mode 100644 index 0000000000..39d5795cf7 --- /dev/null +++ b/tests/data/validators/models/FakedValidationTypedModel.php @@ -0,0 +1,18 @@ +createMock(Logger::class); - $logger->onlyMethods(['log']); + $logger->method('log'); BaseYii::setLogger($logger); diff --git a/tests/framework/ChangeLogTest.php b/tests/framework/ChangeLogTest.php index d3c2447df0..3d72b9e149 100644 --- a/tests/framework/ChangeLogTest.php +++ b/tests/framework/ChangeLogTest.php @@ -56,6 +56,9 @@ class ChangeLogTest extends TestCase * - Description ends without a "." * - Line ends with contributor name between "(" and ")". */ - $this->assertMatchesRegularExpression('/- (Bug|Enh|Chg|New)( #\d+(, #\d+)*)?(\s\(CVE-[\d-]+\))?: .*[^.] \(.+\)$/', $line); + $this->assertMatchesRegularExpression( + '/- (Bug|Enh|Chg|New)( #\d+(, #\d+)*)?(\s\(CVE-[\d-]+\))?: .*[^.] \(.+\)$/', + $line, + ); } } diff --git a/tests/framework/base/ComponentTest.php b/tests/framework/base/ComponentTest.php index 94f1617562..b8f955f535 100644 --- a/tests/framework/base/ComponentTest.php +++ b/tests/framework/base/ComponentTest.php @@ -10,6 +10,8 @@ namespace yiiunit\framework\base; use yii\base\Behavior; use yii\base\Component; use yii\base\Event; +use yii\base\InvalidConfigException; +use yii\base\UnknownMethodException; use yiiunit\TestCase; function globalEventHandler($event): void @@ -331,16 +333,45 @@ class ComponentTest extends TestCase $this->assertSame($behavior, $component->detachBehavior('a')); $this->assertFalse($component->hasProperty('p')); - $this->expectException('yii\base\UnknownMethodException'); - $component->test(); + try { + $component->test(); + $this->fail('Expected exception ' . UnknownMethodException::class . " wasn't thrown"); + } catch (UnknownMethodException $e) { + // Expected + } - $p = 'as b'; $component = new NewComponent(); - $component->$p = ['class' => 'NewBehavior']; - $this->assertSame($behavior, $component->getBehavior('a')); + $component->{'as b'} = ['class' => NewBehavior::class]; + $this->assertInstanceOf(NewBehavior::class, $component->getBehavior('b')); $this->assertTrue($component->hasProperty('p')); $component->test(); $this->assertTrue($component->behaviorCalled); + + $component->{'as c'} = ['__class' => NewBehavior::class]; + $this->assertNotNull($component->getBehavior('c')); + + $component->{'as d'} = [ + '__class' => NewBehavior2::class, + 'class' => NewBehavior::class, + ]; + $this->assertInstanceOf(NewBehavior2::class, $component->getBehavior('d')); + + // CVE-2024-4990 + try { + $component->{'as e'} = [ + '__class' => 'NotExistsBehavior', + 'class' => NewBehavior::class, + ]; + $this->fail('Expected exception ' . InvalidConfigException::class . " wasn't thrown"); + } catch (InvalidConfigException $e) { + $this->assertSame('Class is not of type yii\base\Behavior or its subclasses', $e->getMessage()); + } + + $component = new NewComponent(); + $component->{'as f'} = function () { + return new NewBehavior(); + }; + $this->assertNotNull($component->getBehavior('f')); } public function testAttachBehaviors(): void @@ -541,6 +572,10 @@ class NewBehavior extends Behavior } } +class NewBehavior2 extends Behavior +{ +} + class NewComponent2 extends Component { public $a; diff --git a/tests/framework/base/ErrorExceptionTest.php b/tests/framework/base/ErrorExceptionTest.php index 0a2d9b2b3b..493325a27a 100644 --- a/tests/framework/base/ErrorExceptionTest.php +++ b/tests/framework/base/ErrorExceptionTest.php @@ -41,4 +41,13 @@ class ErrorExceptionTest extends TestCase $this->assertEquals(__FUNCTION__, $e->getTrace()[0]['function']); } } + + public function testStrictError() + { + if (!defined('E_STRICT')) { + $this->markTestSkipped('E_STRICT has been removed.'); + } + $e = new ErrorException('', @E_STRICT); + $this->assertEquals(PHP_VERSION_ID < 80400 ? 'PHP Strict Warning' : 'Error', $e->getName()); + } } diff --git a/tests/framework/base/SecurityTest.php b/tests/framework/base/SecurityTest.php index 18c6eb6ec7..adf9dd5b94 100644 --- a/tests/framework/base/SecurityTest.php +++ b/tests/framework/base/SecurityTest.php @@ -844,7 +844,7 @@ TEXT; $this->assertIsString($key1); $this->assertEquals($length, strlen($key1)); $key2 = $this->security->generateRandomKey($length); - $this->assertIsString('string', $key2); + $this->assertIsString($key2); $this->assertEquals($length, strlen($key2)); $this->assertNotEquals($key1, $key2); } diff --git a/tests/framework/base/WidgetTest.php b/tests/framework/base/WidgetTest.php index ef296c165d..2434050a5b 100644 --- a/tests/framework/base/WidgetTest.php +++ b/tests/framework/base/WidgetTest.php @@ -72,6 +72,27 @@ class WidgetTest extends TestCase $this->assertSame('', $output); } + public function testDependencyInjectionWithCallableConfiguration() + { + Yii::$container = new Container(); + Yii::$container->setDefinitions([ + TestWidgetB::className() => function () { + return new TestWidget(['id' => 'test']); + } + ]); + + ob_start(); + ob_implicit_flush(false); + + $widget = TestWidgetB::begin(['id' => 'test']); + $this->assertTrue($widget instanceof TestWidget); + TestWidgetB::end(); + + $output = ob_get_clean(); + + $this->assertSame('', $output); + } + /** * @depends testBeginEnd */ diff --git a/tests/framework/behaviors/AttributeBehaviorTest.php b/tests/framework/behaviors/AttributeBehaviorTest.php index d3c4148b67..ae5a32ab5f 100644 --- a/tests/framework/behaviors/AttributeBehaviorTest.php +++ b/tests/framework/behaviors/AttributeBehaviorTest.php @@ -33,7 +33,7 @@ class AttributeBehaviorTest extends TestCase } } - public function setUp(): void + protected function setUp(): void { $this->mockApplication([ 'components' => [ @@ -52,7 +52,7 @@ class AttributeBehaviorTest extends TestCase Yii::$app->getDb()->createCommand()->createTable('test_attribute', $columns)->execute(); } - public function tearDown(): void + protected function tearDown(): void { Yii::$app->getDb()->close(); parent::tearDown(); diff --git a/tests/framework/behaviors/AttributeTypecastBehaviorTest.php b/tests/framework/behaviors/AttributeTypecastBehaviorTest.php index 418d6ba472..e387ed6149 100644 --- a/tests/framework/behaviors/AttributeTypecastBehaviorTest.php +++ b/tests/framework/behaviors/AttributeTypecastBehaviorTest.php @@ -7,11 +7,13 @@ namespace yiiunit\framework\behaviors; +use ValueError; use Yii; use yii\base\DynamicModel; use yii\base\Event; use yii\behaviors\AttributeTypecastBehavior; use yii\db\ActiveRecord; +use yiiunit\framework\db\enums\StatusTypeString; use yiiunit\TestCase; /** @@ -47,6 +49,7 @@ class AttributeTypecastBehaviorTest extends TestCase 'price' => 'float', 'isActive' => 'boolean', 'callback' => 'string', + 'status' => 'string', ]; Yii::$app->getDb()->createCommand()->createTable('test_attribute_typecast', $columns)->execute(); } @@ -80,6 +83,55 @@ class AttributeTypecastBehaviorTest extends TestCase $this->assertSame('callback: foo', $model->callback); } + public function testTypecastEnum() + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Can not be tested on PHP < 8.1'); + } + + $model = new ActiveRecordAttributeTypecastWithEnum(); + + $model->status = StatusTypeString::Active; + + $model->getAttributeTypecastBehavior()->typecastAttributes(); + + $this->assertSame(StatusTypeString::Active, $model->status); + } + + /** + * @depends testTypecastEnum + */ + public function testTypecastEnumFromString() + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Can not be tested on PHP < 8.1'); + } + + $model = new ActiveRecordAttributeTypecastWithEnum(); + $model->status = 'active'; // Same as StatusTypeString::ACTIVE->value; + + $model->getAttributeTypecastBehavior()->typecastAttributes(); + + $this->assertSame(StatusTypeString::Active, $model->status); + } + + /** + * @depends testTypecastEnum + */ + public function testTypecastEnumFailWithInvalidValue() + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Can not be tested on PHP < 8.1'); + } + + $model = new ActiveRecordAttributeTypecastWithEnum(); + $model->status = 'invalid'; + + self::expectException(ValueError::class); + + $model->getAttributeTypecastBehavior()->typecastAttributes(); + } + /** * @depends testTypecast */ @@ -337,3 +389,37 @@ class ActiveRecordAttributeTypecast extends ActiveRecord return $this->getBehavior('attributeTypecast'); } } + +/** + * Test Active Record class with [[AttributeTypecastBehavior]] behavior attached with an enum field. + * + * @property StatusTypeString $status + */ +class ActiveRecordAttributeTypecastWithEnum extends ActiveRecord +{ + public function behaviors() + { + return [ + 'attributeTypecast' => [ + 'class' => AttributeTypecastBehavior::className(), + 'attributeTypes' => [ + 'status' => StatusTypeString::class, + ], + 'typecastBeforeSave' => true, + ], + ]; + } + + public static function tableName() + { + return 'test_attribute_typecast'; + } + + /** + * @return AttributeTypecastBehavior + */ + public function getAttributeTypecastBehavior() + { + return $this->getBehavior('attributeTypecast'); + } +} diff --git a/tests/framework/behaviors/AttributesBehaviorTest.php b/tests/framework/behaviors/AttributesBehaviorTest.php index b05ca709ae..1fee342fa1 100644 --- a/tests/framework/behaviors/AttributesBehaviorTest.php +++ b/tests/framework/behaviors/AttributesBehaviorTest.php @@ -33,7 +33,7 @@ class AttributesBehaviorTest extends TestCase } } - public function setUp(): void + protected function setUp(): void { $this->mockApplication([ 'components' => [ @@ -52,7 +52,7 @@ class AttributesBehaviorTest extends TestCase Yii::$app->getDb()->createCommand()->createTable('test_attribute', $columns)->execute(); } - public function tearDown(): void + protected function tearDown(): void { Yii::$app->getDb()->close(); parent::tearDown(); diff --git a/tests/framework/behaviors/BlameableBehaviorConsoleTest.php b/tests/framework/behaviors/BlameableBehaviorConsoleTest.php index 39941416c2..7b1175cdf2 100755 --- a/tests/framework/behaviors/BlameableBehaviorConsoleTest.php +++ b/tests/framework/behaviors/BlameableBehaviorConsoleTest.php @@ -28,7 +28,7 @@ class BlameableBehaviorConsoleTest extends TestCase } } - public function setUp(): void + protected function setUp(): void { $this->mockApplication([ 'components' => [ @@ -47,7 +47,7 @@ class BlameableBehaviorConsoleTest extends TestCase Yii::$app->getDb()->createCommand()->createTable('test_blame', $columns)->execute(); } - public function tearDown(): void + protected function tearDown(): void { Yii::$app->getDb()->close(); parent::tearDown(); diff --git a/tests/framework/behaviors/BlameableBehaviorTest.php b/tests/framework/behaviors/BlameableBehaviorTest.php index 1033c539f4..2558358528 100644 --- a/tests/framework/behaviors/BlameableBehaviorTest.php +++ b/tests/framework/behaviors/BlameableBehaviorTest.php @@ -28,7 +28,7 @@ class BlameableBehaviorTest extends TestCase } } - public function setUp(): void + protected function setUp(): void { $this->mockApplication([ 'components' => [ @@ -52,7 +52,7 @@ class BlameableBehaviorTest extends TestCase $this->getUser()->login(10); } - public function tearDown(): void + protected function tearDown(): void { Yii::$app->getDb()->close(); parent::tearDown(); diff --git a/tests/framework/behaviors/OptimisticLockBehaviorTest.php b/tests/framework/behaviors/OptimisticLockBehaviorTest.php index 84a8af6b6e..8d319d9a62 100644 --- a/tests/framework/behaviors/OptimisticLockBehaviorTest.php +++ b/tests/framework/behaviors/OptimisticLockBehaviorTest.php @@ -36,7 +36,7 @@ class OptimisticLockBehaviorTest extends TestCase } } - public function setUp(): void + protected function setUp(): void { $this->mockApplication([ 'components' => [ @@ -60,7 +60,7 @@ class OptimisticLockBehaviorTest extends TestCase Yii::$app->getDb()->createCommand()->createTable('test_auto_lock_version_string', $columns)->execute(); } - public function tearDown(): void + protected function tearDown(): void { Yii::$app->getDb()->close(); parent::tearDown(); diff --git a/tests/framework/behaviors/SluggableBehaviorTest.php b/tests/framework/behaviors/SluggableBehaviorTest.php index bfe2e1a5e4..59896cebd5 100644 --- a/tests/framework/behaviors/SluggableBehaviorTest.php +++ b/tests/framework/behaviors/SluggableBehaviorTest.php @@ -33,7 +33,7 @@ class SluggableBehaviorTest extends TestCase } } - public function setUp(): void + protected function setUp(): void { $this->mockApplication([ 'components' => [ @@ -60,7 +60,7 @@ class SluggableBehaviorTest extends TestCase Yii::$app->getDb()->createCommand()->createTable('test_slug_related', $columns)->execute(); } - public function tearDown(): void + protected function tearDown(): void { Yii::$app->getDb()->close(); parent::tearDown(); diff --git a/tests/framework/behaviors/TimestampBehaviorTest.php b/tests/framework/behaviors/TimestampBehaviorTest.php index a0c382adbe..b6fc01bf84 100644 --- a/tests/framework/behaviors/TimestampBehaviorTest.php +++ b/tests/framework/behaviors/TimestampBehaviorTest.php @@ -35,7 +35,7 @@ class TimestampBehaviorTest extends TestCase } } - public function setUp(): void + protected function setUp(): void { $this->mockApplication([ 'components' => [ @@ -61,7 +61,7 @@ class TimestampBehaviorTest extends TestCase Yii::$app->getDb()->createCommand()->createTable('test_auto_timestamp_string', $columns)->execute(); } - public function tearDown(): void + protected function tearDown(): void { Yii::$app->getDb()->close(); parent::tearDown(); diff --git a/tests/framework/caching/CallbackDependencyTest.php b/tests/framework/caching/CallbackDependencyTest.php new file mode 100644 index 0000000000..b2a93ff954 --- /dev/null +++ b/tests/framework/caching/CallbackDependencyTest.php @@ -0,0 +1,41 @@ +callback = function () use (&$dependencyValue) { + return $dependencyValue === true; + }; + + $dependency->evaluateDependency($cache); + $this->assertFalse($dependency->isChanged($cache)); + + $dependencyValue = false; + $this->assertTrue($dependency->isChanged($cache)); + } + + public function testDependencyNotChanged() + { + $cache = new ArrayCache(); + + $dependency = new CallbackDependency(); + $dependency->callback = function () { + return 2 + 2; + }; + + $dependency->evaluateDependency($cache); + $this->assertFalse($dependency->isChanged($cache)); + $this->assertFalse($dependency->isChanged($cache)); + } +} diff --git a/tests/framework/caching/FileCacheTest.php b/tests/framework/caching/FileCacheTest.php index b56483b7b8..815cb86ab4 100644 --- a/tests/framework/caching/FileCacheTest.php +++ b/tests/framework/caching/FileCacheTest.php @@ -83,38 +83,23 @@ class FileCacheTest extends CacheTestCase $this->assertEquals($value, $refMethodGet->invoke($cache, $key)); } - public function testCacheRenewalOnDifferentOwnership(): void + public function testStatCache(): void { - $TRAVIS_SECOND_USER = getenv('TRAVIS_SECOND_USER'); - if (empty($TRAVIS_SECOND_USER)) { - $this->markTestSkipped('Travis second user not found'); - } - $cache = $this->getCacheInstance(); + $cache->set(__FUNCTION__, 'cache1', 2); - $cacheValue = uniqid('value_'); - $cachePublicKey = uniqid('key_'); - $cacheInternalKey = $cache->buildKey($cachePublicKey); - - static::$time = \time(); - $this->assertTrue($cache->set($cachePublicKey, $cacheValue, 2)); - $this->assertSame($cacheValue, $cache->get($cachePublicKey)); - + $normalizeKey = $cache->buildKey(__FUNCTION__); $refClass = new \ReflectionClass($cache); $refMethodGetCacheFile = $refClass->getMethod('getCacheFile'); $refMethodGetCacheFile->setAccessible(true); - $cacheFile = $refMethodGetCacheFile->invoke($cache, $cacheInternalKey); - $refMethodGetCacheFile->setAccessible(false); + $cacheFile = $refMethodGetCacheFile->invoke($cache, $normalizeKey); - $output = []; - $returnVar = null; - exec(sprintf('sudo chown %s %s', - escapeshellarg($TRAVIS_SECOND_USER), - escapeshellarg((string) $cacheFile) - ), $output, $returnVar); + // simulate cache expire 10 seconds ago + touch($cacheFile, time() - 10); + clearstatcache(); - $this->assertSame(0, $returnVar, 'Cannot change ownership of cache file to test cache renewal'); - - $this->assertTrue($cache->set($cachePublicKey, uniqid('value_2_'), 2), 'Cannot rebuild cache on different file ownership'); + $this->assertFalse($cache->get(__FUNCTION__)); + $this->assertTrue($cache->set(__FUNCTION__, 'cache2', 2)); + $this->assertSame('cache2', $cache->get(__FUNCTION__)); } } diff --git a/tests/framework/console/ControllerTest.php b/tests/framework/console/ControllerTest.php index ad06abd3f1..1f27a5bacc 100644 --- a/tests/framework/console/ControllerTest.php +++ b/tests/framework/console/ControllerTest.php @@ -91,6 +91,12 @@ class ControllerTest extends TestCase $this->assertEquals('from params', $fromParam); $this->assertEquals('notdefault', $other); + $params = ['a', 'b', 'c1', 'c2', 'c3']; + [$a, $b, $c] = $controller->run('variadic', $params); + $this->assertEquals('a', $a); + $this->assertEquals('b', $b); + $this->assertEquals(['c1', 'c2', 'c3'], $c); + $params = ['avaliable']; $message = Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', ['missing'])]); $this->expectException(Exception::class); diff --git a/tests/framework/console/FakeController.php b/tests/framework/console/FakeController.php index 3bbc9f8a8b..671f1e6ace 100644 --- a/tests/framework/console/FakeController.php +++ b/tests/framework/console/FakeController.php @@ -104,4 +104,9 @@ class FakeController extends Controller $response->exitStatus = (int) $status; return $response; } + + public function actionVariadic($foo, $bar, ...$baz) + { + return [$foo, $bar, $baz]; + } } diff --git a/tests/framework/console/FakePhp71Controller.php b/tests/framework/console/FakePhp71Controller.php index cee8162ac2..1f4806fd5a 100644 --- a/tests/framework/console/FakePhp71Controller.php +++ b/tests/framework/console/FakePhp71Controller.php @@ -19,9 +19,9 @@ class FakePhp71Controller extends Controller Request $request, $between, DummyService $dummyService, - Post $post = null, + ?Post $post, $after - ): void { + ) { } public function actionNullableInjection(?Request $request, ?Post $post): void diff --git a/tests/framework/console/UnknownCommandExceptionTest.php b/tests/framework/console/UnknownCommandExceptionTest.php index 33a5b8dc16..c4e85043b6 100644 --- a/tests/framework/console/UnknownCommandExceptionTest.php +++ b/tests/framework/console/UnknownCommandExceptionTest.php @@ -16,7 +16,7 @@ use yiiunit\TestCase; */ class UnknownCommandExceptionTest extends TestCase { - public function setUp(): void + protected function setUp(): void { $this->mockApplication([ 'enableCoreCommands' => false, diff --git a/tests/framework/console/controllers/AssetControllerTest.php b/tests/framework/console/controllers/AssetControllerTest.php index ea1f9fc364..c8da2022f4 100644 --- a/tests/framework/console/controllers/AssetControllerTest.php +++ b/tests/framework/console/controllers/AssetControllerTest.php @@ -35,7 +35,7 @@ class AssetControllerTest extends TestCase */ protected string $testAssetsBasePath = ''; - public function setUp(): void + protected function setUp(): void { $this->mockApplication(); $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . str_replace('\\', '_', static::class) . uniqid(); @@ -44,7 +44,7 @@ class AssetControllerTest extends TestCase $this->createDir($this->testAssetsBasePath); } - public function tearDown(): void + protected function tearDown(): void { $this->removeDir($this->testFilePath); } diff --git a/tests/framework/console/controllers/BaseMessageControllerTest.php b/tests/framework/console/controllers/BaseMessageControllerTest.php index 61a72d6a29..49a3b79866 100644 --- a/tests/framework/console/controllers/BaseMessageControllerTest.php +++ b/tests/framework/console/controllers/BaseMessageControllerTest.php @@ -25,7 +25,7 @@ abstract class BaseMessageControllerTest extends TestCase protected $configFileName = ''; protected $language = 'en'; - public function setUp(): void + protected function setUp(): void { $this->mockApplication(); $this->sourcePath = Yii::getAlias('@yiiunit/runtime/test_source'); @@ -49,7 +49,7 @@ abstract class BaseMessageControllerTest extends TestCase return $this->configFileName; } - public function tearDown(): void + protected function tearDown(): void { FileHelper::removeDirectory($this->sourcePath); if (file_exists($this->configFileName)) { diff --git a/tests/framework/console/controllers/CacheControllerTest.php b/tests/framework/console/controllers/CacheControllerTest.php index fe7b39de3b..a629fc7b7c 100644 --- a/tests/framework/console/controllers/CacheControllerTest.php +++ b/tests/framework/console/controllers/CacheControllerTest.php @@ -138,6 +138,7 @@ class CacheControllerTest extends TestCase public function testNothingToFlushException(): void { $this->expectException(\yii\console\Exception::class); + $this->expectExceptionMessage('You should specify cache components names'); $this->_cacheController->actionFlush(); } diff --git a/tests/framework/console/controllers/DbMessageControllerTest.php b/tests/framework/console/controllers/DbMessageControllerTest.php index 53216f44a1..ad835a8a39 100644 --- a/tests/framework/console/controllers/DbMessageControllerTest.php +++ b/tests/framework/console/controllers/DbMessageControllerTest.php @@ -76,7 +76,7 @@ class DbMessageControllerTest extends BaseMessageControllerTest parent::tearDownAfterClass(); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); Yii::$app = null; diff --git a/tests/framework/console/controllers/HelpControllerTest.php b/tests/framework/console/controllers/HelpControllerTest.php index 5db6ec0ed2..8dd24e14c6 100644 --- a/tests/framework/console/controllers/HelpControllerTest.php +++ b/tests/framework/console/controllers/HelpControllerTest.php @@ -22,7 +22,7 @@ class HelpControllerTest extends TestCase /** * {@inheritdoc} */ - public function setUp(): void + protected function setUp(): void { $this->mockApplication(); } diff --git a/tests/framework/console/controllers/MigrateControllerTest.php b/tests/framework/console/controllers/MigrateControllerTest.php index 6ff0a3b9cd..16b9406be9 100644 --- a/tests/framework/console/controllers/MigrateControllerTest.php +++ b/tests/framework/console/controllers/MigrateControllerTest.php @@ -26,7 +26,7 @@ class MigrateControllerTest extends TestCase { use MigrateControllerTestTrait; - public function setUp(): void + protected function setUp(): void { $this->migrateControllerClass = EchoMigrateController::class; $this->migrationBaseClass = Migration::class; @@ -44,7 +44,7 @@ class MigrateControllerTest extends TestCase parent::setUp(); } - public function tearDown(): void + protected function tearDown(): void { $this->tearDownMigrationPath(); parent::tearDown(); diff --git a/tests/framework/console/controllers/PHPMessageControllerTest.php b/tests/framework/console/controllers/PHPMessageControllerTest.php index 657468632e..db98082dec 100644 --- a/tests/framework/console/controllers/PHPMessageControllerTest.php +++ b/tests/framework/console/controllers/PHPMessageControllerTest.php @@ -18,14 +18,14 @@ class PHPMessageControllerTest extends BaseMessageControllerTest { protected $messagePath; - public function setUp(): void + protected function setUp(): void { parent::setUp(); $this->messagePath = Yii::getAlias('@yiiunit/runtime/test_messages'); FileHelper::createDirectory($this->messagePath, 0777); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); FileHelper::removeDirectory($this->messagePath); diff --git a/tests/framework/console/controllers/POMessageControllerTest.php b/tests/framework/console/controllers/POMessageControllerTest.php index 321631818f..25d165406b 100644 --- a/tests/framework/console/controllers/POMessageControllerTest.php +++ b/tests/framework/console/controllers/POMessageControllerTest.php @@ -19,7 +19,7 @@ class POMessageControllerTest extends BaseMessageControllerTest protected $messagePath; protected $catalog = 'messages'; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -27,7 +27,7 @@ class POMessageControllerTest extends BaseMessageControllerTest FileHelper::createDirectory($this->messagePath, 0777); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); FileHelper::removeDirectory($this->messagePath); diff --git a/tests/framework/console/controllers/ServeControllerTest.php b/tests/framework/console/controllers/ServeControllerTest.php index 28c11e5405..2032c7dd34 100644 --- a/tests/framework/console/controllers/ServeControllerTest.php +++ b/tests/framework/console/controllers/ServeControllerTest.php @@ -19,7 +19,7 @@ use yiiunit\TestCase; */ class ServeControllerTest extends TestCase { - public function setUp(): void + protected function setUp(): void { $this->mockApplication(); } diff --git a/tests/framework/console/widgets/TableTest.php b/tests/framework/console/widgets/TableTest.php index 919c9005a1..48b53e9d89 100644 --- a/tests/framework/console/widgets/TableTest.php +++ b/tests/framework/console/widgets/TableTest.php @@ -507,7 +507,7 @@ EXPECTED; ->setScreenWidth(200) ->run(); - $columnWidths = \PHPUnit_Framework_Assert::readAttribute($table, "columnWidths"); + $columnWidths = $this->getInaccessibleProperty($table, 'columnWidths'); $this->assertArrayHasKey(1, $columnWidths); $this->assertEquals(4+2, $columnWidths[1]); diff --git a/tests/framework/db/ActiveQueryTest.php b/tests/framework/db/ActiveQueryTest.php index da9452ca10..69d370eccb 100644 --- a/tests/framework/db/ActiveQueryTest.php +++ b/tests/framework/db/ActiveQueryTest.php @@ -22,7 +22,7 @@ use yiiunit\data\ar\Profile; */ abstract class ActiveQueryTest extends DatabaseTestCase { - public function setUp(): void + protected function setUp(): void { parent::setUp(); ActiveRecord::$db = $this->getConnection(); diff --git a/tests/framework/db/ActiveRecordTest.php b/tests/framework/db/ActiveRecordTest.php index 4c922ea2eb..239142951c 100644 --- a/tests/framework/db/ActiveRecordTest.php +++ b/tests/framework/db/ActiveRecordTest.php @@ -1424,10 +1424,7 @@ abstract class ActiveRecordTest extends DatabaseTestCase $this->assertEquals(5, $itemClass::find()->count()); } - /** - * @requires PHP 5.6 - */ - public function testCastValues(): void + public function testCastValues() { $model = new Type(); $model->int_col = 123; @@ -1450,10 +1447,10 @@ abstract class ActiveRecordTest extends DatabaseTestCase $this->assertSame('1337', trim((string) $model->char_col)); $this->assertSame('test', $model->char_col2); $this->assertSame('test123', $model->char_col3); - // $this->assertSame(1337.42, $model->float_col); - // $this->assertSame(42.1337, $model->float_col2); - // $this->assertSame(true, $model->bool_col); - // $this->assertSame(false, $model->bool_col2); + $this->assertSame(3.742, $model->float_col); + $this->assertSame(42.1337, $model->float_col2); + $this->assertSame(true, $model->bool_col); + $this->assertSame(false, $model->bool_col2); } public function testIssues(): void @@ -1985,7 +1982,6 @@ abstract class ActiveRecordTest extends DatabaseTestCase { $this->expectException(\yii\base\InvalidArgumentException::class); $this->expectExceptionMessageMatches('/^Key "(.+)?" is not a column name and can not be used as a filter$/'); - /** @var Query $query */ $query = $this->invokeMethod(\Yii::createObject($modelClassName), 'findByCondition', $filterWithInjection); Customer::getDb()->queryBuilder->build($query); @@ -2101,16 +2097,7 @@ abstract class ActiveRecordTest extends DatabaseTestCase $this->assertEquals(1, sizeof($order->orderItems)); } - public function testIssetException(): void - { - $cat = new Cat(); - $this->assertFalse(isset($cat->exception)); - } - - /** - * @requires PHP 7 - */ - public function testIssetThrowable(): void + public function testIssetThrowable() { $cat = new Cat(); $this->assertFalse(isset($cat->throwable)); diff --git a/tests/framework/db/BaseActiveRecordTest.php b/tests/framework/db/BaseActiveRecordTest.php index 1ef9013aa7..a06fc68d99 100644 --- a/tests/framework/db/BaseActiveRecordTest.php +++ b/tests/framework/db/BaseActiveRecordTest.php @@ -28,6 +28,10 @@ abstract class BaseActiveRecordTest extends DatabaseTestCase ['pineapple' => 2, 'apple' => 5, 'banana' => 1], ['pineapple' => 2, 'apple' => 3, 'banana' => 1], ], + 'multi-dimensional array' => [ + ['foo' => ['c', 'b', 'a']], + ['foo' => ['b', 'c', 'a']], + ], 'filling an empty array' => [ [], diff --git a/tests/framework/db/BatchQueryResultTest.php b/tests/framework/db/BatchQueryResultTest.php index 65a971eefe..1e87b88e56 100644 --- a/tests/framework/db/BatchQueryResultTest.php +++ b/tests/framework/db/BatchQueryResultTest.php @@ -14,7 +14,7 @@ use yiiunit\data\ar\Customer; abstract class BatchQueryResultTest extends DatabaseTestCase { - public function setUp(): void + protected function setUp(): void { parent::setUp(); ActiveRecord::$db = $this->getConnection(); diff --git a/tests/framework/db/CommandTest.php b/tests/framework/db/CommandTest.php index 691df0ddac..7dd820b78f 100644 --- a/tests/framework/db/CommandTest.php +++ b/tests/framework/db/CommandTest.php @@ -1226,9 +1226,13 @@ SQL; public function testAddDropCheck(): void { $db = $this->getConnection(false); + + if (version_compare($db->getServerVersion(), '8.0.16', '<')) { + $this->markTestSkipped('MySQL < 8.0.16 does not support CHECK constraints.'); + } + $tableName = 'test_ck'; $name = 'test_ck_constraint'; - /** @var \yii\db\pgsql\Schema $schema */ $schema = $db->getSchema(); if ($schema->getTableSchema($tableName) !== null) { @@ -1243,6 +1247,7 @@ SQL; $this->assertMatchesRegularExpression('/^.*int1.*>.*1.*$/', $schema->getTableChecks($tableName, true)[0]->expression); $db->createCommand()->dropCheck($name, $tableName)->execute(); + $this->assertEmpty($schema->getTableChecks($tableName, true)); } @@ -1550,15 +1555,15 @@ SQL; $db = $this->getConnection(); $command = $db->createCommand(); - $command->setSql('SELECT :p1')->bindValues([':p1' => enums\Status::ACTIVE]); - $this->assertSame('ACTIVE', $command->params[':p1']); + $command->setSql('SELECT :p1')->bindValues([':p1' => enums\Status::Active]); + $this->assertSame('Active', $command->params[':p1']); - $command->setSql('SELECT :p1')->bindValues([':p1' => enums\StatusTypeString::ACTIVE]); - $this->assertSame('active', $command->params[':p1']); + $command->setSql('SELECT :p1')->bindValues([':p1' => enums\StatusTypeString::Active]); + $this->assertSame('active', $command->params[':p1']); - $command->setSql('SELECT :p1')->bindValues([':p1' => enums\StatusTypeInt::ACTIVE]); - $this->assertSame(1, $command->params[':p1']); - } else { + $command->setSql('SELECT :p1')->bindValues([':p1' => enums\StatusTypeInt::Active]); + $this->assertSame(1, $command->params[':p1']); + } else { $this->markTestSkipped('Enums are not supported in PHP < 8.1'); } } diff --git a/tests/framework/db/ConnectionTest.php b/tests/framework/db/ConnectionTest.php index 190fe039c0..a0f3562602 100644 --- a/tests/framework/db/ConnectionTest.php +++ b/tests/framework/db/ConnectionTest.php @@ -235,7 +235,9 @@ abstract class ConnectionTest extends DatabaseTestCase throw new \Exception('Exception in transaction shortcut'); }); - $profilesCount = $connection->createCommand("SELECT COUNT(*) FROM profile WHERE description = 'test transaction shortcut';")->queryScalar(); + $profilesCount = $connection + ->createCommand("SELECT COUNT(*) FROM profile WHERE description = 'test transaction shortcut';") + ->queryScalar(); $this->assertEquals(0, $profilesCount, 'profile should not be inserted in transaction shortcut'); } diff --git a/tests/framework/db/enums/Status.php b/tests/framework/db/enums/Status.php index 799a552319..13235e730b 100644 --- a/tests/framework/db/enums/Status.php +++ b/tests/framework/db/enums/Status.php @@ -4,6 +4,6 @@ namespace yiiunit\framework\db\enums; enum Status { - case ACTIVE; - case INACTIVE; + case Active; + case Inactive; } diff --git a/tests/framework/db/enums/StatusTypeInt.php b/tests/framework/db/enums/StatusTypeInt.php index ed4739c022..7a8d539c52 100644 --- a/tests/framework/db/enums/StatusTypeInt.php +++ b/tests/framework/db/enums/StatusTypeInt.php @@ -4,6 +4,6 @@ namespace yiiunit\framework\db\enums; enum StatusTypeInt: int { - case ACTIVE = 1; - case INACTIVE = 0; + case Active = 1; + case Inactive = 0; } diff --git a/tests/framework/db/enums/StatusTypeString.php b/tests/framework/db/enums/StatusTypeString.php index 019f4273a7..9f8f2af997 100644 --- a/tests/framework/db/enums/StatusTypeString.php +++ b/tests/framework/db/enums/StatusTypeString.php @@ -4,6 +4,6 @@ namespace yiiunit\framework\db\enums; enum StatusTypeString: string { - case ACTIVE = 'active'; - case INACTIVE = 'inactive'; + case Active = 'active'; + case Inactive = 'inactive'; } diff --git a/tests/framework/db/mssql/ActiveRecordTest.php b/tests/framework/db/mssql/ActiveRecordTest.php index 5f6d07c77e..21a2f6b70d 100644 --- a/tests/framework/db/mssql/ActiveRecordTest.php +++ b/tests/framework/db/mssql/ActiveRecordTest.php @@ -11,6 +11,7 @@ use yii\db\Exception; use yii\db\Expression; use yiiunit\data\ar\TestTrigger; use yiiunit\data\ar\TestTriggerAlert; +use yiiunit\data\ar\Type; /** * @group db @@ -26,6 +27,35 @@ class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest $this->markTestSkipped('MSSQL does not support explicit value for an IDENTITY column.'); } + public function testCastValues() + { + $model = new Type(); + $model->int_col = 123; + $model->int_col2 = 456; + $model->smallint_col = 42; + $model->char_col = '1337'; + $model->char_col2 = 'test'; + $model->char_col3 = 'test123'; + $model->float_col = 3.742; + $model->float_col2 = 42.1337; + $model->bool_col = true; + $model->bool_col2 = false; + $model->save(false); + + /* @var $model Type */ + $model = Type::find()->one(); + $this->assertSame(123, $model->int_col); + $this->assertSame(456, $model->int_col2); + $this->assertSame(42, $model->smallint_col); + $this->assertSame('1337', trim((string) $model->char_col)); + $this->assertSame('test', $model->char_col2); + $this->assertSame('test123', $model->char_col3); + //$this->assertSame(3.742, $model->float_col); + //$this->assertSame(42.1337, $model->float_col2); + //$this->assertSame(true, $model->bool_col); + //$this->assertSame(false, $model->bool_col2); + } + /** * @throws Exception */ diff --git a/tests/framework/db/mssql/CommandTest.php b/tests/framework/db/mssql/CommandTest.php index 54c23afe0c..afc62230c0 100644 --- a/tests/framework/db/mssql/CommandTest.php +++ b/tests/framework/db/mssql/CommandTest.php @@ -69,6 +69,7 @@ class CommandTest extends \yiiunit\framework\db\CommandTest $sql = 'SELECT int_col, char_col, float_col, CONVERT([nvarchar], blob_col) AS blob_col, numeric_col FROM type'; $row = $db->createCommand($sql)->queryOne(); + $this->assertEquals($intCol, $row['int_col']); $this->assertEquals($charCol, trim((string) $row['char_col'])); $this->assertEquals($floatCol, (float) $row['float_col']); diff --git a/tests/framework/db/mssql/QueryBuilderTest.php b/tests/framework/db/mssql/QueryBuilderTest.php index 7d10f8337f..7cf6ef4600 100644 --- a/tests/framework/db/mssql/QueryBuilderTest.php +++ b/tests/framework/db/mssql/QueryBuilderTest.php @@ -365,7 +365,7 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest { $qb = $this->getQueryBuilder(); - $expected = "DBCC CHECKIDENT ('[item]', RESEED, (SELECT COALESCE(MAX([id]),0) FROM [item])+1)"; + $expected = "DBCC CHECKIDENT ('[item]', RESEED, 6)"; $sql = $qb->resetSequence('item'); $this->assertEquals($expected, $sql); diff --git a/tests/framework/db/mysql/ActiveRecordTest.php b/tests/framework/db/mysql/ActiveRecordTest.php index d5cee611e7..a050325e2e 100644 --- a/tests/framework/db/mysql/ActiveRecordTest.php +++ b/tests/framework/db/mysql/ActiveRecordTest.php @@ -8,6 +8,7 @@ namespace yiiunit\framework\db\mysql; use yiiunit\data\ar\Storage; +use yiiunit\data\ar\Type; /** * @group db @@ -18,7 +19,36 @@ class ActiveRecordTest extends \yiiunit\framework\db\ActiveRecordTest public $driverName = 'mysql'; protected static string $driverNameStatic = 'mysql'; - public function testJsonColumn(): void + public function testCastValues() + { + $model = new Type(); + $model->int_col = 123; + $model->int_col2 = 456; + $model->smallint_col = 42; + $model->char_col = '1337'; + $model->char_col2 = 'test'; + $model->char_col3 = 'test123'; + $model->float_col = 3.742; + $model->float_col2 = 42.1337; + $model->bool_col = true; + $model->bool_col2 = false; + $model->save(false); + + /* @var $model Type */ + $model = Type::find()->one(); + $this->assertSame(123, $model->int_col); + $this->assertSame(456, $model->int_col2); + $this->assertSame(42, $model->smallint_col); + $this->assertSame('1337', trim((string) $model->char_col)); + $this->assertSame('test', $model->char_col2); + $this->assertSame('test123', $model->char_col3); + $this->assertSame(3.742, $model->float_col); + $this->assertSame(42.1337, $model->float_col2); + //$this->assertSame(true, $model->bool_col); + //$this->assertSame(false, $model->bool_col2); + } + + public function testJsonColumn() { if (version_compare($this->getConnection()->getSchema()->getServerVersion(), '5.7', '<')) { $this->markTestSkipped('JSON columns are not supported in MySQL < 5.7'); diff --git a/tests/framework/db/mysql/BaseActiveRecordTest.php b/tests/framework/db/mysql/BaseActiveRecordTest.php index 5ea28bdb6c..a6812ccd59 100644 --- a/tests/framework/db/mysql/BaseActiveRecordTest.php +++ b/tests/framework/db/mysql/BaseActiveRecordTest.php @@ -4,6 +4,10 @@ namespace yiiunit\framework\db\mysql; use yiiunit\data\ar\Storage; +/** + * @group db + * @group mysql + */ class BaseActiveRecordTest extends \yiiunit\framework\db\BaseActiveRecordTest { public $driverName = 'mysql'; diff --git a/tests/framework/db/mysql/CommandTest.php b/tests/framework/db/mysql/CommandTest.php index 377b1d3632..4eb6b7c848 100644 --- a/tests/framework/db/mysql/CommandTest.php +++ b/tests/framework/db/mysql/CommandTest.php @@ -19,6 +19,59 @@ class CommandTest extends \yiiunit\framework\db\CommandTest public function testAddDropCheck(): void { - $this->markTestSkipped('MySQL does not support adding/dropping check constraints.'); + $db = $this->getConnection(false); + + if (version_compare($db->getServerVersion(), '8.0.16', '<')) { + $this->markTestSkipped('MySQL < 8.0.16 does not support CHECK constraints.'); + } + + $tableName = 'test_ck_several'; + $schema = $db->getSchema(); + + if ($schema->getTableSchema($tableName) !== null) { + $db->createCommand()->dropTable($tableName)->execute(); + } + $db->createCommand()->createTable($tableName, [ + 'int1' => 'integer', + 'int2' => 'integer', + 'int3' => 'integer', + 'int4' => 'integer', + ])->execute(); + + $this->assertEmpty($schema->getTableChecks($tableName, true)); + + $constraints = [ + ['name' => 'check_int1_positive', 'expression' => '[[int1]] > 0', 'expected' => '(`int1` > 0)'], + ['name' => 'check_int2_nonzero', 'expression' => '[[int2]] <> 0', 'expected' => '(`int2` <> 0)'], + ['name' => 'check_int3_less_than_100', 'expression' => '[[int3]] < 100', 'expected' => '(`int3` < 100)'], + ['name' => 'check_int1_less_than_int2', 'expression' => '[[int1]] < [[int2]]', 'expected' => '(`int1` < `int2`)'], + ]; + + if (\stripos($db->getServerVersion(), 'MariaDb') !== false) { + $constraints[0]['expected'] = '`int1` > 0'; + $constraints[1]['expected'] = '`int2` <> 0'; + $constraints[2]['expected'] = '`int3` < 100'; + $constraints[3]['expected'] = '`int1` < `int2`'; + } + + foreach ($constraints as $constraint) { + $db->createCommand()->addCheck($constraint['name'], $tableName, $constraint['expression'])->execute(); + } + + $tableChecks = $schema->getTableChecks($tableName, true); + $this->assertCount(4, $tableChecks); + + foreach ($constraints as $index => $constraint) { + $this->assertSame( + $constraints[$index]['expected'], + $tableChecks[$index]->expression + ); + } + + foreach ($constraints as $constraint) { + $db->createCommand()->dropCheck($constraint['name'], $tableName)->execute(); + } + + $this->assertEmpty($schema->getTableChecks($tableName, true)); } } diff --git a/tests/framework/db/mysql/QueryBuilderTest.php b/tests/framework/db/mysql/QueryBuilderTest.php index bdad1c83f1..640ad13495 100644 --- a/tests/framework/db/mysql/QueryBuilderTest.php +++ b/tests/framework/db/mysql/QueryBuilderTest.php @@ -263,35 +263,35 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest // json conditions [ ['=', 'jsoncol', new JsonExpression(['lang' => 'uk', 'country' => 'UA'])], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '{"lang":"uk","country":"UA"}'], + '[[jsoncol]] = :qp0', [':qp0' => '{"lang":"uk","country":"UA"}'], ], [ ['=', 'jsoncol', new JsonExpression([false])], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '[false]'] + '[[jsoncol]] = :qp0', [':qp0' => '[false]'] ], 'object with type. Type is ignored for MySQL' => [ ['=', 'prices', new JsonExpression(['seeds' => 15, 'apples' => 25], 'jsonb')], - '[[prices]] = CAST(:qp0 AS JSON)', [':qp0' => '{"seeds":15,"apples":25}'], + '[[prices]] = :qp0', [':qp0' => '{"seeds":15,"apples":25}'], ], 'nested json' => [ ['=', 'data', new JsonExpression(['user' => ['login' => 'silverfire', 'password' => 'c4ny0ur34d17?'], 'props' => ['mood' => 'good']])], - '[[data]] = CAST(:qp0 AS JSON)', [':qp0' => '{"user":{"login":"silverfire","password":"c4ny0ur34d17?"},"props":{"mood":"good"}}'] + '[[data]] = :qp0', [':qp0' => '{"user":{"login":"silverfire","password":"c4ny0ur34d17?"},"props":{"mood":"good"}}'] ], 'null value' => [ ['=', 'jsoncol', new JsonExpression(null)], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => 'null'] + '[[jsoncol]] = :qp0', [':qp0' => 'null'] ], 'null as array value' => [ ['=', 'jsoncol', new JsonExpression([null])], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '[null]'] + '[[jsoncol]] = :qp0', [':qp0' => '[null]'] ], 'null as object value' => [ ['=', 'jsoncol', new JsonExpression(['nil' => null])], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '{"nil":null}'] + '[[jsoncol]] = :qp0', [':qp0' => '{"nil":null}'] ], 'with object as value' => [ ['=', 'jsoncol', new JsonExpression(new DynamicModel(['a' => 1, 'b' => 2]))], - '[[jsoncol]] = CAST(:qp0 AS JSON)', [':qp0' => '{"a":1,"b":2}'] + '[[jsoncol]] = :qp0', [':qp0' => '{"a":1,"b":2}'] ], 'query' => [ ['=', 'jsoncol', new JsonExpression((new Query())->select('params')->from('user')->where(['id' => 1]))], @@ -303,7 +303,7 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest ], 'nested and combined json expression' => [ ['=', 'jsoncol', new JsonExpression(new JsonExpression(['a' => 1, 'b' => 2, 'd' => new JsonExpression(['e' => 3])]))], - "[[jsoncol]] = CAST(:qp0 AS JSON)", [':qp0' => '{"a":1,"b":2,"d":{"e":3}}'] + "[[jsoncol]] = :qp0", [':qp0' => '{"a":1,"b":2,"d":{"e":3}}'] ], 'search by property in JSON column (issue #15838)' => [ ['=', new Expression("(jsoncol->>'$.someKey')"), '42'], diff --git a/tests/framework/db/mysql/SchemaTest.php b/tests/framework/db/mysql/SchemaTest.php index 1c39a43926..5dfb44ad14 100644 --- a/tests/framework/db/mysql/SchemaTest.php +++ b/tests/framework/db/mysql/SchemaTest.php @@ -77,19 +77,126 @@ SQL; public static function constraintsProvider(): array { $result = parent::constraintsProvider(); - $result['1: check'][2] = false; + $result['1: check'][2][0]->columnNames = null; + $result['1: check'][2][0]->expression = "`C_check` <> ''"; $result['2: primary key'][2]->name = null; - $result['2: check'][2] = false; // Work aroung bug in MySQL 5.1 - it creates only this table in lowercase. O_o $result['3: foreign key'][2][0]->foreignTableName = new AnyCaseValue('T_constraints_2'); - $result['3: check'][2] = false; - $result['4: check'][2] = false; return $result; } + /** + * @dataProvider constraintsProvider + * @param string $tableName + * @param string $type + * @param mixed $expected + */ + public function testTableSchemaConstraints($tableName, $type, $expected) + { + $version = $this->getConnection(false)->getServerVersion(); + + if ($expected === false) { + $this->expectException('yii\base\NotSupportedException'); + } + + if ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '<') && + $type === 'checks' + ) { + $this->expectException('yii\base\NotSupportedException'); + } elseif ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '>=') && + $tableName === 'T_constraints_1' && + $type === 'checks' + ) { + $expected[0]->expression = "(`C_check` <> _utf8mb4\\'\\')"; + } + + $constraints = $this->getConnection(false)->getSchema()->{'getTable' . ucfirst($type)}($tableName); + $this->assertMetadataEquals($expected, $constraints); + } + + /** + * @dataProvider uppercaseConstraintsProvider + * @param string $tableName + * @param string $type + * @param mixed $expected + */ + public function testTableSchemaConstraintsWithPdoUppercase($tableName, $type, $expected) + { + $version = $this->getConnection(false)->getServerVersion(); + + if ($expected === false) { + $this->expectException('yii\base\NotSupportedException'); + } + + if ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '<') && + $type === 'checks' + ) { + $this->expectException('yii\base\NotSupportedException'); + } elseif ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '>=') && + $tableName === 'T_constraints_1' && + $type === 'checks' + ) { + $expected[0]->expression = "(`C_check` <> _utf8mb4\\'\\')"; + } + + $connection = $this->getConnection(false); + $connection->getSlavePdo(true)->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_UPPER); + $constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true); + $this->assertMetadataEquals($expected, $constraints); + } + + /** + * @dataProvider lowercaseConstraintsProvider + * @param string $tableName + * @param string $type + * @param mixed $expected + */ + public function testTableSchemaConstraintsWithPdoLowercase($tableName, $type, $expected) + { + $version = $this->getConnection(false)->getServerVersion(); + + if ($expected === false) { + $this->expectException('yii\base\NotSupportedException'); + } + + if ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '<') && + $type === 'checks' + ) { + $this->expectException('yii\base\NotSupportedException'); + } elseif ( + $this->driverName === 'mysql' && + \stripos($version, 'MariaDb') === false && + version_compare($version, '8.0.16', '>=') && + $tableName === 'T_constraints_1' && + $type === 'checks' + ) { + $expected[0]->expression = "(`C_check` <> _utf8mb4\\'\\')"; + } + + $connection = $this->getConnection(false); + $connection->getSlavePdo(true)->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_LOWER); + $constraints = $connection->getSchema()->{'getTable' . ucfirst($type)}($tableName, true); + $this->assertMetadataEquals($expected, $constraints); + } + /** * When displayed in the INFORMATION_SCHEMA.COLUMNS table, a default CURRENT TIMESTAMP is displayed * as CURRENT_TIMESTAMP up until MariaDB 10.2.2, and as current_timestamp() from MariaDB 10.2.3. @@ -148,87 +255,108 @@ SQL; public function getExpectedColumns() { - $version = $this->getConnection()->getSchema()->getServerVersion(); + $version = $this->getConnection(false)->getServerVersion(); $columns = array_merge( parent::getExpectedColumns(), [ 'int_col' => [ 'type' => 'integer', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'int' : 'int(11)', + 'dbType' => 'int(11)', 'phpType' => 'integer', 'allowNull' => false, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 11, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 11, + 'size' => 11, + 'precision' => 11, 'scale' => null, 'defaultValue' => null, ], 'int_col2' => [ 'type' => 'integer', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'int' : 'int(11)', + 'dbType' => 'int(11)', 'phpType' => 'integer', 'allowNull' => true, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 11, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 11, + 'size' => 11, + 'precision' => 11, 'scale' => null, 'defaultValue' => 1, ], 'int_col3' => [ 'type' => 'integer', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'int unsigned' : 'int(11) unsigned', + 'dbType' => 'int(11) unsigned', 'phpType' => 'integer', 'allowNull' => true, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 11, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 11, + 'size' => 11, + 'precision' => 11, 'scale' => null, 'defaultValue' => 1, ], 'tinyint_col' => [ 'type' => 'tinyint', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'tinyint' : 'tinyint(3)', + 'dbType' => 'tinyint(3)', 'phpType' => 'integer', 'allowNull' => true, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 3, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 3, + 'size' => 3, + 'precision' => 3, 'scale' => null, 'defaultValue' => 1, ], 'smallint_col' => [ 'type' => 'smallint', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'smallint' : 'smallint(1)', + 'dbType' => 'smallint(1)', 'phpType' => 'integer', 'allowNull' => true, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 1, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 1, + 'size' => 1, + 'precision' => 1, 'scale' => null, 'defaultValue' => 1, ], 'bigint_col' => [ 'type' => 'bigint', - 'dbType' => \version_compare($version, '8.0.17', '>') ? 'bigint unsigned' : 'bigint(20) unsigned', + 'dbType' => 'bigint(20) unsigned', 'phpType' => 'string', 'allowNull' => true, 'autoIncrement' => false, 'enumValues' => null, - 'size' => \version_compare($version, '8.0.17', '>') ? null : 20, - 'precision' => \version_compare($version, '8.0.17', '>') ? null : 20, + 'size' => 20, + 'precision' => 20, 'scale' => null, 'defaultValue' => null, ], ] ); - if (version_compare($version, '5.7', '<')) { + if (\version_compare($version, '8.0.17', '>') && \stripos($version, 'MariaDb') === false) { + $columns['int_col']['dbType'] = 'int'; + $columns['int_col']['size'] = null; + $columns['int_col']['precision'] = null; + $columns['int_col2']['dbType'] = 'int'; + $columns['int_col2']['size'] = null; + $columns['int_col2']['precision'] = null; + $columns['int_col3']['dbType'] = 'int unsigned'; + $columns['int_col3']['size'] = null; + $columns['int_col3']['precision'] = null; + $columns['tinyint_col']['dbType'] = 'tinyint'; + $columns['tinyint_col']['size'] = null; + $columns['tinyint_col']['precision'] = null; + $columns['smallint_col']['dbType'] = 'smallint'; + $columns['smallint_col']['size'] = null; + $columns['smallint_col']['precision'] = null; + $columns['bigint_col']['dbType'] = 'bigint unsigned'; + $columns['bigint_col']['size'] = null; + $columns['bigint_col']['precision'] = null; + } + + if (version_compare($version, '5.7', '<') && \stripos($version, 'MariaDb') === false) { $columns['int_col3']['phpType'] = 'string'; $columns['json_col']['type'] = 'text'; $columns['json_col']['dbType'] = 'longtext'; diff --git a/tests/framework/db/mysql/connection/DeadLockTest.php b/tests/framework/db/mysql/connection/DeadLockTest.php index de2089abf1..8a71236bf9 100644 --- a/tests/framework/db/mysql/connection/DeadLockTest.php +++ b/tests/framework/db/mysql/connection/DeadLockTest.php @@ -32,6 +32,9 @@ class DeadLockTest extends \yiiunit\framework\db\mysql\ConnectionTest */ public function testDeadlockException(): void { + if (\stripos($this->getConnection(false)->getServerVersion(), 'MariaDB') !== false) { + $this->markTestSkipped('MariaDB does not support this test'); + } if (!\function_exists('pcntl_fork')) { $this->markTestSkipped('pcntl_fork() is not available'); } diff --git a/tests/framework/db/mysql/type/JsonTest.php b/tests/framework/db/mysql/type/JsonTest.php new file mode 100644 index 0000000000..b955c7221c --- /dev/null +++ b/tests/framework/db/mysql/type/JsonTest.php @@ -0,0 +1,85 @@ +getConnection(); + + if ($db->getSchema()->getTableSchema('json') !== null) { + $db->createCommand()->dropTable('json')->execute(); + } + + $command = $db->createCommand(); + $command->createTable('json', ['id' => Schema::TYPE_PK, 'data' => Schema::TYPE_JSON])->execute(); + + $this->assertTrue($db->getTableSchema('json') !== null); + $this->assertSame('data', $db->getTableSchema('json')->getColumn('data')->name); + $this->assertSame('json', $db->getTableSchema('json')->getColumn('data')->type); + } + + public function testInsertAndSelect(): void + { + $db = $this->getConnection(true); + $version = $db->getServerVersion(); + + $command = $db->createCommand(); + $command->insert('storage', ['data' => ['a' => 1, 'b' => 2]])->execute(); + + if (\stripos($version, 'MariaDb') === false) { + $rowExpected = '{"a": 1, "b": 2}'; + } else { + $rowExpected = '{"a":1,"b":2}'; + } + + $this->assertSame( + $rowExpected, + $command->setSql( + <<queryScalar(), + ); + } + + public function testInsertJsonExpressionAndSelect(): void + { + $db = $this->getConnection(true); + $version = $db->getServerVersion(); + + $command = $db->createCommand(); + $command->insert('storage', ['data' => new JsonExpression(['a' => 1, 'b' => 2])])->execute(); + + if (\stripos($version, 'MariaDb') === false) { + $rowExpected = '{"a": 1, "b": 2}'; + } else { + $rowExpected = '{"a":1,"b":2}'; + } + + $this->assertSame( + $rowExpected, + $command->setSql( + <<queryScalar(), + ); + } +} diff --git a/tests/framework/db/oci/QueryBuilderTest.php b/tests/framework/db/oci/QueryBuilderTest.php index 3eb58638b5..68e9720b0c 100644 --- a/tests/framework/db/oci/QueryBuilderTest.php +++ b/tests/framework/db/oci/QueryBuilderTest.php @@ -300,7 +300,7 @@ WHERE rownum <= 1) "EXCLUDED" ON ("T_upsert"."email"="EXCLUDED"."email") WHEN NO if (is_string($expectedSQL)) { $this->assertEqualsWithoutLE($expectedSQL, $actualSQL); } else { - $this->assertContains($actualSQL, $expectedSQL); + $this->assertStringContainsString($actualSQL, $expectedSQL); } if (ArrayHelper::isAssociative($expectedParams)) { $this->assertSame($expectedParams, $actualParams); diff --git a/tests/framework/db/pgsql/ActiveFixtureTest.php b/tests/framework/db/pgsql/ActiveFixtureTest.php index d745fb5538..c00886c728 100644 --- a/tests/framework/db/pgsql/ActiveFixtureTest.php +++ b/tests/framework/db/pgsql/ActiveFixtureTest.php @@ -1,4 +1,5 @@ setUp(); + $fixture = $test->getFixture('customers'); + + $sequenceName = $fixture->getTableSchema()->sequenceName; + $sequenceNextVal = $this->getConnection()->createCommand("SELECT currval('$sequenceName')")->queryScalar(); + $this->assertEquals($fixture->getModel('customer2')->id + 1, $sequenceNextVal); + + $test->tearDown(); + } } diff --git a/tests/framework/db/pgsql/BaseActiveRecordTest.php b/tests/framework/db/pgsql/BaseActiveRecordTest.php index 267e08de08..c0af29e83f 100644 --- a/tests/framework/db/pgsql/BaseActiveRecordTest.php +++ b/tests/framework/db/pgsql/BaseActiveRecordTest.php @@ -5,6 +5,10 @@ namespace yiiunit\framework\db\pgsql; use yii\db\JsonExpression; use yiiunit\data\ar\ActiveRecord; +/** + * @group db + * @group pgsql + */ class BaseActiveRecordTest extends \yiiunit\framework\db\BaseActiveRecordTest { public $driverName = 'pgsql'; diff --git a/tests/framework/di/InstanceTest.php b/tests/framework/di/InstanceTest.php index 0b517f5c80..c017b3d5e1 100644 --- a/tests/framework/di/InstanceTest.php +++ b/tests/framework/di/InstanceTest.php @@ -171,7 +171,10 @@ class InstanceTest extends TestCase $instance = Instance::of('something'); $export = var_export($instance, true); - $this->assertMatchesRegularExpression('~yii\\\\di\\\\Instance::__set_state\(array\(\s+\'id\' => \'something\',\s+\'optional\' => false,\s+\)\)~', $export); + $this->assertMatchesRegularExpression( + '~yii\\\\di\\\\Instance::__set_state\(array\(\s+\'id\' => \'something\',\s+\'optional\' => false,\s+\)\)~', + $export + ); $this->assertEquals($instance, Instance::__set_state([ 'id' => 'something', diff --git a/tests/framework/di/stubs/Alpha.php b/tests/framework/di/stubs/Alpha.php index 151d2f109b..a5af3ffed7 100644 --- a/tests/framework/di/stubs/Alpha.php +++ b/tests/framework/di/stubs/Alpha.php @@ -12,10 +12,10 @@ class Alpha extends BaseObject public $color = true; public function __construct( - Beta $beta = null, - QuxInterface $omega = null, - Unknown $unknown = null, - AbstractColor $color = null + ?Beta $beta = null, + ?QuxInterface $omega = null, + ?Unknown $unknown = null, + ?AbstractColor $color = null ) { $this->beta = $beta; $this->omega = $omega; diff --git a/tests/framework/filters/RateLimiterTest.php b/tests/framework/filters/RateLimiterTest.php index 2bf703d831..b0d006a85f 100644 --- a/tests/framework/filters/RateLimiterTest.php +++ b/tests/framework/filters/RateLimiterTest.php @@ -84,7 +84,10 @@ class RateLimiterTest extends TestCase $result = $rateLimiter->beforeAction('test'); - $this->assertContains('Rate limit skipped: "user" does not implement RateLimitInterface.', Yii::getLogger()->messages); + $this->assertContains( + 'Rate limit skipped: "user" does not implement RateLimitInterface.', + Yii::getLogger()->messages + ); $this->assertTrue($result); } @@ -162,6 +165,9 @@ class RateLimiterTest extends TestCase // testing the evaluation of user closure, which in this case returns not the expect object and therefore // the log message "does not implement RateLimitInterface" is expected. $this->assertInstanceOf(User::class, $rateLimiter->user); - $this->assertContains('Rate limit skipped: "user" does not implement RateLimitInterface.', Yii::getLogger()->messages); + $this->assertContains( + 'Rate limit skipped: "user" does not implement RateLimitInterface.', + Yii::getLogger()->messages + ); } } diff --git a/tests/framework/grid/GridViewTest.php b/tests/framework/grid/GridViewTest.php index 496e67e770..cf1f87886a 100644 --- a/tests/framework/grid/GridViewTest.php +++ b/tests/framework/grid/GridViewTest.php @@ -158,7 +158,6 @@ class GridViewTest extends \yiiunit\TestCase public function testHeaderLabels(): void { // Ensure GridView does not call Model::generateAttributeLabel() to generate labels unless the labels are explicitly used. - $this->mockApplication([ 'components' => [ 'db' => [ @@ -200,7 +199,6 @@ class GridViewTest extends \yiiunit\TestCase 'attributes' => ['attr1', 'attr2'], ]); $grid->renderTableHeader(); - // If NoAutoLabels::generateAttributeLabel() has not been called no exception will be thrown meaning this test passed successfully. $this->expectNotToPerformAssertions(); diff --git a/tests/framework/helpers/ArrayHelperTest.php b/tests/framework/helpers/ArrayHelperTest.php index 2fb7648edb..d6a153b44c 100644 --- a/tests/framework/helpers/ArrayHelperTest.php +++ b/tests/framework/helpers/ArrayHelperTest.php @@ -712,6 +712,57 @@ class ArrayHelperTest extends TestCase '345' => 'ccc', ], ], $result); + + $result = ArrayHelper::map($array, + static function (array $group) { + return $group['id'] . $group['name']; + }, + static function (array $group) { + return $group['name'] . $group['class']; + } + ); + + $this->assertEquals([ + '123aaa' => 'aaax', + '124bbb' => 'bbbx', + '345ccc' => 'cccy', + ], $result); + + $result = ArrayHelper::map($array, + static function (array $group) { + return $group['id'] . $group['name']; + }, + static function (array $group) { + return $group['name'] . $group['class']; + }, + static function (array $group) { + return $group['class'] . '-' . $group['class']; + } + ); + + $this->assertEquals([ + 'x-x' => [ + '123aaa' => 'aaax', + '124bbb' => 'bbbx', + ], + 'y-y' => [ + '345ccc' => 'cccy', + ], + ], $result); + + $array = [ + ['id' => '123', 'name' => 'aaa', 'class' => 'x', 'map' => ['a' => '11', 'b' => '22']], + ['id' => '124', 'name' => 'bbb', 'class' => 'x', 'map' => ['a' => '33', 'b' => '44']], + ['id' => '345', 'name' => 'ccc', 'class' => 'y', 'map' => ['a' => '55', 'b' => '66']], + ]; + + $result = ArrayHelper::map($array, 'map.a', 'map.b'); + + $this->assertEquals([ + '11' => '22', + '33' => '44', + '55' => '66' + ], $result); } public function testKeyExists(): void @@ -1536,6 +1587,125 @@ class ArrayHelperTest extends TestCase ], ]; } + + public function testFlatten() + { + // Test with deeply nested arrays + $array = [ + 'a' => [ + 'b' => [ + 'c' => [ + 'd' => 1, + 'e' => 2, + ], + 'f' => 3, + ], + 'g' => 4, + ], + 'h' => 5, + ]; + $expected = [ + 'a.b.c.d' => 1, + 'a.b.c.e' => 2, + 'a.b.f' => 3, + 'a.g' => 4, + 'h' => 5, + ]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Test with arrays containing different data types + $array = [ + 'a' => [ + 'b' => [ + 'c' => 'string', + 'd' => 123, + 'e' => true, + 'f' => null, + ], + 'g' => [1, 2, 3], + ], + ]; + $expected = [ + 'a.b.c' => 'string', + 'a.b.d' => 123, + 'a.b.e' => true, + 'a.b.f' => null, + 'a.g.0' => 1, + 'a.g.1' => 2, + 'a.g.2' => 3, + ]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Test with arrays containing special characters in keys + $array = [ + 'a.b' => [ + 'c.d' => [ + 'e.f' => 1, + ], + ], + 'g.h' => 2, + ]; + $expected = [ + 'a.b.c.d.e.f' => 1, + 'g.h' => 2, + ]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Test with custom separator + $array = [ + 'a' => [ + 'b' => [ + 'c' => [ + 'd' => 1, + 'e' => 2, + ], + 'f' => 3, + ], + 'g' => 4, + ], + 'h' => 5, + ]; + $result = ArrayHelper::flatten($array, '_'); + $expected = [ + 'a_b_c_d' => 1, + 'a_b_c_e' => 2, + 'a_b_f' => 3, + 'a_g' => 4, + 'h' => 5, + ]; + + $this->assertEquals($expected, $result); + } + + public function testFlattenEdgeCases() + { + // Empty array + $array = []; + $expected = []; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Non-array value + $array = 'string'; + $expected = ['string']; + $this->expectException('yii\base\InvalidArgumentException'); + $this->expectExceptionMessage('Argument $array must be an array or implement Traversable'); + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Special characters in keys + $array = ['a.b' => ['c.d' => 1]]; + $expected = ['a.b.c.d' => 1]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Mixed data types + $array = ['a' => ['b' => 'string', 'c' => 123, 'd' => true, 'e' => null]]; + $expected = ['a.b' => 'string', 'a.c' => 123, 'a.d' => true, 'a.e' => null]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Key collisions + $array = ['a' => ['b' => 1], 'a.b' => 2]; + $expected = ['a.b' => 2]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + } } class Post1 diff --git a/tests/framework/helpers/FileHelperTest.php b/tests/framework/helpers/FileHelperTest.php index 2530fa00f6..4920a5e4a1 100644 --- a/tests/framework/helpers/FileHelperTest.php +++ b/tests/framework/helpers/FileHelperTest.php @@ -21,7 +21,7 @@ class FileHelperTest extends TestCase */ private string $testFilePath = ''; - public function setUp(): void + protected function setUp(): void { $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . static::class; $this->createDir($this->testFilePath); @@ -56,7 +56,7 @@ class FileHelperTest extends TestCase return $mode === '0700'; } - public function tearDown(): void + protected function tearDown(): void { $this->removeDir($this->testFilePath); } diff --git a/tests/framework/helpers/HtmlTest.php b/tests/framework/helpers/HtmlTest.php index 4490b7a354..e3e5e92eec 100644 --- a/tests/framework/helpers/HtmlTest.php +++ b/tests/framework/helpers/HtmlTest.php @@ -90,7 +90,22 @@ class HtmlTest extends TestCase $this->assertEquals("", Html::script($content, ['type' => 'text/js'])); } - public function testCssFile(): void + public function testScriptCustomAttribute() + { + $nonce = Yii::$app->security->generateRandomString(); + $this->mockApplication([ + 'components' => [ + 'view' => [ + 'class' => 'yii\web\View', + 'scriptOptions' => ['nonce' => $nonce], + ], + ], + ]); + $content = 'a <>'; + $this->assertEquals("", Html::script($content)); + } + + public function testCssFile() { $this->assertEquals('', Html::cssFile('http://example.com')); $this->assertEquals('', Html::cssFile('')); @@ -1895,6 +1910,7 @@ EOD; public function testAttributeNameException(string $name): void { $this->expectException('yii\base\InvalidArgumentException'); + Html::getAttributeName($name); } @@ -1933,6 +1949,9 @@ EOD; public function testGetAttributeValueInvalidArgumentException(): void { + $this->expectException(\yii\base\InvalidArgumentException::class); + $this->expectExceptionMessage('Attribute name must contain word characters only.'); + $model = new HtmlTestModel(); $this->expectException(\yii\base\InvalidArgumentException::class); @@ -2121,7 +2140,7 @@ HTML; $this->assertStringContainsString('placeholder="My placeholder: Name"', $html); } - public static function getInputIdDataProvider() + public static function getInputIdDataProvider(): array { return [ [ @@ -2160,7 +2179,7 @@ HTML; ]; } - public static function getInputIdByNameDataProvider() + public static function getInputIdByNameDataProvider(): array { return [ [ diff --git a/tests/framework/i18n/FormatterDateTest.php b/tests/framework/i18n/FormatterDateTest.php index 001cd5cdb8..6ee89da5fe 100644 --- a/tests/framework/i18n/FormatterDateTest.php +++ b/tests/framework/i18n/FormatterDateTest.php @@ -530,6 +530,7 @@ class FormatterDateTest extends TestCase // other options $this->assertSame('minus 244 seconds', $this->formatter->asDuration($interval_244_seconds, ' and ', 'minus ')); $this->assertSame('minus 4 minutes and 4 seconds', $this->formatter->asDuration(-244, ' and ', 'minus ')); + $this->assertSame('1 second', $this->formatter->asDuration(1.5)); // Pass a inverted DateInterval string $this->assertSame('-1 year, 2 months, 10 days, 2 hours, 30 minutes', $this->formatter->asDuration('2008-05-11T15:30:00Z/2007-03-01T13:00:00Z')); diff --git a/tests/framework/log/DbTargetTest.php b/tests/framework/log/DbTargetTest.php index 55d2715273..085eb93012 100644 --- a/tests/framework/log/DbTargetTest.php +++ b/tests/framework/log/DbTargetTest.php @@ -65,7 +65,7 @@ abstract class DbTargetTest extends TestCase } } - public function setUp(): void + protected function setUp(): void { parent::setUp(); $databases = static::getParam('databases'); @@ -79,7 +79,7 @@ abstract class DbTargetTest extends TestCase static::runConsoleAction('migrate/up', ['migrationPath' => '@yii/log/migrations/', 'interactive' => false]); } - public function tearDown(): void + protected function tearDown(): void { self::getConnection()->createCommand()->truncateTable(self::$logTable)->execute(); static::runConsoleAction('migrate/down', ['migrationPath' => '@yii/log/migrations/', 'interactive' => false]); diff --git a/tests/framework/log/EmailTargetTest.php b/tests/framework/log/EmailTargetTest.php index 6bad14c1a1..d6d43a6d0e 100644 --- a/tests/framework/log/EmailTargetTest.php +++ b/tests/framework/log/EmailTargetTest.php @@ -48,7 +48,6 @@ class EmailTargetTest extends TestCase { $this->expectException(\yii\base\InvalidConfigException::class); $this->expectExceptionMessage('The "to" option must be set for EmailTarget::message.'); - new EmailTarget(['mailer' => $this->mailer]); } diff --git a/tests/framework/log/TargetTest.php b/tests/framework/log/TargetTest.php index bd422af738..51ec96c9be 100644 --- a/tests/framework/log/TargetTest.php +++ b/tests/framework/log/TargetTest.php @@ -327,6 +327,45 @@ class TargetTest extends TestCase $logger->log('token.b', Logger::LEVEL_PROFILE_END, 'category'); $logger->log('token.a', Logger::LEVEL_PROFILE_END, 'category'); } + + public function testWildcardsInMaskVars() + { + $keys = [ + 'PASSWORD', + 'password', + 'password_repeat', + 'repeat_password', + 'repeat_password_again', + '1password', + 'password1', + ]; + + $password = '!P@$$w0rd#'; + + $items = array_fill_keys($keys, $password); + + $GLOBALS['_TEST'] = array_merge( + $items, + ['a' => $items], + ['b' => ['c' => $items]], + ['d' => ['e' => ['f' => $items]]], + ); + + $target = new TestTarget([ + 'logVars' => ['_SERVER', '_TEST'], + 'maskVars' => [ + // option 1: exact value(s) + '_SERVER.DOCUMENT_ROOT', + // option 2: pattern(s) + '_TEST.*password*', + ] + ]); + + $message = $target->getContextMessage(); + + $this->assertStringContainsString("'DOCUMENT_ROOT' => '***'", $message); + $this->assertStringNotContainsString($password, $message); + } } class TestTarget extends Target diff --git a/tests/framework/mail/BaseMailerTest.php b/tests/framework/mail/BaseMailerTest.php index 8144cbe2b5..80c88e462c 100644 --- a/tests/framework/mail/BaseMailerTest.php +++ b/tests/framework/mail/BaseMailerTest.php @@ -19,7 +19,7 @@ use yiiunit\TestCase; */ class BaseMailerTest extends TestCase { - public function setUp(): void + protected function setUp(): void { $this->mockApplication([ 'components' => [ @@ -32,7 +32,7 @@ class BaseMailerTest extends TestCase } } - public function tearDown(): void + protected function tearDown(): void { $filePath = $this->getTestFilePath(); if (file_exists($filePath)) { diff --git a/tests/framework/mail/BaseMessageTest.php b/tests/framework/mail/BaseMessageTest.php index 93e72a68a1..6c3846e5ed 100644 --- a/tests/framework/mail/BaseMessageTest.php +++ b/tests/framework/mail/BaseMessageTest.php @@ -17,7 +17,7 @@ use yiiunit\TestCase; */ class BaseMessageTest extends TestCase { - public function setUp(): void + protected function setUp(): void { $this->mockApplication([ 'components' => [ diff --git a/tests/framework/rbac/DbManagerTestCase.php b/tests/framework/rbac/DbManagerTestCase.php index b4c0e84cd6..c02132e3cd 100644 --- a/tests/framework/rbac/DbManagerTestCase.php +++ b/tests/framework/rbac/DbManagerTestCase.php @@ -45,7 +45,7 @@ abstract class DbManagerTestCase extends ManagerTestCase $this->auth = $this->createManager(); $this->assertEquals([], $this->auth->getUserIdsByRole('nonexisting')); - $this->assertEquals(['123', 'reader A'], $this->auth->getUserIdsByRole('reader'), ''); + $this->assertEquals(['123', 'reader A'], $this->auth->getUserIdsByRole('reader'), '', 0.0, 10, true); $this->assertEquals(['author B'], $this->auth->getUserIdsByRole('author')); $this->assertEquals(['admin C'], $this->auth->getUserIdsByRole('admin')); } diff --git a/tests/framework/rbac/ManagerTestCase.php b/tests/framework/rbac/ManagerTestCase.php index 7f381ee5e2..80061ea690 100644 --- a/tests/framework/rbac/ManagerTestCase.php +++ b/tests/framework/rbac/ManagerTestCase.php @@ -373,8 +373,16 @@ abstract class ManagerTestCase extends TestCase $roleNames[] = $role->name; } - $this->assertContains('reader', $roleNames, 'Roles should contain reader. Currently it has: ' . implode(', ', $roleNames)); - $this->assertContains('author', $roleNames, 'Roles should contain author. Currently it has: ' . implode(', ', $roleNames)); + $this->assertContains( + 'reader', + $roleNames, + 'Roles should contain reader. Currently it has: ' . implode(', ', $roleNames) + ); + $this->assertContains( + 'author', + $roleNames, + 'Roles should contain author. Currently it has: ' . implode(', ', $roleNames) + ); } public function testAssignmentsToIntegerId(): void diff --git a/tests/framework/rbac/PhpManagerTest.php b/tests/framework/rbac/PhpManagerTest.php index 09fdacc1fe..123b87583e 100644 --- a/tests/framework/rbac/PhpManagerTest.php +++ b/tests/framework/rbac/PhpManagerTest.php @@ -139,6 +139,7 @@ class PhpManagerTest extends ManagerTestCase public function testOverwriteName(): void { $this->prepareData(); + $name = 'readPost'; $permission = $this->auth->getPermission($name); $permission->name = 'createPost'; diff --git a/tests/framework/rest/SerializerTest.php b/tests/framework/rest/SerializerTest.php index 4d4e3bfe7a..84079f552e 100644 --- a/tests/framework/rest/SerializerTest.php +++ b/tests/framework/rest/SerializerTest.php @@ -10,7 +10,6 @@ namespace yiiunit\framework\rest; use yii\base\Model; use yii\data\ArrayDataProvider; use yii\rest\Serializer; -use yii\web\Request; use yiiunit\TestCase; /** @@ -414,30 +413,6 @@ class SerializerTest extends TestCase $this->assertEquals($expectedResult, $serializer->serialize($dataProvider)); } - /** - * @dataProvider dataProviderSerializeDataProvider - * - * @param \yii\data\DataProviderInterface $dataProvider - */ - public function testHeadSerializeDataProvider(\yii\data\ArrayDataProvider $dataProvider, array $expectedResult, bool $saveKeys = false): void - { - $serializer = new Serializer(); - $serializer->preserveKeys = $saveKeys; - $serializer->collectionEnvelope = 'data'; - - $this->assertEquals($expectedResult, $serializer->serialize($dataProvider)['data']); - - $_SERVER['REQUEST_METHOD'] = 'HEAD'; - $request = new Request(); - $_POST[$request->methodParam] = 'HEAD'; - $serializer = new Serializer([ - 'request' => $request - ]); - $serializer->preserveKeys = $saveKeys; - $this->assertEmpty($serializer->serialize($dataProvider)); - unset($_POST[$request->methodParam], $_SERVER['REQUEST_METHOD']); - } - /** * @see https://github.com/yiisoft/yii2/issues/16334 */ diff --git a/tests/framework/rest/UrlRuleTest.php b/tests/framework/rest/UrlRuleTest.php index 405fbbca38..13d72230d8 100644 --- a/tests/framework/rest/UrlRuleTest.php +++ b/tests/framework/rest/UrlRuleTest.php @@ -375,6 +375,9 @@ class UrlRuleTest extends TestCase /** * @dataProvider getCreateUrlStatusProvider + * + * @param array $ruleConfig + * @param array $tests */ public function testGetCreateUrlStatus(array $ruleConfig, array $tests): void { diff --git a/tests/framework/test/ActiveFixtureTest.php b/tests/framework/test/ActiveFixtureTest.php index 206487b41f..80a5249119 100644 --- a/tests/framework/test/ActiveFixtureTest.php +++ b/tests/framework/test/ActiveFixtureTest.php @@ -23,7 +23,7 @@ class ActiveFixtureTest extends DatabaseTestCase { protected $driverName = 'mysql'; - public function setUp(): void + protected function setUp(): void { parent::setUp(); $db = $this->getConnection(); @@ -31,7 +31,7 @@ class ActiveFixtureTest extends DatabaseTestCase ActiveRecord::$db = $db; } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); } diff --git a/tests/framework/test/ArrayFixtureTest.php b/tests/framework/test/ArrayFixtureTest.php index ca5dddb240..7f77890c5d 100644 --- a/tests/framework/test/ArrayFixtureTest.php +++ b/tests/framework/test/ArrayFixtureTest.php @@ -44,12 +44,11 @@ class ArrayFixtureTest extends TestCase $this->assertEmpty($this->_fixture->data, 'fixture data should not be loaded'); } - public function testWrongDataFileException(): void + public function testWrongDataFileException() { $this->_fixture->dataFile = 'wrong/fixtures/data/path/alias'; $this->expectException(\yii\base\InvalidConfigException::class); - $this->_fixture->load(); } } diff --git a/tests/framework/validators/EachValidatorTest.php b/tests/framework/validators/EachValidatorTest.php index 5c4ac7e5e0..b0da85ed12 100644 --- a/tests/framework/validators/EachValidatorTest.php +++ b/tests/framework/validators/EachValidatorTest.php @@ -209,6 +209,8 @@ class EachValidatorTest extends TestCase * of different type during validation. * (ie: public array $dummy; where $dummy is array of booleans, * validator will try to assign these booleans one by one to $dummy) + * + * @requires PHP >= 7.4 */ public function testTypedProperties(): void { diff --git a/tests/framework/validators/FileValidatorTest.php b/tests/framework/validators/FileValidatorTest.php index 6f4b7604c2..f5a8068e10 100644 --- a/tests/framework/validators/FileValidatorTest.php +++ b/tests/framework/validators/FileValidatorTest.php @@ -12,6 +12,7 @@ use yii\helpers\FileHelper; use yii\validators\FileValidator; use yii\web\UploadedFile; use yiiunit\data\validators\models\FakedValidationModel; +use yiiunit\data\validators\models\FakedValidationTypedModel; use yiiunit\TestCase; /** @@ -112,6 +113,7 @@ class FileValidatorTest extends TestCase $val->validateAttribute($m, 'attr_files'); $this->assertTrue($m->hasErrors('attr_files')); $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files'))); + $m = FakedValidationModel::createWithAttributes( [ 'attr_files' => $this->createTestFiles( @@ -307,6 +309,32 @@ class FileValidatorTest extends TestCase $this->assertTrue($model->hasErrors('attr_images')); } + /** + * https://github.com/yiisoft/yii2/issues/19855 + */ + public function testValidateArrayAttributeWithMinMaxOneAndOneFile() + { + $validator = new FileValidator(['maxFiles' => 1, 'minFiles' => 0]); + $files = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ], + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ], + ] + )[0]; // <-- only one file + $model = FakedValidationModel::createWithAttributes(['attr_images' => [$files]]); + + $validator->validateAttribute($model, 'attr_images'); + $this->assertFalse($model->hasErrors('attr_images')); + } + /** * @param array $params * @return UploadedFile[] @@ -658,4 +686,132 @@ class FileValidatorTest extends TestCase ['image/jxra', 'image/jxrA', true], ]; } + + public function testValidateTypedAttributeNoErrors() + { + if (version_compare(PHP_VERSION, '7.4', '<')) { + $this->markTestSkipped('Requires typed properties'); + } + + $validator = new FileValidator(['minFiles' => 0, 'maxFiles' => 2]); + $file = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ] + ] + ); + $model = new FakedValidationTypedModel(); + $model->single = $file; + $model->multiple = [$file]; + $validator->validateAttribute($model, 'single'); + $this->assertFalse($model->hasErrors('single')); + $validator->validateAttribute($model, 'multiple'); + $this->assertFalse($model->hasErrors('multiple')); + } + + public function testValidateTypedAttributeExactMinNoErrors() + { + if (version_compare(PHP_VERSION, '7.4', '<')) { + $this->markTestSkipped('Requires typed properties'); + } + + $validator = new FileValidator(['minFiles' => 1]); + $file = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ] + ] + ); + $model = new FakedValidationTypedModel(); + $model->single = $file; + $model->multiple = [$file]; + $validator->validateAttribute($model, 'single'); + $this->assertFalse($model->hasErrors('single')); + $validator->validateAttribute($model, 'multiple'); + $this->assertFalse($model->hasErrors('multiple')); + } + + public function testValidateTypedAttributeExactMaxNoErrors() + { + if (version_compare(PHP_VERSION, '7.4', '<')) { + $this->markTestSkipped('Requires typed properties'); + } + + $validator = new FileValidator(['maxFiles' => 1]); + $file = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ] + ] + ); + $model = new FakedValidationTypedModel(); + $model->single = $file; + $model->multiple = [$file]; + $validator->validateAttribute($model, 'single'); + $this->assertFalse($model->hasErrors('single')); + $validator->validateAttribute($model, 'multiple'); + $this->assertFalse($model->hasErrors('multiple')); + } + + public function testValidateTypedAttributeMinError() + { + if (version_compare(PHP_VERSION, '7.4', '<')) { + $this->markTestSkipped('Requires typed properties'); + } + + $validator = new FileValidator(['minFiles' => 2]); + $file = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ] + ] + ); + $model = new FakedValidationTypedModel(); + $model->single = $file; + $model->multiple = [$file]; + $validator->validateAttribute($model, 'single'); + $this->assertTrue($model->hasErrors('single')); + $validator->validateAttribute($model, 'multiple'); + $this->assertTrue($model->hasErrors('multiple')); + } + + public function testValidateTypedAttributeMaxError() + { + if (version_compare(PHP_VERSION, '7.4', '<')) { + $this->markTestSkipped('Requires typed properties'); + } + + $validator = new FileValidator(['maxFiles' => 1]); + $files = $this->createTestFiles( + [ + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ], + [ + 'name' => 'image.png', + 'size' => 1024, + 'type' => 'image/png', + ] + ] + ); + $model = new FakedValidationTypedModel(); + // single attribute cannot be checked because maxFiles = 0 === no limits + $model->multiple = $files; + $validator->validateAttribute($model, 'multiple'); + $this->assertTrue($model->hasErrors('multiple')); + } } diff --git a/tests/framework/validators/RangeValidatorTest.php b/tests/framework/validators/RangeValidatorTest.php index d83151f4fa..7563f8ebee 100644 --- a/tests/framework/validators/RangeValidatorTest.php +++ b/tests/framework/validators/RangeValidatorTest.php @@ -34,7 +34,7 @@ class RangeValidatorTest extends TestCase public function testAssureMessageSetOnInit(): void { $val = new RangeValidator(['range' => []]); - $this->assertIsString('string', $val->message); + $this->assertIsString($val->message); } public function testValidateValue(): void diff --git a/tests/framework/validators/StringValidatorTest.php b/tests/framework/validators/StringValidatorTest.php index 4c6ae73dd9..320e37a62e 100644 --- a/tests/framework/validators/StringValidatorTest.php +++ b/tests/framework/validators/StringValidatorTest.php @@ -16,7 +16,7 @@ use yiiunit\TestCase; */ class StringValidatorTest extends TestCase { - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -112,9 +112,9 @@ class StringValidatorTest extends TestCase public function testEnsureMessagesOnInit(): void { $val = new StringValidator(['min' => 1, 'max' => 2]); - $this->assertIsString('string', $val->message); - $this->assertIsString('string', $val->tooLong); - $this->assertIsString('string', $val->tooShort); + $this->assertIsString($val->message); + $this->assertIsString($val->tooLong); + $this->assertIsString($val->tooShort); } public function testCustomErrorMessageInValidateAttribute(): void diff --git a/tests/framework/web/ErrorHandlerTest.php b/tests/framework/web/ErrorHandlerTest.php index c01d943d32..930cd58492 100644 --- a/tests/framework/web/ErrorHandlerTest.php +++ b/tests/framework/web/ErrorHandlerTest.php @@ -131,7 +131,7 @@ Exception: yii\web\NotFoundHttpException', $out); return [ [ "a \t=<>&\"'\x80`\n", - "a \t=<>&\"'�`\n", + "a \t=<>&"'�`\n", ], [ 'test', @@ -139,11 +139,11 @@ Exception: yii\web\NotFoundHttpException', $out); ], [ '"hello"', - '"hello"', + '"hello"', ], [ "'hello world'", - "'hello world'", + "'hello world'", ], [ 'Chip&Dale', @@ -171,7 +171,7 @@ Exception: yii\web\NotFoundHttpException', $out); $handler = Yii::$app->getErrorHandler(); $text = "a \t=<>&\"'\x80\u{20bd}`\u{000a}\u{000c}\u{0000}"; - $expected = "a \t=<>&\"'�₽`\n\u{000c}\u{0000}"; + $expected = "a \t=<>&"'�₽`\n\u{000c}\u{0000}"; $this->assertSame($expected, $handler->htmlEncode($text)); } diff --git a/tests/framework/web/FakePhp71Controller.php b/tests/framework/web/FakePhp71Controller.php index f63a385c0c..0d9b18c973 100644 --- a/tests/framework/web/FakePhp71Controller.php +++ b/tests/framework/web/FakePhp71Controller.php @@ -26,9 +26,9 @@ class FakePhp71Controller extends Controller Request $request, $between, VendorImage $vendorImage, - Post $post = null, + ?Post $post, $after - ): void { + ) { } public function actionNullableInjection(?Request $request, ?Post $post): void diff --git a/tests/framework/web/FakePhp7Controller.php b/tests/framework/web/FakePhp7Controller.php index a37c4e0d82..74008f49fa 100644 --- a/tests/framework/web/FakePhp7Controller.php +++ b/tests/framework/web/FakePhp7Controller.php @@ -17,11 +17,11 @@ class FakePhp7Controller extends Controller { public $enableCsrfValidation = false; - public function actionAksi1(int $foo, float $bar = null, bool $true, bool $false): void + public function actionAksi1(int $foo, ?float $bar, bool $true, bool $false) { } - public function actionStringy(string $foo = null): void + public function actionStringy(?string $foo = null) { } } diff --git a/tests/framework/web/RequestTest.php b/tests/framework/web/RequestTest.php index e09e3ec087..ec7aeff7a5 100644 --- a/tests/framework/web/RequestTest.php +++ b/tests/framework/web/RequestTest.php @@ -211,7 +211,125 @@ class RequestTest extends TestCase } } - public function testResolve(): void + public function testCustomSafeMethodsCsrfTokenValidation() + { + $this->mockWebApplication(); + + $request = new Request(); + $request->csrfTokenSafeMethods = ['OPTIONS']; + $request->enableCsrfCookie = false; + $request->enableCsrfValidation = true; + + $token = $request->getCsrfToken(); + + // accept any value on custom safe request + foreach (['OPTIONS'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $this->assertTrue($request->validateCsrfToken($token)); + $this->assertTrue($request->validateCsrfToken($token . 'a')); + $this->assertTrue($request->validateCsrfToken([])); + $this->assertTrue($request->validateCsrfToken([$token])); + $this->assertTrue($request->validateCsrfToken(0)); + $this->assertTrue($request->validateCsrfToken(null)); + $this->assertTrue($request->validateCsrfToken()); + } + + // only accept valid token on other requests + foreach (['GET', 'HEAD', 'POST'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $this->assertTrue($request->validateCsrfToken($token)); + $this->assertFalse($request->validateCsrfToken($token . 'a')); + $this->assertFalse($request->validateCsrfToken([])); + $this->assertFalse($request->validateCsrfToken([$token])); + $this->assertFalse($request->validateCsrfToken(0)); + $this->assertFalse($request->validateCsrfToken(null)); + $this->assertFalse($request->validateCsrfToken()); + } + } + + public function testCsrfHeaderValidation() + { + $this->mockWebApplication(); + + $request = new Request(); + $request->validateCsrfHeaderOnly = true; + $request->enableCsrfValidation = true; + + // only accept valid header on unsafe requests + foreach (['GET', 'HEAD', 'POST'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $request->headers->remove(Request::CSRF_HEADER); + $this->assertFalse($request->validateCsrfToken()); + + $request->headers->add(Request::CSRF_HEADER, ''); + $this->assertTrue($request->validateCsrfToken()); + } + + // accept no value on other requests + foreach (['DELETE', 'PATCH', 'PUT', 'OPTIONS'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $this->assertTrue($request->validateCsrfToken()); + } + } + + public function testCustomHeaderCsrfHeaderValidation() + { + $this->mockWebApplication(); + + $request = new Request(); + $request->csrfHeader = 'X-JGURDA'; + $request->validateCsrfHeaderOnly = true; + $request->enableCsrfValidation = true; + + // only accept valid header on unsafe requests + foreach (['GET', 'HEAD', 'POST'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $request->headers->remove('X-JGURDA'); + $this->assertFalse($request->validateCsrfToken()); + + $request->headers->add('X-JGURDA', ''); + $this->assertTrue($request->validateCsrfToken()); + } + } + + public function testCustomUnsafeMethodsCsrfHeaderValidation() + { + $this->mockWebApplication(); + + $request = new Request(); + $request->csrfHeaderUnsafeMethods = ['POST']; + $request->validateCsrfHeaderOnly = true; + $request->enableCsrfValidation = true; + + // only accept valid custom header on unsafe requests + foreach (['POST'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $request->headers->remove(Request::CSRF_HEADER); + $this->assertFalse($request->validateCsrfToken()); + + $request->headers->add(Request::CSRF_HEADER, ''); + $this->assertTrue($request->validateCsrfToken()); + } + + // accept no value on other requests + foreach (['GET', 'HEAD'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $request->headers->remove(Request::CSRF_HEADER); + $this->assertTrue($request->validateCsrfToken()); + } + } + + public function testNoCsrfTokenCsrfHeaderValidation() + { + $this->mockWebApplication(); + + $request = new Request(); + $request->validateCsrfHeaderOnly = true; + + $this->assertEquals($request->getCsrfToken(), null); + } + + public function testResolve() { $this->mockWebApplication([ 'components' => [ @@ -444,7 +562,6 @@ class RequestTest extends TestCase $_SERVER = []; $this->expectException(\yii\base\InvalidConfigException::class); - $request->getScriptUrl(); } diff --git a/tests/framework/web/session/AbstractDbSessionTest.php b/tests/framework/web/session/AbstractDbSessionTest.php index 276f6b5d9f..e99c198e1a 100644 --- a/tests/framework/web/session/AbstractDbSessionTest.php +++ b/tests/framework/web/session/AbstractDbSessionTest.php @@ -127,8 +127,9 @@ abstract class AbstractDbSessionTest extends TestCase $session->db->createCommand() ->update('session', ['expire' => time() - 100], 'id = :id', ['id' => 'expire']) ->execute(); - $session->gcSession(1); + $deleted = $session->gcSession(1); + $this->assertEquals(1, $deleted); $this->assertEquals('', $session->readSession('expire')); $this->assertEquals('new data', $session->readSession('new')); } diff --git a/tests/framework/web/session/SessionTest.php b/tests/framework/web/session/SessionTest.php index 7d6d59647a..8bc5c82068 100644 --- a/tests/framework/web/session/SessionTest.php +++ b/tests/framework/web/session/SessionTest.php @@ -47,8 +47,13 @@ class SessionTest extends TestCase $oldUseTransparentSession = $session->getUseTransparentSessionID(); $session->setUseTransparentSessionID(true); $newUseTransparentSession = $session->getUseTransparentSessionID(); - $this->assertNotEquals($oldUseTransparentSession, $newUseTransparentSession); - $this->assertTrue($newUseTransparentSession); + if (PHP_VERSION_ID < 80400) { + $this->assertNotEquals($oldUseTransparentSession, $newUseTransparentSession); + $this->assertTrue($newUseTransparentSession); + } else { + $this->assertEquals($oldUseTransparentSession, $newUseTransparentSession); + $this->assertFalse($newUseTransparentSession); + } //without this line phpunit will complain about risky tests due to unclosed buffer $session->setUseTransparentSessionID(false); @@ -65,6 +70,7 @@ class SessionTest extends TestCase $this->assertNotEquals($oldUseCookies, $newUseCookies); $this->assertFalse($newUseCookies); } + $session->setUseCookies($oldUseCookies); $oldGcProbability = $session->getGCProbability(); $session->setGCProbability(100); diff --git a/tests/framework/widgets/ActiveFieldTest.php b/tests/framework/widgets/ActiveFieldTest.php index f71749c2d6..679dbfbcc1 100644 --- a/tests/framework/widgets/ActiveFieldTest.php +++ b/tests/framework/widgets/ActiveFieldTest.php @@ -673,6 +673,13 @@ HTML; ]); $this->assertStringContainsString('placeholder="pholder_direct"', (string) $widget); + // use regex clientOptions instead mask + $widget = $this->activeField->widget(TestMaskedInput::className(), [ + 'options' => ['placeholder' => 'pholder_direct'], + 'clientOptions' => ['regex' => '^.*$'], + ]); + $this->assertStringContainsString('placeholder="pholder_direct"', (string) $widget); + // transfer options from ActiveField to widget $this->activeField->inputOptions = ['placeholder' => 'pholder_input']; $widget = $this->activeField->widget(TestMaskedInput::class, [ @@ -796,4 +803,3 @@ class TestMaskedInput extends MaskedInput )); } } - diff --git a/tests/js/tests/yii.activeForm.test.js b/tests/js/tests/yii.activeForm.test.js index f79599b074..506fe38a5a 100644 --- a/tests/js/tests/yii.activeForm.test.js +++ b/tests/js/tests/yii.activeForm.test.js @@ -49,7 +49,8 @@ describe('yii.activeForm', function () { jsdom({ html: html, - src: fs.readFileSync(jQueryPath, 'utf-8') + src: fs.readFileSync(jQueryPath, 'utf-8'), + url: "http://foo.bar" }); before(function () { diff --git a/tests/js/tests/yii.captcha.test.js b/tests/js/tests/yii.captcha.test.js index 9fdd1d1d95..8970b7decf 100644 --- a/tests/js/tests/yii.captcha.test.js +++ b/tests/js/tests/yii.captcha.test.js @@ -30,7 +30,8 @@ describe('yii.captcha', function () { jsdom({ html: html, - src: fs.readFileSync(jQueryPath, 'utf-8') + src: fs.readFileSync(jQueryPath, 'utf-8'), + url: "http://foo.bar" }); before(function () { diff --git a/tests/js/tests/yii.gridView.test.js b/tests/js/tests/yii.gridView.test.js index 85f5b56637..de13b4e709 100644 --- a/tests/js/tests/yii.gridView.test.js +++ b/tests/js/tests/yii.gridView.test.js @@ -51,7 +51,8 @@ describe('yii.gridView', function () { jsdom({ html: html, - src: fs.readFileSync(jQueryPath, 'utf-8') + src: fs.readFileSync(jQueryPath, 'utf-8'), + url: "http://foo.bar" }); before(function () { @@ -787,7 +788,7 @@ describe('yii.gridView', function () { assert.throws(function () { $gridView1.yiiGridView('applyFilter'); - }, "Cannot read property 'settings' of undefined"); + }, "Cannot read properties of undefined (reading \'settings\')"); $gridView1.yiiGridView(settings); // Reinitialize without "beforeFilter" and "afterFilter" event handlers $gridView1.yiiGridView('applyFilter'); diff --git a/tests/js/tests/yii.test.js b/tests/js/tests/yii.test.js index 0164dcf860..2072235002 100644 --- a/tests/js/tests/yii.test.js +++ b/tests/js/tests/yii.test.js @@ -74,7 +74,8 @@ describe('yii', function () { jsdom({ html: fs.readFileSync('tests/js/data/yii.html', 'utf-8'), - src: fs.readFileSync(jQueryPath, 'utf-8') + src: fs.readFileSync(jQueryPath, 'utf-8'), + url: "http://foo.bar" }); before(function () { diff --git a/tests/js/tests/yii.validation.test.js b/tests/js/tests/yii.validation.test.js index 3fa08f4527..5cd742c075 100644 --- a/tests/js/tests/yii.validation.test.js +++ b/tests/js/tests/yii.validation.test.js @@ -76,7 +76,8 @@ describe('yii.validation', function () { } jsdom({ - src: fs.readFileSync('vendor/bower-asset/jquery/dist/jquery.js', 'utf-8') + src: fs.readFileSync('vendor/bower-asset/jquery/dist/jquery.js', 'utf-8'), + url: "http://foo.bar" }); before(function () {