From 044e32936bff23c55ee510993ff8648727291812 Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 10:34:51 +0200 Subject: [PATCH 01/16] Added more CI tools, updated code to match phpstan level 6 --- .github/workflows/ci.yml | 55 +- .gitignore | 1 - composer.json | 16 +- composer.lock | 5511 +++++++++++++++++ phpcs.xml | 3 + phpinsights.php | 138 + phpstan.neon | 8 +- rector.php | 28 + src/Feedback.php | 16 +- src/Matcher.php | 28 +- src/Matchers/BaseMatch.php | 72 +- src/Matchers/Bruteforce.php | 19 +- src/Matchers/DateMatch.php | 188 +- src/Matchers/DictionaryMatch.php | 110 +- src/Matchers/L33tMatch.php | 93 +- src/Matchers/MatchInterface.php | 6 +- src/Matchers/RepeatMatch.php | 65 +- src/Matchers/ReverseDictionaryMatch.php | 30 +- src/Matchers/SequenceMatch.php | 62 +- src/Matchers/SpatialMatch.php | 122 +- src/Matchers/YearMatch.php | 21 +- src/Math/Binomial.php | 20 +- src/Math/BinomialProvider.php | 4 - src/Math/Impl/AbstractBinomialProvider.php | 2 +- .../AbstractBinomialProviderWithFallback.php | 7 +- src/Math/Impl/BinomialProviderInt64.php | 22 +- src/Math/Impl/BinomialProviderPhp73Gmp.php | 2 +- src/Scorer.php | 49 +- src/TimeEstimator.php | 23 +- src/Zxcvbn.php | 42 +- test/FeedbackTest.php | 13 +- test/MatcherTest.php | 20 +- test/Matchers/AbstractMatchTest.php | 45 +- test/Matchers/BruteforceTest.php | 12 +- test/Matchers/DateTest.php | 76 +- test/Matchers/DictionaryTest.php | 74 +- test/Matchers/L33tTest.php | 131 +- test/Matchers/MockL33tMatch.php | 6 + test/Matchers/MockMatch.php | 12 +- test/Matchers/RepeatTest.php | 63 +- test/Matchers/ReverseDictionaryTest.php | 15 +- test/Matchers/SequenceTest.php | 115 +- test/Matchers/SpatialTest.php | 88 +- test/Matchers/YearTest.php | 63 +- test/Math/BinomialTest.php | 39 +- test/ScorerTest.php | 52 +- test/TimeEstimatorTest.php | 68 +- test/ZxcvbnTest.php | 74 +- 48 files changed, 6655 insertions(+), 1074 deletions(-) create mode 100644 composer.lock create mode 100644 phpinsights.php create mode 100644 rector.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 340fdf4..241f418 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,12 @@ name: CI -on: push +on: + push: + branches: [ master ] + schedule: + - cron: "0 6 * * 3" + pull_request: + branches: [ master ] jobs: all: @@ -9,11 +15,8 @@ jobs: matrix: operating-system: - ubuntu-latest - # - windows-latest # Disabled - apparently checkouts have \r\n which breaks phpcs - macos-latest php-versions: - - '7.4' - - '8.0' - '8.1' - '8.2' - '8.3' @@ -21,25 +24,55 @@ jobs: name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} extensions: mbstring, intl - ini-values: post_max_size=256M, short_open_tag=On - coverage: xdebug - tools: php-cs-fixer, phpunit:7 + coverage: pcov + + - name: Get Composer Cache Directory 2 + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v4 + id: actions-cache + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Cache PHP dependencies + uses: actions/cache@v4 + id: vendor-cache + with: + path: vendor + key: ${{ runner.os }}-build-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} - name: Composer Install run: composer install --no-progress - name: Code style checks - run: ./vendor/bin/phpcs . + run: ./vendor/bin/phpcs + + - name: PHPStan code analysis + run: php vendor/bin/phpstan analyze + + - name: PHPinsights code analysis + run: php vendor/bin/phpinsights analyse --no-interaction && true + + - name: Execute Rector + run: vendor/bin/rector --dry-run - name: Unit tests run: ./vendor/bin/phpunit - - name: PHPStan - run: ./vendor/bin/phpstan analyze + - name: Send code coverage report to Codecov.io + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml diff --git a/.gitignore b/.gitignore index 5be813f..c6c60d8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,4 @@ vendor/ build/ phpunit.xml .idea/ -composer.lock .phpunit.result.cache diff --git a/composer.json b/composer.json index ae9c8a9..4ceabba 100644 --- a/composer.json +++ b/composer.json @@ -12,15 +12,18 @@ } ], "require": { - "php": "^7.2 | ^8.0 | ^8.1", + "php" : ">=8.1", "symfony/polyfill-mbstring": ">=1.3.1", "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "^8.5", + "phpunit/phpunit" : "^10 || ^11 ", "php-coveralls/php-coveralls": "*", - "squizlabs/php_codesniffer": "3.*", - "phpstan/phpstan": "^2.0" + "squizlabs/php_codesniffer" : ">=3.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-phpunit": "^2", + "rector/rector": "^2", + "nunomaduro/phpinsights": "^2" }, "suggest": { "ext-gmp": "Required for optimized binomial calculations (also requires PHP >= 7.3)" @@ -30,5 +33,10 @@ }, "autoload-dev": { "psr-4": { "ZxcvbnPhp\\Test\\": "test/" } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..a64f404 --- /dev/null +++ b/composer.lock @@ -0,0 +1,5511 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "5e242366891d201f287593f19f70a1ad", + "packages": [ + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + } + ], + "packages-dev": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "cmgmyr/phploc", + "version": "8.0.4", + "source": { + "type": "git", + "url": "https://github.com/cmgmyr/phploc.git", + "reference": "b0c4ec71f40ef84c9893e1a7212a72e1098b90f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cmgmyr/phploc/zipball/b0c4ec71f40ef84c9893e1a7212a72e1098b90f7", + "reference": "b0c4ec71f40ef84c9893e1a7212a72e1098b90f7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "php": "^7.4 || ^8.0", + "phpunit/php-file-iterator": "^3.0|^4.0|^5.0", + "sebastian/cli-parser": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpunit/phpunit": "^9.0|^10.0", + "vimeo/psalm": "^5.7" + }, + "bin": [ + "phploc" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Chris Gmyr", + "email": "cmgmyr@gmail.com", + "role": "lead" + } + ], + "description": "A tool for quickly measuring the size of a PHP project.", + "homepage": "https://github.com/cmgmyr/phploc", + "support": { + "issues": "https://github.com/cmgmyr/phploc/issues", + "source": "https://github.com/cmgmyr/phploc/tree/8.0.4" + }, + "funding": [ + { + "url": "https://github.com/cmgmyr", + "type": "github" + } + ], + "time": "2024-10-31T19:26:53+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "time": "2023-01-05T11:28:13+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.66.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "cde186799d8e92960c5a00c96e6214bf7f5547a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/cde186799d8e92960c5a00c96e6214bf7f5547a9", + "reference": "cde186799d8e92960c5a00c96e6214bf7f5547a9", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.0", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.2", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", + "sebastian/diff": "^4.0 || ^5.1 || ^6.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", + "symfony/polyfill-mbstring": "^1.31", + "symfony/polyfill-php80": "^1.31", + "symfony/polyfill-php81": "^1.31", + "symfony/process": "^5.4 || ^6.4 || ^7.2", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.4", + "infection/infection": "^0.29.8", + "justinrainbow/json-schema": "^5.3 || ^6.0", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", + "phpunit/phpunit": "^9.6.22 || ^10.5.40 || ^11.5.2", + "symfony/var-dumper": "^5.4.48 || ^6.4.15 || ^7.2.0", + "symfony/yaml": "^5.4.45 || ^6.4.13 || ^7.2.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.66.1" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2025-01-05T14:43:25+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + }, + "time": "2024-07-06T21:00:26+00:00" + }, + { + "name": "league/container", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/container.git", + "reference": "7ea728b013b9a156c409c6f0fc3624071b742dec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/container/zipball/7ea728b013b9a156c409c6f0fc3624071b742dec", + "reference": "7ea728b013b9a156c409c6f0fc3624071b742dec", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "replace": { + "orno/di": "~2.0" + }, + "require-dev": { + "nette/php-generator": "^3.4", + "nikic/php-parser": "^4.10", + "phpstan/phpstan": "^0.12.47", + "phpunit/phpunit": "^8.5.17", + "roave/security-advisories": "dev-latest", + "scrutinizer/ocular": "^1.8", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Container\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Phil Bennett", + "email": "mail@philbennett.co.uk", + "role": "Developer" + } + ], + "description": "A fast and intuitive dependency injection container.", + "homepage": "https://github.com/thephpleague/container", + "keywords": [ + "container", + "dependency", + "di", + "injection", + "league", + "provider", + "service" + ], + "support": { + "issues": "https://github.com/thephpleague/container/issues", + "source": "https://github.com/thephpleague/container/tree/4.2.4" + }, + "funding": [ + { + "url": "https://github.com/philipobenito", + "type": "github" + } + ], + "time": "2024-11-10T12:42:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "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", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-11-08T17:47:46+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "nunomaduro/phpinsights", + "version": "v2.12.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/phpinsights.git", + "reference": "5c12a8d626712de6db5e6d2db52b1eb4e9596650" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/phpinsights/zipball/5c12a8d626712de6db5e6d2db52b1eb4e9596650", + "reference": "5c12a8d626712de6db5e6d2db52b1eb4e9596650", + "shasum": "" + }, + "require": { + "cmgmyr/phploc": "^8.0.3", + "composer/semver": "^3.4", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "friendsofphp/php-cs-fixer": "^3.40.0", + "justinrainbow/json-schema": "^5.2.13", + "league/container": "^3.2|^4.2", + "php": "^7.4|^8.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "psr/container": "^1.0|^2.0.2", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "sebastian/diff": "^4.0|^5.0.3|^6.0", + "slevomat/coding-standard": "^8.14.1", + "squizlabs/php_codesniffer": "^3.7.2", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.4|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0" + }, + "require-dev": { + "ergebnis/phpstan-rules": "^0.15.3", + "illuminate/console": "^5.8|^6.0|^7.0|^8.0|^9.20|^10.0", + "illuminate/support": "^5.8|^6.0|^7.0|^8.0|^9.52.16|^10.0", + "mockery/mockery": "^1.6.6", + "phpstan/phpstan-strict-rules": "^0.12.11", + "phpunit/phpunit": "^8.0|^9.0|^10.4.2", + "rector/rector": "0.11.56", + "symfony/var-dumper": "^5.4|^6.0|^7.0", + "thecodingmachine/phpstan-strict-rules": "^0.12.2" + }, + "suggest": { + "ext-simplexml": "It is needed for the checkstyle formatter" + }, + "bin": [ + "bin/phpinsights" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\PhpInsights\\Application\\Adapters\\Laravel\\InsightsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NunoMaduro\\PhpInsights\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Instant PHP quality checks from your console.", + "keywords": [ + "Insights", + "code", + "console", + "php", + "quality", + "source" + ], + "support": { + "issues": "https://github.com/nunomaduro/phpinsights/issues", + "source": "https://github.com/nunomaduro/phpinsights/tree/v2.12.0" + }, + "funding": [ + { + "url": "https://github.com/JustSteveKing", + "type": "github" + }, + { + "url": "https://github.com/cmgmyr", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2024-11-11T14:42:55+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "php-parallel-lint/php-parallel-lint", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6db563514f27e19595a19f45a4bf757b6401194e", + "reference": "6db563514f27e19595a19f45a4bf757b6401194e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3.0" + }, + "replace": { + "grogy/php-parallel-lint": "*", + "jakub-onderka/php-parallel-lint": "*" + }, + "require-dev": { + "nette/tester": "^1.3 || ^2.0", + "php-parallel-lint/php-console-highlighter": "0.* || ^1.0", + "squizlabs/php_codesniffer": "^3.6" + }, + "suggest": { + "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet" + }, + "bin": [ + "parallel-lint" + ], + "type": "library", + "autoload": { + "classmap": [ + "./src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "ahoj@jakubonderka.cz" + } + ], + "description": "This tool checks the syntax of PHP files about 20x faster than serial check.", + "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint", + "keywords": [ + "lint", + "static analysis" + ], + "support": { + "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues", + "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.4.0" + }, + "time": "2024-03-27T12:14:49+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.33.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0" + }, + "time": "2024-10-13T11:25:22+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7", + "reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-01-05T16:43:48+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "e32ac656788a5bf3dedda89e6a2cad5643bf1a18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/e32ac656788a5bf3dedda89e6a2cad5643bf1a18", + "reference": "e32ac656788a5bf3dedda89e6a2cad5643bf1a18", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.4" + }, + "conflict": { + "phpunit/phpunit": "<7.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPUnit extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.3" + }, + "time": "2024-12-19T09:14:43+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "418c59fd080954f8c4aa5631d9502ecda2387118" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", + "reference": "418c59fd080954f8c4aa5631d9502ecda2387118", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.3.1", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.0" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "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/11.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-12-11T12:34:27+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-27T05:02:59+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "153d0531b9f7e883c5053160cad6dd5ac28140b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/153d0531b9f7e883c5053160cad6dd5ac28140b3", + "reference": "153d0531b9f7e883c5053160cad6dd5ac28140b3", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.8", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.2", + "sebastian/comparator": "^6.2.1", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.3.0", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.0", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.2" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-12-21T05:51:08+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.6", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.6" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-01-01T16:37:48+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "rector/rector", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "fa0cb009dc3df084bf549032ae4080a0481a2036" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/fa0cb009dc3df084bf549032ae4080a0481a2036", + "reference": "fa0cb009dc3df084bf549032ae4080a0481a2036", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.1" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.0.6" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2025-01-06T10:38:36+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", + "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-12-12T09:59:06+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/d4e47a769525c4dd38cea90e5dcd435ddbbc7115", + "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-01-06T10:28:19+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:54:44+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-12-05T09:17:50+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:10:34+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-09-17T13:12:04+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "8.15.0", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "7d1d957421618a3803b593ec31ace470177d7817" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7d1d957421618a3803b593ec31ace470177d7817", + "reference": "7d1d957421618a3803b593ec31ace470177d7817", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", + "php": "^7.2 || ^8.0", + "phpstan/phpdoc-parser": "^1.23.1", + "squizlabs/php_codesniffer": "^3.9.0" + }, + "require-dev": { + "phing/phing": "2.17.4", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpstan/phpstan": "1.10.60", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.16", + "phpstan/phpstan-strict-rules": "1.5.2", + "phpunit/phpunit": "8.5.21|9.6.8|10.5.11" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.15.0" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2024-03-09T15:20:58+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.11.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1368f4a58c3c52114b86b1abe8f4098869cb0079", + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-12-11T16:04:26+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "e7e983596b744c4539f31e79b0350a6cf5878a20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/e7e983596b744c4539f31e79b0350a6cf5878a20", + "reference": "e7e983596b744c4539f31e79b0350a6cf5878a20", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^2.5|^3", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T08:08:50+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/console", + "version": "v7.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.2.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-11T03:49:26+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:15:23+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", + "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-30T19:00:17+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "339ba21476eb184290361542f732ad12c97591ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/339ba21476eb184290361542f732ad12c97591ec", + "reference": "339ba21476eb184290361542f732ad12c97591ec", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-30T18:35:15+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T08:49:48+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-20T11:17:29+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-06T14:24:19+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "e46690d5b9d7164a6d061cab1e8d46141b9f49df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e46690d5b9d7164a6d061cab1e8d46141b9f49df", + "reference": "e46690d5b9d7164a6d061cab1e8d46141b9f49df", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-18T14:28:33+00:00" + }, + { + "name": "symfony/string", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:26+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "1a6a89f95a46af0f142874c9d650a6358d13070d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/1a6a89f95a46af0f142874c9d650a6358d13070d", + "reference": "1a6a89f95a46af0f142874c9d650a6358d13070d", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-18T07:58:17+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.1", + "ext-json": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/phpcs.xml b/phpcs.xml index 6a2b2d3..155e9ac 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -4,6 +4,9 @@ + src + test + diff --git a/phpinsights.php b/phpinsights.php new file mode 100644 index 0000000..654b934 --- /dev/null +++ b/phpinsights.php @@ -0,0 +1,138 @@ + 'default', + + /* + |-------------------------------------------------------------------------- + | IDE + |-------------------------------------------------------------------------- + | + | This options allow to add hyperlinks in your terminal to quickly open + | files in your favorite IDE while browsing your PhpInsights report. + | + | Supported: "textmate", "macvim", "emacs", "sublime", "phpstorm", + | "atom", "vscode". + | + | If you have another IDE that is not in this list but which provide an + | url-handler, you could fill this config with a pattern like this: + | + | myide://open?url=file://%f&line=%l + | + */ + + 'ide' => null, + + /* + |-------------------------------------------------------------------------- + | Configuration + |-------------------------------------------------------------------------- + | + | Here you may adjust all the various `Insights` that will be used by PHP + | Insights. You can either add, remove or configure `Insights`. Keep in + | mind, that all added `Insights` must belong to a specific `Metric`. + | + */ + + 'exclude' => [ + 'data/', + 'data-scripts/', + ], + + 'add' => [ + ], + + 'remove' => [ + ForbiddenSetterSniff::class, + DisallowMixedTypeHintSniff::class, + LineLengthSniff::class, + FunctionLengthSniff::class, + SuperfluousAbstractClassNamingSniff::class, + SuperfluousExceptionNamingSniff::class, + SuperfluousInterfaceNamingSniff::class, + ForbiddenNormalClasses::class, + CyclomaticComplexityIsHigh::class, + UnusedParameterSniff::class, + TodoSniff::class, + ForbiddenPublicPropertySniff::class, + ], + + 'config' => [ + // ExampleInsight::class => [ + // 'key' => 'value', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Requirements + |-------------------------------------------------------------------------- + | + | Here you may define a level you want to reach per `Insights` category. + | When a score is lower than the minimum level defined, then an error + | code will be returned. This is optional and individually defined. + | + */ + + 'requirements' => [ + 'min-quality' => 100, + 'min-complexity' => 0, + 'min-architecture' => 100, + 'min-style' => 100, + ], + + /* + |-------------------------------------------------------------------------- + | Threads + |-------------------------------------------------------------------------- + | + | Here you may adjust how many threads (core) PHPInsights can use to perform + | the analysis. This is optional, don't provide it and the tool will guess + | the max core number available. It accepts null value or integer > 0. + | + */ + + 'threads' => null, + + /* + |-------------------------------------------------------------------------- + | Timeout + |-------------------------------------------------------------------------- + | Here you may adjust the timeout (in seconds) for PHPInsights to run before + | a ProcessTimedOutException is thrown. + | This accepts an int > 0. Default is 60 seconds, which is the default value + | of Symfony's setTimeout function. + | + */ + + 'timeout' => 60, +]; diff --git a/phpstan.neon b/phpstan.neon index ac41719..d438334 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 0 - paths: - - src - - test + level: 6 + paths: + - src + - test diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..17e138d --- /dev/null +++ b/rector.php @@ -0,0 +1,28 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/test', + ]) + // uncomment to reach your current PHP version + ->withPhpSets() + ->withSets( + [ + PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES, + PHPUnitSetList::PHPUNIT_80, + PHPUnitSetList::PHPUNIT_90, + PHPUnitSetList::PHPUNIT_100, + PHPUnitSetList::PHPUNIT_110, + PHPUnitSetList::PHPUNIT_CODE_QUALITY, + ] + ) + ->withRules([ + AddVoidReturnTypeWhereNoReturnRector::class, + ]); diff --git a/src/Feedback.php b/src/Feedback.php index 2982e3d..9aa5ba5 100644 --- a/src/Feedback.php +++ b/src/Feedback.php @@ -4,7 +4,7 @@ namespace ZxcvbnPhp; -use ZxcvbnPhp\Matchers\MatchInterface; +use ZxcvbnPhp\Matchers\BaseMatch; /** * Feedback - gives some user guidance based on the strength @@ -15,19 +15,19 @@ class Feedback { /** - * @param int $score - * @param MatchInterface[] $sequence - * @return array + * @param array $sequence + * + * @return array */ public function getFeedback(int $score, array $sequence): array { // starting feedback if (count($sequence) === 0) { return [ - 'warning' => '', + 'warning' => '', 'suggestions' => [ - "Use a few words, avoid common phrases", - "No need for symbols, digits, or uppercase letters", + 'Use a few words, avoid common phrases', + 'No need for symbols, digits, or uppercase letters', ], ]; } @@ -35,7 +35,7 @@ public function getFeedback(int $score, array $sequence): array // no feedback if score is good or great. if ($score > 2) { return [ - 'warning' => '', + 'warning' => '', 'suggestions' => [], ]; } diff --git a/src/Matcher.php b/src/Matcher.php index c843ab6..849e427 100644 --- a/src/Matcher.php +++ b/src/Matcher.php @@ -20,39 +20,45 @@ class Matcher Matchers\YearMatch::class, ]; - private $additionalMatchers = []; + /** + * @var array + */ + private array $additionalMatchers = []; /** * Get matches for a password. * * @param string $password Password string to match - * @param array $userInputs Array of values related to the user (optional) + * @param array $userInputs Array of values related to the user (optional) + * * @code array('Alice Smith') + * * @endcode * - * @return MatchInterface[] Array of Match objects. + * @return array Array of Match objects. * * @see zxcvbn/src/matching.coffee::omnimatch */ public function getMatches(string $password, array $userInputs = []): array { $matches = []; + /** @var MatchInterface $matcher */ foreach ($this->getMatchers() as $matcher) { $matched = $matcher::match($password, $userInputs); - if (is_array($matched) && !empty($matched)) { + if (! empty($matched)) { $matches[] = $matched; } } $matches = array_merge([], ...$matches); - self::usortStable($matches, [$this, 'compareMatches']); + self::usortStable($matches, $this->compareMatches(...)); return $matches; } public function addMatcher(string $className): self { - if (!is_a($className, MatchInterface::class, true)) { + if (! is_a($className, MatchInterface::class, true)) { throw new \InvalidArgumentException(sprintf('Matcher class must implement %s', MatchInterface::class)); } @@ -71,9 +77,7 @@ public function addMatcher(string $className): self * This function taken from https://github.com/vanderlee/PHP-stable-sort-functions * Copyright © 2015-2018 Martijn van der Lee (http://martijn.vanderlee.com). MIT License applies. * - * @param array $array - * @param callable $value_compare_func - * @return bool + * @param array $array */ public static function usortStable(array &$array, callable $value_compare_func): bool { @@ -81,9 +85,9 @@ public static function usortStable(array &$array, callable $value_compare_func): foreach ($array as &$item) { $item = [$index++, $item]; } - $result = usort($array, function ($a, $b) use ($value_compare_func) { + $result = usort($array, static function ($a, $b) use ($value_compare_func) { $result = $value_compare_func($a[1], $b[1]); - return $result == 0 ? $a[0] - $b[0] : $result; + return $result === 0 ? $a[0] - $b[0] : $result; }); foreach ($array as &$item) { $item = $item[1]; @@ -103,7 +107,7 @@ public static function compareMatches(BaseMatch $a, BaseMatch $b): int /** * Load available Match objects to match against a password. * - * @return array Array of classes implementing MatchInterface + * @return array Array of classes implementing BaseMatch */ protected function getMatchers(): array { diff --git a/src/Matchers/BaseMatch.php b/src/Matchers/BaseMatch.php index 97f77d3..f8f826e 100644 --- a/src/Matchers/BaseMatch.php +++ b/src/Matchers/BaseMatch.php @@ -9,37 +9,10 @@ abstract class BaseMatch implements MatchInterface { - /** - * @var - */ - public $password; - - /** - * @var - */ - public $begin; - - /** - * @var - */ - public $end; + public string $pattern = ''; - /** - * @var - */ - public $token; - - /** - * @var - */ - public $pattern; - - public function __construct(string $password, int $begin, int $end, string $token) + public function __construct(public string $password, public int $begin, public int $end, public string $token) { - $this->password = $password; - $this->begin = $begin; - $this->end = $end; - $this->token = $token; } /** @@ -47,7 +20,8 @@ public function __construct(string $password, int $begin, int $end, string $toke * * @param bool $isSoleMatch * Whether this is the only match in the password - * @return array{'warning': string, "suggestions": string[]} + * + * @return array{'warning': string, "suggestions": array} */ abstract public function getFeedback(bool $isSoleMatch): array; @@ -58,8 +32,8 @@ abstract public function getFeedback(bool $isSoleMatch): array; * String to search. * @param string $regex * Regular expression with captures. - * @param int $offset - * @return array + * + * @return array * Array of capture groups. Captures in a group have named indexes: 'begin', 'end', 'token'. * e.g. fishfish /(fish)/ * array( @@ -82,7 +56,7 @@ public static function findAll(string $string, string $regex, int $offset = 0): $byteOffset = strlen($charsBeforeOffset); $count = preg_match_all($regex, $string, $matches, PREG_SET_ORDER, $byteOffset); - if (!$count) { + if (! $count) { return []; } @@ -90,16 +64,16 @@ public static function findAll(string $string, string $regex, int $offset = 0): foreach ($matches as $group) { $captureBegin = 0; $match = array_shift($group); - $matchBegin = mb_strpos($string, $match, $offset); + $matchBegin = mb_strpos($string, (string) $match, $offset); $captures = [ [ 'begin' => $matchBegin, - 'end' => $matchBegin + mb_strlen($match) - 1, + 'end' => $matchBegin + mb_strlen((string) $match) - 1, 'token' => $match, ], ]; foreach ($group as $capture) { - $captureBegin = mb_strpos($match, $capture, $captureBegin); + $captureBegin = mb_strpos((string) $match, $capture, $captureBegin); $captures[] = [ 'begin' => $matchBegin + $captureBegin, 'end' => $matchBegin + $captureBegin + mb_strlen($capture) - 1, @@ -107,7 +81,7 @@ public static function findAll(string $string, string $regex, int $offset = 0): ]; } $groups[] = $captures; - $offset += mb_strlen($match) - 1; + $offset += mb_strlen((string) $match) - 1; } return $groups; } @@ -115,9 +89,6 @@ public static function findAll(string $string, string $regex, int $offset = 0): /** * Calculate binomial coefficient (n choose k). * - * @param int $n - * @param int $k - * @return float * @deprecated Use {@see Binomial::binom()} instead */ public static function binom(int $n, int $k): float @@ -125,27 +96,26 @@ public static function binom(int $n, int $k): float return Binomial::binom($n, $k); } - abstract protected function getRawGuesses(): float; - public function getGuesses(): float { return max($this->getRawGuesses(), $this->getMinimumGuesses()); } + public function getGuessesLog10(): float + { + return log10($this->getGuesses()); + } + + abstract protected function getRawGuesses(): float; + protected function getMinimumGuesses(): float { - if (mb_strlen($this->token) < mb_strlen($this->password)) { - if (mb_strlen($this->token) === 1) { + if (mb_strlen((string) $this->token) < mb_strlen((string) $this->password)) { + if (mb_strlen((string) $this->token) === 1) { return Scorer::MIN_SUBMATCH_GUESSES_SINGLE_CHAR; - } else { - return Scorer::MIN_SUBMATCH_GUESSES_MULTI_CHAR; } + return Scorer::MIN_SUBMATCH_GUESSES_MULTI_CHAR; } return 0; } - - public function getGuessesLog10(): float - { - return log10($this->getGuesses()); - } } diff --git a/src/Matchers/Bruteforce.php b/src/Matchers/Bruteforce.php index 3e08223..bda81c8 100644 --- a/src/Matchers/Bruteforce.php +++ b/src/Matchers/Bruteforce.php @@ -10,12 +10,10 @@ final class Bruteforce extends BaseMatch { public const BRUTEFORCE_CARDINALITY = 10; - public $pattern = 'bruteforce'; + public string $pattern = 'bruteforce'; /** - * @param string $password - * @param array $userInputs - * @return Bruteforce[] + * @return array */ public static function match(string $password, array $userInputs = []): array { @@ -24,29 +22,28 @@ public static function match(string $password, array $userInputs = []): array return [$match]; } - /** - * @return array{'warning': string, "suggestions": string[]} + * @return array{'warning': string, "suggestions": array} */ public function getFeedback(bool $isSoleMatch): array { return [ - 'warning' => "", + 'warning' => '', 'suggestions' => [ - ] + ], ]; } public function getRawGuesses(): float { - $guesses = pow(self::BRUTEFORCE_CARDINALITY, mb_strlen($this->token)); - if ($guesses === INF) { + $guesses = self::BRUTEFORCE_CARDINALITY ** mb_strlen((string) $this->token); + if ($guesses >= PHP_FLOAT_MAX) { return PHP_FLOAT_MAX; } // small detail: make bruteforce matches at minimum one guess bigger than smallest allowed // submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence. - if (mb_strlen($this->token) === 1) { + if (mb_strlen((string) $this->token) === 1) { $minGuesses = Scorer::MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1; } else { $minGuesses = Scorer::MIN_SUBMATCH_GUESSES_MULTI_CHAR + 1; diff --git a/src/Matchers/DateMatch.php b/src/Matchers/DateMatch.php index 6db9cf9..680ab03 100644 --- a/src/Matchers/DateMatch.php +++ b/src/Matchers/DateMatch.php @@ -18,16 +18,42 @@ class DateMatch extends BaseMatch public const MIN_YEAR_SPACE = 20; - public $pattern = 'date'; + protected const DATE_NO_SEPARATOR = '/^\d{4,8}$/u'; + + /** + * (\d{1,4}) # day, month, year + * ([\s\/\\\\_.-]) # separator + * (\d{1,2}) # day, month + * \2 # same separator + * (\d{1,4}) # day, month, year + */ + protected const DATE_WITH_SEPARATOR = '/^(\d{1,4})([\s\/\\\\_.-])(\d{1,2})\2(\d{1,4})$/u'; + + public string $pattern = 'date'; + + /** @var int The day portion of the date in the token. */ + public int $day; + + /** @var int The month portion of the date in the token. */ + public int $month; + + /** @var int The year portion of the date in the token. */ + public int $year; - private static $DATE_SPLITS = [ + /** @var string The separator used for the date in the token. */ + public string $separator; + + /** + * @var array + */ + private static array $DATE_SPLITS = [ 4 => [ # For length-4 strings, eg 1191 or 9111, two ways to split: [1, 2], # 1 1 91 (2nd split starts at index 1, 3rd at index 2) [2, 3], # 91 1 1 ], 5 => [ [1, 3], # 1 11 91 - [2, 3] # 11 1 91 + [2, 3], # 11 1 91 ], 6 => [ [1, 2], # 1 1 1991 @@ -46,35 +72,24 @@ class DateMatch extends BaseMatch ], ]; - protected const DATE_NO_SEPARATOR = '/^\d{4,8}$/u'; - /** - * (\d{1,4}) # day, month, year - * ([\s\/\\\\_.-]) # separator - * (\d{1,2}) # day, month - * \2 # same separator - * (\d{1,4}) # day, month, year + * @param array{'day': int, 'month': int, 'year': int, 'separator': string} $params */ - protected const DATE_WITH_SEPARATOR = '/^(\d{1,4})([\s\/\\\\_.-])(\d{1,2})\2(\d{1,4})$/u'; - - /** @var int The day portion of the date in the token. */ - public $day; - - /** @var int The month portion of the date in the token. */ - public $month; - - /** @var int The year portion of the date in the token. */ - public $year; - - /** @var string The separator used for the date in the token. */ - public $separator; + public function __construct(string $password, int $begin, int $end, string $token, array $params) + { + parent::__construct($password, $begin, $end, $token); + $this->day = $params['day']; + $this->month = $params['month']; + $this->year = $params['year']; + $this->separator = $params['separator']; + } /** - * Match occurences of dates in a password + * Match occurrences of dates in a password * - * @param string $password - * @param array $userInputs - * @return DateMatch[] + * @param array $userInputs + * + * @return array */ public static function match(string $password, array $userInputs = []): array { @@ -104,45 +119,32 @@ public static function match(string $password, array $userInputs = []): array foreach ($dates as $date) { $matches[] = new static($password, $date['begin'], $date['end'], $date['token'], $date); } - Matcher::usortStable($matches, [Matcher::class, 'compareMatches']); + Matcher::usortStable($matches, Matcher::compareMatches(...)); return $matches; } /** - * @return array{'warning': string, "suggestions": string[]} + * @return array{'warning': string, "suggestions": array} */ public function getFeedback(bool $isSoleMatch): array { return [ - 'warning' => "Dates are often easy to guess", + 'warning' => 'Dates are often easy to guess', 'suggestions' => [ - 'Avoid dates and years that are associated with you' - ] + 'Avoid dates and years that are associated with you', + ], ]; } - /** - * @param string $password - * @param int $begin - * @param int $end - * @param string $token - * @param array $params An array with keys: [day, month, year, separator]. - */ - public function __construct(string $password, int $begin, int $end, string $token, array $params) + public static function getReferenceYear(): int { - parent::__construct($password, $begin, $end, $token); - $this->day = $params['day']; - $this->month = $params['month']; - $this->year = $params['year']; - $this->separator = $params['separator']; + return (int) date('Y'); } /** * Find dates with separators in a password. * - * @param string $password - * - * @return array + * @return array */ protected static function datesWithSeparators(string $password): array { @@ -154,14 +156,14 @@ protected static function datesWithSeparators(string $password): array for ($end = $begin + 5; $end - $begin < 10 && $end < $length; $end++) { $token = mb_substr($password, $begin, $end - $begin + 1); - if (!preg_match(static::DATE_WITH_SEPARATOR, $token, $captures)) { + if (! preg_match(self::DATE_WITH_SEPARATOR, $token, $captures)) { continue; } $date = static::checkDate([ (int) $captures[1], (int) $captures[3], - (int) $captures[4] + (int) $captures[4], ]); if ($date === false) { @@ -186,9 +188,7 @@ protected static function datesWithSeparators(string $password): array /** * Find dates without separators in a password. * - * @param string $password - * - * @return array + * @return array */ protected static function datesWithoutSeparators(string $password): array { @@ -200,17 +200,17 @@ protected static function datesWithoutSeparators(string $password): array for ($end = $begin + 3; $end - $begin < 8 && $end < $length; $end++) { $token = mb_substr($password, $begin, $end - $begin + 1); - if (!preg_match(static::DATE_NO_SEPARATOR, $token)) { + if (! preg_match(self::DATE_NO_SEPARATOR, $token)) { continue; } $candidates = []; - $possibleSplits = static::$DATE_SPLITS[mb_strlen($token)]; + $possibleSplits = self::$DATE_SPLITS[mb_strlen($token)]; foreach ($possibleSplits as $splitPositions) { - $day = (int)mb_substr($token, 0, $splitPositions[0]); - $month = (int)mb_substr($token, $splitPositions[0], $splitPositions[1] - $splitPositions[0]); - $year = (int)mb_substr($token, $splitPositions[1]); + $day = (int) mb_substr($token, 0, $splitPositions[0]); + $month = (int) mb_substr($token, $splitPositions[0], $splitPositions[1] - $splitPositions[0]); + $year = (int) mb_substr($token, $splitPositions[1]); $date = static::checkDate([$day, $month, $year]); if ($date !== false) { @@ -250,7 +250,7 @@ protected static function datesWithoutSeparators(string $password): array 'separator' => '', 'day' => $day, 'month' => $month, - 'year' => $year + 'year' => $year, ]; } } @@ -259,25 +259,22 @@ protected static function datesWithoutSeparators(string $password): array } /** - * @param array $candidate * @return int Returns the number of years between the detected year and the current year for a candidate. + * + * @param array $candidate */ protected static function getDistanceForMatchCandidate(array $candidate): int { - return abs((int)$candidate['year'] - static::getReferenceYear()); - } - - public static function getReferenceYear(): int - { - return (int)date('Y'); + return abs((int) $candidate['year'] - static::getReferenceYear()); } /** - * @param int[] $ints Three numbers in an array representing day, month and year (not necessarily in that order). - * @return array|bool Returns an associative array containing 'day', 'month' and 'year' keys, or false if the + * @param array $ints Three numbers in an array representing day, month and year (not necessarily in that order). + * + * @return array|false Returns an associative array containing 'day', 'month' and 'year' keys, or false if the * provided date array is invalid. */ - protected static function checkDate(array $ints) + protected static function checkDate(array $ints): array|false { # given a 3-tuple, discard if: # middle int is over 31 (for all dmy formats, years are never allowed in the middle) @@ -291,25 +288,17 @@ protected static function checkDate(array $ints) return false; } - $invalidYear = count(array_filter($ints, function (int $int): bool { - return ($int >= 100 && $int < static::MIN_YEAR) - || ($int > static::MAX_YEAR); - })); + $invalidYear = count(array_filter($ints, static fn (int $int): bool => ($int >= 100 && $int < self::MIN_YEAR) + || ($int > self::MAX_YEAR))); if ($invalidYear > 0) { return false; } - $over12 = count(array_filter($ints, function (int $int): bool { - return $int > 12; - })); - $over31 = count(array_filter($ints, function (int $int): bool { - return $int > 31; - })); - $under1 = count(array_filter($ints, function (int $int): bool { - return $int <= 0; - })); - - if ($over31 >= 2 || $over12 == 3 || $under1 >= 2) { + $over12 = count(array_filter($ints, static fn (int $int): bool => $int > 12)); + $over31 = count(array_filter($ints, static fn (int $int): bool => $int > 31)); + $under1 = count(array_filter($ints, static fn (int $int): bool => $int <= 0)); + + if ($over31 >= 2 || $over12 === 3 || $under1 >= 2) { return false; } @@ -320,12 +309,12 @@ protected static function checkDate(array $ints) ]; foreach ($possibleYearSplits as [$year, $rest]) { - if ($year >= static::MIN_YEAR && $year <= static::MAX_YEAR) { + if ($year >= self::MIN_YEAR && $year <= self::MAX_YEAR) { if ($dm = static::mapIntsToDayMonth($rest)) { return [ - 'year' => $year, + 'year' => $year, 'month' => $dm['month'], - 'day' => $dm['day'], + 'day' => $dm['day'], ]; } # for a candidate that includes a four-digit year, @@ -338,9 +327,9 @@ protected static function checkDate(array $ints) foreach ($possibleYearSplits as [$year, $rest]) { if ($dm = static::mapIntsToDayMonth($rest)) { return [ - 'year' => static::twoToFourDigitYear($year), + 'year' => static::twoToFourDigitYear($year), 'month' => $dm['month'], - 'day' => $dm['day'], + 'day' => $dm['day'], ]; } } @@ -349,17 +338,18 @@ protected static function checkDate(array $ints) } /** - * @param int[] $ints Two numbers in an array representing day and month (not necessarily in that order). - * @return array|bool Returns an associative array containing 'day' and 'month' keys, or false if any combination + * @param array $ints Two numbers in an array representing day and month (not necessarily in that order). + * + * @return array|false Returns an associative array containing 'day' and 'month' keys, or false if any combination * of the two numbers does not match a day and month. */ - protected static function mapIntsToDayMonth(array $ints) + protected static function mapIntsToDayMonth(array $ints): array|false { foreach ([$ints, array_reverse($ints)] as [$d, $m]) { if ($d >= 1 && $d <= 31 && $m >= 1 && $m <= 12) { return [ - 'day' => $d, - 'month' => $m + 'day' => $d, + 'month' => $m, ]; } } @@ -369,6 +359,7 @@ protected static function mapIntsToDayMonth(array $ints) /** * @param int $year A two digit number representing a year. + * * @return int Returns the most likely four digit year for the provided number. */ protected static function twoToFourDigitYear(int $year): int @@ -395,12 +386,13 @@ protected static function twoToFourDigitYear(int $year): int * '2015_06_04', in addition to matching 2015_06_04, will also contain * 5(!) other date matches: 15_06_04, 5_06_04, ..., even 2015 (matched as 5/1/2020) * - * @param array $matches An array of matches (not Match objects) - * @return array The provided array of matches, but with matches that are strict substrings of others removed. + * @param array $matches An array of matches (not Match objects) + * + * @return array The provided array of matches, but with matches that are strict substrings of others removed. */ protected static function removeRedundantMatches(array $matches): array { - return array_filter($matches, function (array $match) use ($matches): bool { + return array_filter($matches, static function (array $match) use ($matches): bool { foreach ($matches as $otherMatch) { if ($match === $otherMatch) { continue; @@ -417,7 +409,7 @@ protected static function removeRedundantMatches(array $matches): array protected function getRawGuesses(): float { // base guesses: (year distance from REFERENCE_YEAR) * num_days * num_years - $yearSpace = max(abs($this->year - static::getReferenceYear()), static::MIN_YEAR_SPACE); + $yearSpace = max(abs($this->year - static::getReferenceYear()), self::MIN_YEAR_SPACE); $guesses = $yearSpace * 365; // add factor of 4 for separator selection (one of ~4 choices) diff --git a/src/Matchers/DictionaryMatch.php b/src/Matchers/DictionaryMatch.php index 7be2340..eac6eb6 100644 --- a/src/Matchers/DictionaryMatch.php +++ b/src/Matchers/DictionaryMatch.php @@ -10,38 +10,49 @@ /** @phpstan-consistent-constructor */ class DictionaryMatch extends BaseMatch { - public $pattern = 'dictionary'; + protected const START_UPPER = '/^[A-Z][^A-Z]+$/u'; + protected const END_UPPER = '/^[^A-Z]+[A-Z]$/u'; + protected const ALL_UPPER = '/^[^a-z]+$/u'; + protected const ALL_LOWER = '/^[^A-Z]+$/u'; + public string $pattern = 'dictionary'; /** @var string The name of the dictionary that the token was found in. */ - public $dictionaryName; + public string $dictionaryName = ''; /** @var int The rank of the token in the dictionary. */ - public $rank; + public int $rank = 0; /** @var string The word that was matched from the dictionary. */ - public $matchedWord; + public string $matchedWord = ''; /** @var bool Whether or not the matched word was reversed in the token. */ - public $reversed = false; + public bool $reversed = false; /** @var bool Whether or not the token contained l33t substitutions. */ - public $l33t = false; + public bool $l33t = false; - /** @var array A cache of the frequency_lists json file */ - protected static $rankedDictionaries = []; + /** @var array A cache of the frequency_lists json file */ + protected static array $rankedDictionaries = []; - protected const START_UPPER = "/^[A-Z][^A-Z]+$/u"; - protected const END_UPPER = "/^[^A-Z]+[A-Z]$/u"; - protected const ALL_UPPER = "/^[^a-z]+$/u"; - protected const ALL_LOWER = "/^[^A-Z]+$/u"; + /** + * @param array{'dictionary_name'?: string, 'matched_word'?: string, 'rank'?: int} $params + */ + public function __construct(string $password, int $begin, int $end, string $token, array $params = []) + { + parent::__construct($password, $begin, $end, $token); + + $this->dictionaryName = $params['dictionary_name'] ?? ''; + $this->matchedWord = $params['matched_word'] ?? ''; + $this->rank = $params['rank'] ?? 0; + } /** * Match occurrences of dictionary words in password. * - * @param string $password - * @param array $userInputs - * @param array $rankedDictionaries - * @return DictionaryMatch[] + * @param array $userInputs + * @param array $rankedDictionaries + * + * @return array */ public static function match(string $password, array $userInputs = [], array $rankedDictionaries = []): array { @@ -52,10 +63,10 @@ public static function match(string $password, array $userInputs = [], array $ra $dicts = static::getRankedDictionaries(); } - if (!empty($userInputs)) { + if (! empty($userInputs)) { $dicts['user_inputs'] = []; foreach ($userInputs as $rank => $input) { - $input_lower = mb_strtolower($input); + $input_lower = mb_strtolower((string) $input); $dicts['user_inputs'][$input_lower] = $rank + 1; // rank starts at 1, not 0 } } @@ -66,29 +77,12 @@ public static function match(string $password, array $userInputs = [], array $ra $matches[] = new static($password, $result['begin'], $result['end'], $result['token'], $result); } } - Matcher::usortStable($matches, [Matcher::class, 'compareMatches']); + Matcher::usortStable($matches, Matcher::compareMatches(...)); return $matches; } /** - * @param string $password - * @param int $begin - * @param int $end - * @param string $token - * @param array $params An array with keys: [dictionary_name, matched_word, rank]. - */ - public function __construct(string $password, int $begin, int $end, string $token, array $params = []) - { - parent::__construct($password, $begin, $end, $token); - if (!empty($params)) { - $this->dictionaryName = $params['dictionary_name'] ?? ''; - $this->matchedWord = $params['matched_word'] ?? ''; - $this->rank = $params['rank'] ?? 0; - } - } - - /** - * @return array{'warning': string, "suggestions": string[]} + * @return array{warning: string, suggestions: array} */ public function getFeedback(bool $isSoleMatch): array { @@ -97,13 +91,13 @@ public function getFeedback(bool $isSoleMatch): array $feedback = [ 'warning' => $this->getFeedbackWarning($isSoleMatch), - 'suggestions' => [] + 'suggestions' => [], ]; - if (preg_match($startUpper, $this->token)) { + if (preg_match($startUpper, (string) $this->token)) { $feedback['suggestions'][] = "Capitalization doesn't help very much"; - } elseif (preg_match($allUpper, $this->token) && mb_strtolower($this->token) != $this->token) { - $feedback['suggestions'][] = "All-uppercase is almost as easy to guess as all-lowercase"; + } elseif (preg_match($allUpper, (string) $this->token) && mb_strtolower((string) $this->token) !== $this->token) { + $feedback['suggestions'][] = 'All-uppercase is almost as easy to guess as all-lowercase'; } return $feedback; @@ -113,15 +107,16 @@ public function getFeedbackWarning(bool $isSoleMatch): string { switch ($this->dictionaryName) { case 'passwords': - if ($isSoleMatch && !$this->l33t && !$this->reversed) { + if ($isSoleMatch && ! $this->l33t && ! $this->reversed) { if ($this->rank <= 10) { return 'This is a top-10 common password'; - } elseif ($this->rank <= 100) { + } + if ($this->rank <= 100) { return 'This is a top-100 common password'; - } else { - return 'This is a very common password'; } - } elseif ($this->getGuessesLog10() <= 4) { + return 'This is a very common password'; + } + if ($this->getGuessesLog10() <= 4) { return 'This is similar to a commonly used password'; } break; @@ -135,9 +130,8 @@ public function getFeedbackWarning(bool $isSoleMatch): string case 'female_names': if ($isSoleMatch) { return 'Names and surnames by themselves are easy to guess'; - } else { - return 'Common names and surnames are easy to guess'; } + return 'Common names and surnames are easy to guess'; } return ''; @@ -146,9 +140,9 @@ public function getFeedbackWarning(bool $isSoleMatch): string /** * Attempts to find the provided password (as well as all possible substrings) in a dictionary. * - * @param string $password - * @param array $dict - * @return array + * @param array $dict + * + * @return array */ protected static function dictionaryMatch(string $password, array $dict): array { @@ -179,12 +173,12 @@ protected static function dictionaryMatch(string $password, array $dict): array /** * Load ranked frequency dictionaries. * - * @return array + * @return array */ protected static function getRankedDictionaries(): array { if (empty(self::$rankedDictionaries)) { - $json = file_get_contents(dirname(__FILE__) . '/frequency_lists.json'); + $json = file_get_contents(__DIR__ . '/frequency_lists.json'); $data = json_decode($json, true); $rankedLists = []; @@ -208,15 +202,15 @@ protected function getRawGuesses(): float protected function getUppercaseVariations(): float { $word = $this->token; - if (preg_match(self::ALL_LOWER, $word) || mb_strtolower($word) === $word) { + if (preg_match(self::ALL_LOWER, (string) $word) || mb_strtolower((string) $word) === $word) { return 1; } // a capitalized word is the most common capitalization scheme, // so it only doubles the search space (uncapitalized + capitalized). // allcaps and end-capitalized are common enough too, underestimate as 2x factor to be safe. - foreach (array(self::START_UPPER, self::END_UPPER, self::ALL_UPPER) as $regex) { - if (preg_match($regex, $word)) { + foreach ([self::START_UPPER, self::END_UPPER, self::ALL_UPPER] as $regex) { + if (preg_match($regex, (string) $word)) { return 2; } } @@ -224,8 +218,8 @@ protected function getUppercaseVariations(): float // otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters // with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD), // the number of ways to lowercase U+L letters with L lowercase letters or less. - $uppercase = count(array_filter(preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY), 'ctype_upper')); - $lowercase = count(array_filter(preg_split('//u', $word, -1, PREG_SPLIT_NO_EMPTY), 'ctype_lower')); + $uppercase = count(array_filter(preg_split('//u', (string) $word, -1, PREG_SPLIT_NO_EMPTY), 'ctype_upper')); + $lowercase = count(array_filter(preg_split('//u', (string) $word, -1, PREG_SPLIT_NO_EMPTY), 'ctype_lower')); $variations = 0; for ($i = 1; $i <= min($uppercase, $lowercase); $i++) { diff --git a/src/Matchers/L33tMatch.php b/src/Matchers/L33tMatch.php index 9163706..98e8251 100644 --- a/src/Matchers/L33tMatch.php +++ b/src/Matchers/L33tMatch.php @@ -9,26 +9,35 @@ /** * Class L33tMatch extends DictionaryMatch to translate l33t into dictionary words for matching. + * * @package ZxcvbnPhp\Matchers */ class L33tMatch extends DictionaryMatch { - /** @var array An array of substitutions made to get from the token to the dictionary word. */ - public $sub = []; + /** @var array An array of substitutions made to get from the token to the dictionary word. */ + public array $sub = []; /** @var string A user-readable string that shows which substitutions were detected. */ - public $subDisplay; + public string $subDisplay; /** @var bool Whether or not the token contained l33t substitutions. */ - public $l33t = true; + public bool $l33t = true; /** - * Match occurences of l33t words in password to dictionary words. + * @param array{'sub'?: array, 'sub_display'?: string} $params + */ + public function __construct(string $password, int $begin, int $end, string $token, array $params = []) + { + parent::__construct($password, $begin, $end, $token, $params); + + $this->sub = $params['sub'] ?? []; + $this->subDisplay = $params['sub_display'] ?? ''; + } + + /** + * Match occurrences of l33t words in password to dictionary words. * - * @param string $password - * @param array $userInputs - * @param array $rankedDictionaries - * @return L33tMatch[] + * @return array */ public static function match(string $password, array $userInputs = [], array $rankedDictionaries = []): array { @@ -39,14 +48,14 @@ public static function match(string $password, array $userInputs = [], array $ra } $matches = []; - if (!$rankedDictionaries) { + if (! $rankedDictionaries) { $rankedDictionaries = static::getRankedDictionaries(); } foreach ($maps as $map) { $translatedWord = static::translate($password, $map); - /** @var L33tMatch[] $results */ + /** @var array $results */ $results = parent::match($translatedWord, $userInputs, $rankedDictionaries); foreach ($results as $match) { $token = mb_substr($password, $match->begin, $match->end - $match->begin + 1); @@ -65,9 +74,9 @@ public static function match(string $password, array $userInputs = [], array $ra $display = []; foreach ($map as $i => $t) { - if (mb_strpos($token, (string)$i) !== false) { + if (mb_strpos($token, (string) $i) !== false) { $match->sub[$i] = $t; - $display[] = "$i -> $t"; + $display[] = "{$i} -> {$t}"; } } $match->token = $token; @@ -77,28 +86,12 @@ public static function match(string $password, array $userInputs = [], array $ra } } - Matcher::usortStable($matches, [Matcher::class, 'compareMatches']); + Matcher::usortStable($matches, Matcher::compareMatches(...)); return $matches; } /** - * @param string $password - * @param int $begin - * @param int $end - * @param string $token - * @param array $params An array with keys: [sub, sub_display]. - */ - public function __construct(string $password, int $begin, int $end, string $token, array $params = []) - { - parent::__construct($password, $begin, $end, $token, $params); - if (!empty($params)) { - $this->sub = $params['sub'] ?? []; - $this->subDisplay = $params['sub_display'] ?? null; - } - } - - /** - * @return array{'warning': string, "suggestions": string[]} + * @return array{'warning': string, "suggestions": array} */ public function getFeedback(bool $isSoleMatch): array { @@ -110,15 +103,16 @@ public function getFeedback(bool $isSoleMatch): array } /** - * @param string $string - * @param array $map - * @return string + * @param array $map */ protected static function translate(string $string, array $map): string { return str_replace(array_keys($map), array_values($map), $string); } + /** + * @return array> + */ protected static function getL33tTable(): array { return [ @@ -137,6 +131,9 @@ protected static function getL33tTable(): array ]; } + /** + * @return array> + */ protected static function getL33tSubtable(string $password): array { // The preg_split call below is a multibyte compatible version of str_split @@ -156,19 +153,29 @@ protected static function getL33tSubtable(string $password): array return $subTable; } + /** + * @param array> $subtable + * + * @return array + */ protected static function getL33tSubstitutions(array $subtable): array { $keys = array_keys($subtable); $substitutions = self::substitutionTableHelper($subtable, $keys, [[]]); // Converts the substitution arrays from [ [a, b], [c, d] ] to [ a => b, c => d ] - $substitutions = array_map(function (array $subArray): array { - return array_combine(array_column($subArray, 0), array_column($subArray, 1)); - }, $substitutions); + $substitutions = array_map(static fn (array $subArray): array => array_combine(array_column($subArray, 0), array_column($subArray, 1)), $substitutions); return $substitutions; } + /** + * @param array> $table + * @param array $keys + * @param array $subs + * + * @return array + */ protected static function substitutionTableHelper(array $table, array $keys, array $subs): array { if (empty($keys)) { @@ -217,14 +224,10 @@ protected function getL33tVariations(): float $variations = 1; foreach ($this->sub as $substitution => $letter) { - $characters = preg_split('//u', mb_strtolower($this->token), -1, PREG_SPLIT_NO_EMPTY); - - $subbed = count(array_filter($characters, function ($character) use ($substitution) { - return (string)$character === (string)$substitution; - })); - $unsubbed = count(array_filter($characters, function ($character) use ($letter) { - return (string)$character === (string)$letter; - })); + $characters = preg_split('//u', mb_strtolower((string) $this->token), -1, PREG_SPLIT_NO_EMPTY); + + $subbed = count(array_filter($characters, static fn ($character) => (string) $character === (string) $substitution)); + $unsubbed = count(array_filter($characters, static fn ($character) => (string) $character === (string) $letter)); if ($subbed === 0 || $unsubbed === 0) { // for this sub, password is either fully subbed (444) or fully unsubbed (aaa) diff --git a/src/Matchers/MatchInterface.php b/src/Matchers/MatchInterface.php index 24f1944..e6a4f73 100644 --- a/src/Matchers/MatchInterface.php +++ b/src/Matchers/MatchInterface.php @@ -10,11 +10,13 @@ interface MatchInterface * Match this password. * * @param string $password Password to check for match - * @param array $userInputs Array of values related to the user (optional) + * @param array $userInputs Array of values related to the user (optional) + * * @code array('Alice Smith') + * * @endcode * - * @return array|BaseMatch[] Array of Match objects + * @return array Array of Match objects */ public static function match(string $password, array $userInputs = []): array; diff --git a/src/Matchers/RepeatMatch.php b/src/Matchers/RepeatMatch.php index 1b23074..56ba132 100644 --- a/src/Matchers/RepeatMatch.php +++ b/src/Matchers/RepeatMatch.php @@ -14,26 +14,37 @@ class RepeatMatch extends BaseMatch public const LAZY_MATCH = '/(.+?)\1+/u'; public const ANCHORED_LAZY_MATCH = '/^(.+?)\1+$/u'; - public $pattern = 'repeat'; + public string $pattern = 'repeat'; - /** @var MatchInterface[] An array of matches for the repeated section itself. */ - public $baseMatches = []; + /** @var array An array of matches for the repeated section itself. */ + public array $baseMatches = []; /** @var int The number of guesses required for the repeated section itself. */ - public $baseGuesses; + public int $baseGuesses; /** @var int The number of times the repeated section is repeated. */ - public $repeatCount; + public int $repeatCount; /** @var string The string that was repeated in the token. */ - public $repeatedChar; + public string $repeatedChar; + + /** + * @param array{'repeated_char'?: string, 'base_guesses'?: int, 'base_matches'?: array, 'repeat_count'?: int} $params + */ + public function __construct(string $password, int $begin, int $end, string $token, array $params = []) + { + parent::__construct($password, $begin, $end, $token); + + $this->repeatedChar = $params['repeated_char'] ?? ''; + $this->baseGuesses = isset($params['base_guesses']) ? (int) $params['base_guesses'] : 0; + $this->baseMatches = $params['base_matches'] ?? []; + $this->repeatCount = isset($params['repeat_count']) ? (int) $params['repeat_count'] : 0; + } /** * Match 3 or more repeated characters. * - * @param string $password - * @param array $userInputs - * @return RepeatMatch[] + * @return array */ public static function match(string $password, array $userInputs = []): array { @@ -48,9 +59,9 @@ public static function match(string $password, array $userInputs = []): array break; } - if (mb_strlen($greedyMatches[0][0]['token']) > mb_strlen($lazyMatches[0][0]['token'])) { + if (mb_strlen((string) $greedyMatches[0][0]['token']) > mb_strlen((string) $lazyMatches[0][0]['token'])) { $match = $greedyMatches[0]; - preg_match(self::ANCHORED_LAZY_MATCH, $match[0]['token'], $anchoredMatch); + preg_match(self::ANCHORED_LAZY_MATCH, (string) $match[0]['token'], $anchoredMatch); $repeatedChar = $anchoredMatch[1]; } else { $match = $lazyMatches[0]; @@ -64,7 +75,7 @@ public static function match(string $password, array $userInputs = []): array $baseMatches = $baseAnalysis['sequence']; $baseGuesses = $baseAnalysis['guesses']; - $repeatCount = mb_strlen($match[0]['token']) / mb_strlen($repeatedChar); + $repeatCount = mb_strlen((string) $match[0]['token']) / mb_strlen((string) $repeatedChar); $matches[] = new static( $password, @@ -73,9 +84,9 @@ public static function match(string $password, array $userInputs = []): array $match[0]['token'], [ 'repeated_char' => $repeatedChar, - 'base_guesses' => $baseGuesses, - 'base_matches' => $baseMatches, - 'repeat_count' => $repeatCount, + 'base_guesses' => $baseGuesses, + 'base_matches' => $baseMatches, + 'repeat_count' => $repeatCount, ] ); @@ -86,40 +97,22 @@ public static function match(string $password, array $userInputs = []): array } /** - * @return array{'warning': string, "suggestions": string[]} + * @return array{'warning': string, "suggestions": array} */ public function getFeedback(bool $isSoleMatch): array { - $warning = mb_strlen($this->repeatedChar) == 1 + $warning = mb_strlen($this->repeatedChar) === 1 ? 'Repeats like "aaa" are easy to guess' : 'Repeats like "abcabcabc" are only slightly harder to guess than "abc"'; return [ - 'warning' => $warning, + 'warning' => $warning, 'suggestions' => [ 'Avoid repeated words and characters', ], ]; } - /** - * @param string $password - * @param int $begin - * @param int $end - * @param string $token - * @param array $params An array with keys: [repeated_char, base_guesses, base_matches, repeat_count]. - */ - public function __construct(string $password, int $begin, int $end, string $token, array $params = []) - { - parent::__construct($password, $begin, $end, $token); - if (!empty($params)) { - $this->repeatedChar = $params['repeated_char'] ?? ''; - $this->baseGuesses = $params['base_guesses'] ?? 0; - $this->baseMatches = $params['base_matches'] ?? []; - $this->repeatCount = $params['repeat_count'] ?? 0; - } - } - protected function getRawGuesses(): float { return $this->baseGuesses * $this->repeatCount; diff --git a/src/Matchers/ReverseDictionaryMatch.php b/src/Matchers/ReverseDictionaryMatch.php index a935bff..0cd059b 100644 --- a/src/Matchers/ReverseDictionaryMatch.php +++ b/src/Matchers/ReverseDictionaryMatch.php @@ -9,19 +9,19 @@ class ReverseDictionaryMatch extends DictionaryMatch { /** @var bool Whether or not the matched word was reversed in the token. */ - public $reversed = true; + public bool $reversed = true; /** - * Match occurences of reversed dictionary words in password. + * Match occurrences of reversed dictionary words in password. * - * @param $password - * @param array $userInputs - * @param array $rankedDictionaries - * @return ReverseDictionaryMatch[] + * @param array $userInputs + * @param array $rankedDictionaries + * + * @return array */ public static function match(string $password, array $userInputs = [], array $rankedDictionaries = []): array { - /** @var ReverseDictionaryMatch[] $matches */ + /** @var array $matches */ $matches = parent::match(self::mbStrRev($password), $userInputs, $rankedDictionaries); foreach ($matches as $match) { $tempBegin = $match->begin; @@ -32,23 +32,18 @@ public static function match(string $password, array $userInputs = [], array $ra $match->begin = mb_strlen($password) - 1 - $match->end; $match->end = mb_strlen($password) - 1 - $tempBegin; } - Matcher::usortStable($matches, [Matcher::class, 'compareMatches']); + Matcher::usortStable($matches, Matcher::compareMatches(...)); return $matches; } - protected function getRawGuesses(): float - { - return parent::getRawGuesses() * 2; - } - /** - * @return array{'warning': string, "suggestions": string[]} + * @return array{'warning': string, "suggestions": array} */ public function getFeedback(bool $isSoleMatch): array { $feedback = parent::getFeedback($isSoleMatch); - if (mb_strlen($this->token) >= 4) { + if (mb_strlen((string) $this->token) >= 4) { $feedback['suggestions'][] = "Reversed words aren't much harder to guess"; } @@ -68,4 +63,9 @@ public static function mbStrRev(string $string, ?string $encoding = null): strin return $reversed; } + + protected function getRawGuesses(): float + { + return parent::getRawGuesses() * 2; + } } diff --git a/src/Matchers/SequenceMatch.php b/src/Matchers/SequenceMatch.php index 17a8ea8..da060e4 100644 --- a/src/Matchers/SequenceMatch.php +++ b/src/Matchers/SequenceMatch.php @@ -9,23 +9,33 @@ class SequenceMatch extends BaseMatch { public const MAX_DELTA = 5; - public $pattern = 'sequence'; + public string $pattern = 'sequence'; /** @var string The name of the detected sequence. */ - public $sequenceName; + public string $sequenceName; /** @var int The number of characters in the complete sequence space. */ - public $sequenceSpace; + public int $sequenceSpace; /** @var bool True if the sequence is ascending, and false if it is descending. */ - public $ascending; + public bool $ascending; + + /** + * @param array{'sequenceName'?: string, 'sequenceSpace'?: int, 'ascending'?: bool} $params + */ + public function __construct(string $password, int $begin, int $end, string $token, array $params = ['sequenceName' => '', 'sequenceSpace' => 0, 'ascending' => false]) + { + parent::__construct($password, $begin, $end, $token); + + $this->sequenceName = $params['sequenceName'] ?? ''; + $this->sequenceSpace = $params['sequenceSpace'] ?? 0; + $this->ascending = $params['ascending'] ?? false; + } /** * Match sequences of three or more characters. * - * @param string $password - * @param array $userInputs - * @return SequenceMatch[] + * @return array */ public static function match(string $password, array $userInputs = []): array { @@ -58,7 +68,10 @@ public static function match(string $password, array $userInputs = []): array return $matches; } - public static function findSequenceMatch(string $password, int $begin, int $end, int $delta, array &$matches) + /** + * @param array $matches + */ + public static function findSequenceMatch(string $password, int $begin, int $end, int $delta, array &$matches): void { if ($end - $begin > 1 || abs($delta) === 1) { if (abs($delta) > 0 && abs($delta) <= self::MAX_DELTA) { @@ -87,41 +100,24 @@ public static function findSequenceMatch(string $password, int $begin, int $end, } /** - * @return array{'warning': string, "suggestions": string[]} + * @return array{'warning': string, "suggestions": array} */ public function getFeedback(bool $isSoleMatch): array { return [ - 'warning' => "Sequences like abc or 6543 are easy to guess", + 'warning' => 'Sequences like abc or 6543 are easy to guess', 'suggestions' => [ - 'Avoid sequences' - ] + 'Avoid sequences', + ], ]; } - /** - * @param string $password - * @param int $begin - * @param int $end - * @param string $token - * @param array $params An array with keys: [sequenceName, sequenceSpace, ascending]. - */ - public function __construct(string $password, int $begin, int $end, string $token, array $params = []) - { - parent::__construct($password, $begin, $end, $token); - if (!empty($params)) { - $this->sequenceName = $params['sequenceName'] ?? ''; - $this->sequenceSpace = $params['sequenceSpace'] ?? 0; - $this->ascending = $params['ascending'] ?? false; - } - } - protected function getRawGuesses(): float { - $firstCharacter = mb_substr($this->token, 0, 1); + $firstCharacter = mb_substr((string) $this->token, 0, 1); $guesses = 0; - if (in_array($firstCharacter, array('a', 'A', 'z', 'Z', '0', '1', '9'), true)) { + if (in_array($firstCharacter, ['a', 'A', 'z', 'Z', '0', '1', '9'], true)) { $guesses += 4; // lower guesses for obvious starting points } elseif (ctype_digit($firstCharacter)) { $guesses += 10; // digits @@ -131,12 +127,12 @@ protected function getRawGuesses(): float $guesses += 26; } - if (!$this->ascending) { + if (! $this->ascending) { // need to try a descending sequence in addition to every ascending sequence -> // 2x guesses $guesses *= 2; } - return $guesses * mb_strlen($this->token); + return $guesses * mb_strlen((string) $this->token); } } diff --git a/src/Matchers/SpatialMatch.php b/src/Matchers/SpatialMatch.php index 1a94603..774bc65 100644 --- a/src/Matchers/SpatialMatch.php +++ b/src/Matchers/SpatialMatch.php @@ -18,33 +18,43 @@ class SpatialMatch extends BaseMatch public const KEYBOARD_AVERAGE_DEGREES = 4.5957446809; // 432 / 94 public const KEYPAD_AVERAGE_DEGREES = 5.0666666667; // 76 / 15 - public $pattern = 'spatial'; + public string $pattern = 'spatial'; /** @var int The number of characters the shift key was held for in the token. */ - public $shiftedCount; + public int $shiftedCount = 0; /** @var int The number of turns on the keyboard required to complete the token. */ - public $turns; + public int $turns = 0; /** @var string The keyboard layout that the token is a spatial match on. */ - public $graph; + public string $graph; - /** @var array A cache of the adjacency_graphs json file */ - protected static $adjacencyGraphs = []; + /** @var array A cache of the adjacency_graphs json file */ + protected static array $adjacencyGraphs = []; + + /** + * @param array{'graph'?: string, 'shifted_count'?: int, 'turns'?: int} $params + */ + public function __construct(string $password, int $begin, int $end, string $token, array $params = ['graph' => 'qwerty', 'shifted_count' => 0, 'turns' => 0]) + { + parent::__construct($password, $begin, $end, $token); + $this->graph = $params['graph'] ?? 'qwerty'; + $this->shiftedCount = $params['shifted_count'] ?? 0; + $this->turns = $params['turns'] ?? 0; + } /** * Match spatial patterns based on keyboard layouts (e.g. qwerty, dvorak, keypad). * - * @param string $password - * @param array $userInputs - * @param array $graphs - * @return SpatialMatch[] + * @param array $userInputs + * @param array $graphs + * + * @return array */ public static function match(string $password, array $userInputs = [], array $graphs = []): array { - $matches = []; - if (!$graphs) { + if (! $graphs) { $graphs = static::getAdjacencyGraphs(); } foreach ($graphs as $name => $graph) { @@ -54,50 +64,59 @@ public static function match(string $password, array $userInputs = [], array $gr $matches[] = new static($password, $result['begin'], $result['end'], $result['token'], $result); } } - Matcher::usortStable($matches, [Matcher::class, 'compareMatches']); + Matcher::usortStable($matches, Matcher::compareMatches(...)); return $matches; } /** - * @return array{'warning': string, "suggestions": string[]} + * @return array{'warning': string, "suggestions": array} */ public function getFeedback(bool $isSoleMatch): array { - $warning = $this->turns == 1 + $warning = $this->turns === 1 ? 'Straight rows of keys are easy to guess' : 'Short keyboard patterns are easy to guess'; return [ 'warning' => $warning, 'suggestions' => [ - 'Use a longer keyboard pattern with more turns' - ] + 'Use a longer keyboard pattern with more turns', + ], ]; } /** - * @param string $password - * @param int $begin - * @param int $end - * @param string $token - * @param array $params An array with keys: [graph (required), shifted_count, turns]. + * Load adjacency graphs. + * + * @return array */ - public function __construct(string $password, int $begin, int $end, string $token, array $params = []) + public static function getAdjacencyGraphs(): array { - parent::__construct($password, $begin, $end, $token); - $this->graph = $params['graph']; - if (!empty($params)) { - $this->shiftedCount = $params['shifted_count'] ?? null; - $this->turns = $params['turns'] ?? null; + if (empty(self::$adjacencyGraphs)) { + $json = file_get_contents(__DIR__ . '/adjacency_graphs.json'); + $data = json_decode($json, true); + + // This seems pointless, but the data file is not guaranteed to be in any particular order. + // We want to be in the exact order below so as to match most closely with upstream, because when a match + // can be found in multiple graphs (such as 789), the one that's listed first is that one that will be picked. + $data = [ + 'qwerty' => $data['qwerty'], + 'dvorak' => $data['dvorak'], + 'keypad' => $data['keypad'], + 'mac_keypad' => $data['mac_keypad'], + ]; + self::$adjacencyGraphs = $data; } + + return self::$adjacencyGraphs; } /** * Match spatial patterns in a adjacency graph. - * @param string $password - * @param array $graph - * @param string $graphName - * @return array + * + * @param array $graph + * + * @return array */ protected static function graphMatch(string $password, array $graph, string $graphName): array { @@ -168,7 +187,7 @@ protected static function graphMatch(string $password, array $graph, string $gra 'end' => $j - 1, 'token' => mb_substr($password, $i, $j - $i), 'turns' => $turns, - 'shifted_count' => $shiftedCount + 'shifted_count' => $shiftedCount, ]; } // ...and then start a new search for the rest of the password. @@ -183,42 +202,11 @@ protected static function graphMatch(string $password, array $graph, string $gra /** * Get the index of a string a character first - * - * @param string $string - * @param string $char - * - * @return int */ protected static function indexOf(string $string, string $char): int { $pos = mb_strpos($string, $char); - return ($pos === false ? -1 : $pos); - } - - /** - * Load adjacency graphs. - * - * @return array - */ - public static function getAdjacencyGraphs(): array - { - if (empty(self::$adjacencyGraphs)) { - $json = file_get_contents(dirname(__FILE__) . '/adjacency_graphs.json'); - $data = json_decode($json, true); - - // This seems pointless, but the data file is not guaranteed to be in any particular order. - // We want to be in the exact order below so as to match most closely with upstream, because when a match - // can be found in multiple graphs (such as 789), the one that's listed first is that one that will be picked. - $data = [ - 'qwerty' => $data['qwerty'], - 'dvorak' => $data['dvorak'], - 'keypad' => $data['keypad'], - 'mac_keypad' => $data['mac_keypad'], - ]; - self::$adjacencyGraphs = $data; - } - - return self::$adjacencyGraphs; + return $pos === false ? -1 : $pos; } protected function getRawGuesses(): float @@ -232,14 +220,14 @@ protected function getRawGuesses(): float } $guesses = 0; - $length = mb_strlen($this->token); + $length = mb_strlen((string) $this->token); $turns = $this->turns; // estimate the number of possible patterns w/ length L or less with t turns or less. for ($i = 2; $i <= $length; $i++) { $possibleTurns = min($turns, $i - 1); for ($j = 1; $j <= $possibleTurns; $j++) { - $guesses += Binomial::binom($i - 1, $j - 1) * $startingPosition * pow($averageDegree, $j); + $guesses += Binomial::binom($i - 1, $j - 1) * $startingPosition * $averageDegree ** $j; } } diff --git a/src/Matchers/YearMatch.php b/src/Matchers/YearMatch.php index 84f711c..c45dce9 100644 --- a/src/Matchers/YearMatch.php +++ b/src/Matchers/YearMatch.php @@ -10,45 +10,42 @@ final class YearMatch extends BaseMatch { public const NUM_YEARS = 119; - public $pattern = 'regex'; - public $regexName = 'recent_year'; + public string $pattern = 'regex'; + public string $regexName = 'recent_year'; /** * Match occurrences of years in a password * - * @param string $password - * @param array $userInputs - * @return YearMatch[] + * @return array */ public static function match(string $password, array $userInputs = []): array { $matches = []; - $groups = static::findAll($password, "/(19\d\d|20\d\d)/u"); + $groups = self::findAll($password, "/(19\d\d|20\d\d)/u"); foreach ($groups as $captures) { $matches[] = new static($password, $captures[1]['begin'], $captures[1]['end'], $captures[1]['token']); } - Matcher::usortStable($matches, [Matcher::class, 'compareMatches']); + Matcher::usortStable($matches, Matcher::compareMatches(...)); return $matches; } - /** - * @return array{'warning': string, "suggestions": string[]} + * @return array{'warning': string, "suggestions": array} */ public function getFeedback(bool $isSoleMatch): array { return [ - 'warning' => "Recent years are easy to guess", + 'warning' => 'Recent years are easy to guess', 'suggestions' => [ 'Avoid recent years', 'Avoid years that are associated with you', - ] + ], ]; } protected function getRawGuesses(): float { - $yearSpace = abs($this->token - DateMatch::getReferenceYear()); + $yearSpace = abs((int) $this->token - DateMatch::getReferenceYear()); return max($yearSpace, DateMatch::MIN_YEAR_SPACE); } } diff --git a/src/Math/Binomial.php b/src/Math/Binomial.php index 94e882f..3818df8 100644 --- a/src/Math/Binomial.php +++ b/src/Math/Binomial.php @@ -4,25 +4,21 @@ namespace ZxcvbnPhp\Math; -use ZxcvbnPhp\Math\Impl\BinomialProviderPhp73Gmp; use ZxcvbnPhp\Math\Impl\BinomialProviderFloat64; use ZxcvbnPhp\Math\Impl\BinomialProviderInt64; +use ZxcvbnPhp\Math\Impl\BinomialProviderPhp73Gmp; class Binomial { - private static $provider = null; + private static ?BinomialProvider $provider = null; private function __construct() { - throw new \LogicException(__CLASS__ . " is static"); + throw new \LogicException(self::class . ' is static'); } /** * Calculate binomial coefficient (n choose k). - * - * @param int $n - * @param int $k - * @return float */ public static function binom(int $n, int $k): float { @@ -39,15 +35,15 @@ public static function getProvider(): BinomialProvider } /** - * @return string[] + * @return array */ public static function getUsableProviderClasses(): array { // In order of priority. The first provider with a value of true will be used. $possibleProviderClasses = [ BinomialProviderPhp73Gmp::class => function_exists('gmp_binomial'), - BinomialProviderInt64::class => PHP_INT_SIZE >= 8, - BinomialProviderFloat64::class => PHP_FLOAT_DIG >= 15, + BinomialProviderInt64::class => PHP_INT_SIZE >= 8, + BinomialProviderFloat64::class => PHP_FLOAT_DIG >= 15, ]; $possibleProviderClasses = array_filter($possibleProviderClasses); @@ -59,8 +55,8 @@ private static function initProvider(): BinomialProvider { $providerClasses = self::getUsableProviderClasses(); - if (!$providerClasses) { - throw new \LogicException("No valid providers"); + if (! $providerClasses) { + throw new \LogicException('No valid providers'); } $bestProviderClass = reset($providerClasses); diff --git a/src/Math/BinomialProvider.php b/src/Math/BinomialProvider.php index fbec156..239103a 100644 --- a/src/Math/BinomialProvider.php +++ b/src/Math/BinomialProvider.php @@ -8,10 +8,6 @@ interface BinomialProvider { /** * Calculate binomial coefficient (n choose k). - * - * @param int $n - * @param int $k - * @return float */ public function binom(int $n, int $k): float; } diff --git a/src/Math/Impl/AbstractBinomialProvider.php b/src/Math/Impl/AbstractBinomialProvider.php index fe2a043..da18e9d 100644 --- a/src/Math/Impl/AbstractBinomialProvider.php +++ b/src/Math/Impl/AbstractBinomialProvider.php @@ -11,7 +11,7 @@ abstract class AbstractBinomialProvider implements BinomialProvider public function binom(int $n, int $k): float { if ($k < 0 || $n < 0) { - throw new \DomainException("n and k must be non-negative"); + throw new \DomainException('n and k must be non-negative'); } if ($k > $n) { diff --git a/src/Math/Impl/AbstractBinomialProviderWithFallback.php b/src/Math/Impl/AbstractBinomialProviderWithFallback.php index 1487723..49e014d 100644 --- a/src/Math/Impl/AbstractBinomialProviderWithFallback.php +++ b/src/Math/Impl/AbstractBinomialProviderWithFallback.php @@ -6,14 +6,11 @@ abstract class AbstractBinomialProviderWithFallback extends AbstractBinomialProvider { - /** - * @var AbstractBinomialProvider|null - */ - private $fallback = null; + private ?AbstractBinomialProvider $fallback = null; protected function calculate(int $n, int $k): float { - return $this->tryCalculate($n, $k) ?? $this->getFallbackProvider()->calculate($n, $k); + return $this->tryCalculate($n, $k) ?? $this->getFallbackProvider()->calculate($n, $k); } abstract protected function tryCalculate(int $n, int $k): ?float; diff --git a/src/Math/Impl/BinomialProviderInt64.php b/src/Math/Impl/BinomialProviderInt64.php index 6016172..b5672a1 100644 --- a/src/Math/Impl/BinomialProviderInt64.php +++ b/src/Math/Impl/BinomialProviderInt64.php @@ -4,8 +4,6 @@ namespace ZxcvbnPhp\Math\Impl; -use TypeError; - class BinomialProviderInt64 extends AbstractBinomialProviderWithFallback { protected function initFallbackProvider(): AbstractBinomialProvider @@ -15,19 +13,15 @@ protected function initFallbackProvider(): AbstractBinomialProvider protected function tryCalculate(int $n, int $k): ?float { - try { - $c = 1; - - for ($i = 1; $i <= $k; $i++, $n--) { - // We're aiming for $c * $n / $i, but the $c * $n part could overflow, so use $c / $i * $n instead. The caveat here is that in - // order to get a precise answer, we need to avoid floats, which means we need to deal with whole part and the remainder - // separately. - $c = intdiv($c, $i) * $n + intdiv($c % $i * $n, $i); - } + $c = 1; - return (float)$c; - } catch (TypeError $ex) { - return null; + for ($i = 1; $i <= $k; $i++, $n--) { + // We're aiming for $c * $n / $i, but the $c * $n part could overflow, so use $c / $i * $n instead. The caveat here is that in + // order to get a precise answer, we need to avoid floats, which means we need to deal with whole part and the remainder + // separately. + $c = intdiv($c, $i) * $n + intdiv($c % $i * $n, $i); } + + return (float) $c; } } diff --git a/src/Math/Impl/BinomialProviderPhp73Gmp.php b/src/Math/Impl/BinomialProviderPhp73Gmp.php index 2d5fa3d..317d8fa 100644 --- a/src/Math/Impl/BinomialProviderPhp73Gmp.php +++ b/src/Math/Impl/BinomialProviderPhp73Gmp.php @@ -12,6 +12,6 @@ class BinomialProviderPhp73Gmp extends AbstractBinomialProvider */ protected function calculate(int $n, int $k): float { - return (float)gmp_strval(gmp_binomial($n, $k)); + return (float) gmp_strval(gmp_binomial($n, $k)); } } diff --git a/src/Scorer.php b/src/Scorer.php index 7cbd258..8dc6d7c 100644 --- a/src/Scorer.php +++ b/src/Scorer.php @@ -4,9 +4,8 @@ namespace ZxcvbnPhp; -use ZxcvbnPhp\Matchers\Bruteforce; use ZxcvbnPhp\Matchers\BaseMatch; -use ZxcvbnPhp\Matchers\MatchInterface; +use ZxcvbnPhp\Matchers\Bruteforce; /** * scorer - takes a list of potential matches, ranks and evaluates them, @@ -20,9 +19,13 @@ class Scorer public const MIN_SUBMATCH_GUESSES_SINGLE_CHAR = 10; public const MIN_SUBMATCH_GUESSES_MULTI_CHAR = 50; - protected $password; - protected $excludeAdditive; - protected $optimal = []; + protected string $password = ''; + protected bool $excludeAdditive = false; + + /** + * @var array + */ + protected array $optimal = []; /** * ------------------------------------------------------------------------------ @@ -56,10 +59,9 @@ class Scorer * sequences before length-3. assuming at minimum D guesses per pattern type, * D^(l-1) approximates Sum(D^i for i in [1..l-1] * - * @param string $password - * @param MatchInterface[] $matches - * @param bool $excludeAdditive - * @return array Returns an array with these keys: [password, guesses, guesses_log10, sequence] + * @param array $matches + * + * @return array{'password': string, 'guesses': float, 'guesses_log10': float, 'sequence': mixed} */ public function getMostGuessableMatchSequence(string $password, array $matches, bool $excludeAdditive = false): array { @@ -77,11 +79,7 @@ public function getMostGuessableMatchSequence(string $password, array $matches, // small detail: for deterministic output, sort each sublist by i. foreach ($matchesByEndIndex as &$matches) { - usort($matches, function ($a, $b) { - /** @var $a BaseMatch */ - /** @var $b BaseMatch */ - return $a->begin - $b->begin; - }); + usort($matches, static fn (BaseMatch $a, BaseMatch $b) => $a->begin - $b->begin); } $this->optimal = [ @@ -104,7 +102,7 @@ public function getMostGuessableMatchSequence(string $password, array $matches, foreach ($matchesByEndIndex[$k] as $match) { if ($match->begin > 0) { foreach ($this->optimal['m'][$match->begin - 1] as $l => $null) { - $l = (int)$l; + $l = (int) $l; $this->update($match, $l + 1); } } else { @@ -114,7 +112,6 @@ public function getMostGuessableMatchSequence(string $password, array $matches, $this->bruteforceUpdate($k); } - if ($length === 0) { $guesses = 1.0; $optimalSequence = []; @@ -135,8 +132,6 @@ public function getMostGuessableMatchSequence(string $password, array $matches, /** * helper: considers whether a length-l sequence ending at match m is better (fewer guesses) * than previously encountered sequences, updating state if so. - * @param BaseMatch $match - * @param int $length */ protected function update(BaseMatch $match, int $length): void { @@ -155,8 +150,8 @@ protected function update(BaseMatch $match, int $length): void // calculate the minimization func $g = $this->factorial($length) * $pi; - if (!$this->excludeAdditive) { - $g += pow(self::MIN_GUESSES_BEFORE_GROWING_SEQUENCE, $length - 1); + if (! $this->excludeAdditive) { + $g += self::MIN_GUESSES_BEFORE_GROWING_SEQUENCE ** ($length - 1); } // update state if new best. @@ -184,7 +179,6 @@ protected function update(BaseMatch $match, int $length): void /** * helper: evaluate bruteforce matches ending at k - * @param int $end */ protected function bruteforceUpdate(int $end): void { @@ -198,7 +192,7 @@ protected function bruteforceUpdate(int $end): void for ($i = 1; $i <= $end; $i++) { $match = $this->makeBruteforceMatch($i, $end); foreach ($this->optimal['m'][$i - 1] as $l => $lastM) { - $l = (int)$l; + $l = (int) $l; // corner: an optimal sequence will never have two adjacent bruteforce matches. // it is strictly better to have a single bruteforce match spanning the same region: @@ -215,19 +209,16 @@ protected function bruteforceUpdate(int $end): void /** * helper: make bruteforce match objects spanning i to j, inclusive. - * @param int $begin - * @param int $end - * @return Bruteforce */ protected function makeBruteforceMatch(int $begin, int $end): Bruteforce { - return new Bruteforce($this->password, $begin, $end, mb_substr($this->password, $begin, $end - $begin + 1)); + return new Bruteforce($this->password, $begin, $end, mb_substr((string) $this->password, $begin, $end - $begin + 1)); } /** * helper: step backwards through optimal.m starting at the end, constructing the final optimal match sequence. - * @param int $n - * @return MatchInterface[] + * + * @return array */ protected function unwind(int $n): array { @@ -257,8 +248,6 @@ protected function unwind(int $n): array /** * unoptimized, called only on small n - * @param int $n - * @return int */ protected function factorial(int $n): int { diff --git a/src/TimeEstimator.php b/src/TimeEstimator.php index bf67de9..ddbea79 100644 --- a/src/TimeEstimator.php +++ b/src/TimeEstimator.php @@ -13,16 +13,15 @@ class TimeEstimator { /** - * @param int|float $guesses - * @return array + * @return array */ - public function estimateAttackTimes(float $guesses): array + public function estimateAttackTimes(int|float $guesses): array { $crack_times_seconds = [ 'online_throttling_100_per_hour' => $guesses / (100 / 3600), 'online_no_throttling_10_per_second' => $guesses / 10, 'offline_slow_hashing_1e4_per_second' => $guesses / 1e4, - 'offline_fast_hashing_1e10_per_second' => $guesses / 1e10 + 'offline_fast_hashing_1e10_per_second' => $guesses / 1e10, ]; $crack_times_display = array_map( @@ -33,7 +32,7 @@ public function estimateAttackTimes(float $guesses): array return [ 'crack_times_seconds' => $crack_times_seconds, 'crack_times_display' => $crack_times_display, - 'score' => $this->guessesToScore($guesses) + 'score' => $this->guessesToScore($guesses), ]; } @@ -68,7 +67,7 @@ protected function guessesToScore(float $guesses): int protected function displayTime(float $seconds): string { - $callback = function (float $seconds): array { + $callback = static function (float $seconds): array { $minute = 60; $hour = $minute * 60; $day = $hour * 24; @@ -82,32 +81,32 @@ protected function displayTime(float $seconds): string if ($seconds < $minute) { $base = round($seconds); - return [$base, "$base second"]; + return [$base, "{$base} second"]; } if ($seconds < $hour) { $base = round($seconds / $minute); - return [$base, "$base minute"]; + return [$base, "{$base} minute"]; } if ($seconds < $day) { $base = round($seconds / $hour); - return [$base, "$base hour"]; + return [$base, "{$base} hour"]; } if ($seconds < $month) { $base = round($seconds / $day); - return [$base, "$base day"]; + return [$base, "{$base} day"]; } if ($seconds < $year) { $base = round($seconds / $month); - return [$base, "$base month"]; + return [$base, "{$base} month"]; } if ($seconds < $century) { $base = round($seconds / $year); - return [$base, "$base year"]; + return [$base, "{$base} year"]; } return [null, 'centuries']; diff --git a/src/Zxcvbn.php b/src/Zxcvbn.php index 697dd76..7d8ad70 100644 --- a/src/Zxcvbn.php +++ b/src/Zxcvbn.php @@ -11,32 +11,20 @@ */ class Zxcvbn { - /** - * @var - */ - protected $matcher; + protected Matcher $matcher; - /** - * @var - */ - protected $scorer; + protected Scorer $scorer; - /** - * @var - */ - protected $timeEstimator; + protected TimeEstimator $timeEstimator; - /** - * @var - */ - protected $feedback; + protected Feedback $feedback; public function __construct() { - $this->matcher = new \ZxcvbnPhp\Matcher(); - $this->scorer = new \ZxcvbnPhp\Scorer(); - $this->timeEstimator = new \ZxcvbnPhp\TimeEstimator(); - $this->feedback = new \ZxcvbnPhp\Feedback(); + $this->matcher = new Matcher(); + $this->scorer = new Scorer(); + $this->timeEstimator = new TimeEstimator(); + $this->feedback = new Feedback(); } public function addMatcher(string $className): self @@ -49,10 +37,10 @@ public function addMatcher(string $className): self /** * Calculate password strength via non-overlapping minimum entropy patterns. * - * @param string $password Password to measure - * @param array $userInputs Optional user inputs + * @param string $password Password to measure + * @param array $userInputs Optional user inputs * - * @return array Strength result array with keys: + * @return array Strength result array with keys: * password * entropy * match_sequence @@ -63,9 +51,7 @@ public function passwordStrength(string $password, array $userInputs = []): arra $timeStart = microtime(true); $sanitizedInputs = array_map( - function ($input) { - return mb_strtolower((string) $input); - }, + static fn ($input) => mb_strtolower((string) $input), $userInputs ); @@ -82,8 +68,8 @@ function ($input) { $result, $attackTimes, [ - 'feedback' => $feedback, - 'calc_time' => microtime(true) - $timeStart + 'feedback' => $feedback, + 'calc_time' => microtime(true) - $timeStart, ] ); } diff --git a/test/FeedbackTest.php b/test/FeedbackTest.php index 4140c87..f3efed4 100644 --- a/test/FeedbackTest.php +++ b/test/FeedbackTest.php @@ -12,15 +12,14 @@ class FeedbackTest extends TestCase { - /** @var Feedback */ - private $feedback; + private Feedback $feedback; public function setUp(): void { $this->feedback = new Feedback(); } - public function testFeedbackForEmptyPassword() + public function testFeedbackForEmptyPassword(): void { $feedback = $this->feedback->getFeedback(0, []); @@ -37,7 +36,7 @@ public function testFeedbackForEmptyPassword() ); } - public function testHighScoringSequence() + public function testHighScoringSequence(): void { $match = new Bruteforce('a', 0, 1, 'a'); $feedback = $this->feedback->getFeedback(3, [$match]); @@ -46,7 +45,7 @@ public function testHighScoringSequence() $this->assertEmpty($feedback['suggestions'], "no suggestions for good score"); } - public function testLongestMatchGetsFeedback() + public function testLongestMatchGetsFeedback(): void { $match1 = new SequenceMatch('abcd26-01-1991', 0, 4, 'abcd'); $match2 = new DateMatch('abcd26-01-1991', 4, 14, '26-01-1991', [ @@ -74,7 +73,7 @@ public function testLongestMatchGetsFeedback() ); } - public function testDefaultSuggestion() + public function testDefaultSuggestion(): void { $match = new DateMatch('26-01-1991', 0, 10, '26-01-1991', [ 'day' => 26, @@ -92,7 +91,7 @@ public function testDefaultSuggestion() $this->assertCount(2, $feedback['suggestions'], "default suggestion doesn\'t override existing suggestion"); } - public function testBruteforceFeedback() + public function testBruteforceFeedback(): void { $match = new Bruteforce('qkcriv', 0, 6, 'qkcriv'); $feedback = $this->feedback->getFeedback(1, [$match]); diff --git a/test/MatcherTest.php b/test/MatcherTest.php index 80c4c04..78f3920 100644 --- a/test/MatcherTest.php +++ b/test/MatcherTest.php @@ -4,34 +4,36 @@ namespace ZxcvbnPhp\Test; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use ZxcvbnPhp\Matcher; use ZxcvbnPhp\Matchers\Bruteforce; use ZxcvbnPhp\Matchers\DictionaryMatch; +use ZxcvbnPhp\Matchers\RepeatMatch; -/** - * @covers \ZxcvbnPhp\Matcher - */ +#[CoversClass(Matcher::class)] class MatcherTest extends TestCase { - public function testGetMatches() + public function testGetMatches(): void { $matcher = new Matcher(); $matches = $matcher->getMatches('jjj'); + $this->assertInstanceOf(RepeatMatch::class, $matches[0]); $this->assertSame('repeat', $matches[0]->pattern, 'Pattern incorrect'); $this->assertCount(1, $matches); $matches = $matcher->getMatches('jjjjj'); + $this->assertInstanceOf(RepeatMatch::class, $matches[0]); $this->assertSame('repeat', $matches[0]->pattern, 'Pattern incorrect'); } - public function testEmptyString() + public function testEmptyString(): void { $matcher = new Matcher(); $this->assertEmpty($matcher->getMatches(''), "doesn't match ''"); } - public function testMultiplePatterns() + public function testMultiplePatterns(): void { $matcher = new Matcher(); $password = 'r0sebudmaelstrom11/20/91aaaa'; @@ -58,7 +60,7 @@ public function testMultiplePatterns() * There's a similar test in DictionaryTest for this as well, but this specific test is for ensuring that the * user input gets passed from the Matcher class through to DictionaryMatch function. */ - public function testUserDefinedWords() + public function testUserDefinedWords(): void { $matcher = new Matcher(); $matches = $matcher->getMatches('_wQbgL491', ['PJnD', 'WQBG', 'ZhwZ']); @@ -67,7 +69,7 @@ public function testUserDefinedWords() $this->assertSame('wQbg', $matches[0]->token, "user input match has correct token"); } - public function testAddMatcherWillThrowException() + public function testAddMatcherWillThrowException(): void { $this->expectException(\InvalidArgumentException::class); @@ -77,7 +79,7 @@ public function testAddMatcherWillThrowException() $this->expectNotToPerformAssertions(); } - public function testAddMatcherWillReturnSelf() + public function testAddMatcherWillReturnSelf(): void { $matcher = new Matcher(); $result = $matcher->addMatcher(Bruteforce::class); diff --git a/test/Matchers/AbstractMatchTest.php b/test/Matchers/AbstractMatchTest.php index f1d6645..700d0e7 100644 --- a/test/Matchers/AbstractMatchTest.php +++ b/test/Matchers/AbstractMatchTest.php @@ -5,6 +5,7 @@ namespace ZxcvbnPhp\Test\Matchers; use PHPUnit\Framework\TestCase; +use ZxcvbnPhp\Matchers\BaseMatch; abstract class AbstractMatchTest extends TestCase { @@ -15,12 +16,11 @@ abstract class AbstractMatchTest extends TestCase * * @see test-matching.coffee * - * @param string $pattern - * @param array $prefixes - * @param array $suffixes - * @return array a list of triplets [variant, i, j] where [i,j] is the start/end of the pattern, inclusive + * @param array $prefixes + * @param array $suffixes + * @return array a list of triplets [variant, i, j] where [i,j] is the start/end of the pattern, inclusive */ - protected function generatePasswords($pattern, $prefixes, $suffixes) + protected function generatePasswords(string $pattern, array $prefixes, array $suffixes): array { $output = []; @@ -33,8 +33,8 @@ protected function generatePasswords($pattern, $prefixes, $suffixes) foreach ($prefixes as $prefix) { foreach ($suffixes as $suffix) { - $i = strlen($prefix); - $j = strlen($prefix) + strlen($pattern) - 1; + $i = strlen((string) $prefix); + $j = strlen((string) $prefix) + strlen($pattern) - 1; $output[] = [ $prefix . $pattern . $suffix, @@ -48,31 +48,30 @@ protected function generatePasswords($pattern, $prefixes, $suffixes) } /** - * [checkMatches description] * @param string $prefix This is prepended to the message of any checks that are run - * @param array $matches [description] - * @param array|string $patternNames array of pattern names, or a single pattern which will be repeated - * @param array $patterns [description] - * @param array $ijs [description] - * @param array $props [description] + * @param array $matches + * @param array|string $patternNames array of pattern names, or a single pattern which will be repeated + * @param array $patterns + * @param array $ijs + * @param array $props */ protected function checkMatches( - $prefix, - $matches, - $patternNames, - $patterns, - $ijs, - $props - ) { + string $prefix, + array $matches, + array|string $patternNames, + array $patterns, + array $ijs, + array $props + ): void { if (is_string($patternNames)) { # shortcut: if checking for a list of the same type of patterns, # allow passing a string 'pat' instead of array ['pat', 'pat', ...] $patternNames = array_fill(0, count($patterns), $patternNames); } - $this->assertSame( + $this->assertCount( count($patterns), - count($matches), + $matches, $prefix . ": matches.length == " . count($patterns) ); @@ -80,7 +79,7 @@ protected function checkMatches( $match = $matches[$k]; $patternName = $patternNames[$k]; $pattern = $patterns[$k]; - list($i, $j) = $ijs[$k]; + [$i, $j] = $ijs[$k]; $this->assertSame( $patternName, diff --git a/test/Matchers/BruteforceTest.php b/test/Matchers/BruteforceTest.php index 7d5c7cd..fba8741 100644 --- a/test/Matchers/BruteforceTest.php +++ b/test/Matchers/BruteforceTest.php @@ -8,7 +8,7 @@ class BruteforceTest extends AbstractMatchTest { - public function testMatch() + public function testMatch(): void { $password = 'uH2nvQbugW'; @@ -22,7 +22,7 @@ public function testMatch() ); } - public function testMultibyteMatch() + public function testMultibyteMatch(): void { $password = '中华人民共和国'; @@ -36,17 +36,17 @@ public function testMultibyteMatch() ); } - public function testGuessesMax() + public function testGuessesMax(): void { $token = str_repeat('a', 1000); $match = new Bruteforce($token, 0, 999, $token); - $this->assertNotEquals(INF, $match->getGuesses(), "long string doesn't return infinite guesses"); + $this->assertNotSame(INF, $match->getGuesses(), "long string doesn't return infinite guesses"); } - public function testGuessesMultibyteCharacter() + public function testGuessesMultibyteCharacter(): void { $token = '🙂'; // smiley face emoji $match = new Bruteforce($token, 0, 1, $token); - $this->assertSame(11.0, $match->getGuesses(), "multibyte character treated as one character"); + $this->assertEqualsWithDelta(11.0, $match->getGuesses(), PHP_FLOAT_EPSILON, "multibyte character treated as one character"); } } diff --git a/test/Matchers/DateTest.php b/test/Matchers/DateTest.php index 794c9c5..73542fe 100644 --- a/test/Matchers/DateTest.php +++ b/test/Matchers/DateTest.php @@ -4,28 +4,28 @@ namespace ZxcvbnPhp\Test\Matchers; +use Iterator; +use PHPUnit\Framework\Attributes\DataProvider; use ZxcvbnPhp\Matchers\DateMatch; class DateTest extends AbstractMatchTest { - public function separatorProvider() + public static function separatorProvider(): Iterator { - return [ - [''], - [' '], - ['-'], - ['/'], - ['\\'], - ['_'], - ['.'], - ]; + yield ['']; + yield [' ']; + yield ['-']; + yield ['/']; + yield ['\\']; + yield ['_']; + yield ['.']; } /** - * @dataProvider separatorProvider * @param string $sep */ - public function testSeparators($sep) + #[DataProvider('separatorProvider')] + public function testSeparators($sep): void { $password = "13{$sep}2{$sep}1921"; @@ -44,14 +44,14 @@ public function testSeparators($sep) ); } - public function testDateOrders() + public function testDateOrders(): void { - list($d, $m, $y) = [8, 8, 88]; + [$d, $m, $y] = [8, 8, 88]; $orders = ['mdy', 'dmy', 'ymd', 'ydm']; foreach ($orders as $order) { $password = str_replace( ['y', 'm', 'd'], - [$y, $m, $d], + [(string) $y, (string) $m, (string) $d], $order ); $this->checkMatches( @@ -70,7 +70,7 @@ public function testDateOrders() } } - public function testMatchesClosestToReferenceYear() + public function testMatchesClosestToReferenceYear(): void { $password = '111504'; $this->checkMatches( @@ -88,23 +88,21 @@ public function testMatchesClosestToReferenceYear() ); } - public function normalDateProvider() + public static function normalDateProvider(): Iterator { - return [ - [1, 1, 1999], - [11, 8, 2000], - [9, 12, 2005], - [22, 11, 1551] - ]; + yield [1, 1, 1999]; + yield [11, 8, 2000]; + yield [9, 12, 2005]; + yield [22, 11, 1551]; } /** - * @dataProvider normalDateProvider * @param int $day * @param int $month * @param int $year */ - public function testNormalDatesWithoutSeparator($day, $month, $year) + #[DataProvider('normalDateProvider')] + public function testNormalDatesWithoutSeparator($day, $month, $year): void { $password = "{$year}{$month}{$day}"; $this->checkMatches( @@ -121,12 +119,12 @@ public function testNormalDatesWithoutSeparator($day, $month, $year) } /** - * @dataProvider normalDateProvider * @param int $day * @param int $month * @param int $year */ - public function testNormalDatesWithSeparator($day, $month, $year) + #[DataProvider('normalDateProvider')] + public function testNormalDatesWithSeparator($day, $month, $year): void { $password = "{$year}.{$month}.{$day}"; $this->checkMatches( @@ -142,7 +140,7 @@ public function testNormalDatesWithSeparator($day, $month, $year) ); } - public function testMatchesZeroPaddedDates() + public function testMatchesZeroPaddedDates(): void { $password = "02/02/02"; $this->checkMatches( @@ -160,7 +158,7 @@ public function testMatchesZeroPaddedDates() ); } - public function testFullDateMatched() + public function testFullDateMatched(): void { $password = "2018-01-20"; $this->checkMatches( @@ -178,13 +176,13 @@ public function testFullDateMatched() ); } - public function testMatchesEmbeddedDates() + public function testMatchesEmbeddedDates(): void { $prefixes = ['a', 'ab']; $suffixes = ['!']; $pattern = '1/1/91'; - foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as list($password, $i, $j)) { + foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as [$password, $i, $j]) { $this->checkMatches( "matches embedded dates", DateMatch::match($password), @@ -200,7 +198,7 @@ public function testMatchesEmbeddedDates() } } - public function testMatchesOverlappingDates() + public function testMatchesOverlappingDates(): void { $password = "12/20/1991.12.20"; $this->checkMatches( @@ -218,7 +216,7 @@ public function testMatchesOverlappingDates() ); } - public function testMatchesDatesPadded() + public function testMatchesDatesPadded(): void { $password = "912/20/919"; $this->checkMatches( @@ -236,17 +234,17 @@ public function testMatchesDatesPadded() ); } - public function testReferenceYearImplementation() + public function testReferenceYearImplementation(): void { $this->assertSame((int)date('Y'), DateMatch::getReferenceYear(), "reference year implementation"); } - public function testNonDateThatLooksLikeDate() + public function testNonDateThatLooksLikeDate(): void { $this->assertEmpty(DateMatch::match('30-31-00'), "no match on invalid date"); } - public function testGuessDistanceFromReferenceYear() + public function testGuessDistanceFromReferenceYear(): void { $token = '1123'; $match = new DateMatch($token, 0, strlen($token) - 1, $token, [ @@ -264,7 +262,7 @@ public function testGuessDistanceFromReferenceYear() ); } - public function testGuessMinYearSpace() + public function testGuessMinYearSpace(): void { $token = '112010'; $match = new DateMatch($token, 0, strlen($token) - 1, $token, [ @@ -278,7 +276,7 @@ public function testGuessMinYearSpace() $this->assertSame($expected, $match->getGuesses(), "recent years assume MIN_YEAR_SPACE"); } - public function testGuessWithSeparator() + public function testGuessWithSeparator(): void { $token = '1/1/2010'; $match = new DateMatch($token, 0, strlen($token) - 1, $token, [ @@ -292,7 +290,7 @@ public function testGuessWithSeparator() $this->assertSame($expected, $match->getGuesses(), "extra guesses are added for separators"); } - public function testFeedback() + public function testFeedback(): void { $token = '26/01/1990'; $match = new DateMatch($token, 0, strlen($token) - 1, $token, [ diff --git a/test/Matchers/DictionaryTest.php b/test/Matchers/DictionaryTest.php index 8c2804d..8779375 100644 --- a/test/Matchers/DictionaryTest.php +++ b/test/Matchers/DictionaryTest.php @@ -4,11 +4,16 @@ namespace ZxcvbnPhp\Test\Matchers; +use Iterator; +use PHPUnit\Framework\Attributes\DataProvider; use ZxcvbnPhp\Matchers\DictionaryMatch; class DictionaryTest extends AbstractMatchTest { - protected static $testDicts = [ + /** + * @var array + */ + protected static array $testDicts = [ 'd1' => [ 'motherboard' => 1, 'mother' => 2, @@ -26,20 +31,15 @@ class DictionaryTest extends AbstractMatchTest ]; /** - * @return string[][] + * @return Iterator */ - public function madeUpWordsProvider(): array + public static function madeUpWordsProvider(): Iterator { - return [ - ['jjj'], - ['kdncpqw'], - ]; + yield ['jjj']; + yield ['kdncpqw']; } - /** - * @dataProvider madeUpWordsProvider - * @param string $password - */ + #[DataProvider('madeUpWordsProvider')] public function testWordsNotInDictionary(string $password): void { $matches = DictionaryMatch::match($password); @@ -176,9 +176,7 @@ public function testUserProvidedInput(): void $patterns = ['foo', 'bar']; $matches = DictionaryMatch::match($password, ['foo', 'bar']); - $matches = array_values(array_filter($matches, function ($match) { - return $match->dictionaryName === 'user_inputs'; - })); + $matches = array_values(array_filter($matches, fn($match) => $match->dictionaryName === 'user_inputs')); $this->checkMatches( "matches with provided user input dictionary", @@ -227,7 +225,7 @@ public function testMatchesInMultipleDictionaries(): void public function testGuessesBaseRank(): void { $match = new DictionaryMatch('aaaaa', 0, 5, 'aaaaaa', ['rank' => 32]); - $this->assertSame(32.0, $match->getGuesses(), "base guesses == the rank"); + $this->assertEqualsWithDelta(32.0, $match->getGuesses(), PHP_FLOAT_EPSILON, "base guesses == the rank"); } public function testGuessesCapitalization(): void @@ -238,31 +236,29 @@ public function testGuessesCapitalization(): void } /** - * @return array[] + * @return Iterator */ - public function uppercaseVariationProvider(): array + public static function uppercaseVariationProvider(): Iterator { - return [ - [ '', 1 ], - [ 'a', 1 ], - [ 'A', 2 ], - [ 'abcdef', 1 ], - [ 'Abcdef', 2 ], - [ 'abcdeF', 2 ], - [ 'ABCDEF', 2 ], - [ 'aBcdef', 6 ], // 6 choose 1 - [ 'aBcDef', 21 ], // 6 choose 1 + 6 choose 2 - [ 'ABCDEf', 6 ], // 6 choose 1 - [ 'aBCDEf', 21 ], // 6 choose 1 + 6 choose 2 - [ 'ABCdef', 41 ], // 6 choose 1 + 6 choose 2 + 6 choose 3 - ]; + yield [ '', 1 ]; + yield [ 'a', 1 ]; + yield [ 'A', 2 ]; + yield [ 'abcdef', 1 ]; + yield [ 'Abcdef', 2 ]; + yield [ 'abcdeF', 2 ]; + yield [ 'ABCDEF', 2 ]; + yield [ 'aBcdef', 6 ]; + // 6 choose 1 + yield [ 'aBcDef', 21 ]; + // 6 choose 1 + 6 choose 2 + yield [ 'ABCDEf', 6 ]; + // 6 choose 1 + yield [ 'aBCDEf', 21 ]; + // 6 choose 1 + 6 choose 2 + yield [ 'ABCdef', 41 ]; } - /** - * @dataProvider uppercaseVariationProvider - * @param string $token - * @param float $expectedGuesses - */ + #[DataProvider('uppercaseVariationProvider')] public function testGuessesUppercaseVariations(string $token, float $expectedGuesses): void { $match = new DictionaryMatch($token, 0, strlen($token) - 1, $token, ['rank' => 1]); @@ -394,11 +390,7 @@ public function testFeedbackWordStartsWithUppercase(): void } /** - * @param string $token - * @param string $dictionary - * @param int $rank - * @param bool $soleMatch - * @return array + * @return array */ private function getFeedbackForToken(string $token, string $dictionary, int $rank, bool $soleMatch): array { diff --git a/test/Matchers/L33tTest.php b/test/Matchers/L33tTest.php index 509462e..e356331 100644 --- a/test/Matchers/L33tTest.php +++ b/test/Matchers/L33tTest.php @@ -4,24 +4,34 @@ namespace ZxcvbnPhp\Test\Matchers; +use Iterator; +use PHPUnit\Framework\Attributes\DataProvider; use ReflectionClass; use ZxcvbnPhp\Matchers\L33tMatch; use ZxcvbnPhp\Matchers\BaseMatch; class L33tTest extends AbstractMatchTest { - protected $testTable = [ + /** + * @var array + */ + protected array $testTable = [ 'a' => ['4', '@'], 'c' => ['(', '{', '[', '<'], 'g' => ['6', '9'], 'o' => ['0'], ]; - // Generally we only need to test the public interface of the matchers, but it can be useful - // to occasionally test protected methods to ensure consistency with upstream. + /** + * Generally we only need to test the public interface of the matchers, but it can be useful + * to occasionally test protected methods to ensure consistency with upstream. + * + * @param array $args + * @return array + */ protected static function callProtectedMethod(string $name, array $args) { - $class = new ReflectionClass('\\ZxcvbnPhp\\Test\\Matchers\\MockL33tMatch'); + $class = new ReflectionClass(MockL33tMatch::class); $method = $class->getMethod($name); $method->setAccessible(true); return $method->invokeArgs(null, $args); @@ -120,49 +130,45 @@ public function testCapitalizedDictionaryWordsWithL33tCharactersAfter(): void ); } - public function commonCaseProvider(): array + /** + * @return Iterator + */ + public static function commonCaseProvider(): Iterator { - return [ - [ - 'password' => 'p4ssword', - 'pattern' => 'p4ssword', - 'word' => 'password', - 'dictionary_name' => 'words', - 'rank' => 3, - 'ij' => [0, 7], - 'sub' => ['4' => 'a'] - ], - [ - 'password' => 'p@ssw0rd', - 'pattern' => 'p@ssw0rd', - 'word' => 'password', - 'dictionary_name' => 'words', - 'rank' => 3, - 'ij' => [0, 7], - 'sub' => ['@' => 'a', '0' => 'o'] - ], - [ - 'password' => 'aSdfO{G0asDfO', - 'pattern' => '{G0', - 'word' => 'cgo', - 'dictionary_name' => 'words2', - 'rank' => 1, - 'ij' => [5, 7], - 'sub' => ['{' => 'c', '0' => 'o'] - ], + yield [ + 'password' => 'p4ssword', + 'pattern' => 'p4ssword', + 'word' => 'password', + 'dictionary_name' => 'words', + 'rank' => 3, + 'ij' => [0, 7], + 'sub' => ['4' => 'a'] + ]; + yield [ + 'password' => 'p@ssw0rd', + 'pattern' => 'p@ssw0rd', + 'word' => 'password', + 'dictionary_name' => 'words', + 'rank' => 3, + 'ij' => [0, 7], + 'sub' => ['@' => 'a', '0' => 'o'] + ]; + yield [ + 'password' => 'aSdfO{G0asDfO', + 'pattern' => '{G0', + 'word' => 'cgo', + 'dictionary_name' => 'words2', + 'rank' => 1, + 'ij' => [5, 7], + 'sub' => ['{' => 'c', '0' => 'o'] ]; } /** - * @dataProvider commonCaseProvider - * @param string $password - * @param string $pattern - * @param string $word - * @param string $dictionary - * @param int $rank * @param int[] $ij - * @param array $substitutions + * @param string[] $substitutions */ + #[DataProvider('commonCaseProvider')] public function testCommonL33tSubstitutions(string $password, string $pattern, string $word, string $dictionary, int $rank, array $ij, array $substitutions): void { $this->checkMatches( @@ -245,7 +251,7 @@ public function testSubstitutionSubsets(): void * The character '1' can map to both 'i' and 'l' - there was previously a bug that prevented it from matching * against the latter */ - public function testSubstitutionOfCharacterL() + public function testSubstitutionOfCharacterL(): void { $this->checkMatches( "matches against overlapping l33t patterns", @@ -261,17 +267,17 @@ public function testSubstitutionOfCharacterL() ); } - public function testGuessesL33t() + public function testGuessesL33t(): void { $match = new L33tMatch('aaa@@@', 0, 5, 'aaa@@@', [ 'rank' => 32, - 'sub' => array('@' => 'a') + 'sub' => ['@' => 'a'] ]); $expected = 32.0 * 41; // rank * l33t variations $this->assertSame($expected, $match->getGuesses(), "guesses are doubled when word is reversed"); } - public function testGuessesL33tAndUppercased() + public function testGuessesL33tAndUppercased(): void { $match = new L33tMatch('AaA@@@', 0, 5, 'AaA@@@', [ 'rank' => 32, @@ -285,30 +291,29 @@ public function testGuessesL33tAndUppercased() ); } - public function variationsProvider(): array + /** + * @return Iterator + */ + public static function variationsProvider(): Iterator { - return [ - [ '', 1, [] ], - [ 'a', 1, [] ], - [ '4', 2, ['4' => 'a'] ], - [ '4pple', 2, ['4' => 'a'] ], - [ 'abcet', 1, [] ], - [ '4bcet', 2, ['4' => 'a'] ], - [ 'a8cet', 2, ['8' => 'b'] ], - [ 'abce+', 2, ['+' => 't'] ], - [ '48cet', 4, ['4' => 'a', '8' => 'b'] ], - ['a4a4aa', /* binom(6, 2) */ 15 + /* binom(6, 1) */ 6, ['4' => 'a']], - ['4a4a44', /* binom(6, 2) */ 15 + /* binom(6, 1) */ 6, ['4' => 'a']], - ['a44att+', (/* binom(4, 2) */ 6 + /* binom(4, 1) */ 4) * /* binom(3, 1) */ 3, ['4' => 'a', '+' => 't']], - ]; + yield [ '', 1, [] ]; + yield [ 'a', 1, [] ]; + yield [ '4', 2, ['4' => 'a'] ]; + yield [ '4pple', 2, ['4' => 'a'] ]; + yield [ 'abcet', 1, [] ]; + yield [ '4bcet', 2, ['4' => 'a'] ]; + yield [ 'a8cet', 2, ['8' => 'b'] ]; + yield [ 'abce+', 2, ['+' => 't'] ]; + yield [ '48cet', 4, ['4' => 'a', '8' => 'b'] ]; + yield ['a4a4aa', /* binom(6, 2) */ 15 + /* binom(6, 1) */ 6, ['4' => 'a']]; + yield ['4a4a44', /* binom(6, 2) */ 15 + /* binom(6, 1) */ 6, ['4' => 'a']]; + yield ['a44att+', (/* binom(4, 2) */ 6 + /* binom(4, 1) */ 4) * /* binom(3, 1) */ 3, ['4' => 'a', '+' => 't']]; } /** - * @dataProvider variationsProvider - * @param string $token - * @param float $expectedGuesses - * @param array $substitutions + * @param string[] $substitutions */ + #[DataProvider('variationsProvider')] public function testGuessesL33tVariations(string $token, float $expectedGuesses, array $substitutions): void { $match = new L33tMatch($token, 0, strlen($token) - 1, $token, ['rank' => 1, 'sub' => $substitutions]); diff --git a/test/Matchers/MockL33tMatch.php b/test/Matchers/MockL33tMatch.php index 5474dd3..164b57e 100644 --- a/test/Matchers/MockL33tMatch.php +++ b/test/Matchers/MockL33tMatch.php @@ -8,6 +8,9 @@ class MockL33tMatch extends L33tMatch { + /** + * @return array + */ protected static function getRankedDictionaries(): array { return [ @@ -23,6 +26,9 @@ protected static function getRankedDictionaries(): array ]; } + /** + * @return array + */ protected static function getL33tTable(): array { return [ diff --git a/test/Matchers/MockMatch.php b/test/Matchers/MockMatch.php index 869b7e6..8d360a6 100644 --- a/test/Matchers/MockMatch.php +++ b/test/Matchers/MockMatch.php @@ -8,20 +8,16 @@ class MockMatch extends BaseMatch { - /** @var float */ - protected $guesses; - - public function __construct(int $begin, int $end, float $guesses) + public function __construct(int $begin, int $end, protected float $guesses) { parent::__construct('', $begin, $end, ''); - $this->guesses = $guesses; } /** * Get feedback to a user based on the match. * @param bool $isSoleMatch * Whether this is the only match in the password - * @return array{'warning': string, "suggestions": string[]} + * @return array{warning: string, suggestions: string[]} */ public function getFeedback(bool $isSoleMatch): array { @@ -41,12 +37,12 @@ public function getRawGuesses(): float * * @param string $password * Password to check for match. - * @param array $userInputs + * @param array $userInputs * Array of values related to the user (optional). * @code * array('Alice Smith') * @endcode - * @return array + * @return array * Array of Match objects */ public static function match(string $password, array $userInputs = []): array diff --git a/test/Matchers/RepeatTest.php b/test/Matchers/RepeatTest.php index 9d5ba01..535eb0c 100644 --- a/test/Matchers/RepeatTest.php +++ b/test/Matchers/RepeatTest.php @@ -4,18 +4,19 @@ namespace ZxcvbnPhp\Test\Matchers; +use Iterator; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use ZxcvbnPhp\Matcher; use ZxcvbnPhp\Matchers\Bruteforce; use ZxcvbnPhp\Matchers\RepeatMatch; use ZxcvbnPhp\Matchers\SequenceMatch; use ZxcvbnPhp\Scorer; -/** - * @covers \ZxcvbnPhp\Matchers\RepeatMatch - */ +#[CoversClass(RepeatMatch::class)] class RepeatTest extends AbstractMatchTest { - public function testEmpty() + public function testEmpty(): void { foreach (['', '#'] as $password) { $this->assertEmpty( @@ -25,7 +26,7 @@ public function testEmpty() } } - public function testSingleCharacterEmbeddedRepeats() + public function testSingleCharacterEmbeddedRepeats(): void { $prefixes = ['@', 'y4@']; $suffixes = ['u', 'u%7']; @@ -46,7 +47,7 @@ public function testSingleCharacterEmbeddedRepeats() } } - public function testSingleCharacterRepeats() + public function testSingleCharacterRepeats(): void { foreach ([3, 12] as $length) { foreach (['a', 'Z', '4', '&'] as $chr) { @@ -67,7 +68,7 @@ public function testSingleCharacterRepeats() } } - public function testAdjacentRepeats() + public function testAdjacentRepeats(): void { $str = 'BBB1111aaaaa@@@@@@'; $patterns = ['BBB','1111','aaaaa','@@@@@@']; @@ -84,7 +85,7 @@ public function testAdjacentRepeats() ); } - public function testMultipleNonadjacentRepeeats() + public function testMultipleNonadjacentRepeeats(): void { $str = '2818BBBbzsdf1111@*&@!aaaaaEUDA@@@@@@1729'; $patterns = ['BBB','1111','aaaaa','@@@@@@']; @@ -101,7 +102,7 @@ public function testMultipleNonadjacentRepeeats() ); } - public function testMultiCharacterRepeats() + public function testMultiCharacterRepeats(): void { $pattern = 'abab'; $this->checkMatches( @@ -117,7 +118,7 @@ public function testMultiCharacterRepeats() ); } - public function testGreedyMultiCharacterRepeats() + public function testGreedyMultiCharacterRepeats(): void { $pattern = 'aabaab'; $this->checkMatches( @@ -133,7 +134,7 @@ public function testGreedyMultiCharacterRepeats() ); } - public function testFrequentlyRepeatedMultiCharacterRepeats() + public function testFrequentlyRepeatedMultiCharacterRepeats(): void { $pattern = 'abababab'; $this->checkMatches( @@ -149,7 +150,7 @@ public function testFrequentlyRepeatedMultiCharacterRepeats() ); } - public function testBaseGuesses() + public function testBaseGuesses(): void { $pattern = 'abcabc'; $this->checkMatches( @@ -161,12 +162,12 @@ public function testBaseGuesses() [ 'repeatedChar' => ['abc'], 'repeatCount' => [2], - 'baseGuesses' => [13.0] + 'baseGuesses' => [13] ] ); } - public function testMultibyteRepeat() + public function testMultibyteRepeat(): void { $pattern = '🙂🙂🙂'; @@ -183,7 +184,7 @@ public function testMultibyteRepeat() ); } - public function testRepeatAfterMultibyteCharacters() + public function testRepeatAfterMultibyteCharacters(): void { $pattern = 'niñabella'; @@ -200,17 +201,17 @@ public function testRepeatAfterMultibyteCharacters() ); } - public function testBaseMatches() + public function testBaseMatches(): void { $pattern = 'abcabc'; $match = RepeatMatch::match($pattern)[0]; $baseMatches = $match->baseMatches; - $this->assertSame(1, count($baseMatches)); + $this->assertCount(1, $baseMatches); $this->assertInstanceOf(SequenceMatch::class, $baseMatches[0]); } - public function testBaseMatchesRecursive() + public function testBaseMatchesRecursive(): void { $pattern = 'mqmqmqltltltmqmqmqltltlt'; $match = RepeatMatch::match($pattern)[0]; @@ -224,7 +225,7 @@ public function testBaseMatchesRecursive() $this->assertSame('lt', $baseMatches[1]->repeatedChar); } - public function testDuplicateRepeatsInPassword() + public function testDuplicateRepeatsInPassword(): void { $pattern = 'scoobydoo'; $this->checkMatches( @@ -240,30 +241,28 @@ public function testDuplicateRepeatsInPassword() ); } - public function guessesProvider() + public static function guessesProvider(): Iterator { - return array( - [ 'aa', 'a', 2, 24], - [ '999', '9', 3, 36], - [ '$$$$', '$', 4, 48], - [ 'abab', 'ab', 2, 18], - [ 'batterystaplebatterystaplebatterystaple', 'batterystaple', 3, 85277994] - ); + yield [ 'aa', 'a', 2, 24]; + yield [ '999', '9', 3, 36]; + yield [ '$$$$', '$', 4, 48]; + yield [ 'abab', 'ab', 2, 18]; + yield [ 'batterystaplebatterystaplebatterystaple', 'batterystaple', 3, 85277994]; } /** - * @dataProvider guessesProvider * @param string $token * @param string $repeatedChar * @param int $repeatCount * @param float $expectedGuesses */ + #[DataProvider('guessesProvider')] public function testGuesses(string $token, string $repeatedChar, int $repeatCount, float $expectedGuesses): void { $scorer = new Scorer(); $matcher = new Matcher(); $baseAnalysis = $scorer->getMostGuessableMatchSequence($repeatedChar, $matcher->getMatches($repeatedChar)); - $baseGuesses = $baseAnalysis['guesses']; + $baseGuesses = (int) $baseAnalysis['guesses']; $match = new RepeatMatch($token, 0, strlen($token) - 1, $token, [ 'repeated_char' => $repeatedChar, @@ -271,10 +270,10 @@ public function testGuesses(string $token, string $repeatedChar, int $repeatCoun 'base_guesses' => $baseGuesses, ]); - self::assertSame($expectedGuesses, $match->getGuesses(), "the repeat pattern {$token} has guesses of {$expectedGuesses}"); + $this->assertSame($expectedGuesses, $match->getGuesses(), "the repeat pattern {$token} has guesses of {$expectedGuesses}"); } - public function testFeedbackSingleCharacterRepeat() + public function testFeedbackSingleCharacterRepeat(): void { $token = 'bbbbbb'; $match = new RepeatMatch($token, 0, strlen($token) - 1, $token, [ @@ -295,7 +294,7 @@ public function testFeedbackSingleCharacterRepeat() ); } - public function testFeedbackMultipleCharacterRepeat() + public function testFeedbackMultipleCharacterRepeat(): void { $token = 'bababa'; $match = new RepeatMatch($token, 0, strlen($token) - 1, $token, [ diff --git a/test/Matchers/ReverseDictionaryTest.php b/test/Matchers/ReverseDictionaryTest.php index a0ef9d7..ef3d51d 100644 --- a/test/Matchers/ReverseDictionaryTest.php +++ b/test/Matchers/ReverseDictionaryTest.php @@ -8,7 +8,10 @@ class ReverseDictionaryTest extends AbstractMatchTest { - protected static $testDicts = [ + /** + * @var array + */ + protected static array $testDicts = [ 'd1' => [ '123' => 1, '321' => 2, @@ -17,7 +20,7 @@ class ReverseDictionaryTest extends AbstractMatchTest ], ]; - public function testReversedDictionaryWordWithCustomDictionary() + public function testReversedDictionaryWordWithCustomDictionary(): void { $password = '0123456789'; @@ -36,14 +39,14 @@ public function testReversedDictionaryWordWithCustomDictionary() ); } - public function testGuessesReversed() + public function testGuessesReversed(): void { $match = new ReverseDictionaryMatch('aaa', 0, 2, 'aaa', ['rank' => 32]); $expected = 32.0 * 2; // rank * reversed $this->assertSame($expected, $match->getGuesses(), "guesses are doubled when word is reversed"); } - public function testFeedback() + public function testFeedback(): void { $token = 'ytisrevinu'; $match = new ReverseDictionaryMatch($token, 0, strlen($token) - 1, $token, [ @@ -64,7 +67,7 @@ public function testFeedback() ); } - public function testFeedbackTop100Password() + public function testFeedbackTop100Password(): void { $token = 'retunh'; $match = new ReverseDictionaryMatch($token, 0, strlen($token) - 1, $token, [ @@ -80,7 +83,7 @@ public function testFeedbackTop100Password() ); } - public function testFeedbackShortToken() + public function testFeedbackShortToken(): void { $token = 'eht'; $match = new ReverseDictionaryMatch($token, 0, strlen($token) - 1, $token, [ diff --git a/test/Matchers/SequenceTest.php b/test/Matchers/SequenceTest.php index 9220092..6a47cde 100644 --- a/test/Matchers/SequenceTest.php +++ b/test/Matchers/SequenceTest.php @@ -4,40 +4,39 @@ namespace ZxcvbnPhp\Test\Matchers; +use Iterator; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use ZxcvbnPhp\Matchers\SequenceMatch; -/** - * @covers \ZxcvbnPhp\Matchers\SequenceMatch - */ +#[CoversClass(SequenceMatch::class)] class SequenceTest extends AbstractMatchTest { - public function shortPasswordProvider() + /** + * @return Iterator + */ + public static function shortPasswordProvider(): Iterator { - return [ - [''], - ['a'], - ['1'], - ]; + yield ['']; + yield ['a']; + yield ['1']; } - /** - * @dataProvider shortPasswordProvider - * @param $password - */ - public function testShortPassword($password) + #[DataProvider('shortPasswordProvider')] + public function testShortPassword(string $password): void { $matches = SequenceMatch::match($password); - $this->assertEmpty($matches, "doesn't match length-" . strlen($password) . " sequences"); + $this->assertEmpty($matches, "doesn't match length-" . strlen((string) $password) . " sequences"); } - public function testNonSequence() + public function testNonSequence(): void { $password = 'password'; $matches = SequenceMatch::match($password); $this->assertEmpty($matches, "doesn't match password that's not a sequence"); } - public function testOverlappingPatterns() + public function testOverlappingPatterns(): void { $password = 'abcbabc'; @@ -53,13 +52,13 @@ public function testOverlappingPatterns() ); } - public function testEmbeddedSequencePatterns() + public function testEmbeddedSequencePatterns(): void { $prefixes = ['!', '22']; $suffixes = ['!', '22']; $pattern = 'jihg'; - foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as list($password, $i, $j)) { + foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as [$password, $i, $j]) { $this->checkMatches( "matches embedded sequence patterns", SequenceMatch::match($password), @@ -74,32 +73,28 @@ public function testEmbeddedSequencePatterns() } } - public function sequenceProvider() + /** + * @return Iterator + */ + public static function sequenceProvider(): Iterator { - return [ - ['ABC', 'upper', true], - ['CBA', 'upper', false], - ['PQR', 'upper', true], - ['RQP', 'upper', false], - ['XYZ', 'upper', true], - ['ZYX', 'upper', false], - ['abcd', 'lower', true], - ['dcba', 'lower', false], - ['jihg', 'lower', false], - ['wxyz', 'lower', true], - ['zxvt', 'lower', false], - ['0369', 'digits', true], - ['97531', 'digits', false] - ]; + yield ['ABC', 'upper', true]; + yield ['CBA', 'upper', false]; + yield ['PQR', 'upper', true]; + yield ['RQP', 'upper', false]; + yield ['XYZ', 'upper', true]; + yield ['ZYX', 'upper', false]; + yield ['abcd', 'lower', true]; + yield ['dcba', 'lower', false]; + yield ['jihg', 'lower', false]; + yield ['wxyz', 'lower', true]; + yield ['zxvt', 'lower', false]; + yield ['0369', 'digits', true]; + yield ['97531', 'digits', false]; } - /** - * @dataProvider sequenceProvider - * @param string $password - * @param string $name - * @param bool $ascending - */ - public function testSequenceInformation($password, $name, $ascending) + #[DataProvider('sequenceProvider')] + public function testSequenceInformation(string $password, string $name, bool $ascending): void { $this->checkMatches( "matches " . $password . " as a " . $name . " sequence", @@ -114,7 +109,7 @@ public function testSequenceInformation($password, $name, $ascending) ); } - public function testMultipleMatches() + public function testMultipleMatches(): void { $password = 'pass123wordZYX'; $this->checkMatches( @@ -130,7 +125,7 @@ public function testMultipleMatches() ); } - public function testMultibytePassword() + public function testMultibytePassword(): void { $pattern = 'muÃeca'; @@ -147,7 +142,7 @@ public function testMultibytePassword() ); } - public function testMultibyteSequence() + public function testMultibyteSequence(): void { $pattern = 'αβγδεζ'; @@ -164,23 +159,23 @@ public function testMultibyteSequence() ); } - public function guessProvider() + /** + * @return Iterator + */ + public static function guessProvider(): Iterator { - return array( - array('ab', true, 4 * 2), // obvious start * len-2 - array('XYZ', true, 26 * 3), // base26 * len-3 - array('4567', true, 10 * 4), // base10 * len-4 - array('7654', false, 10 * 4 * 2), // base10 * len-4 * descending - array('ZYX', false, 4 * 3 * 2), // obvious start * len-3 * descending - ); + yield ['ab', true, 4 * 2]; + // obvious start * len-2 + yield ['XYZ', true, 26 * 3]; + // base26 * len-3 + yield ['4567', true, 10 * 4]; + // base10 * len-4 + yield ['7654', false, 10 * 4 * 2]; + // base10 * len-4 * descending + yield ['ZYX', false, 4 * 3 * 2]; } - /** - * @dataProvider guessProvider - * @param string $token - * @param bool $ascending - * @param float $expectedGuesses - */ + #[DataProvider('guessProvider')] public function testGuesses(string $token, bool $ascending, float $expectedGuesses): void { $match = new SequenceMatch($token, 0, strlen($token) - 1, $token, ['ascending' => $ascending]); @@ -191,7 +186,7 @@ public function testGuesses(string $token, bool $ascending, float $expectedGuess ); } - public function testFeedback() + public function testFeedback(): void { $token = 'rstuvw'; $match = new SequenceMatch($token, 0, strlen($token) - 1, $token, ['ascending' => true]); diff --git a/test/Matchers/SpatialTest.php b/test/Matchers/SpatialTest.php index 44d5304..d3a3e9c 100644 --- a/test/Matchers/SpatialTest.php +++ b/test/Matchers/SpatialTest.php @@ -4,32 +4,27 @@ namespace ZxcvbnPhp\Test\Matchers; +use Iterator; +use PHPUnit\Framework\Attributes\DataProvider; use ZxcvbnPhp\Matchers\BaseMatch; use ZxcvbnPhp\Matchers\SpatialMatch; use ZxcvbnPhp\Math\Binomial; -/** - * @covers \ZxcvbnPhp\Matchers\SpatialMatch - */ +#[\PHPUnit\Framework\Attributes\CoversClass(\ZxcvbnPhp\Matchers\SpatialMatch::class)] class SpatialTest extends AbstractMatchTest { /** - * @return string[][] + * @return Iterator */ - public function shortPatternDataProvider(): array + public static function shortPatternDataProvider(): Iterator { - return [ - [''], - ['/'], - ['qw'], - ['*/'], - ]; + yield ['']; + yield ['/']; + yield ['qw']; + yield ['*/']; } - /** - * @dataProvider shortPatternDataProvider - * @param string $password - */ + #[DataProvider('shortPatternDataProvider')] public function testShortPatterns(string $password): void { $this->assertSame( @@ -70,33 +65,34 @@ public function testSurroundedPattern(): void ); } - public function spatialDataProvider(): array + /** + * @return Iterator + */ + public static function spatialDataProvider(): Iterator { - return [ - ['12345', 'qwerty', 1, 0], - ['@WSX', 'qwerty', 1, 4], - ['6tfGHJ', 'qwerty', 2, 3], - ['hGFd', 'qwerty', 1, 2], - ['/;p09876yhn', 'qwerty', 3, 0], - ['Xdr%', 'qwerty', 1, 2], - ['159-', 'keypad', 1, 0], - ['*84', 'keypad', 1, 0], - ['/8520', 'keypad', 1, 0], - ['369', 'keypad', 1, 0], - ['/963.', 'mac_keypad', 1, 0], - ['*-632.0214', 'mac_keypad', 9, 0], - ['aoEP%yIxkjq:', 'dvorak', 4, 5], - [';qoaOQ:Aoq;a', 'dvorak', 11, 4], - ]; + yield ['12345', 'qwerty', 1, 0]; + yield ['@WSX', 'qwerty', 1, 4]; + yield ['6tfGHJ', 'qwerty', 2, 3]; + yield ['hGFd', 'qwerty', 1, 2]; + yield ['/;p09876yhn', 'qwerty', 3, 0]; + yield ['Xdr%', 'qwerty', 1, 2]; + yield ['159-', 'keypad', 1, 0]; + yield ['*84', 'keypad', 1, 0]; + yield ['/8520', 'keypad', 1, 0]; + yield ['369', 'keypad', 1, 0]; + yield ['/963.', 'mac_keypad', 1, 0]; + yield ['*-632.0214', 'mac_keypad', 9, 0]; + yield ['aoEP%yIxkjq:', 'dvorak', 4, 5]; + yield [';qoaOQ:Aoq;a', 'dvorak', 11, 4]; } /** - * @dataProvider spatialDataProvider * @param string $password * @param string $keyboard * @param int $turns * @param int $shifts */ + #[DataProvider('spatialDataProvider')] public function testSpatialPatterns(string $password, string $keyboard, int $turns, int $shifts): void { $graphs = [$keyboard => SpatialMatch::getAdjacencyGraphs()[$keyboard]]; @@ -191,26 +187,19 @@ public function testGuessesEverythingShifted(): void } /** - * @return array[] + * @return Iterator */ - public function complexGuessProvider(): array + public static function complexGuessProvider(): Iterator { - return [ - ['6yhgf', 2, 19596], - ['asde3w', 3, 203315], - ['zxcft6yh', 3, 558460], - ['xcvgy7uj', 3, 558460], - ['ertghjm,.', 5, 30160744], - ['qwerfdsazxcv', 5, 175281377], - ]; + yield ['6yhgf', 2, 19596]; + yield ['asde3w', 3, 203315]; + yield ['zxcft6yh', 3, 558460]; + yield ['xcvgy7uj', 3, 558460]; + yield ['ertghjm,.', 5, 30160744]; + yield ['qwerfdsazxcv', 5, 175281377]; } - /** - * @dataProvider complexGuessProvider - * @param string $token - * @param int $turns - * @param float $expected - */ + #[DataProvider('complexGuessProvider')] public function testGuessesComplexCase(string $token, int $turns, float $expected): void { $match = new SpatialMatch($token, 0, strlen($token) - 1, $token, [ @@ -220,7 +209,6 @@ public function testGuessesComplexCase(string $token, int $turns, float $expecte ]); $actual = $match->getGuesses(); - $this->assertIsFloat($actual); $this->assertEqualsWithDelta( $expected, diff --git a/test/Matchers/YearTest.php b/test/Matchers/YearTest.php index 19a094d..535b612 100644 --- a/test/Matchers/YearTest.php +++ b/test/Matchers/YearTest.php @@ -4,34 +4,30 @@ namespace ZxcvbnPhp\Test\Matchers; +use Iterator; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use ZxcvbnPhp\Matchers\DateMatch; use ZxcvbnPhp\Matchers\YearMatch; -/** - * @covers \ZxcvbnPhp\Matchers\YearMatch - */ +#[CoversClass(YearMatch::class)] class YearTest extends AbstractMatchTest { - public function testNoMatchForNonYear() + public function testNoMatchForNonYear(): void { $password = 'password'; $this->assertEmpty(YearMatch::match($password)); } - public function recentYearProvider() + public static function recentYearProvider(): Iterator { - return [ - ['1922'], - ['2001'], - ['2017'] - ]; + yield ['1922']; + yield ['2001']; + yield ['2017']; } - /** - * @dataProvider recentYearProvider - * @param $password - */ - public function testRecentYears($password) + #[DataProvider('recentYearProvider')] + public function testRecentYears(string $password): void { $this->checkMatches( "matches recent year", @@ -43,32 +39,27 @@ public function testRecentYears($password) ); } - public function nonRecentYearProvider() + public static function nonRecentYearProvider(): Iterator { - return [ - ['1420'], - ['1899'], - ['2345'] - ]; + yield ['1420']; + yield ['1899']; + yield ['2345']; } - /** - * @dataProvider nonRecentYearProvider - * @param $password - */ - public function testNonRecentYears($password) + #[DataProvider('nonRecentYearProvider')] + public function testNonRecentYears(string $password): void { $matches = YearMatch::match($password); $this->assertEmpty($matches, "does not match non-recent year"); } - public function testYearSurroundedByWords() + public function testYearSurroundedByWords(): void { $prefixes = ['car', 'dog']; $suffixes = ['car', 'dog']; $pattern = '1900'; - foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as list($password, $i, $j)) { + foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as [$password, $i, $j]) { $this->checkMatches( "identifies years surrounded by words", YearMatch::match($password), @@ -85,7 +76,7 @@ public function testYearSurroundedByWords() $this->assertSame('1900', $matches[0]->token, 'Token incorrect'); } - public function testYearWithinOtherNumbers() + public function testYearWithinOtherNumbers(): void { $password = '419004'; $this->checkMatches( @@ -98,7 +89,7 @@ public function testYearWithinOtherNumbers() ); } - public function testGuessesPast() + public function testGuessesPast(): void { $token = '1972'; $match = new YearMatch($token, 0, 3, $token); @@ -110,7 +101,7 @@ public function testGuessesPast() ); } - public function testGuessesFuture() + public function testGuessesFuture(): void { $token = '2050'; $match = new YearMatch($token, 0, 3, $token); @@ -122,19 +113,21 @@ public function testGuessesFuture() ); } - public function testGuessesUnderMinimumYearSpace() + public function testGuessesUnderMinimumYearSpace(): void { $token = '2005'; $match = new YearMatch($token, 0, 3, $token); - $this->assertSame( - 20.0, // DateMatch::MIN_YEAR_SPACE + $this->assertEqualsWithDelta( + 20.0, + // DateMatch::MIN_YEAR_SPACE $match->getGuesses(), + PHP_FLOAT_EPSILON, "guesses of MIN_YEAR_SPACE for a year close to REFERENCE_YEAR" ); } - public function testFeedback() + public function testFeedback(): void { $token = '2010'; $match = new YearMatch($token, 0, strlen($token) - 1, $token); diff --git a/test/Math/BinomialTest.php b/test/Math/BinomialTest.php index 61fc87a..68f5c30 100644 --- a/test/Math/BinomialTest.php +++ b/test/Math/BinomialTest.php @@ -4,36 +4,37 @@ namespace ZxcvbnPhp\Test\Math; +use Iterator; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ZxcvbnPhp\Math\Binomial; use ZxcvbnPhp\Math\BinomialProvider; class BinomialTest extends TestCase { - public function binomialDataProvider() + public static function binomialDataProvider(): Iterator { - return [ - [ 0, 0, 1.0 ], - [ 1, 0, 1.0 ], - [ 5, 0, 1.0 ], - [ 0, 1, 0.0 ], - [ 0, 5, 0.0 ], - [ 2, 1, 2.0 ], - [ 4, 2, 6.0 ], - [ 33, 7, 4272048.0 ], - [ 206, 202, 72867865.0 ], - [ 3, 5, 0.0 ], - [ 29847, 2, 445406781.0 ], - [ 49, 12, 92263734836.0 ], - ]; + yield [ 0, 0, 1.0 ]; + yield [ 1, 0, 1.0 ]; + yield [ 5, 0, 1.0 ]; + yield [ 0, 1, 0.0 ]; + yield [ 0, 5, 0.0 ]; + yield [ 2, 1, 2.0 ]; + yield [ 4, 2, 6.0 ]; + yield [ 33, 7, 4272048.0 ]; + yield [ 206, 202, 72867865.0 ]; + yield [ 3, 5, 0.0 ]; + yield [ 29847, 2, 445406781.0 ]; + yield [ 49, 12, 92263734836.0 ]; } - public function testHasProvider() + public function testHasProvider(): void { $this->assertNotEmpty(Binomial::getUsableProviderClasses()); } - public function testChosenProviderMatchesExpected() + public function testChosenProviderMatchesExpected(): void { $providerClasses = Binomial::getUsableProviderClasses(); @@ -41,12 +42,12 @@ public function testChosenProviderMatchesExpected() } /** - * @dataProvider binomialDataProvider * @param int $n * @param int $k * @param float $expected */ - public function testBinomialCoefficient(int $n, int $k, float $expected) + #[DataProvider('binomialDataProvider')] + public function testBinomialCoefficient(int $n, int $k, float $expected): void { foreach (Binomial::getUsableProviderClasses() as $providerClass) { $provider = new $providerClass(); diff --git a/test/ScorerTest.php b/test/ScorerTest.php index 594e711..e9b7415 100644 --- a/test/ScorerTest.php +++ b/test/ScorerTest.php @@ -4,42 +4,40 @@ namespace ZxcvbnPhp\Test; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use ZxcvbnPhp\Scorer; use ZxcvbnPhp\Test\Matchers\MockMatch; -/** - * @covers \ZxcvbnPhp\Scorer - */ +#[CoversClass(Scorer::class)] class ScorerTest extends TestCase { public const PASSWORD = '0123456789'; - /** @var Scorer */ - private $scorer; + private Scorer $scorer; public function setUp(): void { $this->scorer = new Scorer(); } - public function testStrictAssertions() + public function testStrictAssertions(): void { $this->assertNotSame(1, 1.0); } - public function testBlankPassword() + public function testBlankPassword(): void { $result = $this->scorer->getMostGuessableMatchSequence('', []); - $this->assertSame(1.0, $result['guesses']); + $this->assertEqualsWithDelta(1.0, $result['guesses'], PHP_FLOAT_EPSILON); $this->assertEmpty($result['sequence']); } - public function testEmptyMatchSequence() + public function testEmptyMatchSequence(): void { $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, []); - $this->assertSame(1, count($result['sequence']), "result.sequence.length == 1"); - $this->assertSame(10000000001.0, $result['guesses'], "result.guesses == 10000000001"); + $this->assertCount(1, $result['sequence'], "result.sequence.length == 1"); + $this->assertEqualsWithDelta(10000000001.0, $result['guesses'], PHP_FLOAT_EPSILON, "result.guesses == 10000000001"); $match = $result['sequence'][0]; $this->assertSame('bruteforce', $match->pattern, "match.pattern == 'bruteforce'"); @@ -47,12 +45,12 @@ public function testEmptyMatchSequence() $this->assertSame([0, 9], [$match->begin, $match->end], "[i, j] == [0, 9]"); } - public function testMatchAndBruteforceWithPrefix() + public function testMatchAndBruteforceWithPrefix(): void { $match = new MockMatch(0, 5, 1); $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, [$match], true); - $this->assertSame(2, count($result['sequence']), "result.sequence.length == 2"); + $this->assertCount(2, $result['sequence'], "result.sequence.length == 2"); $this->assertSame($match, $result['sequence'][0], "first match is the provided match object"); $match1 = $result['sequence'][1]; @@ -61,12 +59,12 @@ public function testMatchAndBruteforceWithPrefix() $this->assertSame([6, 9], [$match1->begin, $match1->end], "second match covers full suffix after first match"); } - public function testMatchAndBruteforceWithSuffix() + public function testMatchAndBruteforceWithSuffix(): void { $match = new MockMatch(3, 9, 1); $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, [$match], true); - $this->assertSame(2, count($result['sequence']), "result.sequence.length == 2"); + $this->assertCount(2, $result['sequence'], "result.sequence.length == 2"); $this->assertSame($match, $result['sequence'][1], "second match is the provided match object"); $match0 = $result['sequence'][0]; @@ -75,12 +73,12 @@ public function testMatchAndBruteforceWithSuffix() $this->assertSame([0, 2], [$match0->begin, $match0->end], "first match covers full prefix before second match"); } - public function testMatchAndBruteforceWithInfix() + public function testMatchAndBruteforceWithInfix(): void { $match = new MockMatch(1, 8, 1); $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, [$match], true); - $this->assertSame(3, count($result['sequence']), "result.sequence.length == 3"); + $this->assertCount(3, $result['sequence'], "result.sequence.length == 3"); $match0 = $result['sequence'][0]; $match2 = $result['sequence'][2]; @@ -92,7 +90,7 @@ public function testMatchAndBruteforceWithInfix() $this->assertSame([9, 9], [$match2->begin, $match2->end], "third match covers full suffix after second match"); } - public function testBasicGuesses() + public function testBasicGuesses(): void { $matches = [ new MockMatch(0, 9, 1), @@ -100,11 +98,11 @@ public function testBasicGuesses() ]; $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, $matches, true); - $this->assertSame(1, count($result['sequence']), "result.sequence.length == 1"); + $this->assertCount(1, $result['sequence'], "result.sequence.length == 1"); $this->assertSame($matches[0], $result['sequence'][0], "result.sequence[0] == m0"); } - public function testChoosesLowerGuessesMatchesForSameSpan() + public function testChoosesLowerGuessesMatchesForSameSpan(): void { $matches = [ new MockMatch(0, 9, 1), @@ -112,11 +110,11 @@ public function testChoosesLowerGuessesMatchesForSameSpan() ]; $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, $matches, true); - $this->assertSame(1, count($result['sequence']), "result.sequence.length == 1"); + $this->assertCount(1, $result['sequence'], "result.sequence.length == 1"); $this->assertSame($matches[0], $result['sequence'][0], "result.sequence[0] == m0"); } - public function testChoosesLowerGuessesMatchesForSameSpanReversedOrder() + public function testChoosesLowerGuessesMatchesForSameSpanReversedOrder(): void { $matches = [ new MockMatch(0, 9, 2), @@ -124,11 +122,11 @@ public function testChoosesLowerGuessesMatchesForSameSpanReversedOrder() ]; $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, $matches, true); - $this->assertSame(1, count($result['sequence']), "result.sequence.length == 1"); + $this->assertCount(1, $result['sequence'], "result.sequence.length == 1"); $this->assertSame($matches[1], $result['sequence'][0], "result.sequence[0] == m1"); } - public function testChoosesSupersetMatchWhenApplicable() + public function testChoosesSupersetMatchWhenApplicable(): void { $matches = [ new MockMatch(0, 9, 3), @@ -137,11 +135,11 @@ public function testChoosesSupersetMatchWhenApplicable() ]; $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, $matches, true); - $this->assertSame(3.0, $result['guesses'], "total guesses == 3"); + $this->assertEqualsWithDelta(3.0, $result['guesses'], PHP_FLOAT_EPSILON, "total guesses == 3"); $this->assertSame([$matches[0]], $result['sequence'], "sequence is [m0]"); } - public function testChoosesSubsetMatchesWhenApplicable() + public function testChoosesSubsetMatchesWhenApplicable(): void { $matches = [ new MockMatch(0, 9, 5), @@ -150,7 +148,7 @@ public function testChoosesSubsetMatchesWhenApplicable() ]; $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, $matches, true); - $this->assertSame(4.0, $result['guesses'], "total guesses == 4"); + $this->assertEqualsWithDelta(4.0, $result['guesses'], PHP_FLOAT_EPSILON, "total guesses == 4"); $this->assertSame([$matches[1], $matches[2]], $result['sequence'], "sequence is [m1, m2]"); } } diff --git a/test/TimeEstimatorTest.php b/test/TimeEstimatorTest.php index 1efca32..b7d7240 100644 --- a/test/TimeEstimatorTest.php +++ b/test/TimeEstimatorTest.php @@ -4,62 +4,64 @@ namespace ZxcvbnPhp\Test; +use Iterator; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ZxcvbnPhp\TimeEstimator; class TimeEstimatorTest extends TestCase { - /** @var TimeEstimator */ - private $timeEstimator; + private TimeEstimator $timeEstimator; public function setUp(): void { $this->timeEstimator = new TimeEstimator(); } - public function testTime100PerHour() + public function testTime100PerHour(): void { $actual = $this->timeEstimator->estimateAttackTimes(100)['crack_times_display']['online_throttling_100_per_hour']; $this->assertSame('1 hour', $actual, "100 guesses / 100 per hour = 1 hour"); } - public function testTime10PerSecond() + public function testTime10PerSecond(): void { $actual = $this->timeEstimator->estimateAttackTimes(10)['crack_times_display']['online_no_throttling_10_per_second']; $this->assertSame('1 second', $actual, "10 guesses / 10 per second = 1 second"); } - public function testTime1e4PerSecond() + public function testTime1e4PerSecond(): void { $actual = $this->timeEstimator->estimateAttackTimes(1e5)['crack_times_display']['offline_slow_hashing_1e4_per_second']; $this->assertSame('10 seconds', $actual, "1e5 guesses / 1e4 per second = 10 seconds"); } - public function testTime1e10PerSecond() + public function testTime1e10PerSecond(): void { $actual = $this->timeEstimator->estimateAttackTimes(2e11)['crack_times_display']['offline_fast_hashing_1e10_per_second']; $this->assertSame('20 seconds', $actual, "2e11 guesses / 1e10 per second = 20 seconds"); } - public function testTimeLessThanASecond() + public function testTimeLessThanASecond(): void { $actual = $this->timeEstimator->estimateAttackTimes(1)['crack_times_display']['offline_fast_hashing_1e10_per_second']; $this->assertSame('less than a second', $actual, "less than a second"); } - public function testTimeCenturies() + public function testTimeCenturies(): void { $actual = $this->timeEstimator->estimateAttackTimes(1e10)['crack_times_display']['online_throttling_100_per_hour']; $this->assertSame('centuries', $actual, "centuries"); } - public function testTimeRounding() + public function testTimeRounding(): void { $actual = $this->timeEstimator->estimateAttackTimes(1500)['crack_times_display']['online_no_throttling_10_per_second']; $this->assertSame('3 minutes', $actual, "1500 guesses / 10 per second = 3 minutes and not 2.5 minutes"); } - public function testPlurals() + public function testPlurals(): void { $actual = $this->timeEstimator->estimateAttackTimes(12)['crack_times_display']['online_no_throttling_10_per_second']; $this->assertSame('1 second', $actual, "no plural if unit value is 1"); @@ -68,30 +70,28 @@ public function testPlurals() $this->assertSame('2 seconds', $actual, "plural if unit value is more than 1"); } - public function unitProvider() + public static function unitProvider(): Iterator { - return [ - [1e2, '10 seconds'], - [1e3, '2 minutes'], - [1e5, '3 hours'], - [1e7, '12 days'], - [1e8, '4 months'], - [1e9, '3 years'], - ]; + yield [1e2, '10 seconds']; + yield [1e3, '2 minutes']; + yield [1e5, '3 hours']; + yield [1e7, '12 days']; + yield [1e8, '4 months']; + yield [1e9, '3 years']; } /** - * @dataProvider unitProvider * @param int $guesses * @param string $displayText */ - public function testTimeUnits($guesses, $displayText) + #[DataProvider('unitProvider')] + public function testTimeUnits($guesses, $displayText): void { $actual = $this->timeEstimator->estimateAttackTimes($guesses)['crack_times_display']['online_no_throttling_10_per_second']; $this->assertSame($displayText, $actual, "centuries"); } - public function testDifferentSpeeds() + public function testDifferentSpeeds(): void { $results = $this->timeEstimator->estimateAttackTimes(1e10)['crack_times_seconds']; @@ -101,35 +101,33 @@ public function testDifferentSpeeds() $this->assertSame(1e10 / (100 / 3600), $results['online_throttling_100_per_hour']); } - public function testSpeedLessThanOne() + public function testSpeedLessThanOne(): void { $actual = $this->timeEstimator->estimateAttackTimes(100)['crack_times_seconds']['offline_slow_hashing_1e4_per_second']; - $this->assertSame(0.01, $actual, "decimal speed when less than one second"); + $this->assertEqualsWithDelta(0.01, $actual, PHP_FLOAT_EPSILON, "decimal speed when less than one second"); } - public function scoreProvider() + public static function scoreProvider(): Iterator { - return [ - [1e2, 0], - [1e4, 1], - [1e7, 2], - [1e9, 3], - [1e11, 4], - ]; + yield [1e2, 0]; + yield [1e4, 1]; + yield [1e7, 2]; + yield [1e9, 3]; + yield [1e11, 4]; } /** - * @dataProvider scoreProvider * @param int $guesses * @param int $expectedScore */ - public function testScores($guesses, $expectedScore) + #[DataProvider('scoreProvider')] + public function testScores($guesses, $expectedScore): void { $actual = $this->timeEstimator->estimateAttackTimes($guesses)['score']; $this->assertSame($expectedScore, $actual, "correct score"); } - public function testScoreDelta() + public function testScoreDelta(): void { $score = $this->timeEstimator->estimateAttackTimes(1000)['score']; $this->assertSame(0, $score, "guesses at threshold gets lower score"); diff --git a/test/ZxcvbnTest.php b/test/ZxcvbnTest.php index 4aab198..cfbb6ff 100644 --- a/test/ZxcvbnTest.php +++ b/test/ZxcvbnTest.php @@ -4,6 +4,9 @@ namespace ZxcvbnPhp\Test; +use Iterator; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ZxcvbnPhp\Matchers\Bruteforce; use ZxcvbnPhp\Matchers\DictionaryMatch; @@ -12,15 +15,14 @@ class ZxcvbnTest extends TestCase { - /** @var Zxcvbn */ - private $zxcvbn; + private Zxcvbn $zxcvbn; public function setUp(): void { $this->zxcvbn = new Zxcvbn(); } - public function testMinimumGuessesForMultipleMatches() + public function testMinimumGuessesForMultipleMatches(): void { /** @var MatchInterface[] $matches */ $matches = $this->zxcvbn->passwordStrength('rockyou')['sequence']; @@ -28,28 +30,26 @@ public function testMinimumGuessesForMultipleMatches() // zxcvbn will return two matches: 'rock' (rank 359) and 'you' (rank 1). // If tested alone, the word 'you' would return only 1 guess, but because it's part of a larger password, // it should return the minimum number of guesses, which is 50 for a multi-character token. - $this->assertSame(50.0, $matches[1]->getGuesses()); + $this->assertEqualsWithDelta(50.0, $matches[1]->getGuesses(), PHP_FLOAT_EPSILON); } - public function typeDataProvider() + public static function typeDataProvider(): Iterator { - return [ - ['password', 'string'], - ['guesses', 'numeric'], - ['guesses_log10', 'numeric'], - ['sequence', 'array'], - ['crack_times_seconds', 'array'], - ['crack_times_display', 'array'], - ['feedback', 'array'], - ['calc_time', 'numeric'], - ]; + yield ['password', 'string']; + yield ['guesses', 'numeric']; + yield ['guesses_log10', 'numeric']; + yield ['sequence', 'array']; + yield ['crack_times_seconds', 'array']; + yield ['crack_times_display', 'array']; + yield ['feedback', 'array']; + yield ['calc_time', 'numeric']; } /** - * @dataProvider typeDataProvider * @throws \Exception */ - public function testZxcvbnReturnTypes($key, $type) + #[DataProvider('typeDataProvider')] + public function testZxcvbnReturnTypes(string $key, string $type): void { $zxcvbn = new Zxcvbn(); $result = $zxcvbn->passwordStrength('utmostfortitude2018'); @@ -69,33 +69,31 @@ public function testZxcvbnReturnTypes($key, $type) $this->assertTrue($correct, "zxcvbn result value " . $key . " is type " . $type); } - public function sanityCheckDataProvider() + public static function sanityCheckDataProvider(): Iterator { - return [ - ['password', 0, ['dictionary',], 'less than a second', 3], - ['65432', 0, ['sequence',], 'less than a second', 101], - ['sdfgsdfg', 1, ['repeat',], 'less than a second', 2595], - ['fortitude', 1, ['dictionary',], '1 second', 11308], - ['dfjkym', 1, ['bruteforce',], '2 minutes', 1000001], - ['fortitude22', 2, ['dictionary', 'repeat',], '2 minutes', 1140700], - ['absoluteadnap', 2, ['dictionary', 'dictionary',], '25 minutes', 15187504], - ['knifeandspoon', 3, ['dictionary', 'dictionary', 'dictionary'], '1 day', 1108057600], - ['h1dden_26191', 3, ['dictionary', 'bruteforce', 'date'], '3 days', 2993690800], - ['4rfv1236yhn!', 4, ['spatial', 'sequence', 'bruteforce'], '1 month', 38980000000], - ['BVidSNqe3oXVyE1996', 4, ['bruteforce', 'regex',], 'centuries', 10000000000010000], - ]; + yield ['password', 0, ['dictionary',], 'less than a second', 3]; + yield ['65432', 0, ['sequence',], 'less than a second', 101]; + yield ['sdfgsdfg', 1, ['repeat',], 'less than a second', 2595]; + yield ['fortitude', 1, ['dictionary',], '1 second', 11308]; + yield ['dfjkym', 1, ['bruteforce',], '2 minutes', 1000001]; + yield ['fortitude22', 2, ['dictionary', 'repeat',], '2 minutes', 1140700]; + yield ['absoluteadnap', 2, ['dictionary', 'dictionary',], '25 minutes', 15187504]; + yield ['knifeandspoon', 3, ['dictionary', 'dictionary', 'dictionary'], '1 day', 1108057600]; + yield ['h1dden_26191', 3, ['dictionary', 'bruteforce', 'date'], '3 days', 2993690800]; + yield ['4rfv1236yhn!', 4, ['spatial', 'sequence', 'bruteforce'], '1 month', 38980000000]; + yield ['BVidSNqe3oXVyE1996', 4, ['bruteforce', 'regex',], 'centuries', 10000000000010000]; } /** * Some basic sanity checks. All of the underlying functionality is tested in more details in their specific * classes, but this is just to check that it's all tied together correctly at the end. - * @dataProvider sanityCheckDataProvider * @param string $password * @param int $score * @param string[] $patterns * @param string $slowHashingDisplay * @param float $guesses */ + #[DataProvider('sanityCheckDataProvider')] public function testZxcvbnSanityCheck(string $password, int $score, array $patterns, string $slowHashingDisplay, float $guesses): void { $result = $this->zxcvbn->passwordStrength($password); @@ -109,9 +107,7 @@ public function testZxcvbnSanityCheck(string $password, int $score, array $patte ); $this->assertEqualsWithDelta($guesses, $result['guesses'], 1.0, "zxcvbn result has correct guesses"); - $actualPatterns = array_map(function ($match) { - return $match->pattern; - }, $result['sequence']); + $actualPatterns = array_map(fn($match) => $match->pattern, $result['sequence']); $this->assertSame($patterns, $actualPatterns, "zxcvbn result has correct patterns"); } @@ -119,7 +115,7 @@ public function testZxcvbnSanityCheck(string $password, int $score, array $patte * There's a similar test in DictionaryTest for this as well, but this specific test is for ensuring that the * user input gets passed from the Zxcvbn class all the way through to the DictionaryMatch function. */ - public function testUserDefinedWords() + public function testUserDefinedWords(): void { $result = $this->zxcvbn->passwordStrength('_wQbgL491', ['PJnD', 'WQBG', 'ZhwZ']); @@ -127,7 +123,7 @@ public function testUserDefinedWords() $this->assertSame('wQbg', $result['sequence'][1]->token, "user input match has correct token"); } - public function testMultibyteUserDefinedWords() + public function testMultibyteUserDefinedWords(): void { $result = $this->zxcvbn->passwordStrength('المفاتيح', ['العربية', 'المفاتيح', 'لوحة']); @@ -135,7 +131,7 @@ public function testMultibyteUserDefinedWords() $this->assertSame('المفاتيح', $result['sequence'][0]->token, "user input match has correct token"); } - public function testAddMatcherWillThrowException() + public function testAddMatcherWillThrowException(): void { $this->expectException(\InvalidArgumentException::class); @@ -144,7 +140,7 @@ public function testAddMatcherWillThrowException() $this->expectNotToPerformAssertions(); } - public function testAddMatcherWillReturnSelf() + public function testAddMatcherWillReturnSelf(): void { $result = $this->zxcvbn->addMatcher(Bruteforce::class); From 0bef05ae6970b792e838e934afbb37cfcc3a8765 Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 10:35:37 +0200 Subject: [PATCH 02/16] Update composer.lock --- composer.lock | 775 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 767 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index a64f404..7192018 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5e242366891d201f287593f19f70a1ad", + "content-hash": "2cd25cbfa73580e7a9697c4654eecc8f", "packages": [ { "name": "symfony/polyfill-mbstring", @@ -732,6 +732,331 @@ ], "time": "2025-01-05T14:43:25+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-24T11:22:20+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-10-17T10:06:22+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, { "name": "justinrainbow/json-schema", "version": "5.3.0", @@ -1221,6 +1546,89 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "php-coveralls/php-coveralls", + "version": "v2.7.0", + "source": { + "type": "git", + "url": "https://github.com/php-coveralls/php-coveralls.git", + "reference": "b36fa4394e519dafaddc04ae03976bc65a25ba15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-coveralls/php-coveralls/zipball/b36fa4394e519dafaddc04ae03976bc65a25ba15", + "reference": "b36fa4394e519dafaddc04ae03976bc65a25ba15", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "php": "^7.0 || ^8.0", + "psr/log": "^1.0 || ^2.0", + "symfony/config": "^2.1 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/console": "^2.1 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/stopwatch": "^2.0 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/yaml": "^2.0.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.4.3 || ^6.0 || ^7.0 || >=8.0 <8.5.29 || >=9.0 <9.5.23", + "sanmai/phpunit-legacy-adapter": "^6.1 || ^8.0" + }, + "suggest": { + "symfony/http-kernel": "Allows Symfony integration" + }, + "bin": [ + "bin/php-coveralls" + ], + "type": "library", + "autoload": { + "psr-4": { + "PhpCoveralls\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kitamura Satoshi", + "email": "with.no.parachute@gmail.com", + "homepage": "https://www.facebook.com/satooshi.jp", + "role": "Original creator" + }, + { + "name": "Takashi Matsuo", + "email": "tmatsuo@google.com" + }, + { + "name": "Google Inc" + }, + { + "name": "Dariusz Ruminski", + "email": "dariusz.ruminski@gmail.com", + "homepage": "https://github.com/keradus" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-coveralls/php-coveralls/graphs/contributors" + } + ], + "description": "PHP client library for Coveralls API", + "homepage": "https://github.com/php-coveralls/php-coveralls", + "keywords": [ + "ci", + "coverage", + "github", + "test" + ], + "support": { + "issues": "https://github.com/php-coveralls/php-coveralls/issues", + "source": "https://github.com/php-coveralls/php-coveralls/tree/v2.7.0" + }, + "time": "2023-11-22T10:21:01+00:00" + }, { "name": "php-parallel-lint/php-parallel-lint", "version": "v1.4.0", @@ -2014,18 +2422,178 @@ }, "time": "2019-01-08T18:20:26+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, { "name": "psr/log", - "version": "3.0.2", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", "shasum": "" }, "require": { @@ -2034,7 +2602,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -2060,9 +2628,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" + "source": "https://github.com/php-fig/log/tree/2.0.0" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2021-07-14T16:41:46+00:00" }, { "name": "psr/simple-cache", @@ -2115,6 +2683,50 @@ }, "time": "2021-10-29T13:26:27+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "react/cache", "version": "v1.2.0", @@ -3997,6 +4609,81 @@ ], "time": "2024-09-25T14:20:29+00:00" }, + { + "name": "symfony/config", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "bcd3c4adf0144dee5011bb35454728c38adec055" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/bcd3c4adf0144dee5011bb35454728c38adec055", + "reference": "bcd3c4adf0144dee5011bb35454728c38adec055", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-04T11:36:24+00:00" + }, { "name": "symfony/console", "version": "v7.2.1", @@ -5446,6 +6133,78 @@ ], "time": "2024-10-18T07:58:17+00:00" }, + { + "name": "symfony/yaml", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "099581e99f557e9f16b43c5916c26380b54abb22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/099581e99f557e9f16b43c5916c26380b54abb22", + "reference": "099581e99f557e9f16b43c5916c26380b54abb22", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-23T06:56:12+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.3", From 690fa3946e810061a53689941e5c68da774e2cfe Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 10:36:22 +0200 Subject: [PATCH 03/16] Update ci.yml --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 241f418..b93f967 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,6 @@ jobs: - ubuntu-latest - macos-latest php-versions: - - '8.1' - '8.2' - '8.3' - '8.4' From bf3b4f08e2bb3c1b6a23753b160a450d7c32d1a9 Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 10:36:44 +0200 Subject: [PATCH 04/16] Update ci.yml --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b93f967..5f7da94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: all: runs-on: ${{ matrix.operating-system }} strategy: + fail-fast: false matrix: operating-system: - ubuntu-latest From 9b23d8ccb8f66912ab4d7800459dfdb07e6a545c Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 10:37:36 +0200 Subject: [PATCH 05/16] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f7da94..6a48e52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: run: php vendor/bin/phpstan analyze - name: PHPinsights code analysis - run: php vendor/bin/phpinsights analyse --no-interaction && true + run: php vendor/bin/phpinsights analyse --no-interaction || true - name: Execute Rector run: vendor/bin/rector --dry-run From b0bd4cd44ab1c52217c38084e244775990db954f Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 10:43:45 +0200 Subject: [PATCH 06/16] Updated tests --- test/Matchers/L33tTest.php | 12 ++++++------ test/ZxcvbnTest.php | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/Matchers/L33tTest.php b/test/Matchers/L33tTest.php index e356331..098ab89 100644 --- a/test/Matchers/L33tTest.php +++ b/test/Matchers/L33tTest.php @@ -139,7 +139,7 @@ public static function commonCaseProvider(): Iterator 'password' => 'p4ssword', 'pattern' => 'p4ssword', 'word' => 'password', - 'dictionary_name' => 'words', + 'dictionary' => 'words', 'rank' => 3, 'ij' => [0, 7], 'sub' => ['4' => 'a'] @@ -148,7 +148,7 @@ public static function commonCaseProvider(): Iterator 'password' => 'p@ssw0rd', 'pattern' => 'p@ssw0rd', 'word' => 'password', - 'dictionary_name' => 'words', + 'dictionary' => 'words', 'rank' => 3, 'ij' => [0, 7], 'sub' => ['@' => 'a', '0' => 'o'] @@ -157,7 +157,7 @@ public static function commonCaseProvider(): Iterator 'password' => 'aSdfO{G0asDfO', 'pattern' => '{G0', 'word' => 'cgo', - 'dictionary_name' => 'words2', + 'dictionary' => 'words2', 'rank' => 1, 'ij' => [5, 7], 'sub' => ['{' => 'c', '0' => 'o'] @@ -166,10 +166,10 @@ public static function commonCaseProvider(): Iterator /** * @param int[] $ij - * @param string[] $substitutions + * @param string[] $sub */ #[DataProvider('commonCaseProvider')] - public function testCommonL33tSubstitutions(string $password, string $pattern, string $word, string $dictionary, int $rank, array $ij, array $substitutions): void + public function testCommonL33tSubstitutions(string $password, string $pattern, string $word, string $dictionary, int $rank, array $ij, array $sub): void { $this->checkMatches( "matches against common l33t substitutions", @@ -179,7 +179,7 @@ public function testCommonL33tSubstitutions(string $password, string $pattern, s [$ij], [ 'l33t' => [true], - 'sub' => [$substitutions], + 'sub' => [$sub], 'matchedWord' => [$word], 'rank' => [$rank], 'dictionaryName' => [$dictionary] diff --git a/test/ZxcvbnTest.php b/test/ZxcvbnTest.php index cfbb6ff..77d9369 100644 --- a/test/ZxcvbnTest.php +++ b/test/ZxcvbnTest.php @@ -79,7 +79,7 @@ public static function sanityCheckDataProvider(): Iterator yield ['fortitude22', 2, ['dictionary', 'repeat',], '2 minutes', 1140700]; yield ['absoluteadnap', 2, ['dictionary', 'dictionary',], '25 minutes', 15187504]; yield ['knifeandspoon', 3, ['dictionary', 'dictionary', 'dictionary'], '1 day', 1108057600]; - yield ['h1dden_26191', 3, ['dictionary', 'bruteforce', 'date'], '3 days', 2993690800]; + yield ['h1dden_26191', 3, ['dictionary', 'bruteforce', 'date'], '4 days', 3081378400]; yield ['4rfv1236yhn!', 4, ['spatial', 'sequence', 'bruteforce'], '1 month', 38980000000]; yield ['BVidSNqe3oXVyE1996', 4, ['bruteforce', 'regex',], 'centuries', 10000000000010000]; } From e937dcfa914b7a2b95d9d6b85ccc662bfc7a1ebd Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 10:50:56 +0200 Subject: [PATCH 07/16] Update phpunit.xml.dist --- phpunit.xml.dist | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ca52d2e..07f2004 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,29 +1,19 @@ - + + + ./src/ + + + - - test/ - + + test/ + - - - src - - - - - - - - - From 1c2f12a3a1ba6e7282d8cfe6c864a6f3d84fed85 Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 10:58:18 +0200 Subject: [PATCH 08/16] Update phpunit.xml.dist --- phpunit.xml.dist | 3 +++ 1 file changed, 3 insertions(+) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 07f2004..0fd9bfb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -13,6 +13,9 @@ test/ + + tests/Matchers/AbstractMatchTest.php + From a040d07b9dc9899869a5daa9fc0d8b5057bcb5aa Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 12:23:39 +0200 Subject: [PATCH 09/16] Update phpunit.xml.dist --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0fd9bfb..3d0d9ec 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,7 +14,7 @@ test/ - tests/Matchers/AbstractMatchTest.php + test/Matchers/AbstractMatchTest.php From 3fd94286589226d150a5966fb1a16cc1f342f187 Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 12:25:27 +0200 Subject: [PATCH 10/16] Update ci.yml --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a48e52..86a2616 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,6 @@ jobs: matrix: operating-system: - ubuntu-latest - - macos-latest php-versions: - '8.2' - '8.3' From ba5cd7781a85d2dd475a46c1ba693dd37ba4abc3 Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 13:37:13 +0200 Subject: [PATCH 11/16] Updated phpstan to level 7 --- phpinsights.php | 2 + phpstan.neon | 2 +- src/Matcher.php | 24 +++++++---- src/Matchers/BaseMatch.php | 9 +++- src/Matchers/Bruteforce.php | 2 + src/Matchers/DateMatch.php | 8 ++-- src/Matchers/DictionaryMatch.php | 22 +++++++--- src/Matchers/L33tMatch.php | 56 +++++++++++++++---------- src/Matchers/RepeatMatch.php | 10 +++-- src/Matchers/ReverseDictionaryMatch.php | 6 ++- src/Matchers/SequenceMatch.php | 2 + src/Matchers/SpatialMatch.php | 10 ++++- src/Matchers/YearMatch.php | 2 + src/Math/Binomial.php | 12 ++++-- src/Zxcvbn.php | 3 ++ test/MatcherTest.php | 3 +- test/Math/BinomialTest.php | 4 +- test/ZxcvbnTest.php | 3 +- 18 files changed, 125 insertions(+), 55 deletions(-) diff --git a/phpinsights.php b/phpinsights.php index 654b934..7fc0895 100644 --- a/phpinsights.php +++ b/phpinsights.php @@ -13,6 +13,7 @@ use SlevomatCodingStandard\Sniffs\Classes\SuperfluousInterfaceNamingSniff; use SlevomatCodingStandard\Sniffs\Functions\FunctionLengthSniff; use SlevomatCodingStandard\Sniffs\Functions\UnusedParameterSniff; +use SlevomatCodingStandard\Sniffs\Operators\RequireOnlyStandaloneIncrementAndDecrementOperatorsSniff; use SlevomatCodingStandard\Sniffs\TypeHints\DisallowMixedTypeHintSniff; return [ @@ -84,6 +85,7 @@ UnusedParameterSniff::class, TodoSniff::class, ForbiddenPublicPropertySniff::class, + RequireOnlyStandaloneIncrementAndDecrementOperatorsSniff::class, ], 'config' => [ diff --git a/phpstan.neon b/phpstan.neon index d438334..673b56c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 6 + level: 7 paths: - src - test diff --git a/src/Matcher.php b/src/Matcher.php index 849e427..3a9167f 100644 --- a/src/Matcher.php +++ b/src/Matcher.php @@ -9,7 +9,7 @@ class Matcher { - private const DEFAULT_MATCHERS = [ + private const array DEFAULT_MATCHERS = [ Matchers\DateMatch::class, Matchers\DictionaryMatch::class, Matchers\ReverseDictionaryMatch::class, @@ -21,7 +21,7 @@ class Matcher ]; /** - * @var array + * @var array */ private array $additionalMatchers = []; @@ -45,7 +45,7 @@ public function getMatches(string $password, array $userInputs = []): array /** @var MatchInterface $matcher */ foreach ($this->getMatchers() as $matcher) { $matched = $matcher::match($password, $userInputs); - if (! empty($matched)) { + if ($matched !== []) { $matches[] = $matched; } } @@ -56,12 +56,18 @@ public function getMatches(string $password, array $userInputs = []): array return $matches; } + /** + * @param class-string $className + * + * @throws \InvalidArgumentException + */ public function addMatcher(string $className): self { - if (! is_a($className, MatchInterface::class, true)) { - throw new \InvalidArgumentException(sprintf('Matcher class must implement %s', MatchInterface::class)); + if (! is_a($className, BaseMatch::class, true)) { + throw new \InvalidArgumentException(sprintf('Matcher class must extend %s', BaseMatch::class)); } + // @phpstan-ignore-next-line $this->additionalMatchers[$className] = $className; return $this; @@ -107,13 +113,17 @@ public static function compareMatches(BaseMatch $a, BaseMatch $b): int /** * Load available Match objects to match against a password. * - * @return array Array of classes implementing BaseMatch + * @return array Array of classes extending BaseMatch */ protected function getMatchers(): array { + /** @var array $additionalMatchers */ + $additionalMatchers = array_values($this->additionalMatchers); + + // @phpstan-ignore-next-line return array_merge( self::DEFAULT_MATCHERS, - array_values($this->additionalMatchers) + $additionalMatchers ); } } diff --git a/src/Matchers/BaseMatch.php b/src/Matchers/BaseMatch.php index f8f826e..3710f89 100644 --- a/src/Matchers/BaseMatch.php +++ b/src/Matchers/BaseMatch.php @@ -73,7 +73,14 @@ public static function findAll(string $string, string $regex, int $offset = 0): ], ]; foreach ($group as $capture) { - $captureBegin = mb_strpos((string) $match, $capture, $captureBegin); + $captureBeginTemp = mb_strpos((string) $match, $capture, $captureBegin); + + if ($captureBeginTemp === false) { + continue; + } + + $captureBegin = $captureBeginTemp; + $captures[] = [ 'begin' => $matchBegin + $captureBegin, 'end' => $matchBegin + $captureBegin + mb_strlen($capture) - 1, diff --git a/src/Matchers/Bruteforce.php b/src/Matchers/Bruteforce.php index bda81c8..c2535bf 100644 --- a/src/Matchers/Bruteforce.php +++ b/src/Matchers/Bruteforce.php @@ -13,6 +13,8 @@ final class Bruteforce extends BaseMatch public string $pattern = 'bruteforce'; /** + * @param array $userInputs + * * @return array */ public static function match(string $password, array $userInputs = []): array diff --git a/src/Matchers/DateMatch.php b/src/Matchers/DateMatch.php index 680ab03..ebddfe1 100644 --- a/src/Matchers/DateMatch.php +++ b/src/Matchers/DateMatch.php @@ -218,7 +218,7 @@ protected static function datesWithoutSeparators(string $password): array } } - if (empty($candidates)) { + if ($candidates === []) { continue; } @@ -310,7 +310,8 @@ protected static function checkDate(array $ints): array|false foreach ($possibleYearSplits as [$year, $rest]) { if ($year >= self::MIN_YEAR && $year <= self::MAX_YEAR) { - if ($dm = static::mapIntsToDayMonth($rest)) { + $dm = static::mapIntsToDayMonth($rest); + if ($dm !== false) { return [ 'year' => $year, 'month' => $dm['month'], @@ -325,7 +326,8 @@ protected static function checkDate(array $ints): array|false } foreach ($possibleYearSplits as [$year, $rest]) { - if ($dm = static::mapIntsToDayMonth($rest)) { + $dm = static::mapIntsToDayMonth($rest); + if ($dm !== false) { return [ 'year' => static::twoToFourDigitYear($year), 'month' => $dm['month'], diff --git a/src/Matchers/DictionaryMatch.php b/src/Matchers/DictionaryMatch.php index eac6eb6..4818cec 100644 --- a/src/Matchers/DictionaryMatch.php +++ b/src/Matchers/DictionaryMatch.php @@ -63,7 +63,7 @@ public static function match(string $password, array $userInputs = [], array $ra $dicts = static::getRankedDictionaries(); } - if (! empty($userInputs)) { + if ($userInputs !== []) { $dicts['user_inputs'] = []; foreach ($userInputs as $rank => $input) { $input_lower = mb_strtolower((string) $input); @@ -177,8 +177,13 @@ protected static function dictionaryMatch(string $password, array $dict): array */ protected static function getRankedDictionaries(): array { - if (empty(self::$rankedDictionaries)) { + if (self::$rankedDictionaries === []) { $json = file_get_contents(__DIR__ . '/frequency_lists.json'); + + if ($json === false) { + throw new \Exception('Failed to read frequency_lists.json file'); + } + $data = json_decode($json, true); $rankedLists = []; @@ -218,12 +223,17 @@ protected function getUppercaseVariations(): float // otherwise calculate the number of ways to capitalize U+L uppercase+lowercase letters // with U uppercase letters or less. or, if there's more uppercase than lower (for eg. PASSwORD), // the number of ways to lowercase U+L letters with L lowercase letters or less. - $uppercase = count(array_filter(preg_split('//u', (string) $word, -1, PREG_SPLIT_NO_EMPTY), 'ctype_upper')); - $lowercase = count(array_filter(preg_split('//u', (string) $word, -1, PREG_SPLIT_NO_EMPTY), 'ctype_lower')); + $splitWord = preg_split('//u', (string) $word, -1, PREG_SPLIT_NO_EMPTY); $variations = 0; - for ($i = 1; $i <= min($uppercase, $lowercase); $i++) { - $variations += Binomial::binom($uppercase + $lowercase, $i); + if ($splitWord !== false) { + $uppercase = count(array_filter($splitWord, 'ctype_upper')); + $lowercase = count(array_filter($splitWord, 'ctype_lower')); + + $min = min($uppercase, $lowercase); + for ($i = 1; $i <= $min; $i++) { + $variations += Binomial::binom($uppercase + $lowercase, $i); + } } return $variations; } diff --git a/src/Matchers/L33tMatch.php b/src/Matchers/L33tMatch.php index 98e8251..d3fdfdb 100644 --- a/src/Matchers/L33tMatch.php +++ b/src/Matchers/L33tMatch.php @@ -37,18 +37,21 @@ public function __construct(string $password, int $begin, int $end, string $toke /** * Match occurrences of l33t words in password to dictionary words. * + * @param array $userInputs + * @param array $rankedDictionaries + * * @return array */ public static function match(string $password, array $userInputs = [], array $rankedDictionaries = []): array { // Translate l33t password and dictionary match the translated password. $maps = array_filter(static::getL33tSubstitutions(static::getL33tSubtable($password))); - if (empty($maps)) { + if ($maps === []) { return []; } $matches = []; - if (! $rankedDictionaries) { + if ($rankedDictionaries === []) { $rankedDictionaries = static::getRankedDictionaries(); } @@ -136,16 +139,20 @@ protected static function getL33tTable(): array */ protected static function getL33tSubtable(string $password): array { + $subTable = []; + // The preg_split call below is a multibyte compatible version of str_split - $passwordChars = array_unique(preg_split('//u', $password, -1, PREG_SPLIT_NO_EMPTY)); + $splitItems = preg_split('//u', $password, -1, PREG_SPLIT_NO_EMPTY); - $subTable = []; + if ($splitItems !== false) { + $passwordChars = array_unique($splitItems); - $table = static::getL33tTable(); - foreach ($table as $letter => $substitutions) { - foreach ($substitutions as $sub) { - if (in_array($sub, $passwordChars)) { - $subTable[$letter][] = $sub; + $table = static::getL33tTable(); + foreach ($table as $letter => $substitutions) { + foreach ($substitutions as $sub) { + if (in_array($sub, $passwordChars)) { + $subTable[$letter][] = $sub; + } } } } @@ -178,7 +185,7 @@ protected static function getL33tSubstitutions(array $subtable): array */ protected static function substitutionTableHelper(array $table, array $keys, array $subs): array { - if (empty($keys)) { + if ($keys === []) { return $subs; } @@ -226,20 +233,23 @@ protected function getL33tVariations(): float foreach ($this->sub as $substitution => $letter) { $characters = preg_split('//u', mb_strtolower((string) $this->token), -1, PREG_SPLIT_NO_EMPTY); - $subbed = count(array_filter($characters, static fn ($character) => (string) $character === (string) $substitution)); - $unsubbed = count(array_filter($characters, static fn ($character) => (string) $character === (string) $letter)); - - if ($subbed === 0 || $unsubbed === 0) { - // for this sub, password is either fully subbed (444) or fully unsubbed (aaa) - // treat that as doubling the space (attacker needs to try fully subbed chars in addition to - // unsubbed.) - $variations *= 2; - } else { - $possibilities = 0; - for ($i = 1; $i <= min($subbed, $unsubbed); $i++) { - $possibilities += Binomial::binom($subbed + $unsubbed, $i); + if ($characters !== false) { + $subbed = count(array_filter($characters, static fn ($character) => (string) $character === (string) $substitution)); + $unsubbed = count(array_filter($characters, static fn ($character) => (string) $character === (string) $letter)); + + if ($subbed === 0 || $unsubbed === 0) { + // for this sub, password is either fully subbed (444) or fully unsubbed (aaa) + // treat that as doubling the space (attacker needs to try fully subbed chars in addition to + // unsubbed.) + $variations *= 2; + } else { + $possibilities = 0; + $min = min($subbed, $unsubbed); + for ($i = 1; $i <= $min; $i++) { + $possibilities += Binomial::binom($subbed + $unsubbed, $i); + } + $variations *= $possibilities; } - $variations *= $possibilities; } } return $variations; diff --git a/src/Matchers/RepeatMatch.php b/src/Matchers/RepeatMatch.php index 56ba132..c549797 100644 --- a/src/Matchers/RepeatMatch.php +++ b/src/Matchers/RepeatMatch.php @@ -44,6 +44,8 @@ public function __construct(string $password, int $begin, int $end, string $toke /** * Match 3 or more repeated characters. * + * @param array $userInputs + * * @return array */ public static function match(string $password, array $userInputs = []): array @@ -55,14 +57,16 @@ public static function match(string $password, array $userInputs = []): array $greedyMatches = self::findAll($password, self::GREEDY_MATCH, $lastIndex); $lazyMatches = self::findAll($password, self::LAZY_MATCH, $lastIndex); - if (empty($greedyMatches)) { + if ($greedyMatches === []) { break; } if (mb_strlen((string) $greedyMatches[0][0]['token']) > mb_strlen((string) $lazyMatches[0][0]['token'])) { $match = $greedyMatches[0]; - preg_match(self::ANCHORED_LAZY_MATCH, (string) $match[0]['token'], $anchoredMatch); - $repeatedChar = $anchoredMatch[1]; + $repeatedChar = ''; + if (preg_match(self::ANCHORED_LAZY_MATCH, (string) $match[0]['token'], $anchoredMatch)) { + $repeatedChar = $anchoredMatch[1]; + } } else { $match = $lazyMatches[0]; $repeatedChar = $match[1]['token']; diff --git a/src/Matchers/ReverseDictionaryMatch.php b/src/Matchers/ReverseDictionaryMatch.php index 0cd059b..cc4bef8 100644 --- a/src/Matchers/ReverseDictionaryMatch.php +++ b/src/Matchers/ReverseDictionaryMatch.php @@ -53,7 +53,11 @@ public function getFeedback(bool $isSoleMatch): array public static function mbStrRev(string $string, ?string $encoding = null): string { if ($encoding === null) { - $encoding = mb_detect_encoding($string) ?: 'UTF-8'; + $encoding = mb_detect_encoding($string); + + if ($encoding === false) { + $encoding = 'UTF-8'; + } } $length = mb_strlen($string, $encoding); $reversed = ''; diff --git a/src/Matchers/SequenceMatch.php b/src/Matchers/SequenceMatch.php index da060e4..d2279ce 100644 --- a/src/Matchers/SequenceMatch.php +++ b/src/Matchers/SequenceMatch.php @@ -35,6 +35,8 @@ public function __construct(string $password, int $begin, int $end, string $toke /** * Match sequences of three or more characters. * + * @param array $userInputs Array of values related to the user (optional) + * * @return array */ public static function match(string $password, array $userInputs = []): array diff --git a/src/Matchers/SpatialMatch.php b/src/Matchers/SpatialMatch.php index 774bc65..5183189 100644 --- a/src/Matchers/SpatialMatch.php +++ b/src/Matchers/SpatialMatch.php @@ -92,8 +92,13 @@ public function getFeedback(bool $isSoleMatch): array */ public static function getAdjacencyGraphs(): array { - if (empty(self::$adjacencyGraphs)) { + if (self::$adjacencyGraphs === []) { $json = file_get_contents(__DIR__ . '/adjacency_graphs.json'); + + if ($json === false) { + throw new \Exception('Failed to read adjacency_graphs.json file'); + } + $data = json_decode($json, true); // This seems pointless, but the data file is not guaranteed to be in any particular order. @@ -241,7 +246,8 @@ protected function getRawGuesses(): float $guesses *= 2; } else { $variations = 0; - for ($i = 1; $i <= min($shifted, $unshifted); $i++) { + $min = min($shifted, $unshifted); + for ($i = 1; $i <= $min; $i++) { $variations += Binomial::binom($shifted + $unshifted, $i); } $guesses *= $variations; diff --git a/src/Matchers/YearMatch.php b/src/Matchers/YearMatch.php index c45dce9..d0d272e 100644 --- a/src/Matchers/YearMatch.php +++ b/src/Matchers/YearMatch.php @@ -16,6 +16,8 @@ final class YearMatch extends BaseMatch /** * Match occurrences of years in a password * + * @param array $userInputs Array of values related to the user (optional) + * * @return array */ public static function match(string $password, array $userInputs = []): array diff --git a/src/Math/Binomial.php b/src/Math/Binomial.php index 3818df8..62264d1 100644 --- a/src/Math/Binomial.php +++ b/src/Math/Binomial.php @@ -35,7 +35,7 @@ public static function getProvider(): BinomialProvider } /** - * @return array + * @return array */ public static function getUsableProviderClasses(): array { @@ -55,12 +55,18 @@ private static function initProvider(): BinomialProvider { $providerClasses = self::getUsableProviderClasses(); - if (! $providerClasses) { + if ($providerClasses === []) { throw new \LogicException('No valid providers'); } $bestProviderClass = reset($providerClasses); - return new $bestProviderClass(); + $provider = new $bestProviderClass(); + + if (! $provider instanceof BinomialProvider) { + throw new \LogicException('Inval provider class: ' . $bestProviderClass); + } + + return $provider; } } diff --git a/src/Zxcvbn.php b/src/Zxcvbn.php index 7d8ad70..10a6e42 100644 --- a/src/Zxcvbn.php +++ b/src/Zxcvbn.php @@ -27,6 +27,9 @@ public function __construct() $this->feedback = new Feedback(); } + /** + * @param class-string $className + */ public function addMatcher(string $className): self { $this->matcher->addMatcher($className); diff --git a/test/MatcherTest.php b/test/MatcherTest.php index 78f3920..36625e5 100644 --- a/test/MatcherTest.php +++ b/test/MatcherTest.php @@ -74,9 +74,8 @@ public function testAddMatcherWillThrowException(): void $this->expectException(\InvalidArgumentException::class); $matcher = new Matcher(); + // @phpstan-ignore-next-line $matcher->addMatcher('invalid className'); - - $this->expectNotToPerformAssertions(); } public function testAddMatcherWillReturnSelf(): void diff --git a/test/Math/BinomialTest.php b/test/Math/BinomialTest.php index 68f5c30..332c47d 100644 --- a/test/Math/BinomialTest.php +++ b/test/Math/BinomialTest.php @@ -37,8 +37,10 @@ public function testHasProvider(): void public function testChosenProviderMatchesExpected(): void { $providerClasses = Binomial::getUsableProviderClasses(); + $provider = reset($providerClasses); + $this->assertNotFalse($provider); - $this->assertInstanceOf(reset($providerClasses), Binomial::getProvider()); + $this->assertInstanceOf($provider, Binomial::getProvider()); } /** diff --git a/test/ZxcvbnTest.php b/test/ZxcvbnTest.php index 77d9369..a28fbb0 100644 --- a/test/ZxcvbnTest.php +++ b/test/ZxcvbnTest.php @@ -135,9 +135,8 @@ public function testAddMatcherWillThrowException(): void { $this->expectException(\InvalidArgumentException::class); + // @phpstan-ignore-next-line $this->zxcvbn->addMatcher('invalid className'); - - $this->expectNotToPerformAssertions(); } public function testAddMatcherWillReturnSelf(): void From b1cc3474eae2e1b3923c68bc52a9cafa07cdf5a5 Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 14:00:56 +0200 Subject: [PATCH 12/16] Update Matcher.php --- src/Matcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Matcher.php b/src/Matcher.php index 3a9167f..2e1445c 100644 --- a/src/Matcher.php +++ b/src/Matcher.php @@ -9,7 +9,7 @@ class Matcher { - private const array DEFAULT_MATCHERS = [ + private const DEFAULT_MATCHERS = [ Matchers\DateMatch::class, Matchers\DictionaryMatch::class, Matchers\ReverseDictionaryMatch::class, From 0d5057a1c692453c56cff5f8facb702470cd89da Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 14:03:50 +0200 Subject: [PATCH 13/16] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86a2616..d9ea196 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,7 @@ jobs: run: vendor/bin/rector --dry-run - name: Unit tests - run: ./vendor/bin/phpunit + run: ./vendor/bin/phpunit --coverage-clover=coverage.xml - name: Send code coverage report to Codecov.io uses: codecov/codecov-action@v4 From 29c397b1e6ecef62330ff0508fbf4ccd43b7406f Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 14:05:43 +0200 Subject: [PATCH 14/16] Update phpstan to level 8 --- phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 673b56c..c3457b6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 7 + level: 8 paths: - src - test From d1b63c37c59fe70884f31da8f1265d0bba1add75 Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 14:08:58 +0200 Subject: [PATCH 15/16] Fixed default values --- src/Matchers/SequenceMatch.php | 2 +- src/Matchers/SpatialMatch.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Matchers/SequenceMatch.php b/src/Matchers/SequenceMatch.php index d2279ce..e72696f 100644 --- a/src/Matchers/SequenceMatch.php +++ b/src/Matchers/SequenceMatch.php @@ -23,7 +23,7 @@ class SequenceMatch extends BaseMatch /** * @param array{'sequenceName'?: string, 'sequenceSpace'?: int, 'ascending'?: bool} $params */ - public function __construct(string $password, int $begin, int $end, string $token, array $params = ['sequenceName' => '', 'sequenceSpace' => 0, 'ascending' => false]) + public function __construct(string $password, int $begin, int $end, string $token, array $params = []) { parent::__construct($password, $begin, $end, $token); diff --git a/src/Matchers/SpatialMatch.php b/src/Matchers/SpatialMatch.php index 5183189..bda9fce 100644 --- a/src/Matchers/SpatialMatch.php +++ b/src/Matchers/SpatialMatch.php @@ -35,7 +35,7 @@ class SpatialMatch extends BaseMatch /** * @param array{'graph'?: string, 'shifted_count'?: int, 'turns'?: int} $params */ - public function __construct(string $password, int $begin, int $end, string $token, array $params = ['graph' => 'qwerty', 'shifted_count' => 0, 'turns' => 0]) + public function __construct(string $password, int $begin, int $end, string $token, array $params = []) { parent::__construct($password, $begin, $end, $token); $this->graph = $params['graph'] ?? 'qwerty'; From f237a20a0f7d3c70b2274512fd0ae9a77fcda9a7 Mon Sep 17 00:00:00 2001 From: Cristi Radu Date: Tue, 7 Jan 2025 14:11:11 +0200 Subject: [PATCH 16/16] Updated tests --- test/FeedbackTest.php | 34 +++--- test/MatcherTest.php | 8 +- test/Matchers/AbstractMatchTest.php | 17 +-- test/Matchers/BruteforceTest.php | 6 +- test/Matchers/DateTest.php | 107 ++++++++--------- test/Matchers/DictionaryTest.php | 58 ++++----- test/Matchers/L33tTest.php | 151 ++++++++++++------------ test/Matchers/MockL33tMatch.php | 2 +- test/Matchers/MockMatch.php | 7 +- test/Matchers/RepeatTest.php | 33 ++---- test/Matchers/ReverseDictionaryTest.php | 8 +- test/Matchers/SequenceTest.php | 18 +-- test/Matchers/SpatialTest.php | 53 ++++----- test/Matchers/YearTest.php | 24 ++-- test/Math/BinomialTest.php | 10 +- test/ScorerTest.php | 56 ++++----- test/TimeEstimatorTest.php | 43 +++---- test/ZxcvbnTest.php | 50 ++++---- test/config/bootstrap.php | 4 +- 19 files changed, 327 insertions(+), 362 deletions(-) diff --git a/test/FeedbackTest.php b/test/FeedbackTest.php index f3efed4..e60d3ce 100644 --- a/test/FeedbackTest.php +++ b/test/FeedbackTest.php @@ -23,16 +23,16 @@ public function testFeedbackForEmptyPassword(): void { $feedback = $this->feedback->getFeedback(0, []); - $this->assertSame('', $feedback['warning'], "default warning"); + $this->assertSame('', $feedback['warning'], 'default warning'); $this->assertContains( 'Use a few words, avoid common phrases', $feedback['suggestions'], - "default suggestion #1" + 'default suggestion #1' ); $this->assertContains( 'No need for symbols, digits, or uppercase letters', $feedback['suggestions'], - "default suggestion #1" + 'default suggestion #1' ); } @@ -41,17 +41,17 @@ public function testHighScoringSequence(): void $match = new Bruteforce('a', 0, 1, 'a'); $feedback = $this->feedback->getFeedback(3, [$match]); - $this->assertSame('', $feedback['warning'], "no warning for good score"); - $this->assertEmpty($feedback['suggestions'], "no suggestions for good score"); + $this->assertSame('', $feedback['warning'], 'no warning for good score'); + $this->assertEmpty($feedback['suggestions'], 'no suggestions for good score'); } public function testLongestMatchGetsFeedback(): void { $match1 = new SequenceMatch('abcd26-01-1991', 0, 4, 'abcd'); $match2 = new DateMatch('abcd26-01-1991', 4, 14, '26-01-1991', [ - 'day' => 26, - 'month' => 1, - 'year' => 1991, + 'day' => 26, + 'month' => 1, + 'year' => 1991, 'separator' => '-', ]); $feedback = $this->feedback->getFeedback(1, [$match1, $match2]); @@ -59,26 +59,26 @@ public function testLongestMatchGetsFeedback(): void $this->assertSame( 'Dates are often easy to guess', $feedback['warning'], - "warning provided for the longest match" + 'warning provided for the longest match' ); $this->assertContains( 'Avoid dates and years that are associated with you', $feedback['suggestions'], - "suggestion provided for the longest match" + 'suggestion provided for the longest match' ); $this->assertNotContains( 'Avoid sequences', $feedback['suggestions'], - "no suggestion provided for the shorter match" + 'no suggestion provided for the shorter match' ); } public function testDefaultSuggestion(): void { $match = new DateMatch('26-01-1991', 0, 10, '26-01-1991', [ - 'day' => 26, - 'month' => 1, - 'year' => 1991, + 'day' => 26, + 'month' => 1, + 'year' => 1991, 'separator' => '-', ]); $feedback = $this->feedback->getFeedback(1, [$match]); @@ -86,7 +86,7 @@ public function testDefaultSuggestion(): void $this->assertContains( 'Add another word or two. Uncommon words are better.', $feedback['suggestions'], - "default suggestion provided" + 'default suggestion provided' ); $this->assertCount(2, $feedback['suggestions'], "default suggestion doesn\'t override existing suggestion"); } @@ -96,11 +96,11 @@ public function testBruteforceFeedback(): void $match = new Bruteforce('qkcriv', 0, 6, 'qkcriv'); $feedback = $this->feedback->getFeedback(1, [$match]); - $this->assertSame('', $feedback['warning'], "bruteforce match has no warning"); + $this->assertSame('', $feedback['warning'], 'bruteforce match has no warning'); $this->assertSame( ['Add another word or two. Uncommon words are better.'], $feedback['suggestions'], - "bruteforce match only has the default suggestion" + 'bruteforce match only has the default suggestion' ); } } diff --git a/test/MatcherTest.php b/test/MatcherTest.php index 36625e5..329ec12 100644 --- a/test/MatcherTest.php +++ b/test/MatcherTest.php @@ -42,7 +42,7 @@ public function testMultiplePatterns(): void ['dictionary', [ 0, 6]], ['dictionary', [ 7, 15]], ['date', [16, 23]], - ['repeat', [24, 27]] + ['repeat', [24, 27]], ]; $matches = $matcher->getMatches($password); @@ -53,7 +53,7 @@ public function testMultiplePatterns(): void } } - $this->assertEmpty($expectedMatches, "matches multiple patterns"); + $this->assertEmpty($expectedMatches, 'matches multiple patterns'); } /** @@ -65,8 +65,8 @@ public function testUserDefinedWords(): void $matcher = new Matcher(); $matches = $matcher->getMatches('_wQbgL491', ['PJnD', 'WQBG', 'ZhwZ']); - $this->assertInstanceOf(DictionaryMatch::class, $matches[0], "user input match is correct class"); - $this->assertSame('wQbg', $matches[0]->token, "user input match has correct token"); + $this->assertInstanceOf(DictionaryMatch::class, $matches[0], 'user input match is correct class'); + $this->assertSame('wQbg', $matches[0]->token, 'user input match has correct token'); } public function testAddMatcherWillThrowException(): void diff --git a/test/Matchers/AbstractMatchTest.php b/test/Matchers/AbstractMatchTest.php index 700d0e7..b7b2a65 100644 --- a/test/Matchers/AbstractMatchTest.php +++ b/test/Matchers/AbstractMatchTest.php @@ -18,16 +18,17 @@ abstract class AbstractMatchTest extends TestCase * * @param array $prefixes * @param array $suffixes + * * @return array a list of triplets [variant, i, j] where [i,j] is the start/end of the pattern, inclusive */ protected function generatePasswords(string $pattern, array $prefixes, array $suffixes): array { $output = []; - if (!in_array('', $prefixes)) { + if (! in_array('', $prefixes)) { array_unshift($prefixes, ''); } - if (!in_array('', $suffixes)) { + if (! in_array('', $suffixes)) { array_unshift($suffixes, ''); } @@ -39,7 +40,7 @@ protected function generatePasswords(string $pattern, array $prefixes, array $su $output[] = [ $prefix . $pattern . $suffix, $i, - $j + $j, ]; } } @@ -72,7 +73,7 @@ protected function checkMatches( $this->assertCount( count($patterns), $matches, - $prefix . ": matches.length == " . count($patterns) + $prefix . ': matches.length == ' . count($patterns) ); foreach ($patterns as $k => $pattern) { @@ -84,17 +85,17 @@ protected function checkMatches( $this->assertSame( $patternName, $match->pattern, - "$prefix matches[$k].pattern == '$patternName'" + "{$prefix} matches[{$k}].pattern == '{$patternName}'" ); $this->assertSame( [$i, $j], [$match->begin, $match->end], - "$prefix matches[$k] should have [i, j] of [$i, $j]" + "{$prefix} matches[{$k}] should have [i, j] of [{$i}, {$j}]" ); $this->assertSame( $pattern, $match->token, - "$prefix matches[$k].token == '$pattern'" + "{$prefix} matches[{$k}].token == '{$pattern}'" ); foreach ($props as $propName => $propList) { @@ -103,7 +104,7 @@ protected function checkMatches( $this->assertSame( $propList[$k], $match->$propName, - "$prefix matches[$k].$propName == $propMessage" + "{$prefix} matches[{$k}].{$propName} == {$propMessage}" ); } } diff --git a/test/Matchers/BruteforceTest.php b/test/Matchers/BruteforceTest.php index fba8741..aa39fcd 100644 --- a/test/Matchers/BruteforceTest.php +++ b/test/Matchers/BruteforceTest.php @@ -13,7 +13,7 @@ public function testMatch(): void $password = 'uH2nvQbugW'; $this->checkMatches( - "matches entire string", + 'matches entire string', Bruteforce::match($password), 'bruteforce', [$password], @@ -27,7 +27,7 @@ public function testMultibyteMatch(): void $password = '中华人民共和国'; $this->checkMatches( - "matches entire string with multibyte characters", + 'matches entire string with multibyte characters', Bruteforce::match($password), 'bruteforce', [$password], @@ -47,6 +47,6 @@ public function testGuessesMultibyteCharacter(): void { $token = '🙂'; // smiley face emoji $match = new Bruteforce($token, 0, 1, $token); - $this->assertEqualsWithDelta(11.0, $match->getGuesses(), PHP_FLOAT_EPSILON, "multibyte character treated as one character"); + $this->assertEqualsWithDelta(11.0, $match->getGuesses(), PHP_FLOAT_EPSILON, 'multibyte character treated as one character'); } } diff --git a/test/Matchers/DateTest.php b/test/Matchers/DateTest.php index 73542fe..ec30e91 100644 --- a/test/Matchers/DateTest.php +++ b/test/Matchers/DateTest.php @@ -21,16 +21,13 @@ public static function separatorProvider(): Iterator yield ['.']; } - /** - * @param string $sep - */ #[DataProvider('separatorProvider')] - public function testSeparators($sep): void + public function testSeparators(string $sep): void { $password = "13{$sep}2{$sep}1921"; $this->checkMatches( - "matches dates that use '$sep' as a separator", + "matches dates that use '{$sep}' as a separator", DateMatch::match($password), 'date', [$password], @@ -55,16 +52,16 @@ public function testDateOrders(): void $order ); $this->checkMatches( - "matches dates with $order format", + "matches dates with {$order} format", DateMatch::match($password), 'date', [ $password ], [[ 0, strlen($password) - 1 ]], [ 'separator' => [''], - 'year' => [1988], - 'month' => [8], - 'day' => [8], + 'year' => [1988], + 'month' => [8], + 'day' => [8], ] ); } @@ -74,16 +71,16 @@ public function testMatchesClosestToReferenceYear(): void { $password = '111504'; $this->checkMatches( - "matches the date with year closest to REFERENCE_YEAR when ambiguous", + 'matches the date with year closest to REFERENCE_YEAR when ambiguous', DateMatch::match($password), 'date', [ $password ], [[ 0, strlen($password) - 1 ]], [ 'separator' => [''], - 'year' => [2004], // picks '04' -> 2004 as year, not '1504' - 'month' => [11], - 'day' => [15], + 'year' => [2004], // picks '04' -> 2004 as year, not '1504' + 'month' => [11], + 'day' => [15], ] ); } @@ -96,17 +93,12 @@ public static function normalDateProvider(): Iterator yield [22, 11, 1551]; } - /** - * @param int $day - * @param int $month - * @param int $year - */ #[DataProvider('normalDateProvider')] - public function testNormalDatesWithoutSeparator($day, $month, $year): void + public function testNormalDatesWithoutSeparator(int $day, int $month, int $year): void { $password = "{$year}{$month}{$day}"; $this->checkMatches( - "matches $password without a separator", + "matches {$password} without a separator", DateMatch::match($password), 'date', [$password], @@ -118,17 +110,12 @@ public function testNormalDatesWithoutSeparator($day, $month, $year): void ); } - /** - * @param int $day - * @param int $month - * @param int $year - */ #[DataProvider('normalDateProvider')] - public function testNormalDatesWithSeparator($day, $month, $year): void + public function testNormalDatesWithSeparator(int $day, int $month, int $year): void { $password = "{$year}.{$month}.{$day}"; $this->checkMatches( - "matches $password with a separator", + "matches {$password} with a separator", DateMatch::match($password), 'date', [$password], @@ -142,36 +129,36 @@ public function testNormalDatesWithSeparator($day, $month, $year): void public function testMatchesZeroPaddedDates(): void { - $password = "02/02/02"; + $password = '02/02/02'; $this->checkMatches( - "matches zero-padded dates", + 'matches zero-padded dates', DateMatch::match($password), 'date', [ $password ], [[ 0, strlen($password) - 1 ]], [ 'separator' => ['/'], - 'year' => [2002], - 'month' => [2], - 'day' => [2], + 'year' => [2002], + 'month' => [2], + 'day' => [2], ] ); } public function testFullDateMatched(): void { - $password = "2018-01-20"; + $password = '2018-01-20'; $this->checkMatches( - "matches full date and not just year", + 'matches full date and not just year', DateMatch::match($password), 'date', [ $password ], [[ 0, strlen($password) - 1 ]], [ 'separator' => ['-'], - 'year' => [2018], - 'month' => [1], - 'day' => [20], + 'year' => [2018], + 'month' => [1], + 'day' => [20], ] ); } @@ -184,15 +171,15 @@ public function testMatchesEmbeddedDates(): void foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as [$password, $i, $j]) { $this->checkMatches( - "matches embedded dates", + 'matches embedded dates', DateMatch::match($password), 'date', [$pattern], [[$i, $j]], [ - 'year' => [1991], + 'year' => [1991], 'month' => [1], - 'day' => [1] + 'day' => [1], ] ); } @@ -200,48 +187,48 @@ public function testMatchesEmbeddedDates(): void public function testMatchesOverlappingDates(): void { - $password = "12/20/1991.12.20"; + $password = '12/20/1991.12.20'; $this->checkMatches( - "matches overlapping dates", + 'matches overlapping dates', DateMatch::match($password), 'date', [ '12/20/1991', '1991.12.20' ], [[ 0, 9 ], [ 6, 15 ]], [ 'separator' => ['/', '.'], - 'year' => [1991, 1991], - 'month' => [12, 12], - 'day' => [20, 20], + 'year' => [1991, 1991], + 'month' => [12, 12], + 'day' => [20, 20], ] ); } public function testMatchesDatesPadded(): void { - $password = "912/20/919"; + $password = '912/20/919'; $this->checkMatches( - "matches dates padded by non-ambiguous digits", + 'matches dates padded by non-ambiguous digits', DateMatch::match($password), 'date', [ '12/20/91' ], [[ 1, 8 ]], [ 'separator' => ['/'], - 'year' => [1991], - 'month' => [12], - 'day' => [20], + 'year' => [1991], + 'month' => [12], + 'day' => [20], ] ); } public function testReferenceYearImplementation(): void { - $this->assertSame((int)date('Y'), DateMatch::getReferenceYear(), "reference year implementation"); + $this->assertSame((int) date('Y'), DateMatch::getReferenceYear(), 'reference year implementation'); } public function testNonDateThatLooksLikeDate(): void { - $this->assertEmpty(DateMatch::match('30-31-00'), "no match on invalid date"); + $this->assertEmpty(DateMatch::match('30-31-00'), 'no match on invalid date'); } public function testGuessDistanceFromReferenceYear(): void @@ -251,14 +238,14 @@ public function testGuessDistanceFromReferenceYear(): void 'separator' => '', 'year' => 1923, 'month' => 1, - 'day' => 1 + 'day' => 1, ]); $expected = 365.0 * abs(DateMatch::getReferenceYear() - $match->year); $this->assertSame( $expected, $match->getGuesses(), - "guesses for $token is 365 * distance_from_ref_year" + "guesses for {$token} is 365 * distance_from_ref_year" ); } @@ -269,11 +256,11 @@ public function testGuessMinYearSpace(): void 'separator' => '', 'year' => 2010, 'month' => 1, - 'day' => 1 + 'day' => 1, ]); $expected = 7300.0; // 365 * DateMatch::MIN_YEAR_SPACE; - $this->assertSame($expected, $match->getGuesses(), "recent years assume MIN_YEAR_SPACE"); + $this->assertSame($expected, $match->getGuesses(), 'recent years assume MIN_YEAR_SPACE'); } public function testGuessWithSeparator(): void @@ -283,11 +270,11 @@ public function testGuessWithSeparator(): void 'separator' => '/', 'year' => 2010, 'month' => 1, - 'day' => 1 + 'day' => 1, ]); $expected = 29200.0; // 365 * DateMatch::MIN_YEAR_SPACE * 4; - $this->assertSame($expected, $match->getGuesses(), "extra guesses are added for separators"); + $this->assertSame($expected, $match->getGuesses(), 'extra guesses are added for separators'); } public function testFeedback(): void @@ -304,12 +291,12 @@ public function testFeedback(): void $this->assertSame( 'Dates are often easy to guess', $feedback['warning'], - "date match gives correct warning" + 'date match gives correct warning' ); $this->assertContains( 'Avoid dates and years that are associated with you', $feedback['suggestions'], - "date match gives correct suggestion" + 'date match gives correct suggestion' ); } } diff --git a/test/Matchers/DictionaryTest.php b/test/Matchers/DictionaryTest.php index 8779375..4f8b4b0 100644 --- a/test/Matchers/DictionaryTest.php +++ b/test/Matchers/DictionaryTest.php @@ -26,7 +26,7 @@ class DictionaryTest extends AbstractMatchTest '8' => 2, '99' => 3, '$' => 4, - 'asdf1234&*' => 5, + 'asdf1234&*' => 5, ], ]; @@ -43,7 +43,7 @@ public static function madeUpWordsProvider(): Iterator public function testWordsNotInDictionary(string $password): void { $matches = DictionaryMatch::match($password); - $this->assertEmpty($matches, "does not match non-dictionary words"); + $this->assertEmpty($matches, 'does not match non-dictionary words'); } public function testContainingWords(): void @@ -52,7 +52,7 @@ public function testContainingWords(): void $patterns = ['mother', 'motherboard', 'board']; $this->checkMatches( - "matches words that contain other words: $password", + "matches words that contain other words: {$password}", DictionaryMatch::match($password, [], self::$testDicts), 'dictionary', $patterns, @@ -71,7 +71,7 @@ public function testOverlappingWords(): void $patterns = ['abcd', 'cdef']; $this->checkMatches( - "matches multiple words when they overlap", + 'matches multiple words when they overlap', DictionaryMatch::match($password, [], self::$testDicts), 'dictionary', $patterns, @@ -90,7 +90,7 @@ public function testUppercasingIgnored(): void $patterns = ['BoaRd', 'Z']; $this->checkMatches( - "ignores uppercasing", + 'ignores uppercasing', DictionaryMatch::match($password, [], self::$testDicts), 'dictionary', $patterns, @@ -111,7 +111,7 @@ public function testWordsSurroundedByNonWords(): void foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as [$password, $i, $j]) { $this->checkMatches( - "identifies words surrounded by non-words", + 'identifies words surrounded by non-words', DictionaryMatch::match($password, [], self::$testDicts), 'dictionary', [$pattern], @@ -129,14 +129,14 @@ public function testAllDictionaryWords(): void { foreach (self::$testDicts as $dictionaryName => $dict) { foreach ($dict as $word => $rank) { - $word = (string)$word; + $word = (string) $word; if ($word === 'motherboard') { continue; // skip words that contain others } $this->checkMatches( - "matches against all words in provided dictionaries", + 'matches against all words in provided dictionaries', DictionaryMatch::match($word, [], self::$testDicts), 'dictionary', [$word], @@ -157,7 +157,7 @@ public function testDefaultDictionary(): void $patterns = [$password]; $this->checkMatches( - "default dictionaries", + 'default dictionaries', DictionaryMatch::match($password), 'dictionary', $patterns, @@ -176,10 +176,10 @@ public function testUserProvidedInput(): void $patterns = ['foo', 'bar']; $matches = DictionaryMatch::match($password, ['foo', 'bar']); - $matches = array_values(array_filter($matches, fn($match) => $match->dictionaryName === 'user_inputs')); + $matches = array_values(array_filter($matches, static fn ($match) => $match->dictionaryName === 'user_inputs')); $this->checkMatches( - "matches with provided user input dictionary", + 'matches with provided user input dictionary', $matches, 'dictionary', $patterns, @@ -195,7 +195,7 @@ public function testUserProvidedInputInNoOtherDictionary(): void { $password = '39kx9.1x0!3n6'; $this->checkMatches( - "matches with provided user input dictionary", + 'matches with provided user input dictionary', DictionaryMatch::match($password, [$password]), 'dictionary', [$password], @@ -211,13 +211,13 @@ public function testMatchesInMultipleDictionaries(): void { $password = 'pass'; $this->checkMatches( - "matches words in multiple dictionaries", + 'matches words in multiple dictionaries', DictionaryMatch::match($password), 'dictionary', ['pass', 'as', 'ass'], [[0, 3], [1, 2], [1, 3]], [ - 'dictionaryName' => ['passwords', 'english_wikipedia', 'us_tv_and_film'] + 'dictionaryName' => ['passwords', 'english_wikipedia', 'us_tv_and_film'], ] ); } @@ -225,14 +225,14 @@ public function testMatchesInMultipleDictionaries(): void public function testGuessesBaseRank(): void { $match = new DictionaryMatch('aaaaa', 0, 5, 'aaaaaa', ['rank' => 32]); - $this->assertEqualsWithDelta(32.0, $match->getGuesses(), PHP_FLOAT_EPSILON, "base guesses == the rank"); + $this->assertEqualsWithDelta(32.0, $match->getGuesses(), PHP_FLOAT_EPSILON, 'base guesses == the rank'); } public function testGuessesCapitalization(): void { $match = new DictionaryMatch('AAAaaa', 0, 5, 'AAAaaa', ['rank' => 32]); $expected = 32.0 * 41; // rank * uppercase variations - $this->assertSame($expected, $match->getGuesses(), "extra guesses are added for capitalization"); + $this->assertSame($expected, $match->getGuesses(), 'extra guesses are added for capitalization'); } /** @@ -265,7 +265,7 @@ public function testGuessesUppercaseVariations(string $token, float $expectedGue $this->assertSame( $expectedGuesses, $match->getGuesses(), - "guess multiplier of $token is $expectedGuesses" + "guess multiplier of {$token} is {$expectedGuesses}" ); } @@ -275,7 +275,7 @@ public function testFeedbackTop10Password(): void $this->assertSame( 'This is a top-10 common password', $feedback['warning'], - "dictionary match warns about top-10 password" + 'dictionary match warns about top-10 password' ); } @@ -285,7 +285,7 @@ public function testFeedbackTop100Password(): void $this->assertSame( 'This is a top-100 common password', $feedback['warning'], - "dictionary match warns about top-100 password" + 'dictionary match warns about top-100 password' ); } @@ -295,7 +295,7 @@ public function testFeedbackTopPasswordSoleMatch(): void $this->assertSame( 'This is a very common password', $feedback['warning'], - "dictionary match warns about common password" + 'dictionary match warns about common password' ); } @@ -305,7 +305,7 @@ public function testFeedbackTopPasswordNotSoleMatch(): void $this->assertSame( 'This is similar to a commonly used password', $feedback['warning'], - "dictionary match warns about common password (not a sole match)" + 'dictionary match warns about common password (not a sole match)' ); } @@ -315,7 +315,7 @@ public function testFeedbackTopPasswordNotSoleMatchRankTooLow(): void $this->assertSame( '', $feedback['warning'], - "no warning for a non-sole match in the password dictionary" + 'no warning for a non-sole match in the password dictionary' ); } @@ -325,7 +325,7 @@ public function testFeedbackWikipediaWordSoleMatch(): void $this->assertSame( 'A word by itself is easy to guess', $feedback['warning'], - "dictionary match warns about Wikipedia word (sole match)" + 'dictionary match warns about Wikipedia word (sole match)' ); } @@ -345,7 +345,7 @@ public function testFeedbackNameSoleMatch(): void $this->assertSame( 'Names and surnames by themselves are easy to guess', $feedback['warning'], - "dictionary match warns about surname (sole match)" + 'dictionary match warns about surname (sole match)' ); } @@ -355,7 +355,7 @@ public function testFeedbackNameNonSoleMatch(): void $this->assertSame( 'Common names and surnames are easy to guess', $feedback['warning'], - "dictionary match warns about surname (not a sole match)" + 'dictionary match warns about surname (not a sole match)' ); } @@ -365,7 +365,7 @@ public function testFeedbackTvAndFilmDictionary(): void $this->assertSame( '', $feedback['warning'], - "no warning for match from us_tv_and_film dictionary" + 'no warning for match from us_tv_and_film dictionary' ); } @@ -375,7 +375,7 @@ public function testFeedbackAllUppercaseWord(): void $this->assertContains( 'All-uppercase is almost as easy to guess as all-lowercase', $feedback['suggestions'], - "dictionary match gives suggestion for all-uppercase word" + 'dictionary match gives suggestion for all-uppercase word' ); } @@ -385,7 +385,7 @@ public function testFeedbackWordStartsWithUppercase(): void $this->assertContains( 'Capitalization doesn\'t help very much', $feedback['suggestions'], - "dictionary match gives suggestion for word starting with uppercase" + 'dictionary match gives suggestion for word starting with uppercase' ); } @@ -396,7 +396,7 @@ private function getFeedbackForToken(string $token, string $dictionary, int $ran { $match = new DictionaryMatch($token, 0, strlen($token) - 1, $token, [ 'dictionary_name' => $dictionary, - 'rank' => $rank + 'rank' => $rank, ]); return $match->getFeedback($soleMatch); } diff --git a/test/Matchers/L33tTest.php b/test/Matchers/L33tTest.php index 098ab89..2fac695 100644 --- a/test/Matchers/L33tTest.php +++ b/test/Matchers/L33tTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\Attributes\DataProvider; use ReflectionClass; use ZxcvbnPhp\Matchers\L33tMatch; -use ZxcvbnPhp\Matchers\BaseMatch; class L33tTest extends AbstractMatchTest { @@ -22,38 +21,23 @@ class L33tTest extends AbstractMatchTest 'o' => ['0'], ]; - /** - * Generally we only need to test the public interface of the matchers, but it can be useful - * to occasionally test protected methods to ensure consistency with upstream. - * - * @param array $args - * @return array - */ - protected static function callProtectedMethod(string $name, array $args) - { - $class = new ReflectionClass(MockL33tMatch::class); - $method = $class->getMethod($name); - $method->setAccessible(true); - return $method->invokeArgs(null, $args); - } - public function testReducesL33tTable(): void { $cases = [ - '' => [] , - 'abcdefgo123578!#$&*)]}>' => [] , - 'a' => [] , - '4' => [ - 'a' => ['4'] + '' => [], + 'abcdefgo123578!#$&*)]}>' => [], + 'a' => [], + '4' => [ + 'a' => ['4'], ], - '4@' => [ - 'a' => ['4', '@'] + '4@' => [ + 'a' => ['4', '@'], ], '4({60' => [ 'a' => ['4'], - 'c' => ['(','{'], + 'c' => ['(','{'], 'g' => ['6'], - 'o' => ['0'] + 'o' => ['0'], ], ]; @@ -61,7 +45,7 @@ public function testReducesL33tTable(): void $this->assertSame( $expected, static::callProtectedMethod('getL33tSubtable', [$pw]), - "reduces l33t table to only the substitutions that a password might be employing" + 'reduces l33t table to only the substitutions that a password might be employing' ); } } @@ -71,25 +55,27 @@ public function testEnumeratesL33tSubstitutions(): void $cases = [ [ [], - [[]] + [[]], ], [ ['a' => ['@']], // subtable - [['@' => 'a']] ], // expected result + [['@' => 'a']], + ], // expected result [ ['a' => ['@', '4']], - [['@' => 'a'], ['4' => 'a']] ], + [['@' => 'a'], ['4' => 'a']], + ], [ ['a' => ['@', '4'], 'c' => ['(']], - [['@' => 'a', '(' => 'c'], ['4' => 'a', '(' => 'c']] - ] + [['@' => 'a', '(' => 'c'], ['4' => 'a', '(' => 'c']], + ], ]; foreach ($cases as $case) { $this->assertSame( $case[1], static::callProtectedMethod('getL33tSubstitutions', [$case[0]]), - "enumerates the different sets of l33t substitutions a password might be using" + 'enumerates the different sets of l33t substitutions a password might be using' ); } } @@ -136,43 +122,43 @@ public function testCapitalizedDictionaryWordsWithL33tCharactersAfter(): void public static function commonCaseProvider(): Iterator { yield [ - 'password' => 'p4ssword', - 'pattern' => 'p4ssword', - 'word' => 'password', + 'password' => 'p4ssword', + 'pattern' => 'p4ssword', + 'word' => 'password', 'dictionary' => 'words', - 'rank' => 3, - 'ij' => [0, 7], - 'sub' => ['4' => 'a'] + 'rank' => 3, + 'ij' => [0, 7], + 'sub' => ['4' => 'a'], ]; yield [ - 'password' => 'p@ssw0rd', - 'pattern' => 'p@ssw0rd', - 'word' => 'password', + 'password' => 'p@ssw0rd', + 'pattern' => 'p@ssw0rd', + 'word' => 'password', 'dictionary' => 'words', - 'rank' => 3, - 'ij' => [0, 7], - 'sub' => ['@' => 'a', '0' => 'o'] + 'rank' => 3, + 'ij' => [0, 7], + 'sub' => ['@' => 'a', '0' => 'o'], ]; yield [ - 'password' => 'aSdfO{G0asDfO', - 'pattern' => '{G0', - 'word' => 'cgo', + 'password' => 'aSdfO{G0asDfO', + 'pattern' => '{G0', + 'word' => 'cgo', 'dictionary' => 'words2', - 'rank' => 1, - 'ij' => [5, 7], - 'sub' => ['{' => 'c', '0' => 'o'] + 'rank' => 1, + 'ij' => [5, 7], + 'sub' => ['{' => 'c', '0' => 'o'], ]; } /** - * @param int[] $ij - * @param string[] $sub + * @param array $ij + * @param array $sub */ #[DataProvider('commonCaseProvider')] public function testCommonL33tSubstitutions(string $password, string $pattern, string $word, string $dictionary, int $rank, array $ij, array $sub): void { $this->checkMatches( - "matches against common l33t substitutions", + 'matches against common l33t substitutions', MockL33tMatch::match($password), 'dictionary', [$pattern], @@ -182,7 +168,7 @@ public function testCommonL33tSubstitutions(string $password, string $pattern, s 'sub' => [$sub], 'matchedWord' => [$word], 'rank' => [$rank], - 'dictionaryName' => [$dictionary] + 'dictionaryName' => [$dictionary], ] ); } @@ -190,20 +176,20 @@ public function testCommonL33tSubstitutions(string $password, string $pattern, s public function testOverlappingL33tPatterns(): void { $this->checkMatches( - "matches against overlapping l33t patterns", + 'matches against overlapping l33t patterns', MockL33tMatch::match('@a(go{G0'), 'dictionary', ['@a(', '(go', '{G0'], [[0,2], [2,4], [5,7]], [ - 'l33t' => [true, true, true], - 'sub' => [ - ['@' => 'a', '(' => 'c'], - ['(' => 'c'], - ['{' => 'c', '0' => 'o'] - ], - 'matchedWord' => ['aac', 'cgo', 'cgo'], - 'rank' => [1, 1, 1], + 'l33t' => [true, true, true], + 'sub' => [ + ['@' => 'a', '(' => 'c'], + ['(' => 'c'], + ['{' => 'c', '0' => 'o'], + ], + 'matchedWord' => ['aac', 'cgo', 'cgo'], + 'rank' => [1, 1, 1], 'dictionaryName' => ['words', 'words2', 'words2'], ] ); @@ -254,15 +240,15 @@ public function testSubstitutionSubsets(): void public function testSubstitutionOfCharacterL(): void { $this->checkMatches( - "matches against overlapping l33t patterns", + 'matches against overlapping l33t patterns', L33tMatch::match('marie1'), 'dictionary', ['marie1', 'arie1'], [[0,5], [1,5]], [ - 'l33t' => [true, true], - 'sub' => [['1' => 'l'], ['1' => 'l'],], - 'matchedWord' => ['mariel', 'ariel'], + 'l33t' => [true, true], + 'sub' => [['1' => 'l'], ['1' => 'l']], + 'matchedWord' => ['mariel', 'ariel'], ] ); } @@ -271,23 +257,23 @@ public function testGuessesL33t(): void { $match = new L33tMatch('aaa@@@', 0, 5, 'aaa@@@', [ 'rank' => 32, - 'sub' => ['@' => 'a'] + 'sub' => ['@' => 'a'], ]); $expected = 32.0 * 41; // rank * l33t variations - $this->assertSame($expected, $match->getGuesses(), "guesses are doubled when word is reversed"); + $this->assertSame($expected, $match->getGuesses(), 'guesses are doubled when word is reversed'); } public function testGuessesL33tAndUppercased(): void { $match = new L33tMatch('AaA@@@', 0, 5, 'AaA@@@', [ 'rank' => 32, - 'sub' => ['@' => 'a'] + 'sub' => ['@' => 'a'], ]); $expected = 32.0 * 41 * 3; // rank * l33t variations * uppercase variations $this->assertSame( $expected, $match->getGuesses(), - "extra guesses are added for both capitalization and common l33t substitutions" + 'extra guesses are added for both capitalization and common l33t substitutions' ); } @@ -311,7 +297,7 @@ public static function variationsProvider(): Iterator } /** - * @param string[] $substitutions + * @param array $substitutions */ #[DataProvider('variationsProvider')] public function testGuessesL33tVariations(string $token, float $expectedGuesses, array $substitutions): void @@ -320,12 +306,13 @@ public function testGuessesL33tVariations(string $token, float $expectedGuesses, $this->assertSame( $expectedGuesses, $match->getGuesses(), - "extra l33t guesses of $token is $expectedGuesses" + "extra l33t guesses of {$token} is {$expectedGuesses}" ); } /** * This test is not strictly needed as it's testing an internal detail, but it's included to match an upstream test. + * * @link https://github.com/dropbox/zxcvbn/blob/master/test/test-scoring.coffee#L357 */ public function testCapitalisationNotAffectingL33t(): void @@ -361,7 +348,7 @@ public function testFeedback(): void $this->assertContains( 'Predictable substitutions like \'@\' instead of \'a\' don\'t help very much', $feedback['suggestions'], - "l33t match gives correct suggestion" + 'l33t match gives correct suggestion' ); } @@ -381,4 +368,20 @@ public function testFeedbackTop100Password(): void "l33t match doesn't give top-100 warning" ); } + + /** + * Generally we only need to test the public interface of the matchers, but it can be useful + * to occasionally test protected methods to ensure consistency with upstream. + * + * @param array $args + * + * @return array + */ + protected static function callProtectedMethod(string $name, array $args): array + { + $class = new ReflectionClass(MockL33tMatch::class); + $method = $class->getMethod($name); + $method->setAccessible(true); + return $method->invokeArgs(null, $args); + } } diff --git a/test/Matchers/MockL33tMatch.php b/test/Matchers/MockL33tMatch.php index 164b57e..d6ac517 100644 --- a/test/Matchers/MockL33tMatch.php +++ b/test/Matchers/MockL33tMatch.php @@ -22,7 +22,7 @@ protected static function getRankedDictionaries(): array ], 'words2' => [ 'cgo' => 1, - ] + ], ]; } diff --git a/test/Matchers/MockMatch.php b/test/Matchers/MockMatch.php index 8d360a6..18bfcf3 100644 --- a/test/Matchers/MockMatch.php +++ b/test/Matchers/MockMatch.php @@ -15,9 +15,11 @@ public function __construct(int $begin, int $end, protected float $guesses) /** * Get feedback to a user based on the match. + * * @param bool $isSoleMatch * Whether this is the only match in the password - * @return array{warning: string, suggestions: string[]} + * + * @return array{warning: string, suggestions: array} */ public function getFeedback(bool $isSoleMatch): array { @@ -39,9 +41,12 @@ public function getRawGuesses(): float * Password to check for match. * @param array $userInputs * Array of values related to the user (optional). + * * @code * array('Alice Smith') + * * @endcode + * * @return array * Array of Match objects */ diff --git a/test/Matchers/RepeatTest.php b/test/Matchers/RepeatTest.php index 535eb0c..de9e266 100644 --- a/test/Matchers/RepeatTest.php +++ b/test/Matchers/RepeatTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use ZxcvbnPhp\Matcher; -use ZxcvbnPhp\Matchers\Bruteforce; use ZxcvbnPhp\Matchers\RepeatMatch; use ZxcvbnPhp\Matchers\SequenceMatch; use ZxcvbnPhp\Scorer; @@ -21,7 +20,7 @@ public function testEmpty(): void foreach (['', '#'] as $password) { $this->assertEmpty( RepeatMatch::match($password), - "doesn't match length-" . strlen($password) . " repeat patterns" + "doesn't match length-" . strlen($password) . ' repeat patterns' ); } } @@ -34,7 +33,7 @@ public function testSingleCharacterEmbeddedRepeats(): void foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as [$password, $i, $j]) { $this->checkMatches( - "matches embedded repeat patterns", + 'matches embedded repeat patterns', RepeatMatch::match($password), 'repeat', [$pattern], @@ -54,14 +53,14 @@ public function testSingleCharacterRepeats(): void $pattern = str_repeat($chr, $length); $this->checkMatches( - "matches repeats with base character '$chr'", + "matches repeats with base character '{$chr}'", RepeatMatch::match($pattern), 'repeat', [$pattern], [[0, strlen($pattern) - 1]], [ 'repeatedChar' => [$chr], - 'repeatCount' => [$length] + 'repeatCount' => [$length], ] ); } @@ -73,7 +72,7 @@ public function testAdjacentRepeats(): void $str = 'BBB1111aaaaa@@@@@@'; $patterns = ['BBB','1111','aaaaa','@@@@@@']; $this->checkMatches( - "matches multiple adjacent repeats", + 'matches multiple adjacent repeats', RepeatMatch::match($str), 'repeat', $patterns, @@ -162,7 +161,7 @@ public function testBaseGuesses(): void [ 'repeatedChar' => ['abc'], 'repeatCount' => [2], - 'baseGuesses' => [13] + 'baseGuesses' => [13], ] ); } @@ -179,7 +178,7 @@ public function testMultibyteRepeat(): void [[0, 2]], [ 'repeatedChar' => ['🙂'], - 'repeatCount' => [3] + 'repeatCount' => [3], ] ); } @@ -196,7 +195,7 @@ public function testRepeatAfterMultibyteCharacters(): void [[7, 8]], [ 'repeatedChar' => ['l'], - 'repeatCount' => [2] + 'repeatCount' => [2], ] ); } @@ -236,7 +235,7 @@ public function testDuplicateRepeatsInPassword(): void [[2, 3], [7, 8]], [ 'repeatedChar' => ['o', 'o'], - 'repeatCount' => [2, 2] + 'repeatCount' => [2, 2], ] ); } @@ -250,12 +249,6 @@ public static function guessesProvider(): Iterator yield [ 'batterystaplebatterystaplebatterystaple', 'batterystaple', 3, 85277994]; } - /** - * @param string $token - * @param string $repeatedChar - * @param int $repeatCount - * @param float $expectedGuesses - */ #[DataProvider('guessesProvider')] public function testGuesses(string $token, string $repeatedChar, int $repeatCount, float $expectedGuesses): void { @@ -285,12 +278,12 @@ public function testFeedbackSingleCharacterRepeat(): void $this->assertSame( 'Repeats like "aaa" are easy to guess', $feedback['warning'], - "one repeated character gives correct warning" + 'one repeated character gives correct warning' ); $this->assertContains( 'Avoid repeated words and characters', $feedback['suggestions'], - "one repeated character gives correct suggestion" + 'one repeated character gives correct suggestion' ); } @@ -306,12 +299,12 @@ public function testFeedbackMultipleCharacterRepeat(): void $this->assertSame( 'Repeats like "abcabcabc" are only slightly harder to guess than "abc"', $feedback['warning'], - "multiple repeated characters gives correct warning" + 'multiple repeated characters gives correct warning' ); $this->assertContains( 'Avoid repeated words and characters', $feedback['suggestions'], - "multiple repeated characters gives correct suggestion" + 'multiple repeated characters gives correct suggestion' ); } } diff --git a/test/Matchers/ReverseDictionaryTest.php b/test/Matchers/ReverseDictionaryTest.php index ef3d51d..4571993 100644 --- a/test/Matchers/ReverseDictionaryTest.php +++ b/test/Matchers/ReverseDictionaryTest.php @@ -25,7 +25,7 @@ public function testReversedDictionaryWordWithCustomDictionary(): void $password = '0123456789'; $this->checkMatches( - "matches against reversed words in custom dictionary", + 'matches against reversed words in custom dictionary', ReverseDictionaryMatch::match($password, [], self::$testDicts), 'dictionary', ['123', '456'], @@ -43,7 +43,7 @@ public function testGuessesReversed(): void { $match = new ReverseDictionaryMatch('aaa', 0, 2, 'aaa', ['rank' => 32]); $expected = 32.0 * 2; // rank * reversed - $this->assertSame($expected, $match->getGuesses(), "guesses are doubled when word is reversed"); + $this->assertSame($expected, $match->getGuesses(), 'guesses are doubled when word is reversed'); } public function testFeedback(): void @@ -63,7 +63,7 @@ public function testFeedback(): void $this->assertContains( 'Reversed words aren\'t much harder to guess', $feedback['suggestions'], - "reverse dictionary match gives correct suggestion" + 'reverse dictionary match gives correct suggestion' ); } @@ -95,7 +95,7 @@ public function testFeedbackShortToken(): void $this->assertSame( 'A word by itself is easy to guess', $feedback['warning'], - "reverse dictionary match still gives warning for short token" + 'reverse dictionary match still gives warning for short token' ); $this->assertNotContains( 'Reversed words aren\'t much harder to guess', diff --git a/test/Matchers/SequenceTest.php b/test/Matchers/SequenceTest.php index 6a47cde..bb3fa03 100644 --- a/test/Matchers/SequenceTest.php +++ b/test/Matchers/SequenceTest.php @@ -26,7 +26,7 @@ public static function shortPasswordProvider(): Iterator public function testShortPassword(string $password): void { $matches = SequenceMatch::match($password); - $this->assertEmpty($matches, "doesn't match length-" . strlen((string) $password) . " sequences"); + $this->assertEmpty($matches, "doesn't match length-" . strlen((string) $password) . ' sequences'); } public function testNonSequence(): void @@ -41,7 +41,7 @@ public function testOverlappingPatterns(): void $password = 'abcbabc'; $this->checkMatches( - "matches overlapping patterns", + 'matches overlapping patterns', SequenceMatch::match($password), 'sequence', ['abc', 'cba', 'abc'], @@ -60,13 +60,13 @@ public function testEmbeddedSequencePatterns(): void foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as [$password, $i, $j]) { $this->checkMatches( - "matches embedded sequence patterns", + 'matches embedded sequence patterns', SequenceMatch::match($password), 'sequence', [$pattern], [[$i, $j]], [ - 'sequenceName' => ['lower'], + 'sequenceName' => ['lower'], 'ascending' => [false], ] ); @@ -97,7 +97,7 @@ public static function sequenceProvider(): Iterator public function testSequenceInformation(string $password, string $name, bool $ascending): void { $this->checkMatches( - "matches " . $password . " as a " . $name . " sequence", + 'matches ' . $password . ' as a ' . $name . ' sequence', SequenceMatch::match($password), 'sequence', [$password], @@ -113,7 +113,7 @@ public function testMultipleMatches(): void { $password = 'pass123wordZYX'; $this->checkMatches( - "matches password with multiple sequences", + 'matches password with multiple sequences', SequenceMatch::match($password), 'sequence', ['123', 'ZYX'], @@ -182,7 +182,7 @@ public function testGuesses(string $token, bool $ascending, float $expectedGuess $this->assertSame( $expectedGuesses, $match->getGuesses(), - "the sequence pattern '$token' has guesses of $expectedGuesses" + "the sequence pattern '{$token}' has guesses of {$expectedGuesses}" ); } @@ -195,12 +195,12 @@ public function testFeedback(): void $this->assertSame( 'Sequences like abc or 6543 are easy to guess', $feedback['warning'], - "sequence gives correct warning" + 'sequence gives correct warning' ); $this->assertSame( ['Avoid sequences'], $feedback['suggestions'], - "sequence gives correct suggestion" + 'sequence gives correct suggestion' ); } } diff --git a/test/Matchers/SpatialTest.php b/test/Matchers/SpatialTest.php index d3a3e9c..0015e4b 100644 --- a/test/Matchers/SpatialTest.php +++ b/test/Matchers/SpatialTest.php @@ -6,7 +6,6 @@ use Iterator; use PHPUnit\Framework\Attributes\DataProvider; -use ZxcvbnPhp\Matchers\BaseMatch; use ZxcvbnPhp\Matchers\SpatialMatch; use ZxcvbnPhp\Math\Binomial; @@ -45,14 +44,14 @@ public function testNoPattern(): void public function testSurroundedPattern(): void { - $pattern = "6tfGHJ"; + $pattern = '6tfGHJ'; $password = "rz!{$pattern}%z"; // for testing, make a subgraph that contains a single keyboard $graphs = ['qwerty' => SpatialMatch::getAdjacencyGraphs()['qwerty']]; $this->checkMatches( - "matches against spatial patterns surrounded by non-spatial patterns", + 'matches against spatial patterns surrounded by non-spatial patterns', SpatialMatch::match($password, [], $graphs), 'spatial', [$pattern], @@ -86,19 +85,13 @@ public static function spatialDataProvider(): Iterator yield [';qoaOQ:Aoq;a', 'dvorak', 11, 4]; } - /** - * @param string $password - * @param string $keyboard - * @param int $turns - * @param int $shifts - */ #[DataProvider('spatialDataProvider')] public function testSpatialPatterns(string $password, string $keyboard, int $turns, int $shifts): void { $graphs = [$keyboard => SpatialMatch::getAdjacencyGraphs()[$keyboard]]; $this->checkMatches( - "matches '$password' as a $keyboard pattern", + "matches '{$password}' as a {$keyboard} pattern", SpatialMatch::match($password, [], $graphs), 'spatial', [$password], @@ -113,9 +106,9 @@ public function testSpatialPatterns(string $password, string $keyboard, int $tur public function testShiftedCountForMultipleMatches(): void { - $password = "!QAZ1qaz"; + $password = '!QAZ1qaz'; $this->checkMatches( - "shifted count is correct for two matches in a row", + 'shifted count is correct for two matches in a row', SpatialMatch::match($password), 'spatial', ['!QAZ', '1qaz'], @@ -128,16 +121,6 @@ public function testShiftedCountForMultipleMatches(): void ); } - protected function getBaseGuessCount(string $token): float - { - // KEYBOARD_STARTING_POSITIONS * KEYBOARD_AVERAGE_DEGREE * (length - 1) - // - 1 term because: not counting spatial patterns of length 1 - // eg for length==6, multiplier is 5 for needing to try len2,len3,..,len6 - return SpatialMatch::KEYBOARD_STARTING_POSITION - * SpatialMatch::KEYBOARD_AVERAGE_DEGREES - * (strlen($token) - 1); - } - public function testGuessesBasic(): void { $token = 'zxcvbn'; @@ -150,7 +133,7 @@ public function testGuessesBasic(): void $this->assertSame( $this->getBaseGuessCount($token), $match->getGuesses(), - "with no turns or shifts, guesses is starts * degree * (len-1)" + 'with no turns or shifts, guesses is starts * degree * (len-1)' ); } @@ -166,7 +149,7 @@ public function testGuessesShifted(): void $this->assertSame( $this->getBaseGuessCount($token) * (Binomial::binom(6, 2) + Binomial::binom(6, 1)), $match->getGuesses(), - "guesses is added for shifted keys, similar to capitals in dictionary matching" + 'guesses is added for shifted keys, similar to capitals in dictionary matching' ); } @@ -182,7 +165,7 @@ public function testGuessesEverythingShifted(): void $this->assertSame( $this->getBaseGuessCount($token) * 2, $match->getGuesses(), - "when everything is shifted, guesses are double" + 'when everything is shifted, guesses are double' ); } @@ -214,7 +197,7 @@ public function testGuessesComplexCase(string $token, int $turns, float $expecte $expected, $actual, 1.0, - "spatial guesses accounts for turn positions, directions and starting keys" + 'spatial guesses accounts for turn positions, directions and starting keys' ); } @@ -231,12 +214,12 @@ public function testFeedbackStraightLine(): void $this->assertSame( 'Straight rows of keys are easy to guess', $feedback['warning'], - "spatial match in straight line gives correct warning" + 'spatial match in straight line gives correct warning' ); $this->assertContains( 'Use a longer keyboard pattern with more turns', $feedback['suggestions'], - "spatial match in straight line gives correct suggestion" + 'spatial match in straight line gives correct suggestion' ); } @@ -253,12 +236,22 @@ public function testFeedbackWithTurns(): void $this->assertSame( 'Short keyboard patterns are easy to guess', $feedback['warning'], - "spatial match with turns gives correct warning" + 'spatial match with turns gives correct warning' ); $this->assertContains( 'Use a longer keyboard pattern with more turns', $feedback['suggestions'], - "spatial match with turns gives correct suggestion" + 'spatial match with turns gives correct suggestion' ); } + + protected function getBaseGuessCount(string $token): float + { + // KEYBOARD_STARTING_POSITIONS * KEYBOARD_AVERAGE_DEGREE * (length - 1) + // - 1 term because: not counting spatial patterns of length 1 + // eg for length==6, multiplier is 5 for needing to try len2,len3,..,len6 + return SpatialMatch::KEYBOARD_STARTING_POSITION + * SpatialMatch::KEYBOARD_AVERAGE_DEGREES + * (strlen($token) - 1); + } } diff --git a/test/Matchers/YearTest.php b/test/Matchers/YearTest.php index 535b612..31e3034 100644 --- a/test/Matchers/YearTest.php +++ b/test/Matchers/YearTest.php @@ -30,7 +30,7 @@ public static function recentYearProvider(): Iterator public function testRecentYears(string $password): void { $this->checkMatches( - "matches recent year", + 'matches recent year', YearMatch::match($password), 'regex', [$password], @@ -50,7 +50,7 @@ public static function nonRecentYearProvider(): Iterator public function testNonRecentYears(string $password): void { $matches = YearMatch::match($password); - $this->assertEmpty($matches, "does not match non-recent year"); + $this->assertEmpty($matches, 'does not match non-recent year'); } public function testYearSurroundedByWords(): void @@ -61,7 +61,7 @@ public function testYearSurroundedByWords(): void foreach ($this->generatePasswords($pattern, $prefixes, $suffixes) as [$password, $i, $j]) { $this->checkMatches( - "identifies years surrounded by words", + 'identifies years surrounded by words', YearMatch::match($password), 'regex', [$pattern], @@ -80,7 +80,7 @@ public function testYearWithinOtherNumbers(): void { $password = '419004'; $this->checkMatches( - "matches year within other numbers", + 'matches year within other numbers', YearMatch::match($password), 'regex', ['1900'], @@ -95,9 +95,9 @@ public function testGuessesPast(): void $match = new YearMatch($token, 0, 3, $token); $this->assertSame( - (float)(DateMatch::getReferenceYear() - (int)$token), + (float) (DateMatch::getReferenceYear() - (int) $token), $match->getGuesses(), - "guesses of |year - REFERENCE_YEAR| for past year matches" + 'guesses of |year - REFERENCE_YEAR| for past year matches' ); } @@ -107,9 +107,9 @@ public function testGuessesFuture(): void $match = new YearMatch($token, 0, 3, $token); $this->assertSame( - (float)((int)$token - DateMatch::getReferenceYear()), + (float) ((int) $token - DateMatch::getReferenceYear()), $match->getGuesses(), - "guesses of |year - REFERENCE_YEAR| for future year matches" + 'guesses of |year - REFERENCE_YEAR| for future year matches' ); } @@ -123,7 +123,7 @@ public function testGuessesUnderMinimumYearSpace(): void // DateMatch::MIN_YEAR_SPACE $match->getGuesses(), PHP_FLOAT_EPSILON, - "guesses of MIN_YEAR_SPACE for a year close to REFERENCE_YEAR" + 'guesses of MIN_YEAR_SPACE for a year close to REFERENCE_YEAR' ); } @@ -136,17 +136,17 @@ public function testFeedback(): void $this->assertSame( 'Recent years are easy to guess', $feedback['warning'], - "year match gives correct warning" + 'year match gives correct warning' ); $this->assertContains( 'Avoid recent years', $feedback['suggestions'], - "year match gives correct suggestion #1" + 'year match gives correct suggestion #1' ); $this->assertContains( 'Avoid years that are associated with you', $feedback['suggestions'], - "year match gives correct suggestion #2" + 'year match gives correct suggestion #2' ); } } diff --git a/test/Math/BinomialTest.php b/test/Math/BinomialTest.php index 332c47d..99da659 100644 --- a/test/Math/BinomialTest.php +++ b/test/Math/BinomialTest.php @@ -5,7 +5,6 @@ namespace ZxcvbnPhp\Test\Math; use Iterator; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ZxcvbnPhp\Math\Binomial; @@ -43,11 +42,6 @@ public function testChosenProviderMatchesExpected(): void $this->assertInstanceOf($provider, Binomial::getProvider()); } - /** - * @param int $n - * @param int $k - * @param float $expected - */ #[DataProvider('binomialDataProvider')] public function testBinomialCoefficient(int $n, int $k, float $expected): void { @@ -56,11 +50,11 @@ public function testBinomialCoefficient(int $n, int $k, float $expected): void $this->assertInstanceOf(BinomialProvider::class, $provider); $value = $provider->binom($n, $k); - $this->assertSame($expected, $value, "$providerClass returns expected result for ($n, $k)"); + $this->assertSame($expected, $value, "{$providerClass} returns expected result for ({$n}, {$k})"); if ($k <= $n) { // Behavior is undefined for $k > n; don't test that $flippedValue = $provider->binom($n, $n - $k); - $this->assertSame($value, $flippedValue, "$providerClass is symmetrical"); + $this->assertSame($value, $flippedValue, "{$providerClass} is symmetrical"); } } } diff --git a/test/ScorerTest.php b/test/ScorerTest.php index e9b7415..96928ae 100644 --- a/test/ScorerTest.php +++ b/test/ScorerTest.php @@ -36,13 +36,13 @@ public function testBlankPassword(): void public function testEmptyMatchSequence(): void { $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, []); - $this->assertCount(1, $result['sequence'], "result.sequence.length == 1"); - $this->assertEqualsWithDelta(10000000001.0, $result['guesses'], PHP_FLOAT_EPSILON, "result.guesses == 10000000001"); + $this->assertCount(1, $result['sequence'], 'result.sequence.length == 1'); + $this->assertEqualsWithDelta(10000000001.0, $result['guesses'], PHP_FLOAT_EPSILON, 'result.guesses == 10000000001'); $match = $result['sequence'][0]; $this->assertSame('bruteforce', $match->pattern, "match.pattern == 'bruteforce'"); - $this->assertSame(self::PASSWORD, $match->token, "match.token == " . self::PASSWORD); - $this->assertSame([0, 9], [$match->begin, $match->end], "[i, j] == [0, 9]"); + $this->assertSame(self::PASSWORD, $match->token, 'match.token == ' . self::PASSWORD); + $this->assertSame([0, 9], [$match->begin, $match->end], '[i, j] == [0, 9]'); } public function testMatchAndBruteforceWithPrefix(): void @@ -50,13 +50,13 @@ public function testMatchAndBruteforceWithPrefix(): void $match = new MockMatch(0, 5, 1); $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, [$match], true); - $this->assertCount(2, $result['sequence'], "result.sequence.length == 2"); - $this->assertSame($match, $result['sequence'][0], "first match is the provided match object"); + $this->assertCount(2, $result['sequence'], 'result.sequence.length == 2'); + $this->assertSame($match, $result['sequence'][0], 'first match is the provided match object'); $match1 = $result['sequence'][1]; - $this->assertSame('bruteforce', $match1->pattern, "second match is bruteforce"); - $this->assertSame([6, 9], [$match1->begin, $match1->end], "second match covers full suffix after first match"); + $this->assertSame('bruteforce', $match1->pattern, 'second match is bruteforce'); + $this->assertSame([6, 9], [$match1->begin, $match1->end], 'second match covers full suffix after first match'); } public function testMatchAndBruteforceWithSuffix(): void @@ -64,13 +64,13 @@ public function testMatchAndBruteforceWithSuffix(): void $match = new MockMatch(3, 9, 1); $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, [$match], true); - $this->assertCount(2, $result['sequence'], "result.sequence.length == 2"); - $this->assertSame($match, $result['sequence'][1], "second match is the provided match object"); + $this->assertCount(2, $result['sequence'], 'result.sequence.length == 2'); + $this->assertSame($match, $result['sequence'][1], 'second match is the provided match object'); $match0 = $result['sequence'][0]; - $this->assertSame('bruteforce', $match0->pattern, "first match is bruteforce"); - $this->assertSame([0, 2], [$match0->begin, $match0->end], "first match covers full prefix before second match"); + $this->assertSame('bruteforce', $match0->pattern, 'first match is bruteforce'); + $this->assertSame([0, 2], [$match0->begin, $match0->end], 'first match covers full prefix before second match'); } public function testMatchAndBruteforceWithInfix(): void @@ -78,16 +78,16 @@ public function testMatchAndBruteforceWithInfix(): void $match = new MockMatch(1, 8, 1); $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, [$match], true); - $this->assertCount(3, $result['sequence'], "result.sequence.length == 3"); + $this->assertCount(3, $result['sequence'], 'result.sequence.length == 3'); $match0 = $result['sequence'][0]; $match2 = $result['sequence'][2]; - $this->assertSame($match, $result['sequence'][1], "middle match is the provided match object"); - $this->assertSame('bruteforce', $match0->pattern, "first match is bruteforce"); - $this->assertSame('bruteforce', $match2->pattern, "third match is bruteforce"); - $this->assertSame([0, 0], [$match0->begin, $match0->end], "first match covers full prefix before second match"); - $this->assertSame([9, 9], [$match2->begin, $match2->end], "third match covers full suffix after second match"); + $this->assertSame($match, $result['sequence'][1], 'middle match is the provided match object'); + $this->assertSame('bruteforce', $match0->pattern, 'first match is bruteforce'); + $this->assertSame('bruteforce', $match2->pattern, 'third match is bruteforce'); + $this->assertSame([0, 0], [$match0->begin, $match0->end], 'first match covers full prefix before second match'); + $this->assertSame([9, 9], [$match2->begin, $match2->end], 'third match covers full suffix after second match'); } public function testBasicGuesses(): void @@ -98,8 +98,8 @@ public function testBasicGuesses(): void ]; $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, $matches, true); - $this->assertCount(1, $result['sequence'], "result.sequence.length == 1"); - $this->assertSame($matches[0], $result['sequence'][0], "result.sequence[0] == m0"); + $this->assertCount(1, $result['sequence'], 'result.sequence.length == 1'); + $this->assertSame($matches[0], $result['sequence'][0], 'result.sequence[0] == m0'); } public function testChoosesLowerGuessesMatchesForSameSpan(): void @@ -110,8 +110,8 @@ public function testChoosesLowerGuessesMatchesForSameSpan(): void ]; $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, $matches, true); - $this->assertCount(1, $result['sequence'], "result.sequence.length == 1"); - $this->assertSame($matches[0], $result['sequence'][0], "result.sequence[0] == m0"); + $this->assertCount(1, $result['sequence'], 'result.sequence.length == 1'); + $this->assertSame($matches[0], $result['sequence'][0], 'result.sequence[0] == m0'); } public function testChoosesLowerGuessesMatchesForSameSpanReversedOrder(): void @@ -122,8 +122,8 @@ public function testChoosesLowerGuessesMatchesForSameSpanReversedOrder(): void ]; $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, $matches, true); - $this->assertCount(1, $result['sequence'], "result.sequence.length == 1"); - $this->assertSame($matches[1], $result['sequence'][0], "result.sequence[0] == m1"); + $this->assertCount(1, $result['sequence'], 'result.sequence.length == 1'); + $this->assertSame($matches[1], $result['sequence'][0], 'result.sequence[0] == m1'); } public function testChoosesSupersetMatchWhenApplicable(): void @@ -135,8 +135,8 @@ public function testChoosesSupersetMatchWhenApplicable(): void ]; $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, $matches, true); - $this->assertEqualsWithDelta(3.0, $result['guesses'], PHP_FLOAT_EPSILON, "total guesses == 3"); - $this->assertSame([$matches[0]], $result['sequence'], "sequence is [m0]"); + $this->assertEqualsWithDelta(3.0, $result['guesses'], PHP_FLOAT_EPSILON, 'total guesses == 3'); + $this->assertSame([$matches[0]], $result['sequence'], 'sequence is [m0]'); } public function testChoosesSubsetMatchesWhenApplicable(): void @@ -148,7 +148,7 @@ public function testChoosesSubsetMatchesWhenApplicable(): void ]; $result = $this->scorer->getMostGuessableMatchSequence(self::PASSWORD, $matches, true); - $this->assertEqualsWithDelta(4.0, $result['guesses'], PHP_FLOAT_EPSILON, "total guesses == 4"); - $this->assertSame([$matches[1], $matches[2]], $result['sequence'], "sequence is [m1, m2]"); + $this->assertEqualsWithDelta(4.0, $result['guesses'], PHP_FLOAT_EPSILON, 'total guesses == 4'); + $this->assertSame([$matches[1], $matches[2]], $result['sequence'], 'sequence is [m1, m2]'); } } diff --git a/test/TimeEstimatorTest.php b/test/TimeEstimatorTest.php index b7d7240..4123c7f 100644 --- a/test/TimeEstimatorTest.php +++ b/test/TimeEstimatorTest.php @@ -5,7 +5,6 @@ namespace ZxcvbnPhp\Test; use Iterator; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ZxcvbnPhp\TimeEstimator; @@ -22,52 +21,52 @@ public function setUp(): void public function testTime100PerHour(): void { $actual = $this->timeEstimator->estimateAttackTimes(100)['crack_times_display']['online_throttling_100_per_hour']; - $this->assertSame('1 hour', $actual, "100 guesses / 100 per hour = 1 hour"); + $this->assertSame('1 hour', $actual, '100 guesses / 100 per hour = 1 hour'); } public function testTime10PerSecond(): void { $actual = $this->timeEstimator->estimateAttackTimes(10)['crack_times_display']['online_no_throttling_10_per_second']; - $this->assertSame('1 second', $actual, "10 guesses / 10 per second = 1 second"); + $this->assertSame('1 second', $actual, '10 guesses / 10 per second = 1 second'); } public function testTime1e4PerSecond(): void { $actual = $this->timeEstimator->estimateAttackTimes(1e5)['crack_times_display']['offline_slow_hashing_1e4_per_second']; - $this->assertSame('10 seconds', $actual, "1e5 guesses / 1e4 per second = 10 seconds"); + $this->assertSame('10 seconds', $actual, '1e5 guesses / 1e4 per second = 10 seconds'); } public function testTime1e10PerSecond(): void { $actual = $this->timeEstimator->estimateAttackTimes(2e11)['crack_times_display']['offline_fast_hashing_1e10_per_second']; - $this->assertSame('20 seconds', $actual, "2e11 guesses / 1e10 per second = 20 seconds"); + $this->assertSame('20 seconds', $actual, '2e11 guesses / 1e10 per second = 20 seconds'); } public function testTimeLessThanASecond(): void { $actual = $this->timeEstimator->estimateAttackTimes(1)['crack_times_display']['offline_fast_hashing_1e10_per_second']; - $this->assertSame('less than a second', $actual, "less than a second"); + $this->assertSame('less than a second', $actual, 'less than a second'); } public function testTimeCenturies(): void { $actual = $this->timeEstimator->estimateAttackTimes(1e10)['crack_times_display']['online_throttling_100_per_hour']; - $this->assertSame('centuries', $actual, "centuries"); + $this->assertSame('centuries', $actual, 'centuries'); } public function testTimeRounding(): void { $actual = $this->timeEstimator->estimateAttackTimes(1500)['crack_times_display']['online_no_throttling_10_per_second']; - $this->assertSame('3 minutes', $actual, "1500 guesses / 10 per second = 3 minutes and not 2.5 minutes"); + $this->assertSame('3 minutes', $actual, '1500 guesses / 10 per second = 3 minutes and not 2.5 minutes'); } public function testPlurals(): void { $actual = $this->timeEstimator->estimateAttackTimes(12)['crack_times_display']['online_no_throttling_10_per_second']; - $this->assertSame('1 second', $actual, "no plural if unit value is 1"); + $this->assertSame('1 second', $actual, 'no plural if unit value is 1'); $actual = $this->timeEstimator->estimateAttackTimes(22)['crack_times_display']['online_no_throttling_10_per_second']; - $this->assertSame('2 seconds', $actual, "plural if unit value is more than 1"); + $this->assertSame('2 seconds', $actual, 'plural if unit value is more than 1'); } public static function unitProvider(): Iterator @@ -80,15 +79,11 @@ public static function unitProvider(): Iterator yield [1e9, '3 years']; } - /** - * @param int $guesses - * @param string $displayText - */ #[DataProvider('unitProvider')] - public function testTimeUnits($guesses, $displayText): void + public function testTimeUnits(float $guesses, string $displayText): void { $actual = $this->timeEstimator->estimateAttackTimes($guesses)['crack_times_display']['online_no_throttling_10_per_second']; - $this->assertSame($displayText, $actual, "centuries"); + $this->assertSame($displayText, $actual, 'centuries'); } public function testDifferentSpeeds(): void @@ -104,7 +99,7 @@ public function testDifferentSpeeds(): void public function testSpeedLessThanOne(): void { $actual = $this->timeEstimator->estimateAttackTimes(100)['crack_times_seconds']['offline_slow_hashing_1e4_per_second']; - $this->assertEqualsWithDelta(0.01, $actual, PHP_FLOAT_EPSILON, "decimal speed when less than one second"); + $this->assertEqualsWithDelta(0.01, $actual, PHP_FLOAT_EPSILON, 'decimal speed when less than one second'); } public static function scoreProvider(): Iterator @@ -116,26 +111,22 @@ public static function scoreProvider(): Iterator yield [1e11, 4]; } - /** - * @param int $guesses - * @param int $expectedScore - */ #[DataProvider('scoreProvider')] - public function testScores($guesses, $expectedScore): void + public function testScores(float $guesses, int $expectedScore): void { $actual = $this->timeEstimator->estimateAttackTimes($guesses)['score']; - $this->assertSame($expectedScore, $actual, "correct score"); + $this->assertSame($expectedScore, $actual, 'correct score'); } public function testScoreDelta(): void { $score = $this->timeEstimator->estimateAttackTimes(1000)['score']; - $this->assertSame(0, $score, "guesses at threshold gets lower score"); + $this->assertSame(0, $score, 'guesses at threshold gets lower score'); $score = $this->timeEstimator->estimateAttackTimes(1003)['score']; - $this->assertSame(0, $score, "guesses just above threshold gets lower score"); + $this->assertSame(0, $score, 'guesses just above threshold gets lower score'); $score = $this->timeEstimator->estimateAttackTimes(1010)['score']; - $this->assertSame(1, $score, "guesses above delta gets higher score"); + $this->assertSame(1, $score, 'guesses above delta gets higher score'); } } diff --git a/test/ZxcvbnTest.php b/test/ZxcvbnTest.php index a28fbb0..4955afc 100644 --- a/test/ZxcvbnTest.php +++ b/test/ZxcvbnTest.php @@ -5,7 +5,6 @@ namespace ZxcvbnPhp\Test; use Iterator; -use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ZxcvbnPhp\Matchers\Bruteforce; @@ -24,7 +23,7 @@ public function setUp(): void public function testMinimumGuessesForMultipleMatches(): void { - /** @var MatchInterface[] $matches */ + /** @var array $matches */ $matches = $this->zxcvbn->passwordStrength('rockyou')['sequence']; // zxcvbn will return two matches: 'rock' (rank 359) and 'you' (rank 1). @@ -54,7 +53,7 @@ public function testZxcvbnReturnTypes(string $key, string $type): void $zxcvbn = new Zxcvbn(); $result = $zxcvbn->passwordStrength('utmostfortitude2018'); - $this->assertArrayHasKey($key, $result, "zxcvbn result has key " . $key); + $this->assertArrayHasKey($key, $result, 'zxcvbn result has key ' . $key); if ($type === 'string') { $correct = is_string($result[$key]); @@ -66,49 +65,46 @@ public function testZxcvbnReturnTypes(string $key, string $type): void throw new \Exception('Invalid test case'); } - $this->assertTrue($correct, "zxcvbn result value " . $key . " is type " . $type); + $this->assertTrue($correct, 'zxcvbn result value ' . $key . ' is type ' . $type); } public static function sanityCheckDataProvider(): Iterator { - yield ['password', 0, ['dictionary',], 'less than a second', 3]; - yield ['65432', 0, ['sequence',], 'less than a second', 101]; - yield ['sdfgsdfg', 1, ['repeat',], 'less than a second', 2595]; - yield ['fortitude', 1, ['dictionary',], '1 second', 11308]; - yield ['dfjkym', 1, ['bruteforce',], '2 minutes', 1000001]; - yield ['fortitude22', 2, ['dictionary', 'repeat',], '2 minutes', 1140700]; - yield ['absoluteadnap', 2, ['dictionary', 'dictionary',], '25 minutes', 15187504]; + yield ['password', 0, ['dictionary'], 'less than a second', 3]; + yield ['65432', 0, ['sequence'], 'less than a second', 101]; + yield ['sdfgsdfg', 1, ['repeat'], 'less than a second', 2595]; + yield ['fortitude', 1, ['dictionary'], '1 second', 11308]; + yield ['dfjkym', 1, ['bruteforce'], '2 minutes', 1000001]; + yield ['fortitude22', 2, ['dictionary', 'repeat'], '2 minutes', 1140700]; + yield ['absoluteadnap', 2, ['dictionary', 'dictionary'], '25 minutes', 15187504]; yield ['knifeandspoon', 3, ['dictionary', 'dictionary', 'dictionary'], '1 day', 1108057600]; yield ['h1dden_26191', 3, ['dictionary', 'bruteforce', 'date'], '4 days', 3081378400]; yield ['4rfv1236yhn!', 4, ['spatial', 'sequence', 'bruteforce'], '1 month', 38980000000]; - yield ['BVidSNqe3oXVyE1996', 4, ['bruteforce', 'regex',], 'centuries', 10000000000010000]; + yield ['BVidSNqe3oXVyE1996', 4, ['bruteforce', 'regex'], 'centuries', 10000000000010000]; } /** * Some basic sanity checks. All of the underlying functionality is tested in more details in their specific * classes, but this is just to check that it's all tied together correctly at the end. - * @param string $password - * @param int $score - * @param string[] $patterns - * @param string $slowHashingDisplay - * @param float $guesses + * + * @param array $patterns */ #[DataProvider('sanityCheckDataProvider')] public function testZxcvbnSanityCheck(string $password, int $score, array $patterns, string $slowHashingDisplay, float $guesses): void { $result = $this->zxcvbn->passwordStrength($password); - $this->assertSame($password, $result['password'], "zxcvbn result has correct password"); - $this->assertSame($score, $result['score'], "zxcvbn result has correct score"); + $this->assertSame($password, $result['password'], 'zxcvbn result has correct password'); + $this->assertSame($score, $result['score'], 'zxcvbn result has correct score'); $this->assertSame( $slowHashingDisplay, $result['crack_times_display']['offline_slow_hashing_1e4_per_second'], - "zxcvbn result has correct display time for offline slow hashing" + 'zxcvbn result has correct display time for offline slow hashing' ); - $this->assertEqualsWithDelta($guesses, $result['guesses'], 1.0, "zxcvbn result has correct guesses"); + $this->assertEqualsWithDelta($guesses, $result['guesses'], 1.0, 'zxcvbn result has correct guesses'); - $actualPatterns = array_map(fn($match) => $match->pattern, $result['sequence']); - $this->assertSame($patterns, $actualPatterns, "zxcvbn result has correct patterns"); + $actualPatterns = array_map(static fn ($match) => $match->pattern, $result['sequence']); + $this->assertSame($patterns, $actualPatterns, 'zxcvbn result has correct patterns'); } /** @@ -119,16 +115,16 @@ public function testUserDefinedWords(): void { $result = $this->zxcvbn->passwordStrength('_wQbgL491', ['PJnD', 'WQBG', 'ZhwZ']); - $this->assertInstanceOf(DictionaryMatch::class, $result['sequence'][1], "user input match is correct class"); - $this->assertSame('wQbg', $result['sequence'][1]->token, "user input match has correct token"); + $this->assertInstanceOf(DictionaryMatch::class, $result['sequence'][1], 'user input match is correct class'); + $this->assertSame('wQbg', $result['sequence'][1]->token, 'user input match has correct token'); } public function testMultibyteUserDefinedWords(): void { $result = $this->zxcvbn->passwordStrength('المفاتيح', ['العربية', 'المفاتيح', 'لوحة']); - $this->assertInstanceOf(DictionaryMatch::class, $result['sequence'][0], "user input match is correct class"); - $this->assertSame('المفاتيح', $result['sequence'][0]->token, "user input match has correct token"); + $this->assertInstanceOf(DictionaryMatch::class, $result['sequence'][0], 'user input match is correct class'); + $this->assertSame('المفاتيح', $result['sequence'][0]->token, 'user input match has correct token'); } public function testAddMatcherWillThrowException(): void diff --git a/test/config/bootstrap.php b/test/config/bootstrap.php index f408832..1664d4a 100644 --- a/test/config/bootstrap.php +++ b/test/config/bootstrap.php @@ -1,7 +1,9 @@