diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9e6459bfce..79e72a1409 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,6 +6,13 @@ about: Create a report to help us improve # **Help us to keep this issue-tracker clean! For questions or support, please refer to our [mailing lists](https://simplesamlphp.org/lists)** +**Specifics of your environment** +1. Are you acting as SP/IdP/proxy? +2. SimpleSAMLphp: What version are you using? +3. PHP: What version are you using? +4. Platform: unix or Windows? +5. Webserver: Apache/Nginx/ISS? + **Describe the bug** A clear and concise description of what the bug is. diff --git a/.github/build/full.json b/.github/build/full.json index 9a13185260..dcb97c4a57 100644 --- a/.github/build/full.json +++ b/.github/build/full.json @@ -2,35 +2,35 @@ "modules": { "authorize": { "repository": "simplesamlphp/simplesamlphp-module-authorize", - "version": "~1.6.2" + "version": "~1.8.0" }, "consent": { "repository": "simplesamlphp/simplesamlphp-module-consent", - "version": "~1.4.1" + "version": "~1.5.0" }, "consentadmin": { "repository": "simplesamlphp/simplesamlphp-module-consentadmin", - "version": "~1.1.0" + "version": "~1.2.1" }, "discopower": { "repository": "simplesamlphp/simplesamlphp-module-discopower", - "version": "~1.5.1" + "version": "~1.6.0" }, "ldap": { "repository": "simplesamlphp/simplesamlphp-module-ldap", - "version": "~2.4.3" + "version": "~2.5.2" }, "metarefresh": { "repository": "simplesamlphp/simplesamlphp-module-metarefresh", - "version": "~1.2.4" + "version": "~1.3.0" }, "radius": { "repository": "simplesamlphp/simplesamlphp-module-radius", - "version": "~2.0.2" + "version": "~2.1.1" }, "sqlauth": { "repository": "simplesamlphp/simplesamlphp-module-sqlauth", - "version": "~1.3.1" + "version": "~1.4.0" } } } diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index d8bf605419..55ef1a7486 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -23,7 +23,7 @@ jobs: uses: shivammathur/setup-php@v2 with: # Should match the minimum required version for SimpleSAMLphp - php-version: '8.1' + php-version: '8.3' tools: composer:v2, phive extensions: mbstring, xml coverage: none diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 9c98602a8d..4cadfce9b3 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -18,9 +18,9 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['8.1', '8.2', '8.3', '8.4'] + php-version: ['8.3', '8.4', '8.5'] - uses: simplesamlphp/simplesamlphp-test-framework/.github/workflows/reusable_phplinter.yml@v1.9.2 + uses: simplesamlphp/simplesamlphp-test-framework/.github/workflows/reusable_phplinter.yml@v1.11.0 with: php-version: ${{ matrix.php-version }} @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false - uses: simplesamlphp/simplesamlphp-test-framework/.github/workflows/reusable_linter.yml@v1.9.2 + uses: simplesamlphp/simplesamlphp-test-framework/.github/workflows/reusable_linter.yml@v1.11.0 with: enable_eslinter: true enable_jsonlinter: true @@ -44,7 +44,7 @@ jobs: fail-fast: false matrix: operating-system: [ubuntu-latest] - php-versions: ['8.1', '8.2', '8.3'] + php-versions: ['8.3', '8.4', '8.5'] steps: - name: Setup PHP, with composer and extensions @@ -52,7 +52,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: intl, mbstring, mysql, pdo, pdo_sqlite, soap, xml + extensions: intl, mbstring, mysql, pdo, pdo_sqlite, soap, sodium, xml tools: composer:v2 ini-values: error_reporting=E_ALL coverage: xdebug @@ -82,7 +82,7 @@ jobs: - name: Cache composer dependencies uses: actions/cache@v4 with: - path: $COMPOSER_CACHE + path: ${{ env.COMPOSER_CACHE }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- @@ -90,15 +90,15 @@ jobs: run: composer install --no-progress --prefer-dist --optimize-autoloader - name: Run unit tests with coverage - if: ${{ matrix.php-versions == '8.1' }} + if: ${{ matrix.php-versions == '8.5' }} run: ./vendor/bin/phpunit - name: Run unit tests (no coverage) - if: ${{ matrix.php-versions != '8.1' }} + if: ${{ matrix.php-versions != '8.5' }} run: ./vendor/bin/phpunit --no-coverage - name: Save coverage data - if: ${{ matrix.php-versions == '8.1' }} + if: ${{ matrix.php-versions == '8.5' }} uses: actions/upload-artifact@v4 with: name: coverage-data @@ -112,7 +112,7 @@ jobs: fail-fast: true matrix: operating-system: [windows-latest] - php-versions: ['8.1', '8.2', '8.3'] + php-versions: ['8.3', '8.4', '8.5'] steps: - name: Setup PHP, with composer and extensions @@ -120,7 +120,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: intl, mbstring, mysql, pdo, pdo_sqlite, soap, xml + extensions: intl, mbstring, mysql, pdo, pdo_sqlite, soap, sodium, xml tools: composer:v2 ini-values: error_reporting=E_ALL coverage: none @@ -144,7 +144,7 @@ jobs: - name: Cache composer dependencies uses: actions/cache@v4 with: - path: $COMPOSER_CACHE + path: ${{ env.COMPOSER_CACHE }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- @@ -165,8 +165,8 @@ jobs: # https://github.com/shivammathur/setup-php uses: shivammathur/setup-php@v2 with: - php-version: '8.3' - tools: composer, phpcs, psalm + php-version: '8.5' + tools: composer extensions: mbstring, soap, xml coverage: none @@ -187,7 +187,7 @@ jobs: - name: Cache composer dependencies uses: actions/cache@v4 with: - path: $COMPOSER_CACHE + path: ${{ env.COMPOSER_CACHE }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- @@ -195,35 +195,22 @@ jobs: run: composer validate - name: Install Composer dependencies - run: composer install --no-progress --prefer-dist --optimize-autoloader + run: | + composer install \ + --no-progress \ + --prefer-dist --optimize-autoloader \ + --ignore-platform-req=maglnet/composer-require-checker - name: PHP Code Sniffer - run: phpcs - - - name: Psalm - continue-on-error: true - run: | - psalm \ - -c psalm.xml \ - --show-info=true \ - --shepherd \ - --php-version=${{ steps.setup-php.outputs.php-version }} + run: vendor/bin/phpcs - - name: Psalm (testsuite) - continue-on-error: true + - name: PHPStan run: | - psalm \ - -c psalm-dev.xml \ - --show-info=true \ - --shepherd \ - --php-version=${{ steps.setup-php.outputs.php-version }} + vendor/bin/phpstan analyze -c phpstan.neon - - name: Psalter + - name: PHPStan (testsuite) run: | - psalm --alter \ - --issues=UnnecessaryVarAnnotation \ - --dry-run \ - --php-version=${{ steps.setup-php.outputs.php-version }} + vendor/bin/phpstan analyze -c phpstan-dev.neon - name: Check for unused translations continue-on-error: true @@ -238,7 +225,7 @@ jobs: # https://github.com/shivammathur/setup-php uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.3' extensions: mbstring, soap, xml tools: composer:v2 coverage: none @@ -260,7 +247,7 @@ jobs: - name: Cache composer dependencies uses: actions/cache@v4 with: - path: $COMPOSER_CACHE + path: ${{ env.COMPOSER_CACHE }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- diff --git a/.gitignore b/.gitignore index cf6571b554..a7b41cc710 100644 --- a/.gitignore +++ b/.gitignore @@ -234,3 +234,4 @@ typings/ # dotenv environment variables file .env +cache diff --git a/README.md b/README.md index 78a9c89c62..33f04bcfa8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ [![Type coverage](https://shepherd.dev/github/simplesamlphp/simplesamlphp/coverage.svg)](https://shepherd.dev/github/simplesamlphp/simplesamlphp) [![BrowserStack Status](https://automate.browserstack.com/badge.svg?badge_key=LzlCL29sZEVDRXJpdGtxZUdITFA3YjYyUFBBYkVVZDVDcG1YZXRaN2pvTT0tLVhCNzkwVUNGVFVjVFVicUg0R1BNR0E9PQ==--f9efb6f330bd98dd6e3c7b816ac2f0982275a872)](https://automate.browserstack.com/public-build/LzlCL29sZEVDRXJpdGtxZUdITFA3YjYyUFBBYkVVZDVDcG1YZXRaN2pvTT0tLVhCNzkwVUNGVFVjVFVicUg0R1BNR0E9PQ==--f9efb6f330bd98dd6e3c7b816ac2f0982275a872) +## Developed using JetBrains IDEs + +[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSource) + This is the official repository of the SimpleSAMLphp software. * [SimpleSAMLphp homepage](https://simplesamlphp.org) diff --git a/composer.json b/composer.json index cc4e5d4b75..509d55829b 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "files": ["tests/_autoload_modules.php"] }, "require": { - "php": "^8.1", + "php": "^8.3", "ext-date": "*", "ext-dom": "*", "ext-hash": "*", @@ -57,47 +57,51 @@ "ext-SPL": "*", "ext-zlib": "*", - "gettext/gettext": "^5.7", - "gettext/translator": "^1.1", - "phpmailer/phpmailer": "^6.8", - "psr/log": "^3.0", - "simplesamlphp/assert": "^1.1", - "simplesamlphp/composer-module-installer": "^1.3", - "simplesamlphp/saml2": "^5.0.0", - "simplesamlphp/saml2-legacy": "^4.18.1", - "simplesamlphp/simplesamlphp-assets-base": "~2.3.0", - "simplesamlphp/xml-common": "^1.24.2", - "simplesamlphp/xml-security": "^1.7", - "symfony/cache": "^6.4", - "symfony/config": "^6.4", - "symfony/console": "^6.4", - "symfony/dependency-injection": "^6.4", - "symfony/expression-language": "~6.4.0", - "symfony/filesystem": "^6.4", - "symfony/finder": "^6.4", - "symfony/framework-bundle": "^6.4", - "symfony/http-foundation": "^6.4", - "symfony/http-kernel": "^6.4", - "symfony/intl": "^6.4", - "symfony/password-hasher": "^6.4", - "symfony/polyfill-intl-icu": "^1.28", - "symfony/routing": "^6.4", - "symfony/translation-contracts": "^3.0", - "symfony/twig-bridge": "^6.4", - "symfony/var-exporter": "^6.4", - "symfony/yaml": "^6.4", - "twig/intl-extra": "^3.7", - "twig/twig": "^3.14.0" + "gettext/gettext": "~5.7", + "gettext/translator": "~1.2", + "phpmailer/phpmailer": "~6.10", + "psr/event-dispatcher": "~1.0", + "psr/log": "~3.0", + "simplesamlphp/assert": "~2.0", + "simplesamlphp/composer-module-installer": "~1.7", + "simplesamlphp/saml2": "~6.1", + "simplesamlphp/saml2-legacy": "~4.20", + "simplesamlphp/simplesamlphp-assets-base": "~2.5.0", + "simplesamlphp/xml-common": "~2.7", + "simplesamlphp/xml-security": "~2.3", + "symfony/cache": "~7.4", + "symfony/config": "~7.4", + "symfony/console": "~7.4", + "symfony/dependency-injection": "~7.4", + "symfony/event-dispatcher": "~7.4", + "symfony/expression-language": "~7.4", + "symfony/filesystem": "~7.4", + "symfony/finder": "~7.4", + "symfony/framework-bundle": "~7.4", + "symfony/http-client": "~7.4", + "symfony/http-client-contracts": "~3.5", + "symfony/http-foundation": "~7.4", + "symfony/http-kernel": "~7.4", + "symfony/intl": "~7.4", + "symfony/password-hasher": "~7.4", + "symfony/polyfill-intl-icu": "~1.33", + "symfony/routing": "~7.4", + "symfony/translation-contracts": "~3.6", + "symfony/twig-bridge": "~7.4", + "symfony/var-exporter": "~7.4", + "symfony/yaml": "~7.4", + "twig/intl-extra": "~3.21", + "twig/twig": "~3.21" }, "require-dev": { "ext-curl": "*", "ext-pdo_sqlite": "*", - "gettext/php-scanner": "1.3.1", + "gettext/php-scanner": "~2.0", "mikey179/vfsstream": "~1.6", - "predis/predis": "^2.2", - "simplesamlphp/simplesamlphp-test-framework": "^1.9.2", - "symfony/translation": "^6.4" + "predis/predis": "~3.3", + "simplesamlphp/simplesamlphp-test-framework": "~1.11", + "symfony/translation": "~7.4" }, "suggest": { "predis/predis": "Needed if a Redis server is used to store session information", @@ -142,5 +146,5 @@ "echo 'Post-install tasks completed!'" ] }, - "version": "v2.4.2" + "version": "v2.5.0" } diff --git a/composer.lock b/composer.lock index b51e53b9ef..99557f63fa 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": "3b3959487c032fcc02f83769ff84cb67", + "content-hash": "4f6e880f7a30af56e681689c4fd70209", "packages": [ { "name": "gettext/gettext", @@ -82,16 +82,16 @@ }, { "name": "gettext/languages", - "version": "2.12.1", + "version": "2.12.2", "source": { "type": "git", "url": "https://github.com/php-gettext/Languages.git", - "reference": "0b0b0851c55168e1dfb14305735c64019732b5f1" + "reference": "079d6f4842cbcbf5673a70d8e93169a684e7aadd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Languages/zipball/0b0b0851c55168e1dfb14305735c64019732b5f1", - "reference": "0b0b0851c55168e1dfb14305735c64019732b5f1", + "url": "https://api.github.com/repos/php-gettext/Languages/zipball/079d6f4842cbcbf5673a70d8e93169a684e7aadd", + "reference": "079d6f4842cbcbf5673a70d8e93169a684e7aadd", "shasum": "" }, "require": { @@ -141,7 +141,7 @@ ], "support": { "issues": "https://github.com/php-gettext/Languages/issues", - "source": "https://github.com/php-gettext/Languages/tree/2.12.1" + "source": "https://github.com/php-gettext/Languages/tree/2.12.2" }, "funding": [ { @@ -153,7 +153,7 @@ "type": "github" } ], - "time": "2025-03-19T11:14:02+00:00" + "time": "2026-02-23T14:05:50+00:00" }, { "name": "gettext/translator", @@ -231,16 +231,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.7.1", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", "shasum": "" }, "require": { @@ -256,7 +256,8 @@ "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" + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -327,7 +328,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.1" + "source": "https://github.com/guzzle/psr7/tree/2.9.0" }, "funding": [ { @@ -343,7 +344,7 @@ "type": "tidelift" } ], - "time": "2025-03-27T12:30:47+00:00" + "time": "2026-03-10T16:41:02+00:00" }, { "name": "nyholm/psr7", @@ -425,16 +426,16 @@ }, { "name": "phpmailer/phpmailer", - "version": "v6.10.0", + "version": "v6.12.0", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144" + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144", - "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d1ac35d784bf9f5e61b424901d5a014967f15b12", + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12", "shasum": "" }, "require": { @@ -494,7 +495,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.12.0" }, "funding": [ { @@ -502,7 +503,7 @@ "type": "github" } ], - "time": "2025-04-24T15:19:31+00:00" + "time": "2025-10-15T16:49:08+00:00" }, { "name": "psr/cache", @@ -908,16 +909,16 @@ }, { "name": "robrichards/xmlseclibs", - "version": "3.1.3", + "version": "3.1.5", "source": { "type": "git", "url": "https://github.com/robrichards/xmlseclibs.git", - "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07" + "reference": "03062be78178cbb5e8f605cd255dc32a14981f92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/2bdfd742624d739dfadbd415f00181b4a77aaf07", - "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/03062be78178cbb5e8f605cd255dc32a14981f92", + "reference": "03062be78178cbb5e8f605cd255dc32a14981f92", "shasum": "" }, "require": { @@ -944,22 +945,22 @@ ], "support": { "issues": "https://github.com/robrichards/xmlseclibs/issues", - "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.3" + "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.5" }, - "time": "2024-11-20T21:13:56+00:00" + "time": "2026-03-13T10:31:56+00:00" }, { "name": "simplesamlphp/assert", - "version": "v1.8.1", + "version": "v2.0.2", "source": { "type": "git", "url": "https://github.com/simplesamlphp/assert.git", - "reference": "f35e26e1201ec41be19404e23f87e53cf747ed3a" + "reference": "9e08dc5525a2259d75e6f6d0ae6754a7b4c13470" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/assert/zipball/f35e26e1201ec41be19404e23f87e53cf747ed3a", - "reference": "f35e26e1201ec41be19404e23f87e53cf747ed3a", + "url": "https://api.github.com/repos/simplesamlphp/assert/zipball/9e08dc5525a2259d75e6f6d0ae6754a7b4c13470", + "reference": "9e08dc5525a2259d75e6f6d0ae6754a7b4c13470", "shasum": "" }, "require": { @@ -967,18 +968,18 @@ "ext-filter": "*", "ext-pcre": "*", "ext-spl": "*", - "guzzlehttp/psr7": "~2.7.0", - "php": "^8.1", - "webmozart/assert": "~1.11.0" + "guzzlehttp/psr7": "~2.8", + "php": "^8.3", + "webmozart/assert": "~2.1" }, "require-dev": { "ext-intl": "*", - "simplesamlphp/simplesamlphp-test-framework": "~1.8.0" + "simplesamlphp/simplesamlphp-test-framework": "~1.11" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "v1.1.x-dev" + "dev-master": "v1.10.x-dev" } }, "autoload": { @@ -1003,32 +1004,33 @@ "description": "A wrapper around webmozart/assert to make it useful beyond checking method arguments", "support": { "issues": "https://github.com/simplesamlphp/assert/issues", - "source": "https://github.com/simplesamlphp/assert/tree/v1.8.1" + "source": "https://github.com/simplesamlphp/assert/tree/v2.0.2" }, - "time": "2025-01-09T12:10:04+00:00" + "time": "2026-03-06T18:04:32+00:00" }, { "name": "simplesamlphp/composer-module-installer", - "version": "v1.4.0", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/simplesamlphp/composer-module-installer.git", - "reference": "edb2155d200e2a208816d06f42cfa78bfd9e7cf4" + "reference": "7560ebd48e74001329a0c2ba44cc467c2e823368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/composer-module-installer/zipball/edb2155d200e2a208816d06f42cfa78bfd9e7cf4", - "reference": "edb2155d200e2a208816d06f42cfa78bfd9e7cf4", + "url": "https://api.github.com/repos/simplesamlphp/composer-module-installer/zipball/7560ebd48e74001329a0c2ba44cc467c2e823368", + "reference": "7560ebd48e74001329a0c2ba44cc467c2e823368", "shasum": "" }, "require": { - "composer-plugin-api": "^2.6", - "php": "^8.1", - "simplesamlphp/assert": "^1.6" + "composer-plugin-api": "~2.9", + "ext-mbstring": "*", + "php": "^8.3", + "simplesamlphp/assert": "~2.0" }, "require-dev": { - "composer/composer": "^2.8.3", - "simplesamlphp/simplesamlphp-test-framework": "^1.8.0" + "composer/composer": "~2.9", + "simplesamlphp/simplesamlphp-test-framework": "~1.11" }, "type": "composer-plugin", "extra": { @@ -1046,55 +1048,96 @@ "description": "A Composer plugin that allows installing SimpleSAMLphp modules through Composer.", "support": { "issues": "https://github.com/simplesamlphp/composer-module-installer/issues", - "source": "https://github.com/simplesamlphp/composer-module-installer/tree/v1.4.0" + "source": "https://github.com/simplesamlphp/composer-module-installer/tree/v1.7.0" + }, + "time": "2026-02-19T23:36:00+00:00" + }, + { + "name": "simplesamlphp/composer-xmlprovider-installer", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/composer-xmlprovider-installer.git", + "reference": "80a2d6a30c730a5fd90358f4875b94140114c643" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/composer-xmlprovider-installer/zipball/80a2d6a30c730a5fd90358f4875b94140114c643", + "reference": "80a2d6a30c730a5fd90358f4875b94140114c643", + "shasum": "" + }, + "require": { + "composer-plugin-api": "~2.9", + "php": "^8.3" + }, + "require-dev": { + "composer/composer": "~2.9", + "simplesamlphp/simplesamlphp-test-framework": "~1.11" }, - "time": "2024-12-08T16:57:03+00:00" + "type": "composer-plugin", + "extra": { + "class": "SimpleSAML\\Composer\\XMLProvider\\XMLProviderInstallerPlugin" + }, + "autoload": { + "psr-4": { + "SimpleSAML\\Composer\\XMLProvider\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "description": "A composer plugin that will auto-generate a classmap with all classes that implement SerializableElementInterface.", + "support": { + "issues": "https://github.com/simplesamlphp/composer-xmlprovider-installer/issues", + "source": "https://github.com/simplesamlphp/composer-xmlprovider-installer/tree/v1.3.0" + }, + "time": "2026-02-19T23:34:51+00:00" }, { "name": "simplesamlphp/saml2", - "version": "v5.0.1", + "version": "v6.1.2", "source": { "type": "git", "url": "https://github.com/simplesamlphp/saml2.git", - "reference": "9c0040881eec1d9447dcc5a043d91450e62f81f4" + "reference": "2a2c3bc9801f26ca2b2905d699dc0b7cff382b22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/saml2/zipball/9c0040881eec1d9447dcc5a043d91450e62f81f4", - "reference": "9c0040881eec1d9447dcc5a043d91450e62f81f4", + "url": "https://api.github.com/repos/simplesamlphp/saml2/zipball/2a2c3bc9801f26ca2b2905d699dc0b7cff382b22", + "reference": "2a2c3bc9801f26ca2b2905d699dc0b7cff382b22", "shasum": "" }, "require": { "ext-date": "*", "ext-dom": "*", "ext-filter": "*", - "ext-libxml": "*", "ext-openssl": "*", "ext-pcre": "*", "ext-zlib": "*", - "nyholm/psr7": "^1.8", - "php": "^8.1", - "psr/clock": "^1.0", - "psr/http-message": "^2.0", - "psr/log": "^2.0 || ^3.0", - "simplesamlphp/assert": "^1.1", - "simplesamlphp/xml-common": "^1.16", - "simplesamlphp/xml-security": "^1.6", - "simplesamlphp/xml-soap": "^1.3" + "nyholm/psr7": "~1.8", + "php": "^8.3", + "psr/clock": "~1.0", + "psr/http-message": "~2.0", + "psr/log": "~3.0", + "simplesamlphp/assert": "~2.0", + "simplesamlphp/xml-common": "~2.7", + "simplesamlphp/xml-security": "~2.3", + "simplesamlphp/xml-soap": "~2.3" }, "require-dev": { - "beste/clock": "^3.0", - "icanhazstring/composer-unused": "^0.9.3", - "mockery/mockery": "^1.6", - "simplesamlphp/simplesamlphp-test-framework": "^1.5" + "beste/clock": "~3.0", + "ext-intl": "*", + "mockery/mockery": "~1.6", + "simplesamlphp/simplesamlphp-test-framework": "~1.11" }, "suggest": { "ext-soap": "*" }, - "type": "library", + "type": "simplesamlphp-xmlprovider", "extra": { "branch-alias": { - "dev-master": "v5.0.x-dev" + "dev-master": "v6.1.x-dev" } }, "autoload": { @@ -1110,47 +1153,53 @@ { "name": "Andreas Åkre Solberg", "email": "andreas.solberg@uninett.no" + }, + { + "name": "Jaime Pérez Crespo", + "email": "jaime.perez@uninett.no" + }, + { + "name": "Tim van Dijen", + "email": "tvdijen@gmail.com" } ], "description": "SAML2 PHP library from SimpleSAMLphp", "support": { "issues": "https://github.com/simplesamlphp/saml2/issues", - "source": "https://github.com/simplesamlphp/saml2/tree/v5.0.1" + "source": "https://github.com/simplesamlphp/saml2/tree/v6.1.2" }, - "time": "2025-05-13T18:12:37+00:00" + "time": "2026-03-14T09:33:34+00:00" }, { "name": "simplesamlphp/saml2-legacy", - "version": "v4.18.1", + "version": "v4.20.1", "source": { "type": "git", "url": "https://github.com/simplesamlphp/saml2-legacy.git", - "reference": "9bbf43a5ace9c8e5107dad3a613b014b456ecd56" + "reference": "ac325057307cc8765e190fae20f7721d5f7b9f69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/saml2-legacy/zipball/9bbf43a5ace9c8e5107dad3a613b014b456ecd56", - "reference": "9bbf43a5ace9c8e5107dad3a613b014b456ecd56", + "url": "https://api.github.com/repos/simplesamlphp/saml2-legacy/zipball/ac325057307cc8765e190fae20f7721d5f7b9f69", + "reference": "ac325057307cc8765e190fae20f7721d5f7b9f69", "shasum": "" }, "require": { "ext-dom": "*", "ext-openssl": "*", "ext-zlib": "*", - "php": ">=7.1 || ^8.0", - "psr/log": "~1.1 || ^2.0 || ^3.0", - "robrichards/xmlseclibs": "^3.1.1", - "webmozart/assert": "^1.9" + "php": "^8.3", + "psr/log": "^2.0 || ^3.0", + "robrichards/xmlseclibs": "^3.1.5", + "simplesamlphp/xml-common": "^2.7", + "webmozart/assert": "^2.0" }, "conflict": { "robrichards/xmlseclibs": "3.1.2" }, "require-dev": { - "mockery/mockery": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "sebastian/phpcpd": "~4.1 || ^5.0 || ^6.0", - "simplesamlphp/simplesamlphp-test-framework": "~0.1.0", - "squizlabs/php_codesniffer": "~3.5" + "mockery/mockery": "~1.6", + "simplesamlphp/simplesamlphp-test-framework": "~1.11" }, "type": "library", "extra": { @@ -1175,27 +1224,27 @@ ], "description": "SAML2 PHP library from SimpleSAMLphp", "support": { - "source": "https://github.com/simplesamlphp/saml2-legacy/tree/v4.18.1" + "source": "https://github.com/simplesamlphp/saml2-legacy/tree/v4.20.1" }, - "time": "2025-03-16T11:50:02+00:00" + "time": "2026-03-13T12:39:43+00:00" }, { "name": "simplesamlphp/simplesamlphp-assets-base", - "version": "v2.3.9", + "version": "v2.5.13", "source": { "type": "git", "url": "https://github.com/simplesamlphp/simplesamlphp-assets-base.git", - "reference": "75bd31897ed3634d97de2815b5fa668625f8134d" + "reference": "b59015c77c52e477bcfb2452f8c0f8755c13be1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp-assets-base/zipball/75bd31897ed3634d97de2815b5fa668625f8134d", - "reference": "75bd31897ed3634d97de2815b5fa668625f8134d", + "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp-assets-base/zipball/b59015c77c52e477bcfb2452f8c0f8755c13be1d", + "reference": "b59015c77c52e477bcfb2452f8c0f8755c13be1d", "shasum": "" }, "require": { - "php": "^8.1", - "simplesamlphp/composer-module-installer": "^1.3.4" + "php": "^8.3", + "simplesamlphp/composer-module-installer": "~1.7" }, "type": "simplesamlphp-module", "notification-url": "https://packagist.org/downloads/", @@ -1211,42 +1260,47 @@ "description": "Assets for the SimpleSAMLphp main repository", "support": { "issues": "https://github.com/simplesamlphp/simplesamlphp-assets-base/issues", - "source": "https://github.com/simplesamlphp/simplesamlphp-assets-base/tree/v2.3.9" + "source": "https://github.com/simplesamlphp/simplesamlphp-assets-base/tree/v2.5.13" }, - "time": "2025-03-02T17:54:55+00:00" + "time": "2026-03-04T16:48:40+00:00" }, { "name": "simplesamlphp/xml-common", - "version": "v1.25.0", + "version": "v2.7.2", "source": { "type": "git", "url": "https://github.com/simplesamlphp/xml-common.git", - "reference": "2250c05aa0062bba4baad9d2357e94f9aa10ffc0" + "reference": "219b76fc2189ea36f26ff2ed642afd19e12143ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/xml-common/zipball/2250c05aa0062bba4baad9d2357e94f9aa10ffc0", - "reference": "2250c05aa0062bba4baad9d2357e94f9aa10ffc0", + "url": "https://api.github.com/repos/simplesamlphp/xml-common/zipball/219b76fc2189ea36f26ff2ed642afd19e12143ea", + "reference": "219b76fc2189ea36f26ff2ed642afd19e12143ea", "shasum": "" }, "require": { + "ext-bcmath": "*", "ext-date": "*", "ext-dom": "*", "ext-filter": "*", "ext-libxml": "*", "ext-pcre": "*", "ext-spl": "*", - "php": "^8.1", - "simplesamlphp/assert": "~1.8.1", - "symfony/finder": "~6.4.0" + "guzzlehttp/psr7": "~2.8", + "php": "^8.3", + "psr/clock": "~1.0", + "simplesamlphp/assert": "~2.0", + "simplesamlphp/composer-xmlprovider-installer": "~1.2" }, "require-dev": { - "simplesamlphp/simplesamlphp-test-framework": "~1.9.2" + "simplesamlphp/simplesamlphp-test-framework": "~1.11" }, "type": "simplesamlphp-xmlprovider", "autoload": { "psr-4": { - "SimpleSAML\\XML\\": "src/" + "SimpleSAML\\XML\\": "src/XML/", + "SimpleSAML\\XPath\\": "src/XPath/", + "SimpleSAML\\XMLSchema\\": "src/XMLSchema/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1266,27 +1320,29 @@ "description": "A library with classes and utilities for handling XML structures.", "homepage": "http://simplesamlphp.org", "keywords": [ - "saml", - "xml" + "Xpath", + "schema", + "xml", + "xsd" ], "support": { "issues": "https://github.com/simplesamlphp/xml-common/issues", "source": "https://github.com/simplesamlphp/xml-common" }, - "time": "2025-06-02T21:40:38+00:00" + "time": "2026-03-11T16:16:22+00:00" }, { "name": "simplesamlphp/xml-security", - "version": "v1.13.2", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/simplesamlphp/xml-security.git", - "reference": "a7f2e09632da1179ecb9a021c3c2fbcbcd28071a" + "reference": "ded07df15a53ee87a179e8bff79870ffea31e716" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/xml-security/zipball/a7f2e09632da1179ecb9a021c3c2fbcbcd28071a", - "reference": "a7f2e09632da1179ecb9a021c3c2fbcbcd28071a", + "url": "https://api.github.com/repos/simplesamlphp/xml-security/zipball/ded07df15a53ee87a179e8bff79870ffea31e716", + "reference": "ded07df15a53ee87a179e8bff79870ffea31e716", "shasum": "" }, "require": { @@ -1296,12 +1352,12 @@ "ext-openssl": "*", "ext-pcre": "*", "ext-spl": "*", - "php": "^8.1", - "simplesamlphp/assert": "~1.8.1", - "simplesamlphp/xml-common": "~1.25.0" + "php": "^8.3", + "simplesamlphp/assert": "~2.0", + "simplesamlphp/xml-common": "~2.7" }, "require-dev": { - "simplesamlphp/simplesamlphp-test-framework": "~1.9.2" + "simplesamlphp/simplesamlphp-test-framework": "~1.11" }, "type": "simplesamlphp-xmlprovider", "autoload": { @@ -1335,33 +1391,33 @@ ], "support": { "issues": "https://github.com/simplesamlphp/xml-security/issues", - "source": "https://github.com/simplesamlphp/xml-security/tree/v1.13.2" + "source": "https://github.com/simplesamlphp/xml-security/tree/v2.3.1" }, - "time": "2025-06-04T21:49:04+00:00" + "time": "2026-03-13T16:57:53+00:00" }, { "name": "simplesamlphp/xml-soap", - "version": "v1.7.1", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/simplesamlphp/xml-soap.git", - "reference": "ca1ee4ea29c62fa66fc30d040b4013b4543f4f76" + "reference": "014a0846d3b3a1a29f8d39c5bf75a827de216501" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/xml-soap/zipball/ca1ee4ea29c62fa66fc30d040b4013b4543f4f76", - "reference": "ca1ee4ea29c62fa66fc30d040b4013b4543f4f76", + "url": "https://api.github.com/repos/simplesamlphp/xml-soap/zipball/014a0846d3b3a1a29f8d39c5bf75a827de216501", + "reference": "014a0846d3b3a1a29f8d39c5bf75a827de216501", "shasum": "" }, "require": { "ext-dom": "*", "ext-pcre": "*", - "php": "^8.1", - "simplesamlphp/assert": "~1.8.1", - "simplesamlphp/xml-common": "~1.25.0" + "php": "^8.3", + "simplesamlphp/assert": "~2.0", + "simplesamlphp/xml-common": "~2.7" }, "require-dev": { - "simplesamlphp/simplesamlphp-test-framework": "~1.9.2" + "simplesamlphp/simplesamlphp-test-framework": "~1.11" }, "type": "simplesamlphp-xmlprovider", "extra": { @@ -1371,7 +1427,8 @@ }, "autoload": { "psr-4": { - "SimpleSAML\\SOAP\\": "src/" + "SimpleSAML\\SOAP11\\": "src/SOAP11/", + "SimpleSAML\\SOAP12\\": "src/SOAP12/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1387,37 +1444,40 @@ "description": "SimpleSAMLphp library for XML SOAP", "support": { "issues": "https://github.com/simplesamlphp/xml-soap/issues", - "source": "https://github.com/simplesamlphp/xml-soap/tree/v1.7.1" + "source": "https://github.com/simplesamlphp/xml-soap/tree/v2.3.0" }, - "time": "2025-06-03T21:07:04+00:00" + "time": "2026-02-17T20:22:50+00:00" }, { "name": "symfony/cache", - "version": "v6.4.21", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "d1abcf763a7414f2e572f676f22da7a06c8cd9ee" + "reference": "665522ec357540e66c294c08583b40ee576574f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/d1abcf763a7414f2e572f676f22da7a06c8cd9ee", - "reference": "d1abcf763a7414f2e572f676f22da7a06c8cd9ee", + "url": "https://api.github.com/repos/symfony/cache/zipball/665522ec357540e66c294c08583b40ee576574f0", + "reference": "665522ec357540e66c294c08583b40ee576574f0", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", - "symfony/cache-contracts": "^2.5|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^6.3.6|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "conflict": { - "doctrine/dbal": "<2.13.1", - "symfony/dependency-injection": "<5.4", - "symfony/http-kernel": "<5.4", - "symfony/var-dumper": "<5.4" + "doctrine/dbal": "<3.6", + "ext-redis": "<6.1", + "ext-relay": "<0.12.1", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" }, "provide": { "psr/cache-implementation": "2.0|3.0", @@ -1426,15 +1486,16 @@ }, "require-dev": { "cache/integration-tests": "dev-master", - "doctrine/dbal": "^2.13.1|^3|^4", + "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/filesystem": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1469,7 +1530,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.21" + "source": "https://github.com/symfony/cache/tree/v7.4.7" }, "funding": [ { @@ -1480,12 +1541,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-08T08:21:20+00:00" + "time": "2026-03-06T08:14:57+00:00" }, { "name": "symfony/cache-contracts", @@ -1565,34 +1630,34 @@ }, { "name": "symfony/config", - "version": "v6.4.22", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "af5917a3b1571f54689e56677a3f06440d2fe4c7" + "reference": "6c17162555bfb58957a55bb0e43e00035b6ae3d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/af5917a3b1571f54689e56677a3f06440d2fe4c7", - "reference": "af5917a3b1571f54689e56677a3f06440d2fe4c7", + "url": "https://api.github.com/repos/symfony/config/zipball/6c17162555bfb58957a55bb0e43e00035b6ae3d5", + "reference": "6c17162555bfb58957a55bb0e43e00035b6ae3d5", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^7.1|^8.0", "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/finder": "<5.4", + "symfony/finder": "<6.4", "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1620,7 +1685,7 @@ "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/v6.4.22" + "source": "https://github.com/symfony/config/tree/v7.4.7" }, "funding": [ { @@ -1631,56 +1696,60 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-14T06:00:01+00:00" + "time": "2026-03-06T10:41:14+00:00" }, { "name": "symfony/console", - "version": "v6.4.22", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7d29659bc3c9d8e9a34e2c3414ef9e9e003e6cf3" + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7d29659bc3c9d8e9a34e2c3414ef9e9e003e6cf3", - "reference": "7d29659bc3c9d8e9a34e2c3414ef9e9e003e6cf3", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^7.2|^8.0" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "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": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1714,7 +1783,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.22" + "source": "https://github.com/symfony/console/tree/v7.4.7" }, "funding": [ { @@ -1725,49 +1794,52 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-07T07:05:04+00:00" + "time": "2026-03-06T14:06:20+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.22", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "8cb11f833d1f5bfbb2df97dfc23c92b4d42c18d9" + "reference": "0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8cb11f833d1f5bfbb2df97dfc23c92b4d42c18d9", - "reference": "8cb11f833d1f5bfbb2df97dfc23c92b4d42c18d9", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db", + "reference": "0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/service-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4.20|^7.2.5" + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" }, "conflict": { "ext-psr": "<1.1|>=2", - "symfony/config": "<6.1", - "symfony/finder": "<5.4", - "symfony/proxy-manager-bridge": "<6.3", - "symfony/yaml": "<5.4" + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" }, "provide": { "psr/container-implementation": "1.1|2.0", "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^6.1|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1795,7 +1867,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.22" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.7" }, "funding": [ { @@ -1806,12 +1878,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-17T07:35:26+00:00" + "time": "2026-03-03T07:48:48+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1882,31 +1958,34 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.22", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "ce765a2d28b3cce61de1fb916e207767a73171d1" + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/ce765a2d28b3cce61de1fb916e207767a73171d1", - "reference": "ce765a2d28b3cce61de1fb916e207767a73171d1", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^5.4|^6.0|^7.0" + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ "Resources/bin/patch-type-declarations" @@ -1937,7 +2016,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.22" + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" }, "funding": [ { @@ -1948,33 +2027,37 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-28T12:00:15+00:00" + "time": "2026-01-20T16:42:42+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.13", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" + "reference": "dc2c0eba1af673e736bb851d747d266108aea746" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<5.4", + "symfony/dependency-injection": "<6.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -1983,13 +2066,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^5.4|^6.0|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2017,7 +2101,7 @@ "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/v6.4.13" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4" }, "funding": [ { @@ -2028,12 +2112,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2026-01-05T11:45:34+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -2113,21 +2201,21 @@ }, { "name": "symfony/expression-language", - "version": "v6.4.13", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/expression-language.git", - "reference": "3524904fb026356a5230cd197f9a4e6a61e0e7df" + "reference": "f3a6497eb6573e185f2ec41cd3b3f0cd68ddf667" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/expression-language/zipball/3524904fb026356a5230cd197f9a4e6a61e0e7df", - "reference": "3524904fb026356a5230cd197f9a4e6a61e0e7df", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/f3a6497eb6573e185f2ec41cd3b3f0cd68ddf667", + "reference": "f3a6497eb6573e185f2ec41cd3b3f0cd68ddf667", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/cache": "^5.4|^6.0|^7.0", + "php": ">=8.2", + "symfony/cache": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3" }, @@ -2157,7 +2245,7 @@ "description": "Provides an engine that can compile and evaluate expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/expression-language/tree/v6.4.13" + "source": "https://github.com/symfony/expression-language/tree/v7.4.4" }, "funding": [ { @@ -2168,34 +2256,38 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-09T08:40:40+00:00" + "time": "2026-01-05T08:47:25+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.13", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" + "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", + "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^5.4|^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2223,7 +2315,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.13" + "source": "https://github.com/symfony/filesystem/tree/v7.4.6" }, "funding": [ { @@ -2234,32 +2326,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:07:50+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/finder", - "version": "v6.4.17", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", - "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.0|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2287,7 +2383,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.17" + "source": "https://github.com/symfony/finder/tree/v7.4.6" }, "funding": [ { @@ -2298,117 +2394,126 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-29T13:51:37+00:00" + "time": "2026-01-29T09:40:50+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.22", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "b1de19b2083484d0ce945977f6c6484e9e493a2e" + "reference": "c94bc78c85d76af67918404a95d44940f66a7c2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/b1de19b2083484d0ce945977f6c6484e9e493a2e", - "reference": "b1de19b2083484d0ce945977f6c6484e9e493a2e", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/c94bc78c85d76af67918404a95d44940f66a7c2f", + "reference": "c94bc78c85d76af67918404a95d44940f66a7c2f", "shasum": "" }, "require": { "composer-runtime-api": ">=2.1", "ext-xml": "*", - "php": ">=8.1", - "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/config": "^6.1|^7.0", - "symfony/dependency-injection": "^6.4.12|^7.0", + "php": ">=8.2", + "symfony/cache": "^6.4.12|^7.0|^8.0", + "symfony/config": "^7.4.4|^8.0.4", + "symfony/dependency-injection": "^7.4.4|^8.0.4", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.1|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/filesystem": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4", + "symfony/error-handler": "^7.3|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^7.1|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/routing": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/routing": "^7.4|^8.0" }, "conflict": { - "doctrine/annotations": "<1.13.1", "doctrine/persistence": "<1.3", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", - "symfony/asset": "<5.4", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/asset": "<6.4", "symfony/asset-mapper": "<6.4", - "symfony/clock": "<6.3", - "symfony/console": "<5.4|>=7.0", + "symfony/clock": "<6.4", + "symfony/console": "<6.4", "symfony/dom-crawler": "<6.4", - "symfony/dotenv": "<5.4", - "symfony/form": "<5.4", - "symfony/http-client": "<6.3", - "symfony/lock": "<5.4", - "symfony/mailer": "<5.4", - "symfony/messenger": "<6.3", + "symfony/dotenv": "<6.4", + "symfony/form": "<7.4", + "symfony/http-client": "<6.4", + "symfony/lock": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<7.4", "symfony/mime": "<6.4", - "symfony/property-access": "<5.4", - "symfony/property-info": "<5.4", - "symfony/runtime": "<5.4.45|>=6.0,<6.4.13|>=7.0,<7.1.6", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", - "symfony/security-core": "<5.4", - "symfony/security-csrf": "<5.4", - "symfony/serializer": "<6.4", - "symfony/stopwatch": "<5.4", - "symfony/translation": "<6.4", - "symfony/twig-bridge": "<5.4", - "symfony/twig-bundle": "<5.4", + "symfony/security-core": "<6.4", + "symfony/security-csrf": "<7.2", + "symfony/serializer": "<7.2.5", + "symfony/stopwatch": "<6.4", + "symfony/translation": "<7.3", + "symfony/twig-bridge": "<6.4", + "symfony/twig-bundle": "<6.4", "symfony/validator": "<6.4", "symfony/web-profiler-bundle": "<6.4", - "symfony/workflow": "<6.4" + "symfony/webhook": "<7.2", + "symfony/workflow": "<7.4" }, "require-dev": { - "doctrine/annotations": "^1.13.1|^2", "doctrine/persistence": "^1.3|^2|^3", "dragonmantank/cron-expression": "^3.1", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", "seld/jsonlint": "^1.10", - "symfony/asset": "^5.4|^6.0|^7.0", - "symfony/asset-mapper": "^6.4|^7.0", - "symfony/browser-kit": "^5.4|^6.0|^7.0", - "symfony/clock": "^6.2|^7.0", - "symfony/console": "^5.4.9|^6.0.9|^7.0", - "symfony/css-selector": "^5.4|^6.0|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/dotenv": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/form": "^5.4|^6.0|^7.0", - "symfony/html-sanitizer": "^6.1|^7.0", - "symfony/http-client": "^6.3|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/mailer": "^5.4|^6.0|^7.0", - "symfony/messenger": "^6.3|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/notifier": "^5.4|^6.0|^7.0", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/json-streamer": "^7.3|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/notifier": "^6.4|^7.0|^8.0", + "symfony/object-mapper": "^7.3|^8.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/property-info": "^5.4|^6.0|^7.0", - "symfony/rate-limiter": "^5.4|^6.0|^7.0", - "symfony/scheduler": "^6.4.4|^7.0.4", - "symfony/security-bundle": "^5.4|^6.0|^7.0", - "symfony/semaphore": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/string": "^5.4|^6.0|^7.0", - "symfony/translation": "^6.4|^7.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0", - "symfony/uid": "^5.4|^6.0|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/web-link": "^5.4|^6.0|^7.0", - "symfony/workflow": "^6.4|^7.0", - "symfony/yaml": "^5.4|^6.0|^7.0", - "twig/twig": "^2.10|^3.0.4" + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/scheduler": "^6.4.4|^7.0.4|^8.0", + "symfony/security-bundle": "^6.4|^7.0|^8.0", + "symfony/semaphore": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.2.5|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^7.3|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/webhook": "^7.2|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.3|^8.0", + "twig/twig": "^3.12" }, "type": "symfony-bundle", "autoload": { @@ -2436,7 +2541,190 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.22" + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T15:39:55+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "1010624285470eb60e88ed10035102c75b4ea6af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af", + "reference": "1010624285470eb60e88ed10035102c75b4ea6af", + "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/polyfill-php83": "^1.29", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "amphp/socket": "<1.1", + "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", + "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/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.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.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-05T11:16:58+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "75d7043853a42837e68111812f4d964b01e5101c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-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.6.0" }, "funding": [ { @@ -2452,40 +2740,41 @@ "type": "tidelift" } ], - "time": "2025-05-14T07:14:36+00:00" + "time": "2025-04-29T11:18:49+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.4.22", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6b7c97fe1ddac8df3cc9ba6410c8abc683e148ae" + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6b7c97fe1ddac8df3cc9ba6410c8abc683e148ae", - "reference": "6b7c97fe1ddac8df3cc9ba6410c8abc683e148ae", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { + "doctrine/dbal": "<3.6", "symfony/cache": "<6.4.12|>=7.0,<7.1.5" }, "require-dev": { - "doctrine/dbal": "^2.13.1|^3|^4", + "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", - "symfony/mime": "^5.4|^6.0|^7.0", - "symfony/rate-limiter": "^5.4|^6.0|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2513,7 +2802,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.22" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.7" }, "funding": [ { @@ -2524,82 +2813,87 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-11T15:36:20+00:00" + "time": "2026-03-06T13:15:18+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.22", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "15c105b839a7cfa1bc0989c091bfb6477f23b673" + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/15c105b839a7cfa1bc0989c091bfb6477f23b673", - "reference": "15c105b839a7cfa1bc0989c091bfb6477f23b673", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1", + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/browser-kit": "<5.4", - "symfony/cache": "<5.4", - "symfony/config": "<6.1", - "symfony/console": "<5.4", + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", - "symfony/doctrine-bridge": "<5.4", - "symfony/form": "<5.4", - "symfony/http-client": "<5.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/mailer": "<5.4", - "symfony/messenger": "<5.4", - "symfony/translation": "<5.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", "symfony/translation-contracts": "<2.5", - "symfony/twig-bridge": "<5.4", + "symfony/twig-bridge": "<6.4", "symfony/validator": "<6.4", - "symfony/var-dumper": "<6.3", - "twig/twig": "<2.13" + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^5.4|^6.0|^7.0", - "symfony/clock": "^6.2|^7.0", - "symfony/config": "^6.1|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/css-selector": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/property-access": "^5.4.5|^6.0.5|^7.0", - "symfony/routing": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.4.4|^7.0.4", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^5.4|^6.0|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^5.4|^6.4|^7.0", - "symfony/var-exporter": "^6.2|^7.0", - "twig/twig": "^2.13|^3.0.4" + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" }, "type": "library", "autoload": { @@ -2627,7 +2921,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.22" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.7" }, "funding": [ { @@ -2638,34 +2932,41 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:23:40+00:00" + "time": "2026-03-06T16:33:18+00:00" }, { "name": "symfony/intl", - "version": "v6.4.22", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/intl.git", - "reference": "aaecb52f18a6f95766a239ca0a6cc0df983d92cc" + "reference": "6d6a398b18f73b3110140dbb030dcee2ae4ea81f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/intl/zipball/aaecb52f18a6f95766a239ca0a6cc0df983d92cc", - "reference": "aaecb52f18a6f95766a239ca0a6cc0df983d92cc", + "url": "https://api.github.com/repos/symfony/intl/zipball/6d6a398b18f73b3110140dbb030dcee2ae4ea81f", + "reference": "6d6a398b18f73b3110140dbb030dcee2ae4ea81f", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/string": "<7.1" }, "require-dev": { - "symfony/filesystem": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2710,7 +3011,7 @@ "localization" ], "support": { - "source": "https://github.com/symfony/intl/tree/v6.4.22" + "source": "https://github.com/symfony/intl/tree/v7.4.6" }, "funding": [ { @@ -2721,36 +3022,40 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-04T12:02:38+00:00" + "time": "2026-02-09T09:33:46+00:00" }, { "name": "symfony/password-hasher", - "version": "v6.4.13", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/password-hasher.git", - "reference": "e97a1b31f60b8bdfc1fdedab4398538da9441d47" + "reference": "376755eb9c9857d78aedb68341ad2f46d1908b29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/password-hasher/zipball/e97a1b31f60b8bdfc1fdedab4398538da9441d47", - "reference": "e97a1b31f60b8bdfc1fdedab4398538da9441d47", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/376755eb9c9857d78aedb68341ad2f46d1908b29", + "reference": "376755eb9c9857d78aedb68341ad2f46d1908b29", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "conflict": { - "symfony/security-core": "<5.4" + "symfony/security-core": "<6.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/security-core": "^5.4|^6.0|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2782,7 +3087,7 @@ "password" ], "support": { - "source": "https://github.com/symfony/password-hasher/tree/v6.4.13" + "source": "https://github.com/symfony/password-hasher/tree/v7.4.6" }, "funding": [ { @@ -2793,16 +3098,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2026-02-11T16:03:16+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -2861,7 +3170,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -2872,6 +3181,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -2881,16 +3194,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -2939,7 +3252,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -2950,25 +3263,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-icu", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-icu.git", - "reference": "763d2a91fea5681509ca01acbc1c5e450d127811" + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/763d2a91fea5681509ca01acbc1c5e450d127811", - "reference": "763d2a91fea5681509ca01acbc1c5e450d127811", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", "shasum": "" }, "require": { @@ -3023,7 +3340,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.33.0" }, "funding": [ { @@ -3034,16 +3351,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-21T18:38:29+00:00" + "time": "2025-06-20T22:24:30+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3104,7 +3425,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -3115,6 +3436,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -3124,7 +3449,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -3185,7 +3510,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -3196,6 +3521,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -3205,16 +3534,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -3261,7 +3590,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -3272,45 +3601,127 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "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\\Php85\\": "" + }, + "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.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" }, { "name": "symfony/routing", - "version": "v6.4.22", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "1f5234e8457164a3a0038a4c0a4ba27876a9c670" + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/1f5234e8457164a3a0038a4c0a4ba27876a9c670", - "reference": "1f5234e8457164a3a0038a4c0a4ba27876a9c670", + "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938", + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { - "doctrine/annotations": "<1.12", - "symfony/config": "<6.2", - "symfony/dependency-injection": "<5.4", - "symfony/yaml": "<5.4" + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" }, "require-dev": { - "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", - "symfony/config": "^6.2|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3344,7 +3755,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.22" + "source": "https://github.com/symfony/routing/tree/v7.4.6" }, "funding": [ { @@ -3355,25 +3766,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-27T16:08:38+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -3427,7 +3842,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -3438,31 +3853,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v6.4.21", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "73e2c6966a5aef1d4892873ed5322245295370c6" + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6", - "reference": "73e2c6966a5aef1d4892873ed5322245295370c6", + "url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b", + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.33", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -3470,11 +3890,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/intl": "^6.2|^7.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3513,7 +3933,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.21" + "source": "https://github.com/symfony/string/tree/v7.4.6" }, "funding": [ { @@ -3524,25 +3944,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-18T15:23:29+00:00" + "time": "2026-02-09T09:33:46+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -3591,7 +4015,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -3602,77 +4026,83 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/twig-bridge", - "version": "v6.4.22", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/twig-bridge.git", - "reference": "04ab306a2f2c9dbd46f4363383812954f704af9d" + "reference": "c67219ca6b79a57b64e36bbb2cd8ba741286587e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/04ab306a2f2c9dbd46f4363383812954f704af9d", - "reference": "04ab306a2f2c9dbd46f4363383812954f704af9d", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/c67219ca6b79a57b64e36bbb2cd8ba741286587e", + "reference": "c67219ca6b79a57b64e36bbb2cd8ba741286587e", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/translation-contracts": "^2.5|^3", - "twig/twig": "^2.13|^3.0.4" + "twig/twig": "^3.21" }, "conflict": { - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", - "symfony/console": "<5.4", - "symfony/form": "<6.3", - "symfony/http-foundation": "<5.4", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/console": "<6.4", + "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4", + "symfony/http-foundation": "<6.4", "symfony/http-kernel": "<6.4", - "symfony/mime": "<6.2", + "symfony/mime": "<6.4", "symfony/serializer": "<6.4", - "symfony/translation": "<5.4", - "symfony/workflow": "<5.4" + "symfony/translation": "<6.4", + "symfony/workflow": "<6.4" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/asset": "^5.4|^6.0|^7.0", - "symfony/asset-mapper": "^6.3|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", - "symfony/form": "^6.4.20|^7.2.5", - "symfony/html-sanitizer": "^6.1|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^5.4|^6.0|^7.0", - "symfony/mime": "^6.2|^7.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4.32|~7.3.10|^7.4.4|^8.0.4", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.3|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/property-info": "^5.4|^6.0|^7.0", - "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/security-acl": "^2.8|^3.0", - "symfony/security-core": "^5.4|^6.0|^7.0", - "symfony/security-csrf": "^5.4|^6.0|^7.0", - "symfony/security-http": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/translation": "^6.1|^7.0", - "symfony/web-link": "^5.4|^6.0|^7.0", - "symfony/workflow": "^5.4|^6.0|^7.0", - "symfony/yaml": "^5.4|^6.0|^7.0", - "twig/cssinliner-extra": "^2.12|^3", - "twig/inky-extra": "^2.12|^3", - "twig/markdown-extra": "^2.12|^3" + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/workflow": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/cssinliner-extra": "^3", + "twig/inky-extra": "^3", + "twig/markdown-extra": "^3" }, "type": "symfony-bridge", "autoload": { @@ -3700,7 +4130,7 @@ "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/twig-bridge/tree/v6.4.22" + "source": "https://github.com/symfony/twig-bridge/tree/v7.4.7" }, "funding": [ { @@ -3711,43 +4141,45 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-16T08:23:44+00:00" + "time": "2026-03-04T15:37:05+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.21", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5" + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", - "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/console": "<5.4" + "symfony/console": "<6.4" }, "require-dev": { - "ext-iconv": "*", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^6.3|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/uid": "^5.4|^6.0|^7.0", - "twig/twig": "^2.13|^3.0.4" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" }, "bin": [ "Resources/bin/var-dump-server" @@ -3785,7 +4217,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.21" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" }, "funding": [ { @@ -3796,35 +4228,39 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-09T07:34:50+00:00" + "time": "2026-02-15T10:53:20+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.22", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "f28cf841f5654955c9f88ceaf4b9dc29571988a9" + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/f28cf841f5654955c9f88ceaf4b9dc29571988a9", - "reference": "f28cf841f5654955c9f88ceaf4b9dc29571988a9", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", + "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -3862,7 +4298,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.22" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" }, "funding": [ { @@ -3873,37 +4309,41 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-14T13:00:13+00:00" + "time": "2025-09-11T10:15:23+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.21", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "f01987f45676778b474468aa266fe2eda1f2bc7e" + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/f01987f45676778b474468aa266fe2eda1f2bc7e", - "reference": "f01987f45676778b474468aa266fe2eda1f2bc7e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<5.4" + "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -3934,7 +4374,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.21" + "source": "https://github.com/symfony/yaml/tree/v7.4.6" }, "funding": [ { @@ -3945,30 +4385,34 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-04T09:48:44+00:00" + "time": "2026-02-09T09:33:46+00:00" }, { "name": "twig/intl-extra", - "version": "v3.21.0", + "version": "v3.23.0", "source": { "type": "git", "url": "https://github.com/twigphp/intl-extra.git", - "reference": "05bc5d46b9df9e62399eae53e7c0b0633298b146" + "reference": "32f15a38d45a8d0ec11bc8a3d97d3ac2a261499f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/05bc5d46b9df9e62399eae53e7c0b0633298b146", - "reference": "05bc5d46b9df9e62399eae53e7c0b0633298b146", + "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/32f15a38d45a8d0ec11bc8a3d97d3ac2a261499f", + "reference": "32f15a38d45a8d0ec11bc8a3d97d3ac2a261499f", "shasum": "" }, "require": { "php": ">=8.1.0", - "symfony/intl": "^5.4|^6.4|^7.0", + "symfony/intl": "^5.4|^6.4|^7.0|^8.0", "twig/twig": "^3.13|^4.0" }, "require-dev": { @@ -4002,7 +4446,7 @@ "twig" ], "support": { - "source": "https://github.com/twigphp/intl-extra/tree/v3.21.0" + "source": "https://github.com/twigphp/intl-extra/tree/v3.23.0" }, "funding": [ { @@ -4014,20 +4458,20 @@ "type": "tidelift" } ], - "time": "2025-01-31T20:45:36+00:00" + "time": "2026-01-17T13:57:47+00:00" }, { "name": "twig/twig", - "version": "v3.21.1", + "version": "v3.23.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", - "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", "shasum": "" }, "require": { @@ -4081,7 +4525,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.21.1" + "source": "https://github.com/twigphp/Twig/tree/v3.23.0" }, "funding": [ { @@ -4093,37 +4537,37 @@ "type": "tidelift" } ], - "time": "2025-05-03T07:21:55+00:00" + "time": "2026-01-23T21:00:41+00:00" }, { "name": "webmozart/assert", - "version": "1.11.0", + "version": "2.1.6", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", "shasum": "" }, "require": { "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10-dev" + "dev-feature/2-0": "2.0-dev" } }, "autoload": { @@ -4139,6 +4583,10 @@ { "name": "Bernhard Schussek", "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" } ], "description": "Assertions to validate method input/output with nice error messages.", @@ -4149,46 +4597,59 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/2.1.6" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2026-02-27T10:28:38+00:00" } ], "packages-dev": [ { - "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "name": "azjezz/psl", + "version": "4.3.0", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "url": "https://github.com/azjezz/psl.git", + "reference": "74c95be0214eb7ea39146ed00ac4eb71b45d787b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/azjezz/psl/zipball/74c95be0214eb7ea39146ed00ac4eb71b45d787b", + "reference": "74c95be0214eb7ea39146ed00ac4eb71b45d787b", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.4", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + "ext-bcmath": "*", + "ext-intl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-sodium": "*", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "revolt/event-loop": "^1.0.7" }, "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" + "carthage-software/mago": "^1.6.0", + "infection/infection": "^0.31.2", + "php-coveralls/php-coveralls": "^2.7.0", + "phpbench/phpbench": "^1.4.0", + "phpunit/phpunit": "^9.6.22" }, - "type": "composer-plugin", + "suggest": { + "php-standard-library/phpstan-extension": "PHPStan integration", + "php-standard-library/psalm-plugin": "Psalm integration" + }, + "type": "library", "extra": { - "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "thanks": { + "url": "https://github.com/hhvm/hsl", + "name": "hhvm/hsl" + } }, "autoload": { + "files": [ + "src/bootstrap.php" + ], "psr-4": { - "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "Psl\\": "src/Psl" } }, "notification-url": "https://packagist.org/downloads/", @@ -4197,71 +4658,48 @@ ], "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" + "name": "azjezz", + "email": "azjezz@protonmail.com" } ], - "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" - ], + "description": "PHP Standard Library", "support": { - "issues": "https://github.com/PHPCSStandards/composer-installer/issues", - "source": "https://github.com/PHPCSStandards/composer-installer" + "issues": "https://github.com/azjezz/psl/issues", + "source": "https://github.com/azjezz/psl/tree/4.3.0" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/azjezz", + "type": "github" + }, + { + "url": "https://github.com/veewee", + "type": "github" + } + ], + "time": "2026-02-24T01:58:53+00:00" }, { - "name": "gettext/php-scanner", - "version": "v1.3.1", + "name": "composer-unused/contracts", + "version": "0.3.0", "source": { "type": "git", - "url": "https://github.com/php-gettext/PHP-Scanner.git", - "reference": "989a2cffa1d0f43d13b14c83a50429119b5eb8e4" + "url": "https://github.com/composer-unused/contracts.git", + "reference": "5ec448d3ee80735dccad6a21a3266c377d0845ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/PHP-Scanner/zipball/989a2cffa1d0f43d13b14c83a50429119b5eb8e4", - "reference": "989a2cffa1d0f43d13b14c83a50429119b5eb8e4", + "url": "https://api.github.com/repos/composer-unused/contracts/zipball/5ec448d3ee80735dccad6a21a3266c377d0845ae", + "reference": "5ec448d3ee80735dccad6a21a3266c377d0845ae", "shasum": "" }, "require": { - "gettext/gettext": "^5.5.0", - "nikic/php-parser": "^4.2", - "php": ">=7.2" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.15", - "oscarotero/php-cs-fixer-config": "^1.0", - "phpunit/phpunit": "^8.0", - "squizlabs/php_codesniffer": "^3.0" + "php": "^7.4 || ^8.0" }, "type": "library", "autoload": { "psr-4": { - "Gettext\\Scanner\\": "src" + "ComposerUnused\\Contracts\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4270,92 +4708,646 @@ ], "authors": [ { - "name": "Oscar Otero", - "email": "oom@oscarotero.com", - "homepage": "http://oscarotero.com", - "role": "Developer" + "name": "Andreas Frömer", + "email": "composer-unused@icanhazstring.com" } ], - "description": "PHP scanner for gettext", - "homepage": "https://github.com/php-gettext/PHP-Scanner", - "keywords": [ - "gettext", - "i18n", - "php", - "scanner", - "translation" - ], + "description": "Contract repository for composer-unused", "support": { - "email": "oom@oscarotero.com", - "issues": "https://github.com/php-gettext/PHP-Scanner/issues", - "source": "https://github.com/php-gettext/PHP-Scanner/tree/v1.3.1" + "issues": "https://github.com/composer-unused/contracts/issues", + "source": "https://github.com/composer-unused/contracts/tree/0.3.0" }, - "time": "2022-03-18T11:47:55+00:00" + "funding": [ + { + "url": "https://github.com/icanhazstring", + "type": "github" + } + ], + "time": "2023-03-17T00:41:49+00:00" }, { - "name": "mikey179/vfsstream", - "version": "v1.6.12", + "name": "composer-unused/symbol-parser", + "version": "0.3.3", "source": { "type": "git", - "url": "https://github.com/bovigo/vfsStream.git", - "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0" + "url": "https://github.com/composer-unused/symbol-parser.git", + "reference": "afa62007cca768bd1ecbc0e8ed347c675c239410" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", - "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "url": "https://api.github.com/repos/composer-unused/symbol-parser/zipball/afa62007cca768bd1ecbc0e8ed347c675c239410", + "reference": "afa62007cca768bd1ecbc0e8ed347c675c239410", "shasum": "" }, "require": { - "php": ">=7.1.0" + "composer-unused/contracts": "^0.3", + "nikic/php-parser": "^5.0", + "php": "^7.4 || ^8.0", + "phpstan/phpdoc-parser": "^1.25 || ^2", + "psr/container": "^1.0 || ^2.0", + "psr/log": "^1.1 || ^2 || ^3", + "symfony/finder": "^5.3 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^7.5||^8.5||^9.6", - "yoast/phpunit-polyfills": "^2.0" + "ergebnis/composer-normalize": "^2.49", + "ext-ds": "*", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.5", + "roave/security-advisories": "dev-master", + "squizlabs/php_codesniffer": "^4.0.1", + "symfony/property-access": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/serializer": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6.x-dev" - } - }, "autoload": { - "psr-0": { - "org\\bovigo\\vfs\\": "src/main/php" + "psr-4": { + "ComposerUnused\\SymbolParser\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Frank Kleine", - "homepage": "http://frankkleine.de/", - "role": "Developer" + "name": "Andreas Frömer", + "email": "composer-unused@icanhazstring.com" } ], - "description": "Virtual file system to mock the real file system in unit tests.", - "homepage": "http://vfs.bovigo.org/", + "description": "Toolkit to parse symbols from a composer package", + "homepage": "https://github.com/composer-unused/symbol-parser", + "keywords": [ + "composer", + "parser", + "symbol" + ], "support": { - "issues": "https://github.com/bovigo/vfsStream/issues", - "source": "https://github.com/bovigo/vfsStream/tree/master", - "wiki": "https://github.com/bovigo/vfsStream/wiki" + "issues": "https://github.com/composer-unused/symbol-parser/issues", + "source": "https://github.com/composer-unused/symbol-parser" }, - "time": "2024-08-29T18:43:31+00:00" + "funding": [ + { + "url": "https://github.com/sponsors/icanhazstring", + "type": "github" + }, + { + "url": "https://paypal.me/icanhazstring", + "type": "other" + } + ], + "time": "2026-01-29T13:38:57+00:00" }, { - "name": "myclabs/deep-copy", - "version": "1.13.1", + "name": "composer/pcre", + "version": "3.3.2", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "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/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.2.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "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": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "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", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-11T04:32:07+00:00" + }, + { + "name": "gettext/php-scanner", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-gettext/PHP-Scanner.git", + "reference": "43f53b6e0d5f9ac5ea165d8202c18267d6bd5dfd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-gettext/PHP-Scanner/zipball/43f53b6e0d5f9ac5ea165d8202c18267d6bd5dfd", + "reference": "43f53b6e0d5f9ac5ea165d8202c18267d6bd5dfd", + "shasum": "" + }, + "require": { + "gettext/gettext": "^5.5.0", + "nikic/php-parser": "^5", + "php": ">=7.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "oscarotero/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\Scanner\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "PHP scanner for gettext", + "homepage": "https://github.com/php-gettext/PHP-Scanner", + "keywords": [ + "gettext", + "i18n", + "php", + "scanner", + "translation" + ], + "support": { + "email": "oom@oscarotero.com", + "issues": "https://github.com/php-gettext/PHP-Scanner/issues", + "source": "https://github.com/php-gettext/PHP-Scanner/tree/v2.0.1" + }, + "time": "2024-11-29T20:14:52+00:00" + }, + { + "name": "icanhazstring/composer-unused", + "version": "0.9.6", + "source": { + "type": "git", + "url": "https://github.com/composer-unused/composer-unused.git", + "reference": "c60030af7954a528746dd2180c10b5e0871e84c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer-unused/composer-unused/zipball/c60030af7954a528746dd2180c10b5e0871e84c7", + "reference": "c60030af7954a528746dd2180c10b5e0871e84c7", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "composer-unused/contracts": "^0.3", + "composer-unused/symbol-parser": "^0.3.1", + "composer/xdebug-handler": "^3.0", + "ext-json": "*", + "nikic/php-parser": "^5.0", + "ondram/ci-detector": "^4.1", + "php": "^8.1", + "phpstan/phpdoc-parser": "^1.25 || ^2", + "psr/container": "^1.0 || ^2.0", + "psr/log": "^1.1 || ^2 || ^3", + "symfony/config": "^6.0 || ^7.0 || ^8.0", + "symfony/console": "^6.0 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^6.0 || ^7.0 || ^8.0", + "symfony/property-access": "^6.0 || ^7.0 || ^8.0", + "symfony/serializer": "^6.0 || ^7.0 || ^8.0", + "webmozart/assert": "^1.10 || ^2.0", + "webmozart/glob": "^4.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8", + "codeception/verify": "^3.1", + "dg/bypass-finals": "^1.6", + "ergebnis/composer-normalize": "^2.49", + "ext-ds": "*", + "ext-zend-opcache": "*", + "jangregor/phpstan-prophecy": "^2.1.1", + "mikey179/vfsstream": "^1.6.10", + "php-ds/php-ds": "^1.5", + "phpspec/prophecy-phpunit": "^2.2.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^2.1.37", + "phpstan/phpstan-phpunit": "^2.0.12", + "phpunit/phpunit": "^9.6.34", + "roave/security-advisories": "dev-master", + "squizlabs/php_codesniffer": "^3.13" + }, + "bin": [ + "bin/composer-unused" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + } + }, + "autoload": { + "psr-4": { + "ComposerUnused\\ComposerUnused\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Frömer", + "email": "composer-unused@icanhazstring.com" + } + ], + "description": "Show unused packages by scanning your code", + "homepage": "https://github.com/composer-unused/composer-unused", + "keywords": [ + "composer", + "php-parser", + "static analysis", + "unused" + ], + "support": { + "issues": "https://github.com/composer-unused/composer-unused/issues", + "source": "https://github.com/composer-unused/composer-unused" + }, + "funding": [ + { + "url": "https://github.com/sponsors/icanhazstring", + "type": "github" + }, + { + "url": "https://paypal.me/icanhazstring", + "type": "other" + } + ], + "time": "2026-01-30T05:52:24+00:00" + }, + { + "name": "maglnet/composer-require-checker", + "version": "4.20.0", + "source": { + "type": "git", + "url": "https://github.com/maglnet/ComposerRequireChecker.git", + "reference": "c62d517ef5ac2d347dd9b3d02c1cc16c0f1091e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maglnet/ComposerRequireChecker/zipball/c62d517ef5ac2d347dd9b3d02c1cc16c0f1091e2", + "reference": "c62d517ef5ac2d347dd9b3d02c1cc16c0f1091e2", + "shasum": "" + }, + "require": { + "azjezz/psl": "^4.2.0", + "composer-runtime-api": "^2.0.0", + "ext-phar": "*", + "nikic/php-parser": "^5.7.0", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "symfony/console": "^7.4.1", + "webmozart/glob": "^4.7.0" + }, + "conflict": { + "revolt/event-loop": "< 1.0.8" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "ext-zend-opcache": "*", + "phing/phing": "^3.1.1", + "php-standard-library/phpstan-extension": "^2.0.2", + "php-standard-library/psalm-plugin": "^2.3", + "phpstan/phpstan": "^2.1.33", + "phpunit/phpunit": "^12.5.4", + "psalm/plugin-phpunit": "^0.19.5", + "roave/infection-static-analysis-plugin": "^1.42.0", + "spatie/temporary-directory": "^2.3.0", + "vimeo/psalm": "^6.14.3" + }, + "bin": [ + "bin/composer-require-checker" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "psr-4": { + "ComposerRequireChecker\\": "src/ComposerRequireChecker" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.io/" + }, + { + "name": "Matthias Glaub", + "email": "magl@magl.net", + "homepage": "http://magl.net" + } + ], + "description": "CLI tool to analyze composer dependencies and verify that no unknown symbols are used in the sources of a package", + "homepage": "https://github.com/maglnet/ComposerRequireChecker", + "keywords": [ + "cli", + "composer", + "dependency", + "imports", + "require", + "requirements", + "static analysis" + ], + "support": { + "issues": "https://github.com/maglnet/ComposerRequireChecker/issues", + "source": "https://github.com/maglnet/ComposerRequireChecker/tree/4.20.0" + }, + "time": "2025-12-29T11:34:42+00:00" + }, + { + "name": "mikey179/vfsstream", + "version": "v1.6.12", + "source": { + "type": "git", + "url": "https://github.com/bovigo/vfsStream.git", + "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5||^8.5||^9.6", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "support": { + "issues": "https://github.com/bovigo/vfsStream/issues", + "source": "https://github.com/bovigo/vfsStream/tree/master", + "wiki": "https://github.com/bovigo/vfsStream/wiki" + }, + "time": "2024-08-29T18:43:31+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -4394,7 +5386,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -4402,29 +5394,31 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v4.19.4", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -4432,7 +5426,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -4446,19 +5440,97 @@ ], "authors": [ { - "name": "Nikita Popov" + "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.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "ondram/ci-detector", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/OndraM/ci-detector.git", + "reference": "8b0223b5ed235fd377c75fdd1bfcad05c0f168b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/OndraM/ci-detector/zipball/8b0223b5ed235fd377c75fdd1bfcad05c0f168b8", + "reference": "8b0223b5ed235fd377c75fdd1bfcad05c0f168b8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.13.2", + "lmc/coding-standard": "^3.0.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.1.0", + "phpstan/phpstan": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpunit/phpunit": "^9.6.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "OndraM\\CiDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Machulda", + "email": "ondrej.machulda@gmail.com" } ], - "description": "A PHP parser written in PHP", + "description": "Detect continuous integration environment and provide unified access to properties of current build", "keywords": [ - "parser", - "php" + "CircleCI", + "Codeship", + "Wercker", + "adapter", + "appveyor", + "aws", + "aws codebuild", + "azure", + "azure devops", + "azure pipelines", + "bamboo", + "bitbucket", + "buddy", + "ci-info", + "codebuild", + "continuous integration", + "continuousphp", + "devops", + "drone", + "github", + "gitlab", + "interface", + "jenkins", + "pipelines", + "sourcehut", + "teamcity", + "travis" ], "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4" + "issues": "https://github.com/OndraM/ci-detector/issues", + "source": "https://github.com/OndraM/ci-detector/tree/4.2.0" }, - "time": "2024-09-29T15:01:53+00:00" + "time": "2024-03-12T13:22:30+00:00" }, { "name": "phar-io/manifest", @@ -4610,217 +5682,537 @@ "psr-4": { "PHPStan\\ExtensionInstaller\\": "src/" } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Composer plugin for automatic installation of PHPStan extensions", - "keywords": [ - "dev", - "static analysis" + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "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/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.40", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "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": "2026-02-23T15:04:35+00:00" + }, + { + "name": "phpstan/phpstan-mockery", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-mockery.git", + "reference": "89a949d0ac64298e88b7c7fa00caee565c198394" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-mockery/zipball/89a949d0ac64298e88b7c7fa00caee565c198394", + "reference": "89a949d0ac64298e88b7c7fa00caee565c198394", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "mockery/mockery": "^1.6.11", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan Mockery extension", + "support": { + "issues": "https://github.com/phpstan/phpstan-mockery/issues", + "source": "https://github.com/phpstan/phpstan-mockery/tree/2.0.0" + }, + "time": "2024-10-14T03:18:12+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "2.0.16", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "6ab598e1bc106e6827fd346ae4a12b4a5d634c32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/6ab598e1bc106e6827fd346ae4a12b4a5d634c32", + "reference": "6ab598e1bc106e6827fd346ae4a12b4a5d634c32", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.32" + }, + "conflict": { + "phpunit/phpunit": "<7.0" + }, + "require-dev": { + "nikic/php-parser": "^5", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "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", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.16" + }, + "time": "2026-02-14T09:05:21+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.1" + }, + "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": "12.5.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/12.5.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } ], - "support": { - "issues": "https://github.com/phpstan/extension-installer/issues", - "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" - }, - "time": "2024-09-04T20:21:43+00:00" + "time": "2026-02-06T06:01:44+00:00" }, { - "name": "phpstan/phpdoc-parser", - "version": "2.1.0", + "name": "phpunit/php-file-iterator", + "version": "6.0.1", "source": { "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": ">=8.3" }, "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" + "phpunit/phpunit": "^12.0" }, "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "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" ], - "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/2.1.0" + "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/6.0.1" }, - "time": "2025-02-19T13:28:12+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" }, { - "name": "phpstan/phpstan", - "version": "1.12.27", + "name": "phpunit/php-invoker", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162" + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3a6e423c076ab39dfedc307e2ac627ef579db162", - "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": ">=8.3" }, - "conflict": { - "phpstan/phpstan-shim": "*" + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" }, - "bin": [ - "phpstan", - "phpstan.phar" - ], "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, "autoload": { - "files": [ - "bootstrap.php" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPStan - PHP Static Analysis Tool", + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", "keywords": [ - "dev", - "static analysis" + "process" ], "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" + "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/6.0.0" }, "funding": [ { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://github.com/phpstan", + "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2025-05-21T20:51:45+00:00" + "time": "2025-02-07T04:58:58+00:00" }, { - "name": "phpstan/phpstan-mockery", - "version": "1.1.3", + "name": "phpunit/php-text-template", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan-mockery.git", - "reference": "98cac6e256b4ee60fdeb26a7dd81bb271b454e80" + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-mockery/zipball/98cac6e256b4ee60fdeb26a7dd81bb271b454e80", - "reference": "98cac6e256b4ee60fdeb26a7dd81bb271b454e80", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12" + "php": ">=8.3" }, "require-dev": { - "mockery/mockery": "^1.6.11", - "nikic/php-parser": "^4.13.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.4", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^12.0" }, - "type": "phpstan-extension", + "type": "library", "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] + "branch-alias": { + "dev-main": "5.0-dev" } }, "autoload": { - "psr-4": { - "PHPStan\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "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" ], - "description": "PHPStan Mockery extension", "support": { - "issues": "https://github.com/phpstan/phpstan-mockery/issues", - "source": "https://github.com/phpstan/phpstan-mockery/tree/1.1.3" + "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/5.0.0" }, - "time": "2024-09-11T15:47:29+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "10.1.16", + "name": "phpunit/php-timer", + "version": "8.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", - "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=8.1", - "phpunit/php-file-iterator": "^4.1.0", - "phpunit/php-text-template": "^3.0.1", - "sebastian/code-unit-reverse-lookup": "^3.0.0", - "sebastian/complexity": "^3.2.0", - "sebastian/environment": "^6.1.0", - "sebastian/lines-of-code": "^2.0.2", - "sebastian/version": "^4.0.1", - "theseer/tokenizer": "^1.2.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^10.1" - }, - "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1.x-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -4839,17 +6231,15 @@ "role": "lead" } ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ - "coverage", - "testing", - "xunit" + "timer" ], "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/10.1.16" + "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/8.0.0" }, "funding": [ { @@ -4857,35 +6247,63 @@ "type": "github" } ], - "time": "2024-08-22T04:31:57+00:00" + "time": "2025-02-07T04:59:38+00:00" }, { - "name": "phpunit/php-file-iterator", - "version": "4.1.0", + "name": "phpunit/phpunit", + "version": "12.5.14", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", - "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0", "shasum": "" }, "require": { - "php": ">=8.1" - }, - "require-dev": { - "phpunit/phpunit": "^10.0" + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.4", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" }, + "bin": [ + "phpunit" + ], "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "12.5-dev" } }, "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], "classmap": [ "src/" ] @@ -4901,172 +6319,201 @@ "role": "lead" } ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", "keywords": [ - "filesystem", - "iterator" + "phpunit", + "testing", + "xunit" ], "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/4.1.0" + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14" }, "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2023-08-31T06:24:48+00:00" + "time": "2026-02-18T12:38:40+00:00" }, { - "name": "phpunit/php-invoker", - "version": "4.0.0", + "name": "predis/predis", + "version": "v3.4.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + "url": "https://github.com/predis/predis.git", + "reference": "2033429520d8997a7815a2485f56abe6d2d0e075" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", - "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "url": "https://api.github.com/repos/predis/predis/zipball/2033429520d8997a7815a2485f56abe6d2d0e075", + "reference": "2033429520d8997a7815a2485f56abe6d2d0e075", "shasum": "" }, "require": { - "php": ">=8.1" + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.0|^2.0" }, "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^10.0" + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", + "phpunit/phpcov": "^6.0 || ^8.0", + "phpunit/phpunit": "^8.0 || ~9.4.4" }, "suggest": { - "ext-pcntl": "*" + "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Predis\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Till Krüss", + "homepage": "https://till.im", + "role": "Maintainer" } ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "description": "A flexible and feature-complete Redis/Valkey client for PHP.", + "homepage": "http://github.com/predis/predis", "keywords": [ - "process" + "nosql", + "predis", + "redis" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v3.4.2" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/sponsors/tillkruss", "type": "github" } ], - "time": "2023-02-03T06:56:09+00:00" + "time": "2026-03-09T20:33:04+00:00" }, { - "name": "phpunit/php-text-template", - "version": "3.0.1", + "name": "revolt/event-loop", + "version": "v1.0.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", - "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/b6fc06dce8e9b523c9946138fa5e62181934f91c", + "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "1.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Revolt\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "description": "Rock-solid event loop for concurrent PHP applications.", "keywords": [ - "template" + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" ], "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/3.0.1" + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.8" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-08-31T14:07:24+00:00" + "time": "2025-08-27T21:33:23+00:00" }, { - "name": "phpunit/php-timer", - "version": "6.0.0", + "name": "sebastian/cli-parser", + "version": "4.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", - "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -5085,81 +6532,67 @@ "role": "lead" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + "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/4.2.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "time": "2023-02-03T06:57:52+00:00" + "time": "2025-09-14T09:36:45+00:00" }, { - "name": "phpunit/phpunit", - "version": "10.5.46", + "name": "sebastian/comparator", + "version": "7.1.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d" + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8080be387a5be380dda48c6f41cee4a13aadab3d", - "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", "shasum": "" }, "require": { "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", - "phar-io/manifest": "^2.0.4", - "phar-io/version": "^3.2.1", - "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.16", - "phpunit/php-file-iterator": "^4.1.0", - "phpunit/php-invoker": "^4.0.0", - "phpunit/php-text-template": "^3.0.1", - "phpunit/php-timer": "^6.0.0", - "sebastian/cli-parser": "^2.0.1", - "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.3", - "sebastian/diff": "^5.1.1", - "sebastian/environment": "^6.1.0", - "sebastian/exporter": "^5.1.2", - "sebastian/global-state": "^6.0.2", - "sebastian/object-enumerator": "^5.0.0", - "sebastian/recursion-context": "^5.0.0", - "sebastian/type": "^4.0.0", - "sebastian/version": "^4.0.1" + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" }, "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files" + "ext-bcmath": "For comparing BcMath\\Number objects" }, - "bin": [ - "phpunit" - ], "type": "library", "extra": { "branch-alias": { - "dev-main": "10.5-dev" + "dev-main": "7.1-dev" } }, "autoload": { - "files": [ - "src/Framework/Assert/Functions.php" - ], "classmap": [ "src/" ] @@ -5171,27 +6604,34 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "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": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ - "phpunit", - "testing", - "xunit" + "comparator", + "compare", + "equality" ], "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.46" + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" }, "funding": [ - { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, { "url": "https://github.com/sebastianbergmann", "type": "github" @@ -5205,98 +6645,95 @@ "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", "type": "tidelift" } ], - "time": "2025-05-02T06:46:24+00:00" + "time": "2026-01-24T09:28:48+00:00" }, { - "name": "predis/predis", - "version": "v2.4.0", + "name": "sebastian/complexity", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/predis/predis.git", - "reference": "f49e13ee3a2a825631562aa0223ac922ec5d058b" + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/f49e13ee3a2a825631562aa0223ac922ec5d058b", - "reference": "f49e13ee3a2a825631562aa0223ac922ec5d058b", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.3", - "phpstan/phpstan": "^1.9", - "phpunit/phpcov": "^6.0 || ^8.0", - "phpunit/phpunit": "^8.0 || ^9.4" - }, - "suggest": { - "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" + "phpunit/phpunit": "^12.0" }, "type": "library", - "autoload": { - "psr-4": { - "Predis\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Till Krüss", - "homepage": "https://till.im", - "role": "Maintainer" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "A flexible and feature-complete Redis/Valkey client for PHP.", - "homepage": "http://github.com/predis/predis", - "keywords": [ - "nosql", - "predis", - "redis" - ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", "support": { - "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v2.4.0" + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" }, "funding": [ { - "url": "https://github.com/sponsors/tillkruss", + "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2025-04-30T15:16:02+00:00" + "time": "2025-02-07T04:55:25+00:00" }, { - "name": "sebastian/cli-parser", - "version": "2.0.1", + "name": "sebastian/diff", + "version": "7.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", - "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -5311,16 +6748,25 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], "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/2.0.1" + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" }, "funding": [ { @@ -5328,32 +6774,35 @@ "type": "github" } ], - "time": "2024-03-02T07:12:49+00:00" + "time": "2025-02-07T04:55:46+00:00" }, { - "name": "sebastian/code-unit", - "version": "2.0.0", + "name": "sebastian/environment", + "version": "8.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", - "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68", + "reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -5368,48 +6817,67 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.3" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2023-02-03T06:58:43+00:00" + "time": "2025-08-12T14:11:56+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "3.0.0", + "name": "sebastian/exporter", + "version": "7.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", - "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", "shasum": "" }, "require": { - "php": ">=8.1" + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -5425,50 +6893,82 @@ { "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": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "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/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2023-02-03T06:59:15+00:00" + "time": "2025-09-24T06:16:11+00:00" }, { - "name": "sebastian/comparator", - "version": "5.0.3", + "name": "sebastian/global-state", + "version": "8.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-mbstring": "*", - "php": ">=8.1", - "sebastian/diff": "^5.0", - "sebastian/exporter": "^5.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^10.5" + "ext-dom": "*", + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -5484,65 +6984,63 @@ { "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", + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ - "comparator", - "compare", - "equality" + "global state" ], "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + "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/8.0.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-10-18T14:56:07+00:00" + "time": "2025-08-29T11:29:25+00:00" }, { - "name": "sebastian/complexity", - "version": "3.2.0", + "name": "sebastian/lines-of-code", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "68ff824baeae169ec9f2137158ee529584553799" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", - "reference": "68ff824baeae169ec9f2137158ee529584553799", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=8.1" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.2-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -5561,12 +7059,12 @@ "role": "lead" } ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", + "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/complexity/issues", - "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + "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/4.0.0" }, "funding": [ { @@ -5574,33 +7072,34 @@ "type": "github" } ], - "time": "2023-12-21T08:37:17+00:00" + "time": "2025-02-07T04:57:28+00:00" }, { - "name": "sebastian/diff", - "version": "5.1.1", + "name": "sebastian/object-enumerator", + "version": "7.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", - "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^10.0", - "symfony/process": "^6.4" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -5616,24 +7115,14 @@ { "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" - ], + "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/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + "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/7.0.0" }, "funding": [ { @@ -5641,35 +7130,32 @@ "type": "github" } ], - "time": "2024-03-02T07:15:17+00:00" + "time": "2025-02-07T04:57:48+00:00" }, { - "name": "sebastian/environment", - "version": "6.1.0", + "name": "sebastian/object-reflector", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", - "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" - }, - "suggest": { - "ext-posix": "*" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -5687,17 +7173,12 @@ "email": "sebastian@phpunit.de" } ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "https://github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], + "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/environment/issues", - "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + "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/5.0.0" }, "funding": [ { @@ -5705,34 +7186,32 @@ "type": "github" } ], - "time": "2024-03-23T08:47:14+00:00" + "time": "2025-02-07T04:58:17+00:00" }, { - "name": "sebastian/exporter", - "version": "5.1.2", + "name": "sebastian/recursion-context", + "version": "7.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": ">=8.1", - "sebastian/recursion-context": "^5.0" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.1-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -5753,60 +7232,57 @@ "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" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + "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/7.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2024-03-02T07:17:12+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { - "name": "sebastian/global-state", - "version": "6.0.2", + "name": "sebastian/type", + "version": "6.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", - "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", "shasum": "" }, "require": { - "php": ">=8.1", - "sebastian/object-reflector": "^3.0", - "sebastian/recursion-context": "^5.0" + "php": ">=8.3" }, "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { @@ -5826,52 +7302,58 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Snapshotting of global state", - "homepage": "https://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], + "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/global-state/issues", - "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2024-03-02T07:19:19+00:00" + "time": "2025-08-09T06:57:12+00:00" }, { - "name": "sebastian/lines-of-code", - "version": "2.0.2", + "name": "sebastian/version", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", - "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=8.1" - }, - "require-dev": { - "phpunit/phpunit": "^10.0" + "php": ">=8.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -5890,12 +7372,12 @@ "role": "lead" } ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "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/lines-of-code/issues", - "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" }, "funding": [ { @@ -5903,540 +7385,660 @@ "type": "github" } ], - "time": "2023-12-21T08:38:20+00:00" + "time": "2025-02-07T05:00:38+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "5.0.0", + "name": "simplesamlphp/simplesamlphp-test-framework", + "version": "v1.11.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + "url": "https://github.com/simplesamlphp/simplesamlphp-test-framework.git", + "reference": "ed52c9077af1965c832c749e7ed062408722f6f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", - "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp-test-framework/zipball/ed52c9077af1965c832c749e7ed062408722f6f7", + "reference": "ed52c9077af1965c832c749e7ed062408722f6f7", "shasum": "" }, "require": { - "php": ">=8.1", - "sebastian/object-reflector": "^3.0", - "sebastian/recursion-context": "^5.0" - }, - "require-dev": { - "phpunit/phpunit": "^10.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } + "ext-curl": "*", + "icanhazstring/composer-unused": "^0.9", + "maglnet/composer-require-checker": "^4.19", + "php": "^8.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^12.3 || ^13.0", + "psr/log": "^3.0", + "simplesamlphp/simplesamlphp": "^2.5", + "slevomat/coding-standard": "^8.25", + "squizlabs/php_codesniffer": "^4.0", + "symfony/phpunit-bridge": "^8.0", + "twig/twig": "~3.23" }, + "type": "project", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "SimpleSAML\\TestUtils\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "LGPL-2.1-or-later" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Tim van Dijen", + "email": "tvdijen@gmail.com" } ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "description": "Test framework for SimpleSAMLphp and related repositories ", + "keywords": [ + "test-framework" + ], "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + "issues": "https://github.com/simplesamlphp/simplesamlphp-test-framework/issues", + "source": "https://github.com/simplesamlphp/simplesamlphp-test-framework" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-03T07:08:32+00:00" + "time": "2026-03-14T20:43:16+00:00" }, { - "name": "sebastian/object-reflector", - "version": "3.0.0", + "name": "slevomat/coding-standard", + "version": "8.28.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "0cd4b30cc1037eca54091c188d260d570e61770c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", - "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/0cd4b30cc1037eca54091c188d260d570e61770c", + "reference": "0cd4b30cc1037eca54091c188d260d570e61770c", "shasum": "" }, "require": { - "php": ">=8.1" + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.2.0", + "php": "^7.4 || ^8.0", + "phpstan/phpdoc-parser": "^2.3.2", + "squizlabs/php_codesniffer": "^4.0.1" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phing/phing": "3.0.1|3.1.2", + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/phpstan": "2.1.40", + "phpstan/phpstan-deprecation-rules": "2.0.4", + "phpstan/phpstan-phpunit": "2.0.16", + "phpstan/phpstan-strict-rules": "2.0.10", + "phpunit/phpunit": "9.6.34|10.5.63|11.4.4|11.5.50|12.5.14" }, - "type": "library", + "type": "phpcodesniffer-standard", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-master": "8.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" ], - "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", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.28.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/kukulich", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" } ], - "time": "2023-02-03T07:06:18+00:00" + "time": "2026-02-23T21:35:24+00:00" }, { - "name": "sebastian/recursion-context", - "version": "5.0.0", + "name": "squizlabs/php_codesniffer", + "version": "4.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0525c73950de35ded110cffafb9892946d7771b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5", + "reference": "0525c73950de35ded110cffafb9892946d7771b5", "shasum": "" }, "require": { - "php": ">=8.1" + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=7.2.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31" }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], "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" + "name": "Greg Sherwood", + "role": "Former lead" }, { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" + "name": "Juliette Reinders Folmer", + "role": "Current lead" }, { - "name": "Adam Harvey", - "email": "aharvey@php.net" + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", + "description": "PHP_CodeSniffer tokenizes PHP 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/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + "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/sebastianbergmann", + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2023-02-03T07:05:40+00:00" + "time": "2025-11-10T16:43:36+00:00" }, { - "name": "sebastian/type", - "version": "4.0.0", + "name": "staabm/side-effects-detector", + "version": "1.0.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", - "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", "shasum": "" }, "require": { - "php": ">=8.1" + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "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", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, "autoload": { "classmap": [ - "src/" + "lib/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" ], - "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", - "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + "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/sebastianbergmann", + "url": "https://github.com/staabm", "type": "github" } ], - "time": "2023-02-03T07:10:45+00:00" + "time": "2024-10-20T05:08:20+00:00" }, { - "name": "sebastian/version", - "version": "4.0.1", + "name": "symfony/phpunit-bridge", + "version": "v8.0.7", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + "url": "https://github.com/symfony/phpunit-bridge.git", + "reference": "f95d88d54e34b13ee220a81133261a3c8a6a287a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", - "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/f95d88d54e34b13ee220a81133261a3c8a6a287a", + "reference": "f95d88d54e34b13ee220a81133261a3c8a6a287a", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.1.0" }, - "type": "library", + "require-dev": { + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4.3|^7.0.3|^8.0" + }, + "bin": [ + "bin/simple-phpunit" + ], + "type": "symfony-bridge", "extra": { - "branch-alias": { - "dev-main": "4.0-dev" + "thanks": { + "url": "https://github.com/sebastianbergmann/phpunit", + "name": "phpunit/phpunit" } }, "autoload": { - "classmap": [ - "src/" + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Bridge\\PhpUnit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/bin/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", + "description": "Provides utilities for PHPUnit, especially user deprecation notices management", + "homepage": "https://symfony.com", + "keywords": [ + "testing" + ], "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + "source": "https://github.com/symfony/phpunit-bridge/tree/v8.0.7" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2023-02-07T11:34:05+00:00" + "time": "2026-03-04T13:55:34+00:00" }, { - "name": "simplesamlphp/simplesamlphp-test-framework", - "version": "v1.9.2", + "name": "symfony/polyfill-php84", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/simplesamlphp/simplesamlphp-test-framework.git", - "reference": "b8506d7b59c658ff4bb9bdd301bf24d1635019d1" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp-test-framework/zipball/b8506d7b59c658ff4bb9bdd301bf24d1635019d1", - "reference": "b8506d7b59c658ff4bb9bdd301bf24d1635019d1", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", "shasum": "" }, "require": { - "ext-curl": "*", - "php": "^8.1", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-mockery": "^1.1", - "phpunit/phpunit": "^10.0 || ^11.0", - "psr/log": "^2.0 || ^3.0", - "slevomat/coding-standard": "^8.15", - "symfony/phpunit-bridge": "^6.4 || ^7.0" + "php": ">=7.2" }, - "require-dev": { - "simplesamlphp/simplesamlphp": "^2.2" + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } }, - "type": "project", "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "SimpleSAML\\TestUtils\\": "lib/" - } + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-or-later" + "MIT" ], "authors": [ { - "name": "Tim van Dijen", - "email": "tvdijen@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "Test framework for SimpleSAMLphp and related repositories ", - "keywords": [ - "test-framework" - ], - "support": { - "issues": "https://github.com/simplesamlphp/simplesamlphp-test-framework/issues", - "source": "https://github.com/simplesamlphp/simplesamlphp-test-framework" - }, - "time": "2025-04-07T20:08:55+00:00" + "time": "2025-06-24T13:30:11+00:00" }, { - "name": "slevomat/coding-standard", - "version": "8.19.1", + "name": "symfony/property-access", + "version": "v7.4.4", "source": { "type": "git", - "url": "https://github.com/slevomat/coding-standard.git", - "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220" + "url": "https://github.com/symfony/property-access.git", + "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/458d665acd49009efebd7e0cb385d71ae9ac3220", - "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220", + "url": "https://api.github.com/repos/symfony/property-access/zipball/fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1", + "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", - "php": "^7.4 || ^8.0", - "phpstan/phpdoc-parser": "^2.1.0", - "squizlabs/php_codesniffer": "^3.13.0" + "php": ">=8.2", + "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4" }, "require-dev": { - "phing/phing": "3.0.1", - "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.17", - "phpstan/phpstan-deprecation-rules": "2.0.3", - "phpstan/phpstan-phpunit": "2.0.6", - "phpstan/phpstan-strict-rules": "2.0.4", - "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" - }, - "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-master": "8.x-dev" - } + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0" }, + "type": "library", "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard/" - } + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "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.", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", "keywords": [ - "dev", - "phpcs" + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" ], "support": { - "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.19.1" + "source": "https://github.com/symfony/property-access/tree/v7.4.4" }, "funding": [ { - "url": "https://github.com/kukulich", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-09T17:53:57+00:00" + "time": "2026-01-05T08:47:25+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.13.1", + "name": "symfony/property-info", + "version": "v7.4.7", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "1b71b4dd7e7ef651ac749cea67e513c0c832f4bd" + "url": "https://github.com/symfony/property-info.git", + "reference": "02501d75fd834345da3ecdd8e3200ced39e370f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1b71b4dd7e7ef651ac749cea67e513c0c832f4bd", - "reference": "1b71b4dd7e7ef651ac749cea67e513c0c832f4bd", + "url": "https://api.github.com/repos/symfony/property-info/zipball/02501d75fd834345da3ecdd8e3200ced39e370f8", + "reference": "02501d75fd834345da3ecdd8e3200ced39e370f8", "shasum": "" }, "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0" }, - "bin": [ - "bin/phpcbf", - "bin/phpcs" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Greg Sherwood", - "role": "Former lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "Current lead" + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" }, { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + "name": "Symfony Community", + "homepage": "https://symfony.com/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", + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", "keywords": [ - "phpcs", - "standards", - "static analysis" + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" ], "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" + "source": "https://github.com/symfony/property-info/tree/v7.4.7" }, "funding": [ { - "url": "https://github.com/PHPCSStandards", - "type": "github" + "url": "https://symfony.com/sponsor", + "type": "custom" }, { - "url": "https://github.com/jrfnl", + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" + "url": "https://github.com/nicolas-grekas", + "type": "github" }, { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2025-06-12T15:04:34+00:00" + "time": "2026-03-04T15:53:26+00:00" }, { - "name": "symfony/phpunit-bridge", - "version": "v7.3.0", + "name": "symfony/serializer", + "version": "v7.4.7", "source": { "type": "git", - "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "2eabda563921f21cbce1d1e3247b3c36568905e6" + "url": "https://github.com/symfony/serializer.git", + "reference": "bd395bbc6fabd136a48e1a6f91b09f88b5050b0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/2eabda563921f21cbce1d1e3247b3c36568905e6", - "reference": "2eabda563921f21cbce1d1e3247b3c36568905e6", + "url": "https://api.github.com/repos/symfony/serializer/zipball/bd395bbc6fabd136a48e1a6f91b09f88b5050b0b", + "reference": "bd395bbc6fabd136a48e1a6f91b09f88b5050b0b", "shasum": "" }, "require": { - "php": ">=7.2.5" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php84": "^1.30" }, "conflict": { - "phpunit/phpunit": "<7.5|9.1.2" + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/dependency-injection": "<6.4", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/type-info": "<7.2.5", + "symfony/uid": "<6.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<6.4" }, "require-dev": { - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/error-handler": "^5.4|^6.4|^7.0", - "symfony/polyfill-php81": "^1.27" - }, - "bin": [ - "bin/simple-phpunit" - ], - "type": "symfony-bridge", - "extra": { - "thanks": { - "url": "https://github.com/sebastianbergmann/phpunit", - "name": "phpunit/phpunit" - } + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^7.2|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.2.5|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, + "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Bridge\\PhpUnit\\": "" + "Symfony\\Component\\Serializer\\": "" }, "exclude-from-classmap": [ - "/Tests/", - "/bin/" + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6445,18 +8047,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides utilities for PHPUnit, especially user deprecation notices management", + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v7.3.0" + "source": "https://github.com/symfony/serializer/tree/v7.4.7" }, "funding": [ { @@ -6467,60 +8069,65 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-23T07:26:30+00:00" + "time": "2026-03-06T13:15:18+00:00" }, { "name": "symfony/translation", - "version": "v6.4.22", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "7e3b3b7146c6fab36ddff304a8041174bf6e17ad" + "reference": "1888cf064399868af3784b9e043240f1d89d25ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/7e3b3b7146c6fab36ddff304a8041174bf6e17ad", - "reference": "7e3b3b7146c6fab36ddff304a8041174bf6e17ad", + "url": "https://api.github.com/repos/symfony/translation/zipball/1888cf064399868af3784b9e043240f1d89d25ce", + "reference": "1888cf064399868af3784b9e043240f1d89d25ce", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { - "symfony/config": "<5.4", - "symfony/console": "<5.4", - "symfony/dependency-injection": "<5.4", + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<5.4", + "symfony/http-kernel": "<6.4", "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<5.4", - "symfony/yaml": "<5.4" + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" }, "provide": { "symfony/translation-implementation": "2.3|3.0" }, "require-dev": { - "nikic/php-parser": "^4.18|^5.0", + "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6551,7 +8158,90 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.22" + "source": "https://github.com/symfony/translation/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-17T07:53:42+00:00" + }, + { + "name": "symfony/type-info", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "31f1e40cbf7851c7354281c90eb1b352c4cb8269" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/31f1e40cbf7851c7354281c90eb1b352c4cb8269", + "reference": "31f1e40cbf7851c7354281c90eb1b352c4cb8269", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v7.4.7" }, "funding": [ { @@ -6562,32 +8252,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-05-29T07:06:44+00:00" + "time": "2026-03-04T12:49:16+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "type": "library", "autoload": { @@ -6609,7 +8303,7 @@ "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" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -6617,7 +8311,56 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "webmozart/glob", + "version": "4.7.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/glob.git", + "reference": "8a2842112d6916e61e0e15e316465b611f3abc17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/glob/zipball/8a2842112d6916e61e0e15e316465b611f3abc17", + "reference": "8a2842112d6916e61e0e15e316465b611f3abc17", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "symfony/filesystem": "^5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Glob\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "A PHP implementation of Ant's glob.", + "support": { + "issues": "https://github.com/webmozarts/glob/issues", + "source": "https://github.com/webmozarts/glob/tree/4.7.0" + }, + "time": "2024-03-07T20:33:40+00:00" } ], "aliases": [], @@ -6626,7 +8369,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.1", + "php": "^8.3", "ext-date": "*", "ext-dom": "*", "ext-hash": "*", @@ -6643,5 +8386,5 @@ "ext-curl": "*", "ext-pdo_sqlite": "*" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/authsources.php.dist b/config/authsources.php.dist index cdde34d65b..15dcfc759a 100644 --- a/config/authsources.php.dist +++ b/config/authsources.php.dist @@ -135,7 +135,7 @@ $config = [ /* // This authentication source serves as an example of integration with an // external authentication engine. Take a look at the comment in the beginning - // of modules/exampleauth/lib/Auth/Source/External.php for a description of + // of modules/exampleauth/src/Auth/Source/External.php for a description of // how to adjust it to your own site. 'example-external' => [ 'exampleauth:External', diff --git a/config/config.php.dist b/config/config.php.dist index 362cb17019..077540583a 100644 --- a/config/config.php.dist +++ b/config/config.php.dist @@ -188,6 +188,11 @@ $config = [ * * A possible way to generate a random salt is by running the following command from a unix shell: * LC_ALL=C tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' /dev/null;echo + * + * If you alter this value on a live system then your old admin-password will + * no longer work if you have a hashed password. If you use persistent NameIDs + * they will be different and also subject-id / pairwise-id will change. + * It will also invalidate all sessions. */ 'secretsalt' => 'defaultsecretsalt', @@ -281,7 +286,17 @@ $config = [ * Whenever you change any of these headers, make sure to validate your config by running your * hostname through a security-test like https://en.internet.nl 'headers.security' => [ - 'Content-Security-Policy' => "default-src 'none'; frame-ancestors 'self'; object-src 'none'; script-src 'self'; style-src 'self'; font-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'none'", + 'Content-Security-Policy' => + "default-src 'none'; " . + "frame-ancestors 'self'; " . + "object-src 'none'; " . + "script-src 'self'; " . + "style-src 'self'; " . + "font-src 'self'; " . + "connect-src 'self'; " . + "media-src data:; " . + "img-src 'self' data:; " . + "base-uri 'none'", 'X-Frame-Options' => 'SAMEORIGIN', 'X-Content-Type-Options' => 'nosniff', 'Referrer-Policy' => 'origin-when-cross-origin', @@ -452,6 +467,9 @@ $config = [ ***********************/ /* + * @DEPRECATED: set your proxy using environment variables. + * @see: https://symfony.com/doc/current/http_client.html#http-proxies + * * Proxy to use for retrieving URLs. * * Example: @@ -549,24 +567,32 @@ $config = [ ***********/ /* - * Configuration for enabling/disabling modules. By default the 'core', 'admin' and 'saml' modules are enabled. + * Configuration for enabling/disabling modules. By default, the 'core', 'admin' and 'saml' modules are enabled. * * Example: * * 'module.enable' => [ * 'exampleauth' => true, // Setting to TRUE enables. * 'consent' => false, // Setting to FALSE disables. - * 'core' => null, // Unset or NULL uses default. + * 'core' => null, // Unset or NULL uses default from \SimpleSAML\Module::$core_modules. * ], */ - 'module.enable' => [ - 'exampleauth' => false, 'core' => true, 'admin' => true, 'saml' => true, ], + /************************************* + | POST/REDIRECT UI CONFIGURATION | + *************************************/ + + /* + * Delay (in milliseconds) before revealing the slow-post message on the auto-submit POST page. + * If unset, defaults to 30000 ms. Negative values are coerced to 30000. + * Set to 0 to reveal immediately. + */ + //'slow_post_delay_ms' => 30000, /************************* | SESSION CONFIGURATION | diff --git a/docs/index.md b/docs/index.md index b1ce371d7a..fe3731f20f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,6 +22,7 @@ * [Identity Provider Advanced Topics](simplesamlphp-idp-more) * [Holder-of-Key profile](simplesamlphp-hok-idp) * Further topics + * [Testing your installation](simplesamlphp-testing-install) - Test out your new installation * [Maintenance and configuration](simplesamlphp-maintenance) - covers session handling, php configuration etc. * [Automated Metadata Management](/docs/contrib_modules/metarefresh/simplesamlphp-automated_metadata) * [Key rollover](./saml:keyrollover) diff --git a/docs/simplesamlphp-advancedfeatures.md b/docs/simplesamlphp-advancedfeatures.md index 47430e9878..3b00fc13d9 100644 --- a/docs/simplesamlphp-advancedfeatures.md +++ b/docs/simplesamlphp-advancedfeatures.md @@ -116,23 +116,23 @@ class CustomCode * @param \SimpleSAML\Session $session The session to approve/reject * @param bool $init true if called during session init. */ - public static function checkSession(\SimpleSAML\Session $session, bool $init = false) + public static function checkSession(\SimpleSAML\Session $session, bool $init = false): bool { $authority = "default-sp"; - if( $init ) { + if ($init) { // init can not fail // return value is ignored return true; } - $ad = $session->getAuthData($authority,"Attributes"); - if( !$ad ) { + $ad = $session->getAuthData($authority, "Attributes"); + if (empty($ad)) { return true; } $uid = $ad["uid"]; - if( in_array("badboy@localhost.localdomain",$uid)) { + if (in_array("badboy@localhost.localdomain", $uid)) { // drop the session return false; } diff --git a/docs/simplesamlphp-authproc.md b/docs/simplesamlphp-authproc.md index 78f2f28fa8..682c459f23 100644 --- a/docs/simplesamlphp-authproc.md +++ b/docs/simplesamlphp-authproc.md @@ -130,7 +130,9 @@ The code must return either `true` to run the filter, or `false` to skip it. The following filters are included in the SimpleSAMLphp distribution: * [`core:AttributeAdd`](./core:authproc_attributeadd): Add attributes to the response. +* [`core:AttributeConditionalAdd`](./core:authproc_attributeconditionaladd): Add attributes to the response if certain conditions are met. * [`core:AttributeCopy`](./core:authproc_attributecopy): Copy existing attributes to the response. +* [`core:AttributeDump`](./core:authproc_attributedump): Dump attribute values to the log file. * [`core:AttributeAlter`](./core:authproc_attributealter): Do search-and-replace on attributevalues. * [`core:AttributeLimit`](./core:authproc_attributelimit): Limit the attributes in the response. * [`core:AttributeMap`](./core:authproc_attributemap): Change the name of the attributes. diff --git a/docs/simplesamlphp-changelog-1.x.md b/docs/simplesamlphp-changelog-1.x.md index d372b377e9..6fad58eb04 100644 --- a/docs/simplesamlphp-changelog-1.x.md +++ b/docs/simplesamlphp-changelog-1.x.md @@ -5,6 +5,24 @@ This document lists the changes between versions of SimpleSAMLphp. See the upgrade notes for specific information about upgrading. +## Version 1.19.8 + +Released 2-3-2023 + +* Dependencies have been bumped (CVE-2022-24894) + +## Version 1.19.7 + +Released 5-12-2022 + +* Backported fix to error report page (#1637) +* Many doc fixes +* Fixed the handling of SAML AttributeQuery and SOAP-binding (#314 @ saml2) +* Fixed serialization of complex AttributeValue structures +* Bump composer + npm dependencies (includes a fix for CVE-2022-39261) +* Many updated translations +* Handle ETag/If-None-Match logic (#1672 + #1673) + ## Version 1.19.6 Released 2022-07-01 diff --git a/docs/simplesamlphp-changelog.md b/docs/simplesamlphp-changelog.md index 76d513690b..1ec7670201 100644 --- a/docs/simplesamlphp-changelog.md +++ b/docs/simplesamlphp-changelog.md @@ -5,6 +5,38 @@ This document lists the changes between versions of SimpleSAMLphp. See the [upgrade notes](https://simplesamlphp.org/docs/stable/simplesamlphp-upgrade-notes.html) for specific information about upgrading. +## Version 2.5.1 + +Released TBD + +## Version 2.5.0 + +Released 2026-03-14 + +* Add ProfileAuth authsource (#2499) +* Ukrainian translation for simplesamlphp (#2501) +* Use PSR-14 events to replace hook infrastructure (#2560) +* Add regular expression support to AttributeValueMap authproc filter (#2558) +* Replaced several uses of guzzlehttp and curl with Symfony's HTTP-client (#2574) +* Many small improvements to code and documentation + +`authorize` + +* Add entityID based filtering (simplesamlphp/simplesamlphp-module-authorize#30) (v1.7.0) + +`discopower` + +* Add Italian translations (simplesamlphp/simplesamlphp-module-discopower#22) (v1.5.0) +* Make tab-names case-insensitive (v1.5.1) + +`ldap` + +* Verify attribute value is a string before calling strlen (simplesamlphp/simplesamlphp-module-ldap#64) (v2.5.2) + +`radius` + +* Always set the NAS identifier; if not set fall back to the hostname (simplesamlphp/simplesamlphp-module-radius#28) (v2.1.1) + ## Version 2.4.3 Released TBD @@ -21,6 +53,7 @@ Released TBD * Fix override over errorURL * Introduced a new assets.salt to allow cache busting without leaking version information (#2490) * If session.check_function is set and can not be called an error is raised (#2498) +* Add 'login_hint' and 'LoginHint' as additional supported username hints from SPs in HTTP parameters (#2609) `authorize` @@ -186,6 +219,48 @@ Released 2024-08-20 * Add username_regex option - run regular expression over username before querying the database (simplesamlphp/simplesamlphp-module-sqlauth#11) (v1.3.0) +## Version 2.2.6 + +Released 2025-08-20 + +* Fix auth state AuthnInstant (#2478) + +`cron` + +* Fixed a security-issue where cron-jobs could be executed using the default key, + even if a different one was set (#2453) + +`metarefresh` + +* Fixed parsing of large metadata files (v1.2.4) + +## Version 2.2.5 + +Released 2025-03-11 + +* Fixed PHP 8.4 deprecation notices +* Fixed infinite recursion (#2367) +* Fixed "Undefined array key" warning in RequestedAuthnContextSelector if no RAC is present in the request +* Bumped vulnerable saml2-library to v4.17.0 + +## Version 2.2.4 + +Released 2024-12-02 + +`Security` + +* A security bug was patched in the `saml2-library` that allowed for XXE during the parsing + of SAML2-messages (CVE-2024-52596) + +`Other fixes` + +* Run ob_end_clean() on configuration file parsing error (#2219) +* Fix typo that caused the metadata file-upload to be hidden (#2271) + +`admin` + +* Fix metadata-viewer to output a valid PHP array + ## Version 2.2.3 Released 2024-08-20 diff --git a/docs/simplesamlphp-customauth.md b/docs/simplesamlphp-customauth.md index 72b336ebb1..a6de7a20de 100644 --- a/docs/simplesamlphp-customauth.md +++ b/docs/simplesamlphp-customauth.md @@ -352,6 +352,7 @@ class MyAuth extends \SimpleSAML\Module\core\Auth\UserPassBase /* Connect to the database. */ $db = new PDO($this->dsn, $this->username, $this->password, $this->options); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); /* Ensure that we are operating with UTF-8 encoding. * This command is for MySQL. Other databases may need different commands. diff --git a/docs/simplesamlphp-ecp-idp.md b/docs/simplesamlphp-ecp-idp.md index e541c0056e..dd0ffa2948 100644 --- a/docs/simplesamlphp-ecp-idp.md +++ b/docs/simplesamlphp-ecp-idp.md @@ -37,11 +37,11 @@ The `saml20-idp-remote` metadata for simpleSAMLphp SPs should contain something 'SingleSignOnService' => [ 0 => [ 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - 'Location' => 'https://idp.example.org/simplesaml/saml2/idp/SSOService.php', + 'Location' => 'https://idp.example.org/simplesaml/module.php/saml/idp/singleSignOnService', ], 1 => [ 'index' => 0, - 'Location' => 'https://didp.example.org/simplesaml/saml2/idp/SSOService.php', + 'Location' => 'https://didp.example.org/simplesaml/module.php/saml/idp/singleSignOnService', 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP', ], ], diff --git a/docs/simplesamlphp-errorhandling.md b/docs/simplesamlphp-errorhandling.md index 99200bc1e9..3a5e59539b 100644 --- a/docs/simplesamlphp-errorhandling.md +++ b/docs/simplesamlphp-errorhandling.md @@ -85,7 +85,7 @@ Throwing SAML 2 errors will tie your code directly to the SAML 2 protocol, and i On the SP side, we want to convert SAML 2 errors to SimpleSAMLphp exceptions again. This is handled by the `toException()` method in `\SimpleSAML\Module\saml\Error`. -The assertion consumer script of the SAML 2 authentication source (`modules/saml2/sp/acs.php`) uses this method. +The assertion consumer service of the SAML 2 authentication source uses this method. The result is that generic exceptions are thrown from that authentication source. For example, `NoPassive` errors will be converted back to instances of `\SAML2\Exception\Protocol\NoPassiveException`. diff --git a/docs/simplesamlphp-googleapps.md b/docs/simplesamlphp-googleapps.md index 7db85832f5..db7adfc947 100644 --- a/docs/simplesamlphp-googleapps.md +++ b/docs/simplesamlphp-googleapps.md @@ -187,7 +187,7 @@ You will find in the metadata the XML tag `` which contains the right URL to input in the field, it will look something like this: -`https://dev2.andreas.feide.no/simplesaml/saml2/idp/SSOService.php` +`https://dev2.andreas.feide.no/simplesaml/module.php/saml/idp/singleSignOnService` The Sign-out page or change password URL can be static pages on your server. (Google does not support SAML Single Log Out.) diff --git a/docs/simplesamlphp-hok-idp.md b/docs/simplesamlphp-hok-idp.md index d11463a08a..527924d42b 100644 --- a/docs/simplesamlphp-hok-idp.md +++ b/docs/simplesamlphp-hok-idp.md @@ -47,11 +47,19 @@ The `saml20-idp-remote` metadata for SimpleSAMLphp SPs should contain something [ 'hoksso:ProtocolBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', 'Binding' => 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser', - 'Location' => 'https://idp.example.org/simplesaml/saml2/idp/SSOService.php', + 'Location' => 'https://idp.example.org/simplesaml/module.php/saml/idp/singleSignOnService', + 'attributes' => [ + [ + 'namespaceURI' => 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser', + 'namespacePrefix' => 'hoksso', + 'attrName' => 'ProtocolBinding', + 'attrValue' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + ], + ], ], [ 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - 'Location' => 'https://idp.example.org/simplesaml/saml2/idp/SSOService.php', + 'Location' => 'https://idp.example.org/simplesaml/module.php/saml/idp/singleSignOnService', ], ], ``` diff --git a/docs/simplesamlphp-idp-more.md b/docs/simplesamlphp-idp-more.md index 28181dd31f..4dadf4ca4f 100644 --- a/docs/simplesamlphp-idp-more.md +++ b/docs/simplesamlphp-idp-more.md @@ -35,11 +35,11 @@ If you do not want to start the SSO flow at the SP, you may use the IdP-first se Here is an example of such a URL: -`https://idp.example.org/simplesaml/saml2/idp/SSOService.php?spentityid=urn:mace:feide.no:someservice` +`https://idp.example.org/simplesaml/module.php/saml/idp/singleSignOnService?spentityid=urn:mace:feide.no:someservice` You can also add a `RelayState` parameter to the IdP-first URL: -`https://idp.example.org/simplesaml/saml2/idp/SSOService.php?spentityid=urn:mace:feide.no:someservice&RelayState=https://sp.example.org/somepage` +`https://idp.example.org/simplesaml/module.php/saml/idp/singleSignOnService?spentityid=urn:mace:feide.no:someservice&RelayState=https://sp.example.org/somepage` The `RelayState` parameter is often used to carry the URL the SP should redirect to after authentication. It is also possible to specify the Assertion Consumer URL with the `ConsumerURL` parameter. diff --git a/docs/simplesamlphp-idp.md b/docs/simplesamlphp-idp.md index 9c7eb9d2e9..b5af78fca3 100644 --- a/docs/simplesamlphp-idp.md +++ b/docs/simplesamlphp-idp.md @@ -232,14 +232,14 @@ If you do not want to start the SSO flow at the SP, you may use the IdP-first se Here is an example of such a URL: -`https://idp.example.org/simplesaml/saml2/idp/SSOService.php?spentityid=sp.example.org` +`https://idp.example.org/simplesaml/module.php/saml/idp/singleSignOnService?spentityid=sp.example.org` If the SP is a SimpleSAMLphp SP, you must also specify a `RelayState` parameter for the SP. This must be set to a URL the user should be redirected to after authentication. The `RelayState` parameter can be specified in the [SP configuration](./saml:sp), or it can be sent from the IdP. To send the RelayState parameter from a SimpleSAMLphp IdP, specify it in the query string to SSOService.php: -`https://idp.example.org/simplesaml/saml2/idp/SSOService.php?spentityid=sp.example.org&RelayState=https://sp.example.org/welcome.php` +`https://idp.example.org/simplesaml/module.php/saml/idp/singleSignOnService?spentityid=sp.example.org&RelayState=https://sp.example.org/welcome.php` To set it in the SP configuration, add it to `authsources.php`: diff --git a/docs/simplesamlphp-install.md b/docs/simplesamlphp-install.md index eba882de27..48fbc1f0ae 100644 --- a/docs/simplesamlphp-install.md +++ b/docs/simplesamlphp-install.md @@ -14,7 +14,7 @@ repository](simplesamlphp-install-repo). ## Prerequisites * A web server capable of executing PHP scripts. -* PHP version >= 8.1.0. +* PHP version >= 8.3.0. * Support for the following PHP extensions: * Always required: `date`, `dom`, `fileinfo`, `filter`, `hash`, `json`, `libxml`, `mbstring`, `openssl`, `pcre`, `session`, `simplexml`, `sodium`, `SPL` and `zlib` @@ -279,6 +279,13 @@ Here is an example of the configuration option: You can see [a list of Supported Timezones at php.net](http://php.net/manual/en/timezones.php). +You might consider setting showerrors to `false` to hide error +descriptions and backtraces from the browser. + +```php +'showerrors' => false, +``` + ## Configuring PHP ### Sending e-mails from PHP @@ -400,7 +407,7 @@ As an example, let's see how you can install SimpleSAMLphp in your home director just created in your `public_html` directory. For example, if your home directory is reachable in `https://host.example/~myaccount/`, set the base URL path accordingly: - ```bash + ```php 'baseurlpath' => 'https://host.example/~myaccount/simplesaml/', ``` @@ -430,13 +437,13 @@ Now, we need to make a few configuration changes. First, let's edit `~/public_ht Change the two lines from: ```php -require_once(dirname(_FILE__, 2) . '/lib/_autoload.php'); +require_once(dirname(_FILE__, 2) . '/src/_autoload.php'); ``` to something like: ```php -require_once(dirname(__FILE__, 3) . '/lib/_autoload.php'); +require_once(dirname(__FILE__, 3) . '/src/_autoload.php'); ``` **Warning**: note that this will make upgrading SimpleSAMLphp much more difficult, since you will need to move the diff --git a/docs/simplesamlphp-metadata-extensions-rpi.md b/docs/simplesamlphp-metadata-extensions-rpi.md index 148e53f4fe..a2fa17f01d 100644 --- a/docs/simplesamlphp-metadata-extensions-rpi.md +++ b/docs/simplesamlphp-metadata-extensions-rpi.md @@ -30,16 +30,16 @@ RegistrationInfo Items The configuration is the same for all the different files, and consists of a single directive called `RegistrationInfo`, which **must** be an indexed array with the following options: -`authority` +`RegistrationAuthority` : A string containing an identifier of the authority who has registered this metadata. This parameter is **mandatory**. -`instant` +`RegistrationInstant` : A string containing the instant when the entity or entities where registered by the authority. This parameter is optional, and must be expressed in the UTC timezone with the *zulu* (`Z`) timezone identifier. If omitted, there will be no `registrationInstant` in the resulting metadata, except in the `aggregator2` module, which will use the instant when the metadata was generated. -`policies` +`RegistrationPolicy` : An indexed array containing URLs pointing to the policy under which the entity or entities where registered. Each index must be the language code corresponding to the language of the URL. This parameter is optional, and will be omitted in the resulting metadata if not configured. @@ -54,9 +54,9 @@ Service Provider: 'entityID' => NULL, ... 'RegistrationInfo' => [ - 'authority' => 'urn:mace:sp.example.org', - 'instant' => '2008-01-17T11:28:03.577Z', - 'policies' => ['en' => 'http://sp.example.org/policy', 'es' => 'http://sp.example.org/politica'], + 'RegistrationAuthority' => 'urn:mace:sp.example.org', + 'RegistrationInstant' => '2008-01-17T11:28:03.577Z', + 'RegistrationPolicy' => ['en' => 'http://sp.example.org/policy', 'es' => 'http://sp.example.org/politica'], ], ], @@ -66,8 +66,8 @@ Identity Provider: 'host' => '__DEFAULT__', ... 'RegistrationInfo' => [ - 'authority' => 'urn:mace:idp.example.org', - 'instant' => '2008-01-17T11:28:03.577Z', + 'RegistrationAuthority' => 'urn:mace:idp.example.org', + 'RegistrationInstant' => '2008-01-17T11:28:03.577Z', ], ]; @@ -79,8 +79,8 @@ Identity Provider: ... ], 'RegistrationInfo' => [ - 'authority' => 'urn:mace:example.federation', - 'policies' => ['en' => 'http://example.org/federation_policy', 'es' => 'https://example.org/politica_federacion'], + 'RegistrationAuthority' => 'urn:mace:example.federation', + 'RegistrationPolicy' => ['en' => 'http://example.org/federation_policy', 'es' => 'https://example.org/politica_federacion'], ], ], ]; diff --git a/docs/simplesamlphp-modules.md b/docs/simplesamlphp-modules.md index 1271b0ee46..0a1c4ef745 100644 --- a/docs/simplesamlphp-modules.md +++ b/docs/simplesamlphp-modules.md @@ -185,7 +185,128 @@ replaces `modules/core/templates/default/loginuserpass.php`. `templates/default/frontpage.php`. This theme can be activated by setting `theme.use` to `example:test`. -## Hook interface +## Event listeners (new Hooks) + +In SimpleSAMLphp 2.5 the hook interface was updated to use PSR-14 +events [PSR-14 events](https://www.php-fig.org/psr/psr-14) as well as +the legacy custom "hook" interface. When hooks are called in 2.5 both +the new PSR-14 event listeners are called followed by the older custom +hook. You are likely better to create new event listeners in new +configurations and might like to consider moving your hooks to event +listeners over time. + +If you are using an older version of SimpleSAMLphp please see the +below section for information on how to use the older interface. + +This section discusses how to make new event listeners with an +eye toward moving an existing hook to become an event listener. + +The PSR events that can be used are defined in your module's +src/Event subdirectory. For example, for the admin module you can +see modules/admin/src/Event/ConfigPageEvent.php which defines +a class ConfigPageEvent in the `SimpleSAML\Module\admin\Event` namespace. + +That event takes some state in the constructor and likely has one or +more access methods to later retrieve that state. + +The event might be listened to by the same or another module by +defining a `Listener` class. In the running concrete example the class +in modules/cron/src/Event/Listener/ConfigPageListener.php defines a +`ConfigPageListener` class in the +`SimpleSAML\Module\cron\Event\Listener` namespace. That listener class +has an `__invoke` method that will be passed the `ConfigPageEvent` +object and has no return value. + +The `ConfigPageEvent` is a reasonable starting point for testing your +own listener. If you create a new listener for ConfigPageEvent you can +use `Logger::error` to check that you get called and use the +admin/config page through the admin module to trigger this event +fairly easily. You might like to base your new listener on the +ConfigPageListener for the cron module. + +To update a hook from a 2.4 release or below you will need to move +your hook class from the `hooks` directory to the `src/Event/Listener` +subdirectory. Considering the config page example again, the +`hooks/hook_configpage.php` file should move to `src/Event/Listener`. +That hook defined a single top level function with a specific function +name `cron_hook_configpage`. In the event listener the new +`src/Event/Listener/ConfigPageListener.php` file will need to be in +the `SimpleSAML\Module\cron\Event\Listener` namespace and define a +class `ConfigPageListener`. The single top level function is moved +into a member function `__invoke` taking a single parameter +`ConfigPageEvent`. + +The old hook function took a reference `Template &$template` directly +as the single argument. The new PSR class takes a `ConfigPageEvent` +object. The ConfigPageEvent object provides access to the template +using the `getTemplate` method on the event. + +The old hook function in `hooks` might look like the following. + + // old hook interface + function cron_hook_configpage(Template &$template): void + { + $template->data['links'][] = [ + 'href' => Module::getModuleURL('cron/info'), + 'text' => Translate::noop('Cron module information page'), + ]; + ... + } + +The new Listener version in `src/Event/Listener` is shown below. + + class ConfigPageListener + { + public function __invoke(ConfigPageEvent $event): void + { + $template = $event->getTemplate(); + + $template->data['links'][] = [ + 'href' => \SimpleSAML\Module::getModuleURL('cron/info'), + 'text' => \SimpleSAML\Locale\Translate::noop('Cron module information page'), + ]; + ... + } + } + +That `ConfigPageEvent` class is defined in +`modules/admin/src/Event/ConfigPageEvent.php`. + + class ConfigPageEvent + { + public function __construct( + private readonly XHTML\Template $template, + ) + {} + + public function getTemplate(): XHTML\Template + { + return $this->template; + } + } + +In summary to move a hook to the new PSR-14 events you have to move +the hook file to your module's src/Event/Listener directory and have +it take a new Event object that passes the function argument to it. +The event will be a simple class defined in your `src/Event` +directory. + +When you wish to call event listeners in your code you can use code +like the following. The event dispatcher is passed an event which +contains all of the state you wish to make available to the event +listeners. You can then get the state which may have been updated by +one or more listeners using a getter method and do something with the +result. + + $eventDispatcher = ModuleEventDispatcherFactory::getInstance(); + $event = $eventDispatcher->dispatch(new ConfigPageEvent($t)); + $t = $event->getTemplate(); + +## Hook interface (SimpleSAMLphp 2.4 and below) + +Releases 2.4 and below use a custom hook interface to allow code to +run when specific things of interest are happening. If you are using +SimpleSAMLphp 2.5 or above please see the above section on Event listeners instead. The hook interface allows you to call a hook function in all enabled modules which define that hook. Hook functions are stored in a diff --git a/docs/simplesamlphp-reference-idp-hosted.md b/docs/simplesamlphp-reference-idp-hosted.md index 9fc0628d92..e21c52a49a 100644 --- a/docs/simplesamlphp-reference-idp-hosted.md +++ b/docs/simplesamlphp-reference-idp-hosted.md @@ -60,12 +60,12 @@ in the advanced features page. ```php 'contacts' => [ [ - 'contactType' => 'support', - 'emailAddress' => 'support@example.org', - 'givenName' => 'John', - 'surName' => 'Doe', - 'telephoneNumber' => '+31(0)12345678', - 'company' => 'Example Inc.', + 'ContactType' => 'support', + 'EmailAddress' => 'support@example.org', + 'GivenName' => 'John', + 'SurName' => 'Doe', + 'TelephoneNumber' => '+31(0)12345678', + 'Company' => 'Example Inc.', ], ], ``` @@ -75,15 +75,19 @@ in the advanced features page. ```php 'contacts' => [ [ - 'contactType' => 'other', - 'emailAddress' => 'mailto:abuse@example.org', - 'givenName' => 'John', - 'surName' => 'Doe', - 'telephoneNumber' => '+31(0)12345678', - 'company' => 'Example Inc.', - 'attributes' => [ - 'xmlns:remd' => 'http://refeds.org/metadata', - 'remd:contactType' => 'http://refeds.org/metadata/contactType/security', + 'ContactType' => 'other', + 'EmailAddress' => 'mailto:abuse@example.org', + 'GivenName' => 'John', + 'SurName' => 'Doe', + 'TelephoneNumber' => '+31(0)12345678', + 'Company' => 'Example Inc.', + 'attributes' => [ + [ + 'namespaceURI' => 'http://refeds.org/metadata', + 'namespacePrefix' => 'remd', + 'attrName' => 'contactType', + 'attrValue' => 'http://refeds.org/metadata/contactType/security', + ], ], ], ], @@ -399,7 +403,7 @@ See the documentation for those extensions for more details: For other metadata extensions, you can use the `saml:Extensions` option: `saml:Extensions` -: An array of `\SAML2\XML\Chunk`s to include in the IdP metadata extensions, at the same level as `EntityAttributes`. +: An array of `\SimpleSAML\XML\Chunk`s to include in the IdP metadata extensions, at the same level as `EntityAttributes`. `Examples`: @@ -434,11 +438,11 @@ $metadata['https://example.org/saml-idp'] = [ ```php createElementNS('http://eduid.cz/schema/metadata/1.0', 'eduidmd:RepublishRequest'); $republishTarget = $dom->createElementNS('http://eduid.cz/schema/metadata/1.0', 'eduidmd:RepublishTarget', 'http://edugain.org/'); $republishRequest->appendChild($republishTarget); -$ext = [new \SAML2\XML\Chunk($republishRequest)]; +$ext = [new \SimpleSAML\XML\Chunk($republishRequest)]; $metadata['https://example.org/saml-idp'] = [ 'host' => '__DEFAULT__', diff --git a/docs/simplesamlphp-reference-sp-remote.md b/docs/simplesamlphp-reference-sp-remote.md index 593b8d0c8e..a2d3d44f1b 100644 --- a/docs/simplesamlphp-reference-sp-remote.md +++ b/docs/simplesamlphp-reference-sp-remote.md @@ -110,7 +110,7 @@ The following options can be set: the actual value used is fetched from metadata by the following priority: -: +: 1. SP Remote Metadata 2. IdP Hosted Metadata diff --git a/docs/simplesamlphp-sp.md b/docs/simplesamlphp-sp.md index 99b93e2256..1e9f0a1958 100644 --- a/docs/simplesamlphp-sp.md +++ b/docs/simplesamlphp-sp.md @@ -92,13 +92,13 @@ metadata file: $metadata['https://example.org/saml-idp'] = [ 'SingleSignOnService' => [ [ - 'Location' => 'https://example.org/simplesaml/saml2/idp/SSOService.php', + 'Location' => 'https://example.org/simplesaml/module.php/saml/idp/singleSignOnService', 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', ], ], 'SingleLogoutService' => [ [ - 'Location' => 'https://example.org/simplesaml/saml2/idp/SingleLogoutService.php', + 'Location' => 'https://example.org/simplesaml/module.php/saml/idp/singleLogout', 'ResponseLocation' => 'https://sp.example.org/LogoutResponse', 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', ], diff --git a/docs/simplesamlphp-testing-install.md b/docs/simplesamlphp-testing-install.md new file mode 100644 index 0000000000..afa893b848 --- /dev/null +++ b/docs/simplesamlphp-testing-install.md @@ -0,0 +1,67 @@ +# SimpleSAMLphp Testing installation + +[TOC] + +## Testing a SimpleSAMLphp installation + +The admin interface available as `/module.php/admin/` on your site +allows for testing logins to the authentication sources available on +your installation. + +You can also see configuration including which modules are enabled and +what extensions are available in your PHP environment. + +To see the admin page you should set a password in the adminpassword +configuration directive (the config.php.dist has details of this +process). Also please check that the admin module must be enabled. + +The relevant parts of config.php: + +```php + 'auth.adminpassword' => '123', + ... + 'module.enable' => [ + 'admin' => true, + ... +``` + +## Testing various user profiles + +The ProfileAuth authentication source can be used to test login as +various users by simply clicking on the user. Note that this testing +method allows for login without password so should be disabled in any +non testing environment. + +The ProfileAuth functionality can be accessed through the path +`/module.php/admin/test` page using for example a `profileauth` +authentication source with the following `config.php` fragment. + +The module must be enabled (and admin to get to it easily) and a small +configuration to show what users you wish to "click to authenticate" +as. If there is only a single item in the users array then you will +not have to click and will be authenticated as that user when you +visit the authentication source. + +```php + 'module.enable' => [ + 'admin' => true, + 'exampleauth' => true, + ... + ], + + 'profileauth' => [ + 'exampleauth:UserClick', + 'users' => [ + [ + 'uid' => ['student'], + 'displayName' => ['Student'], + 'eduPersonAffiliation' => ['student', 'member'], + ], + [ + 'uid' => ['employee'], + 'displayName' => ['Employee'], + 'eduPersonAffiliation' => ['employee', 'member'], + ], + ], + ], +``` diff --git a/docs/simplesamlphp-upgrade-notes-2.0.md b/docs/simplesamlphp-upgrade-notes-2.0.md index f03a29538e..8356ffc274 100644 --- a/docs/simplesamlphp-upgrade-notes-2.0.md +++ b/docs/simplesamlphp-upgrade-notes-2.0.md @@ -27,6 +27,8 @@ composer require simplesamlphp/simplesamlphp-module-ldap --update-no-dev If you are using a database to store metadata, make sure to replace any `__DYNAMIC:__` entityID's with a real value manually. Dynamic records are no longer loaded from the database. See the "Upgrading and EntityIDs" section at the end of the document for more information. +- EntityIDs are now checked for validity in accordance to SAML 2.0 Core specification, section 8.3.6 Entity Identifier: + "... The syntax of such an identifier is a URI of not more than 1024 characters in length." - Modules must be enabled through the `module.enable` option in `config.php`. Modules can no longer be enabled by having a file named `enable` or `default-enable` in the module's root directory. - The base URL of the SimpleSAMLphp installation no longer provides an admin menu. Instead this is now at the location diff --git a/docs/simplesamlphp-upgrade-notes-2.5.md b/docs/simplesamlphp-upgrade-notes-2.5.md new file mode 100644 index 0000000000..6e65c6a7e4 --- /dev/null +++ b/docs/simplesamlphp-upgrade-notes-2.5.md @@ -0,0 +1,27 @@ +# Upgrade notes for SimpleSAMLphp 2.5 + +SimpleSAMLphp 2.5 is a minor new release which introduces a few new features. +The following changes are relevant for installers and/or developers. + +## Software requirements + +- The minimum PHP version required is now PHP 8.3. +- Symfony was upgraded to 7.4 (LTS). + +## General Upgrade Advice + +When updating SimpleSAMLphp you might like to run the following to +remove any cached objects that might have older code signatures to the +new SimpleSAMLphp version. If you encounter a permissions error +running the clear-symfony-cache shown below please see the [the +SimpleSAMLphp installation instructions](simplesamlphp-install) for +information about how to update the filesystem permissions. + +```sh +composer clear-symfony-cache +``` + +The above command is particularly useful if you see a message after +your update about "has required constructor arguments and does not +exist in the container" or "Did you forget to define the controller as +a service?" as these error messages might indicate a stale cache. diff --git a/extra/simplesamlphp.spec b/extra/simplesamlphp.spec index 215acd2ecd..470a87b9a0 100644 --- a/extra/simplesamlphp.spec +++ b/extra/simplesamlphp.spec @@ -1,6 +1,6 @@ %define name simplesamlphp %define summary SAML IDP/SP written in PHP -%define version 2.4.2 +%define version 2.5.0 %define release 1 %define license LGPL 2.1 %define group Networking/WWW diff --git a/locales/uk/LC_MESSAGES/attributes.po b/locales/uk/LC_MESSAGES/attributes.po new file mode 100644 index 0000000000..4599dafbb3 --- /dev/null +++ b/locales/uk/LC_MESSAGES/attributes.po @@ -0,0 +1,2061 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2);\n" +"X-Generator: Poedit 3.7\n" + +msgid "aRecord" +msgstr "aRecord" + +msgid "urn:mace:dir:attribute-def:aRecord" +msgstr "urn:mace:dir:attribute-def:aRecord" + +msgid "urn:oid:0.9.2342.19200300.100.1.26" +msgstr "urn:oid:0.9.2342.19200300.100.1.26" + +msgid "aliasedEntryName" +msgstr "aliasedEntryName" + +msgid "urn:mace:dir:attribute-def:aliasedEntryName" +msgstr "urn:mace:dir:attribute-def:aliasedEntryName" + +msgid "urn:oid:2.5.4.1" +msgstr "urn:oid:2.5.4.1" + +msgid "aliasedObjectName" +msgstr "aliasedObjectName" + +msgid "urn:mace:dir:attribute-def:aliasedObjectName" +msgstr "urn:mace:dir:attribute-def:aliasedObjectName" + +msgid "associatedDomain" +msgstr "associatedDomain" + +msgid "urn:mace:dir:attribute-def:associatedDomain" +msgstr "urn:mace:dir:attribute-def:associatedDomain" + +msgid "urn:oid:0.9.2342.19200300.100.1.37" +msgstr "urn:oid:0.9.2342.19200300.100.1.37" + +msgid "associatedName" +msgstr "associatedName" + +msgid "urn:mace:dir:attribute-def:associatedName" +msgstr "urn:mace:dir:attribute-def:associatedName" + +msgid "urn:oid:0.9.2342.19200300.100.1.38" +msgstr "urn:oid:0.9.2342.19200300.100.1.38" + +msgid "audio" +msgstr "audio" + +msgid "urn:mace:dir:attribute-def:audio" +msgstr "urn:mace:dir:attribute-def:audio" + +msgid "urn:oid:0.9.2342.19200300.100.1.55" +msgstr "urn:oid:0.9.2342.19200300.100.1.55" + +msgid "authorityRevocationList" +msgstr "authorityRevocationList" + +msgid "urn:mace:dir:attribute-def:authorityRevocationList" +msgstr "urn:mace:dir:attribute-def:authorityRevocationList" + +msgid "urn:oid:2.5.4.38" +msgstr "urn:oid:2.5.4.38" + +msgid "buildingName" +msgstr "buildingName" + +msgid "urn:mace:dir:attribute-def:buildingName" +msgstr "urn:mace:dir:attribute-def:buildingName" + +msgid "urn:oid:0.9.2342.19200300.100.1.48" +msgstr "urn:oid:0.9.2342.19200300.100.1.48" + +msgid "businessCategory" +msgstr "businessCategory" + +msgid "urn:mace:dir:attribute-def:businessCategory" +msgstr "urn:mace:dir:attribute-def:businessCategory" + +msgid "urn:oid:2.5.4.15" +msgstr "urn:oid:2.5.4.15" + +msgid "c" +msgstr "c" + +msgid "urn:mace:dir:attribute-def:c" +msgstr "urn:mace:dir:attribute-def:c" + +msgid "urn:oid:2.5.4.6" +msgstr "urn:oid:2.5.4.6" + +msgid "cACertificate" +msgstr "cACertificate" + +msgid "urn:mace:dir:attribute-def:cACertificate" +msgstr "urn:mace:dir:attribute-def:cACertificate" + +msgid "urn:oid:2.5.4.37" +msgstr "urn:oid:2.5.4.37" + +msgid "cNAMERecord" +msgstr "cNAMERecord" + +msgid "urn:mace:dir:attribute-def:cNAMERecord" +msgstr "urn:mace:dir:attribute-def:cNAMERecord" + +msgid "urn:oid:0.9.2342.19200300.100.1.31" +msgstr "urn:oid:0.9.2342.19200300.100.1.31" + +msgid "carLicense" +msgstr "carLicense" + +msgid "urn:mace:dir:attribute-def:carLicense" +msgstr "urn:mace:dir:attribute-def:carLicense" + +msgid "urn:oid:2.16.840.1.113730.3.1.1" +msgstr "urn:oid:2.16.840.1.113730.3.1.1" + +msgid "certificateRevocationList" +msgstr "certificateRevocationList" + +msgid "urn:mace:dir:attribute-def:certificateRevocationList" +msgstr "urn:mace:dir:attribute-def:certificateRevocationList" + +msgid "urn:oid:2.5.4.39" +msgstr "urn:oid:2.5.4.39" + +# English string: Common name +msgid "cn" +msgstr "Повне ім'я" + +# English string: Common name +msgid "urn:mace:dir:attribute-def:cn" +msgstr "Повне ім'я" + +# English string: Common name +msgid "urn:oid:2.5.4.3" +msgstr "Повне ім'я" + +msgid "co" +msgstr "co" + +msgid "urn:mace:dir:attribute-def:co" +msgstr "urn:mace:dir:attribute-def:co" + +msgid "urn:oid:0.9.2342.19200300.100.1.43" +msgstr "urn:oid:0.9.2342.19200300.100.1.43" + +msgid "commonName" +msgstr "commonName" + +msgid "urn:mace:dir:attribute-def:commonName" +msgstr "urn:mace:dir:attribute-def:commonName" + +msgid "countryName" +msgstr "countryName" + +msgid "urn:mace:dir:attribute-def:countryName" +msgstr "urn:mace:dir:attribute-def:countryName" + +msgid "crossCertificatePair" +msgstr "crossCertificatePair" + +msgid "urn:mace:dir:attribute-def:crossCertificatePair" +msgstr "urn:mace:dir:attribute-def:crossCertificatePair" + +msgid "urn:oid:2.5.4.40" +msgstr "urn:oid:2.5.4.40" + +msgid "dITRedirect" +msgstr "dITRedirect" + +msgid "urn:mace:dir:attribute-def:dITRedirect" +msgstr "urn:mace:dir:attribute-def:dITRedirect" + +msgid "urn:oid:0.9.2342.19200300.100.1.54" +msgstr "urn:oid:0.9.2342.19200300.100.1.54" + +msgid "dSAQuality" +msgstr "dSAQuality" + +msgid "urn:mace:dir:attribute-def:dSAQuality" +msgstr "urn:mace:dir:attribute-def:dSAQuality" + +msgid "urn:oid:0.9.2342.19200300.100.1.49" +msgstr "urn:oid:0.9.2342.19200300.100.1.49" + +# English string: Domain component (DC) +msgid "dc" +msgstr "Компонент домену (DC)" + +# English string: Domain component (DC) +msgid "urn:mace:dir:attribute-def:dc" +msgstr "Компонент домену (DC)" + +# English string: Domain component (DC) +msgid "urn:oid:0.9.2342.19200300.100.1.25" +msgstr "Компонент домену (DC)" + +msgid "deltaRevocationList" +msgstr "deltaRevocationList" + +msgid "urn:mace:dir:attribute-def:deltaRevocationList" +msgstr "urn:mace:dir:attribute-def:deltaRevocationList" + +msgid "urn:oid:2.5.4.53" +msgstr "urn:oid:2.5.4.53" + +msgid "departmentNumber" +msgstr "departmentNumber" + +msgid "urn:mace:dir:attribute-def:departmentNumber" +msgstr "urn:mace:dir:attribute-def:departmentNumber" + +msgid "urn:oid:2.16.840.1.113730.3.1.2" +msgstr "urn:oid:2.16.840.1.113730.3.1.2" + +msgid "description" +msgstr "description" + +msgid "urn:mace:dir:attribute-def:description" +msgstr "urn:mace:dir:attribute-def:description" + +msgid "urn:oid:2.5.4.13" +msgstr "urn:oid:2.5.4.13" + +msgid "destinationIndicator" +msgstr "destinationIndicator" + +msgid "urn:mace:dir:attribute-def:destinationIndicator" +msgstr "urn:mace:dir:attribute-def:destinationIndicator" + +msgid "urn:oid:2.5.4.27" +msgstr "urn:oid:2.5.4.27" + +# English string: Display name +msgid "displayName" +msgstr "Відображуване ім’я" + +# English string: Display name +msgid "urn:mace:dir:attribute-def:displayName" +msgstr "Відображуване ім’я" + +# English string: Display name +msgid "urn:oid:2.16.840.1.113730.3.1.241" +msgstr "Відображуване ім’я" + +msgid "distinguishedName" +msgstr "distinguishedName" + +msgid "urn:mace:dir:attribute-def:distinguishedName" +msgstr "urn:mace:dir:attribute-def:distinguishedName" + +msgid "urn:oid:2.5.4.49" +msgstr "urn:oid:2.5.4.49" + +msgid "dmdName" +msgstr "dmdName" + +msgid "urn:mace:dir:attribute-def:dmdName" +msgstr "urn:mace:dir:attribute-def:dmdName" + +msgid "urn:oid:2.5.4.54" +msgstr "urn:oid:2.5.4.54" + +msgid "dnQualifier" +msgstr "dnQualifier" + +msgid "urn:mace:dir:attribute-def:dnQualifier" +msgstr "urn:mace:dir:attribute-def:dnQualifier" + +msgid "urn:oid:2.5.4.46" +msgstr "urn:oid:2.5.4.46" + +msgid "documentAuthor" +msgstr "documentAuthor" + +msgid "urn:mace:dir:attribute-def:documentAuthor" +msgstr "urn:mace:dir:attribute-def:documentAuthor" + +msgid "urn:oid:0.9.2342.19200300.100.1.14" +msgstr "urn:oid:0.9.2342.19200300.100.1.14" + +msgid "documentIdentifier" +msgstr "documentIdentifier" + +msgid "urn:mace:dir:attribute-def:documentIdentifier" +msgstr "urn:mace:dir:attribute-def:documentIdentifier" + +msgid "urn:oid:0.9.2342.19200300.100.1.11" +msgstr "urn:oid:0.9.2342.19200300.100.1.11" + +msgid "documentLocation" +msgstr "documentLocation" + +msgid "urn:mace:dir:attribute-def:documentLocation" +msgstr "urn:mace:dir:attribute-def:documentLocation" + +msgid "urn:oid:0.9.2342.19200300.100.1.15" +msgstr "urn:oid:0.9.2342.19200300.100.1.15" + +msgid "documentPublisher" +msgstr "documentPublisher" + +msgid "urn:mace:dir:attribute-def:documentPublisher" +msgstr "urn:mace:dir:attribute-def:documentPublisher" + +msgid "urn:oid:0.9.2342.19200300.100.1.56" +msgstr "urn:oid:0.9.2342.19200300.100.1.56" + +msgid "documentTitle" +msgstr "documentTitle" + +msgid "urn:mace:dir:attribute-def:documentTitle" +msgstr "urn:mace:dir:attribute-def:documentTitle" + +msgid "urn:oid:0.9.2342.19200300.100.1.12" +msgstr "urn:oid:0.9.2342.19200300.100.1.12" + +msgid "documentVersion" +msgstr "documentVersion" + +msgid "urn:mace:dir:attribute-def:documentVersion" +msgstr "urn:mace:dir:attribute-def:documentVersion" + +msgid "urn:oid:0.9.2342.19200300.100.1.13" +msgstr "urn:oid:0.9.2342.19200300.100.1.13" + +msgid "domainComponent" +msgstr "domainComponent" + +msgid "urn:mace:dir:attribute-def:domainComponent" +msgstr "urn:mace:dir:attribute-def:domainComponent" + +msgid "drink" +msgstr "drink" + +msgid "urn:mace:dir:attribute-def:drink" +msgstr "urn:mace:dir:attribute-def:drink" + +msgid "urn:oid:0.9.2342.19200300.100.1.5" +msgstr "urn:oid:0.9.2342.19200300.100.1.5" + +# English string: Organizational homepage +msgid "eduOrgHomePageURI" +msgstr "Домашня сторінка організації" + +# English string: Organizational homepage +msgid "urn:mace:dir:attribute-def:eduOrgHomePageURI" +msgstr "Домашня сторінка організації" + +# English string: Organizational homepage +msgid "urn:oid:1.3.6.1.4.1.5923.1.2.1.2" +msgstr "Домашня сторінка організації" + +msgid "eduOrgIdentityAuthNPolicyURI" +msgstr "eduOrgIdentityAuthNPolicyURI" + +msgid "urn:mace:dir:attribute-def:eduOrgIdentityAuthNPolicyURI" +msgstr "urn:mace:dir:attribute-def:eduOrgIdentityAuthNPolicyURI" + +msgid "urn:oid:1.3.6.1.4.1.5923.1.2.1.3" +msgstr "urn:oid:1.3.6.1.4.1.5923.1.2.1.3" + +# English string: Organization's legal name +msgid "eduOrgLegalName" +msgstr "Юридична назва організації" + +# English string: Organization's legal name +msgid "urn:mace:dir:attribute-def:eduOrgLegalName" +msgstr "Юридична назва організації" + +# English string: Organization's legal name +msgid "urn:oid:1.3.6.1.4.1.5923.1.2.1.4" +msgstr "Юридична назва організації" + +msgid "eduOrgSuperiorURI" +msgstr "eduOrgSuperiorURI" + +msgid "urn:mace:dir:attribute-def:eduOrgSuperiorURI" +msgstr "urn:mace:dir:attribute-def:eduOrgSuperiorURI" + +msgid "urn:oid:1.3.6.1.4.1.5923.1.2.1.5" +msgstr "urn:oid:1.3.6.1.4.1.5923.1.2.1.5" + +msgid "eduOrgWhitePagesURI" +msgstr "eduOrgWhitePagesURI" + +msgid "urn:mace:dir:attribute-def:eduOrgWhitePagesURI" +msgstr "urn:mace:dir:attribute-def:eduOrgWhitePagesURI" + +msgid "urn:oid:1.3.6.1.4.1.5923.1.2.1.6" +msgstr "urn:oid:1.3.6.1.4.1.5923.1.2.1.6" + +# English string: Affiliation +msgid "eduPersonAffiliation" +msgstr "Членство" + +# English string: Affiliation +msgid "urn:mace:dir:attribute-def:eduPersonAffiliation" +msgstr "Членство" + +# English string: Affiliation +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.1" +msgstr "Членство" + +# English string: Identity assurance profile +msgid "eduPersonAssurance" +msgstr "Ідентифікатор гарантованого профайлу" + +# English string: Identity assurance profile +msgid "urn:mace:dir:attribute-def:eduPersonAssurance" +msgstr "Ідентифікатор гарантованого профайлу" + +# English string: Identity assurance profile +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.11" +msgstr "Ідентифікатор гарантованого профайлу" + +# English string: Entitlement regarding the service +msgid "eduPersonEntitlement" +msgstr "Право на надання послуг" + +# English string: Entitlement regarding the service +msgid "urn:mace:dir:attribute-def:eduPersonEntitlement" +msgstr "Право на надання послуг" + +# English string: Entitlement regarding the service +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.7" +msgstr "Право на надання послуг" + +# English string: Nickname +msgid "eduPersonNickname" +msgstr "Псевдонім" + +# English string: Nickname +msgid "urn:mace:dir:attribute-def:eduPersonNickname" +msgstr "Псевдонім" + +# English string: Nickname +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.2" +msgstr "Псевдонім" + +# English string: Distinguished name (DN) of person's home organization +msgid "eduPersonOrgDN" +msgstr "Відмінне ім’я (DN) користувача домашньої організації" + +# English string: Distinguished name (DN) of person's home organization +msgid "urn:mace:dir:attribute-def:eduPersonOrgDN" +msgstr "Відмінне ім’я (DN) користувача домашньої організації" + +# English string: Distinguished name (DN) of person's home organization +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.3" +msgstr "Відмінне ім’я (DN) користувача домашньої організації" + +# English string: Distinguished name (DN) of the person's home organizational unit +msgid "eduPersonOrgUnitDN" +msgstr "Відмінне ім'я (DN) користувача з підрозділу домашньої організації" + +# English string: Distinguished name (DN) of the person's home organizational unit +msgid "urn:mace:dir:attribute-def:eduPersonOrgUnitDN" +msgstr "Відмінне ім'я (DN) користувача з підрозділу домашньої організації" + +# English string: Distinguished name (DN) of the person's home organizational unit +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.4" +msgstr "Відмінне ім'я (DN) користувача з підрозділу домашньої організації" + +# English string: Primary affiliation +msgid "eduPersonPrimaryAffiliation" +msgstr "Головне членство" + +# English string: Primary affiliation +msgid "urn:mace:dir:attribute-def:eduPersonPrimaryAffiliation" +msgstr "Головне членство" + +# English string: Primary affiliation +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.5" +msgstr "Головне членство" + +# English string: Distinguished name (DN) of person's primary Organizational Unit +msgid "eduPersonPrimaryOrgUnitDN" +msgstr "Відмінне ім'я (DN) користувача основного підрозділу організації" + +# English string: Distinguished name (DN) of person's primary Organizational Unit +msgid "urn:mace:dir:attribute-def:eduPersonPrimaryOrgUnitDN" +msgstr "Відмінне ім'я (DN) користувача основного підрозділу організації" + +# English string: Distinguished name (DN) of person's primary Organizational Unit +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.8" +msgstr "Відмінне ім'я (DN) користувача основного підрозділу організації" + +# English string: Person's principal name at home organization +msgid "eduPersonPrincipalName" +msgstr "Ім'я керівника головної організації" + +# English string: Person's principal name at home organization +msgid "urn:mace:dir:attribute-def:eduPersonPrincipalName" +msgstr "Ім'я керівника головної організації" + +# English string: Person's principal name at home organization +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.6" +msgstr "Ім'я керівника головної організації" + +# English string: Affiliation at home organization +msgid "eduPersonScopedAffiliation" +msgstr "Членство в головній організації" + +# English string: Affiliation at home organization +msgid "urn:mace:dir:attribute-def:eduPersonScopedAffiliation" +msgstr "Членство в головній організації" + +# English string: Affiliation at home organization +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.9" +msgstr "Членство в головній організації" + +# English string: Persistent pseudonymous ID +msgid "eduPersonTargetedID" +msgstr "ID постійного псевдоніма" + +# English string: Persistent pseudonymous ID +msgid "urn:mace:dir:attribute-def:eduPersonTargetedID" +msgstr "ID постійного псевдоніма" + +# English string: Persistent pseudonymous ID +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.10" +msgstr "ID постійного псевдоніма" + +# English string: Person's non-reassignable, persistent pseudonymous ID at home organization +msgid "eduPersonUniqueId" +msgstr "eduPersonUniqueId" + +# English string: Person's non-reassignable, persistent pseudonymous ID at home organization +msgid "urn:mace:dir:attribute-def:eduPersonUniqueId" +msgstr "urn:mace:dir:attribute-def:eduPersonUniqueId" + +# English string: Person's non-reassignable, persistent pseudonymous ID at home organization +msgid "urn:oid:1.3.6.1.4.1.5923.1.1.1.13" +msgstr "urn:oid:1.3.6.1.4.1.5923.1.1.1.13" + +msgid "email" +msgstr "email" + +msgid "urn:mace:dir:attribute-def:email" +msgstr "urn:mace:dir:attribute-def:email" + +msgid "urn:oid:1.2.840.113549.1.9.1" +msgstr "urn:oid:1.2.840.113549.1.9.1" + +msgid "emailAddress" +msgstr "emailAddress" + +msgid "urn:mace:dir:attribute-def:emailAddress" +msgstr "urn:mace:dir:attribute-def:emailAddress" + +msgid "employeeNumber" +msgstr "employeeNumber" + +msgid "urn:mace:dir:attribute-def:employeeNumber" +msgstr "urn:mace:dir:attribute-def:employeeNumber" + +msgid "urn:oid:2.16.840.1.113730.3.1.3" +msgstr "urn:oid:2.16.840.1.113730.3.1.3" + +msgid "employeeType" +msgstr "employeeType" + +msgid "urn:mace:dir:attribute-def:employeeType" +msgstr "urn:mace:dir:attribute-def:employeeType" + +msgid "urn:oid:2.16.840.1.113730.3.1.4" +msgstr "urn:oid:2.16.840.1.113730.3.1.4" + +msgid "enhancedSearchGuide" +msgstr "enhancedSearchGuide" + +msgid "urn:mace:dir:attribute-def:enhancedSearchGuide" +msgstr "urn:mace:dir:attribute-def:enhancedSearchGuide" + +msgid "urn:oid:2.5.4.47" +msgstr "urn:oid:2.5.4.47" + +# English string: Fax number +msgid "facsimileTelephoneNumber" +msgstr "Номер факсу" + +# English string: Fax number +msgid "urn:mace:dir:attribute-def:facsimileTelephoneNumber" +msgstr "Номер факсу" + +# English string: Fax number +msgid "urn:oid:2.5.4.23" +msgstr "Номер факсу" + +msgid "favouriteDrink" +msgstr "favouriteDrink" + +msgid "urn:mace:dir:attribute-def:favouriteDrink" +msgstr "urn:mace:dir:attribute-def:favouriteDrink" + +msgid "fax" +msgstr "fax" + +msgid "urn:mace:dir:attribute-def:fax" +msgstr "urn:mace:dir:attribute-def:fax" + +msgid "federationFeideSchemaVersion" +msgstr "federationFeideSchemaVersion" + +msgid "urn:mace:dir:attribute-def:federationFeideSchemaVersion" +msgstr "urn:mace:dir:attribute-def:federationFeideSchemaVersion" + +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.9" +msgstr "urn:oid:1.3.6.1.4.1.2428.90.1.9" + +msgid "friendlyCountryName" +msgstr "friendlyCountryName" + +msgid "urn:mace:dir:attribute-def:friendlyCountryName" +msgstr "urn:mace:dir:attribute-def:friendlyCountryName" + +msgid "generationQualifier" +msgstr "generationQualifier" + +msgid "urn:mace:dir:attribute-def:generationQualifier" +msgstr "urn:mace:dir:attribute-def:generationQualifier" + +msgid "urn:oid:2.5.4.44" +msgstr "urn:oid:2.5.4.44" + +# English string: Given name +msgid "givenName" +msgstr "Ім'я" + +# English string: Given name +msgid "urn:mace:dir:attribute-def:givenName" +msgstr "Ім'я" + +# English string: Given name +msgid "urn:oid:2.5.4.42" +msgstr "Ім'я" + +msgid "gn" +msgstr "gn" + +msgid "urn:mace:dir:attribute-def:gn" +msgstr "urn:mace:dir:attribute-def:gn" + +# English string: Home telephone +msgid "homePhone" +msgstr "Домашній телефон" + +# English string: Home telephone +msgid "urn:mace:dir:attribute-def:homePhone" +msgstr "Домашній телефон" + +# English string: Home telephone +msgid "urn:oid:0.9.2342.19200300.100.1.20" +msgstr "Домашній телефон" + +# English string: Home postal address +msgid "homePostalAddress" +msgstr "Домашня поштова адреса" + +# English string: Home postal address +msgid "urn:mace:dir:attribute-def:homePostalAddress" +msgstr "Домашня поштова адреса" + +# English string: Home postal address +msgid "urn:oid:0.9.2342.19200300.100.1.39" +msgstr "Домашня поштова адреса" + +msgid "homeTelephoneNumber" +msgstr "homeTelephoneNumber" + +msgid "urn:mace:dir:attribute-def:homeTelephoneNumber" +msgstr "urn:mace:dir:attribute-def:homeTelephoneNumber" + +msgid "host" +msgstr "host" + +msgid "urn:mace:dir:attribute-def:host" +msgstr "urn:mace:dir:attribute-def:host" + +msgid "urn:oid:0.9.2342.19200300.100.1.9" +msgstr "urn:oid:0.9.2342.19200300.100.1.9" + +msgid "houseIdentifier" +msgstr "houseIdentifier" + +msgid "urn:mace:dir:attribute-def:houseIdentifier" +msgstr "urn:mace:dir:attribute-def:houseIdentifier" + +msgid "urn:oid:2.5.4.51" +msgstr "urn:oid:2.5.4.51" + +msgid "info" +msgstr "info" + +msgid "urn:mace:dir:attribute-def:info" +msgstr "urn:mace:dir:attribute-def:info" + +msgid "urn:oid:0.9.2342.19200300.100.1.4" +msgstr "urn:oid:0.9.2342.19200300.100.1.4" + +msgid "initials" +msgstr "initials" + +msgid "urn:mace:dir:attribute-def:initials" +msgstr "urn:mace:dir:attribute-def:initials" + +msgid "urn:oid:2.5.4.43" +msgstr "urn:oid:2.5.4.43" + +msgid "internationaliSDNNumber" +msgstr "internationaliSDNNumber" + +msgid "urn:mace:dir:attribute-def:internationaliSDNNumber" +msgstr "urn:mace:dir:attribute-def:internationaliSDNNumber" + +msgid "urn:oid:2.5.4.25" +msgstr "urn:oid:2.5.4.25" + +msgid "janetMailbox" +msgstr "janetMailbox" + +msgid "urn:mace:dir:attribute-def:janetMailbox" +msgstr "urn:mace:dir:attribute-def:janetMailbox" + +msgid "urn:oid:0.9.2342.19200300.100.1.46" +msgstr "urn:oid:0.9.2342.19200300.100.1.46" + +# English string: JPEG Photo +msgid "jpegPhoto" +msgstr "Фотографія у форматі JPEG" + +# English string: JPEG Photo +msgid "urn:mace:dir:attribute-def:jpegPhoto" +msgstr "Фотографія у форматі JPEG" + +# English string: JPEG Photo +msgid "urn:oid:0.9.2342.19200300.100.1.60" +msgstr "Фотографія у форматі JPEG" + +msgid "knowledgeInformation" +msgstr "knowledgeInformation" + +msgid "urn:mace:dir:attribute-def:knowledgeInformation" +msgstr "urn:mace:dir:attribute-def:knowledgeInformation" + +msgid "urn:oid:2.5.4.2" +msgstr "urn:oid:2.5.4.2" + +# English string: Locality +msgid "l" +msgstr "Район" + +# English string: Locality +msgid "urn:mace:dir:attribute-def:l" +msgstr "Район" + +# English string: Locality +msgid "urn:oid:2.5.4.7" +msgstr "Район" + +# English string: Labeled URI +msgid "labeledURI" +msgstr "Маркований URI (LabeledURI)" + +# English string: Labeled URI +msgid "urn:mace:dir:attribute-def:labeledURI" +msgstr "Маркований URI (LabeledURI)" + +# English string: Labeled URI +msgid "urn:oid:1.3.6.1.4.1.250.1.57" +msgstr "Маркований URI (LabeledURI)" + +msgid "localityName" +msgstr "localityName" + +msgid "urn:mace:dir:attribute-def:localityName" +msgstr "urn:mace:dir:attribute-def:localityName" + +msgid "mDRecord" +msgstr "mDRecord" + +msgid "urn:mace:dir:attribute-def:mDRecord" +msgstr "urn:mace:dir:attribute-def:mDRecord" + +msgid "urn:oid:0.9.2342.19200300.100.1.27" +msgstr "urn:oid:0.9.2342.19200300.100.1.27" + +msgid "mXRecord" +msgstr "mXRecord" + +msgid "urn:mace:dir:attribute-def:mXRecord" +msgstr "urn:mace:dir:attribute-def:mXRecord" + +msgid "urn:oid:0.9.2342.19200300.100.1.28" +msgstr "urn:oid:0.9.2342.19200300.100.1.28" + +# English string: Mail +msgid "mail" +msgstr "Пошта" + +# English string: Mail +msgid "urn:mace:dir:attribute-def:mail" +msgstr "Пошта" + +# English string: Mail +msgid "urn:oid:0.9.2342.19200300.100.1.3" +msgstr "Пошта" + +msgid "mailPreferenceOption" +msgstr "mailPreferenceOption" + +msgid "urn:mace:dir:attribute-def:mailPreferenceOption" +msgstr "urn:mace:dir:attribute-def:mailPreferenceOption" + +msgid "urn:oid:0.9.2342.19200300.100.1.47" +msgstr "urn:oid:0.9.2342.19200300.100.1.47" + +# English string: Manager +msgid "manager" +msgstr "Керуючий" + +# English string: Manager +msgid "urn:mace:dir:attribute-def:manager" +msgstr "Керуючий" + +# English string: Manager +msgid "urn:oid:0.9.2342.19200300.100.1.10" +msgstr "Керуючий" + +msgid "member" +msgstr "member" + +msgid "urn:mace:dir:attribute-def:member" +msgstr "urn:mace:dir:attribute-def:member" + +msgid "urn:oid:2.5.4.31" +msgstr "urn:oid:2.5.4.31" + +# English string: Mobile +msgid "mobile" +msgstr "Мобільний" + +# English string: Mobile +msgid "urn:mace:dir:attribute-def:mobile" +msgstr "Мобільний" + +# English string: Mobile +msgid "urn:oid:0.9.2342.19200300.100.1.41" +msgstr "Мобільний" + +msgid "mobileTelephoneNumber" +msgstr "mobileTelephoneNumber" + +msgid "urn:mace:dir:attribute-def:mobileTelephoneNumber" +msgstr "urn:mace:dir:attribute-def:mobileTelephoneNumber" + +msgid "nSRecord" +msgstr "nSRecord" + +msgid "urn:mace:dir:attribute-def:nSRecord" +msgstr "urn:mace:dir:attribute-def:nSRecord" + +msgid "urn:oid:0.9.2342.19200300.100.1.29" +msgstr "urn:oid:0.9.2342.19200300.100.1.29" + +msgid "name" +msgstr "name" + +msgid "urn:mace:dir:attribute-def:name" +msgstr "urn:mace:dir:attribute-def:name" + +msgid "urn:oid:2.5.4.41" +msgstr "urn:oid:2.5.4.41" + +msgid "norEduOrgAcronym" +msgstr "norEduOrgAcronym" + +msgid "urn:mace:dir:attribute-def:norEduOrgAcronym" +msgstr "urn:mace:dir:attribute-def:norEduOrgAcronym" + +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.6" +msgstr "urn:oid:1.3.6.1.4.1.2428.90.1.6" + +# English string: Organizational number +msgid "norEduOrgNIN" +msgstr "Номер організації" + +# English string: Organizational number +msgid "urn:mace:dir:attribute-def:norEduOrgNIN" +msgstr "Номер організації" + +# English string: Organizational number +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.12" +msgstr "Номер організації" + +msgid "norEduOrgSchemaVersion" +msgstr "norEduOrgSchemaVersion" + +msgid "urn:mace:dir:attribute-def:norEduOrgSchemaVersion" +msgstr "urn:mace:dir:attribute-def:norEduOrgSchemaVersion" + +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.11" +msgstr "urn:oid:1.3.6.1.4.1.2428.90.1.11" + +msgid "norEduOrgUniqueIdentifier" +msgstr "norEduOrgUniqueIdentifier" + +msgid "urn:mace:dir:attribute-def:norEduOrgUniqueIdentifier" +msgstr "urn:mace:dir:attribute-def:norEduOrgUniqueIdentifier" + +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.7" +msgstr "urn:oid:1.3.6.1.4.1.2428.90.1.7" + +msgid "norEduOrgUniqueNumber" +msgstr "norEduOrgUniqueNumber" + +msgid "urn:mace:dir:attribute-def:norEduOrgUniqueNumber" +msgstr "urn:mace:dir:attribute-def:norEduOrgUniqueNumber" + +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.1" +msgstr "urn:oid:1.3.6.1.4.1.2428.90.1.1" + +msgid "norEduOrgUnitUniqueIdentifier" +msgstr "norEduOrgUnitUniqueIdentifier" + +msgid "urn:mace:dir:attribute-def:norEduOrgUnitUniqueIdentifier" +msgstr "urn:mace:dir:attribute-def:norEduOrgUnitUniqueIdentifier" + +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.8" +msgstr "urn:oid:1.3.6.1.4.1.2428.90.1.8" + +msgid "norEduOrgUnitUniqueNumber" +msgstr "norEduOrgUnitUniqueNumber" + +msgid "urn:mace:dir:attribute-def:norEduOrgUnitUniqueNumber" +msgstr "urn:mace:dir:attribute-def:norEduOrgUnitUniqueNumber" + +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.2" +msgstr "urn:oid:1.3.6.1.4.1.2428.90.1.2" + +# English string: Date of birth +msgid "norEduPersonBirthDate" +msgstr "Дата народження" + +# English string: Date of birth +msgid "urn:mace:dir:attribute-def:norEduPersonBirthDate" +msgstr "Дата народження" + +# English string: Date of birth +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.3" +msgstr "Дата народження" + +# English string: Local identity number +msgid "norEduPersonLIN" +msgstr "Локальний ідентифікаційний номер" + +# English string: Local identity number +msgid "urn:mace:dir:attribute-def:norEduPersonLIN" +msgstr "Локальний ідентифікаційний номер" + +# English string: Local identity number +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.4" +msgstr "Локальний ідентифікаційний номер" + +# English string: Identity number assigned by public authorities +msgid "norEduPersonNIN" +msgstr "Ідентифікаційний номер, що присвоюється органами державної влади" + +# English string: Identity number assigned by public authorities +msgid "urn:mace:dir:attribute-def:norEduPersonNIN" +msgstr "Ідентифікаційний номер, що присвоюється органами державної влади" + +# English string: Identity number assigned by public authorities +msgid "urn:oid:1.3.6.1.4.1.2428.90.1.5" +msgstr "Ідентифікаційний номер, що присвоюється органами державної влади" + +# English string: Organization name +msgid "o" +msgstr "Назва організації" + +# English string: Organization name +msgid "urn:mace:dir:attribute-def:o" +msgstr "Назва організації" + +# English string: Organization name +msgid "urn:oid:2.5.4.10" +msgstr "Назва організації" + +msgid "objectClass" +msgstr "objectClass" + +msgid "urn:mace:dir:attribute-def:objectClass" +msgstr "urn:mace:dir:attribute-def:objectClass" + +msgid "urn:oid:2.5.4.0" +msgstr "urn:oid:2.5.4.0" + +msgid "organizationName" +msgstr "organizationName" + +msgid "urn:mace:dir:attribute-def:organizationName" +msgstr "urn:mace:dir:attribute-def:organizationName" + +msgid "organizationalStatus" +msgstr "organizationalStatus" + +msgid "urn:mace:dir:attribute-def:organizationalStatus" +msgstr "urn:mace:dir:attribute-def:organizationalStatus" + +msgid "urn:oid:0.9.2342.19200300.100.1.45" +msgstr "urn:oid:0.9.2342.19200300.100.1.45" + +msgid "organizationalUnitName" +msgstr "organizationalUnitName" + +msgid "urn:mace:dir:attribute-def:organizationalUnitName" +msgstr "urn:mace:dir:attribute-def:organizationalUnitName" + +# English string: Organizational unit +msgid "urn:oid:2.5.4.11" +msgstr "Підрозділ організації" + +msgid "otherMailbox" +msgstr "otherMailbox" + +msgid "urn:mace:dir:attribute-def:otherMailbox" +msgstr "urn:mace:dir:attribute-def:otherMailbox" + +msgid "urn:oid:0.9.2342.19200300.100.1.22" +msgstr "urn:oid:0.9.2342.19200300.100.1.22" + +# English string: Organizational unit +msgid "ou" +msgstr "Підрозділ організації" + +# English string: Organizational unit +msgid "urn:mace:dir:attribute-def:ou" +msgstr "Підрозділ організації" + +msgid "owner" +msgstr "owner" + +msgid "urn:mace:dir:attribute-def:owner" +msgstr "urn:mace:dir:attribute-def:owner" + +msgid "urn:oid:2.5.4.32" +msgstr "urn:oid:2.5.4.32" + +msgid "pager" +msgstr "pager" + +msgid "urn:mace:dir:attribute-def:pager" +msgstr "urn:mace:dir:attribute-def:pager" + +msgid "urn:oid:0.9.2342.19200300.100.1.42" +msgstr "urn:oid:0.9.2342.19200300.100.1.42" + +msgid "pagerTelephoneNumber" +msgstr "pagerTelephoneNumber" + +msgid "urn:mace:dir:attribute-def:pagerTelephoneNumber" +msgstr "urn:mace:dir:attribute-def:pagerTelephoneNumber" + +# English string: Service-specific pseudonymous ID at home organization +msgid "pairwise-id" +msgstr "pairwise-id" + +# English string: Service-specific pseudonymous ID at home organization +msgid "urn:oasis:names:tc:SAML:attribute:pairwise-id" +msgstr "urn:oasis:names:tc:SAML:attribute:pairwise-id" + +msgid "personalSignature" +msgstr "personalSignature" + +msgid "urn:mace:dir:attribute-def:personalSignature" +msgstr "urn:mace:dir:attribute-def:personalSignature" + +msgid "urn:oid:0.9.2342.19200300.100.1.53" +msgstr "urn:oid:0.9.2342.19200300.100.1.53" + +msgid "personalTitle" +msgstr "personalTitle" + +msgid "urn:mace:dir:attribute-def:personalTitle" +msgstr "urn:mace:dir:attribute-def:personalTitle" + +msgid "urn:oid:0.9.2342.19200300.100.1.40" +msgstr "urn:oid:0.9.2342.19200300.100.1.40" + +msgid "photo" +msgstr "photo" + +msgid "urn:mace:dir:attribute-def:photo" +msgstr "urn:mace:dir:attribute-def:photo" + +msgid "urn:oid:0.9.2342.19200300.100.1.7" +msgstr "urn:oid:0.9.2342.19200300.100.1.7" + +msgid "physicalDeliveryOfficeName" +msgstr "physicalDeliveryOfficeName" + +msgid "urn:mace:dir:attribute-def:physicalDeliveryOfficeName" +msgstr "urn:mace:dir:attribute-def:physicalDeliveryOfficeName" + +msgid "urn:oid:2.5.4.19" +msgstr "urn:oid:2.5.4.19" + +msgid "pkcs9email" +msgstr "pkcs9email" + +msgid "urn:mace:dir:attribute-def:pkcs9email" +msgstr "urn:mace:dir:attribute-def:pkcs9email" + +# English string: Post office box +msgid "postOfficeBox" +msgstr "Абонентська поштова скринька" + +# English string: Post office box +msgid "urn:mace:dir:attribute-def:postOfficeBox" +msgstr "Абонентська поштова скринька" + +# English string: Post office box +msgid "urn:oid:2.5.4.18" +msgstr "Абонентська поштова скринька" + +# English string: Postal address +msgid "postalAddress" +msgstr "Поштова адреса" + +# English string: Postal address +msgid "urn:mace:dir:attribute-def:postalAddress" +msgstr "Поштова адреса" + +# English string: Postal address +msgid "urn:oid:2.5.4.16" +msgstr "Поштова адреса" + +# English string: Postal code +msgid "postalCode" +msgstr "Поштовий індекс" + +# English string: Postal code +msgid "urn:mace:dir:attribute-def:postalCode" +msgstr "Поштовий індекс" + +# English string: Postal code +msgid "urn:oid:2.5.4.17" +msgstr "Поштовий індекс" + +msgid "preferredDeliveryMethod" +msgstr "preferredDeliveryMethod" + +msgid "urn:mace:dir:attribute-def:preferredDeliveryMethod" +msgstr "urn:mace:dir:attribute-def:preferredDeliveryMethod" + +msgid "urn:oid:2.5.4.28" +msgstr "urn:oid:2.5.4.28" + +# English string: Preferred language +msgid "preferredLanguage" +msgstr "Бажана мова" + +# English string: Preferred language +msgid "urn:mace:dir:attribute-def:preferredLanguage" +msgstr "Бажана мова" + +# English string: Preferred language +msgid "urn:oid:2.16.840.1.113730.3.1.39" +msgstr "Бажана мова" + +msgid "presentationAddress" +msgstr "presentationAddress" + +msgid "urn:mace:dir:attribute-def:presentationAddress" +msgstr "urn:mace:dir:attribute-def:presentationAddress" + +msgid "urn:oid:2.5.4.29" +msgstr "urn:oid:2.5.4.29" + +msgid "protocolInformation" +msgstr "protocolInformation" + +msgid "urn:mace:dir:attribute-def:protocolInformation" +msgstr "urn:mace:dir:attribute-def:protocolInformation" + +msgid "urn:oid:2.5.4.48" +msgstr "urn:oid:2.5.4.48" + +msgid "pseudonym" +msgstr "pseudonym" + +msgid "urn:mace:dir:attribute-def:pseudonym" +msgstr "urn:mace:dir:attribute-def:pseudonym" + +msgid "urn:oid:2.5.4.65" +msgstr "urn:oid:2.5.4.65" + +msgid "registeredAddress" +msgstr "registeredAddress" + +msgid "urn:mace:dir:attribute-def:registeredAddress" +msgstr "urn:mace:dir:attribute-def:registeredAddress" + +msgid "urn:oid:2.5.4.26" +msgstr "urn:oid:2.5.4.26" + +msgid "rfc822Mailbox" +msgstr "rfc822Mailbox" + +msgid "urn:mace:dir:attribute-def:rfc822Mailbox" +msgstr "urn:mace:dir:attribute-def:rfc822Mailbox" + +msgid "roleOccupant" +msgstr "roleOccupant" + +msgid "urn:mace:dir:attribute-def:roleOccupant" +msgstr "urn:mace:dir:attribute-def:roleOccupant" + +msgid "urn:oid:2.5.4.33" +msgstr "urn:oid:2.5.4.33" + +msgid "roomNumber" +msgstr "roomNumber" + +msgid "urn:mace:dir:attribute-def:roomNumber" +msgstr "urn:mace:dir:attribute-def:roomNumber" + +msgid "urn:oid:0.9.2342.19200300.100.1.6" +msgstr "urn:oid:0.9.2342.19200300.100.1.6" + +msgid "sOARecord" +msgstr "sOARecord" + +msgid "urn:mace:dir:attribute-def:sOARecord" +msgstr "urn:mace:dir:attribute-def:sOARecord" + +msgid "urn:oid:0.9.2342.19200300.100.1.30" +msgstr "urn:oid:0.9.2342.19200300.100.1.30" + +msgid "schacCountryOfCitizenship" +msgstr "schacCountryOfCitizenship" + +msgid "urn:mace:terena.org:attribute-def:schacCountryOfCitizenship" +msgstr "urn:mace:terena.org:attribute-def:schacCountryOfCitizenship" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.5" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.5" + +msgid "urn:schac:attribute-def:schacCountryOfCitizenship" +msgstr "urn:schac:attribute-def:schacCountryOfCitizenship" + +msgid "schacCountryOfResidence" +msgstr "schacCountryOfResidence" + +msgid "urn:mace:terena.org:attribute-def:schacCountryOfResidence" +msgstr "urn:mace:terena.org:attribute-def:schacCountryOfResidence" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.11" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.11" + +msgid "urn:schac:attribute-def:schacCountryOfResidence" +msgstr "urn:schac:attribute-def:schacCountryOfResidence" + +msgid "schacDateOfBirth" +msgstr "schacDateOfBirth" + +msgid "urn:mace:terena.org:attribute-def:schacDateOfBirth" +msgstr "urn:mace:terena.org:attribute-def:schacDateOfBirth" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.3" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.3" + +msgid "urn:schac:attribute-def:schacDateOfBirth" +msgstr "urn:schac:attribute-def:schacDateOfBirth" + +msgid "schacExpiryDate" +msgstr "schacExpiryDate" + +msgid "urn:mace:terena.org:attribute-def:schacExpiryDate" +msgstr "urn:mace:terena.org:attribute-def:schacExpiryDate" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.17" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.17" + +msgid "urn:schac:attribute-def:schacExpiryDate" +msgstr "urn:schac:attribute-def:schacExpiryDate" + +msgid "schacGender" +msgstr "schacGender" + +msgid "urn:mace:terena.org:attribute-def:schacGender" +msgstr "urn:mace:terena.org:attribute-def:schacGender" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.2" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.2" + +msgid "urn:schac:attribute-def:schacGender" +msgstr "urn:schac:attribute-def:schacGender" + +# English string: Home organization domain name +msgid "schacHomeOrganization" +msgstr "Доменне ім'я головної організації" + +# English string: Home organization domain name +msgid "urn:mace:terena.org:attribute-def:schacHomeOrganization" +msgstr "Доменне ім'я головної організації" + +# English string: Home organization domain name +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.9" +msgstr "Доменне ім'я головної організації" + +# English string: Home organization domain name +msgid "urn:schac:attribute-def:schacHomeOrganization" +msgstr "Доменне ім'я головної організації" + +msgid "schacHomeOrganizationType" +msgstr "schacHomeOrganizationType" + +msgid "urn:mace:terena.org:attribute-def:schacHomeOrganizationType" +msgstr "urn:mace:terena.org:attribute-def:schacHomeOrganizationType" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.10" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.10" + +msgid "urn:schac:attribute-def:schacHomeOrganizationType" +msgstr "urn:schac:attribute-def:schacHomeOrganizationType" + +msgid "schacMotherTongue" +msgstr "schacMotherTongue" + +msgid "urn:mace:terena.org:attribute-def:schacMotherTongue" +msgstr "urn:mace:terena.org:attribute-def:schacMotherTongue" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.1" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.1" + +msgid "urn:schac:attribute-def:schacMotherTongue" +msgstr "urn:schac:attribute-def:schacMotherTongue" + +msgid "schacPersonalPosition" +msgstr "schacPersonalPosition" + +msgid "urn:mace:terena.org:attribute-def:schacPersonalPosition" +msgstr "urn:mace:terena.org:attribute-def:schacPersonalPosition" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.13" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.13" + +msgid "urn:schac:attribute-def:schacPersonalPosition" +msgstr "urn:schac:attribute-def:schacPersonalPosition" + +msgid "schacPersonalTitle" +msgstr "schacPersonalTitle" + +msgid "urn:mace:terena.org:attribute-def:schacPersonalTitle" +msgstr "urn:mace:terena.org:attribute-def:schacPersonalTitle" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.8" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.8" + +msgid "urn:schac:attribute-def:schacPersonalTitle" +msgstr "urn:schac:attribute-def:schacPersonalTitle" + +msgid "schacPersonalUniqueCode" +msgstr "schacPersonalUniqueCode" + +msgid "urn:mace:terena.org:attribute-def:schacPersonalUniqueCode" +msgstr "urn:mace:terena.org:attribute-def:schacPersonalUniqueCode" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.14" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.14" + +msgid "urn:schac:attribute-def:schacPersonalUniqueCode" +msgstr "urn:schac:attribute-def:schacPersonalUniqueCode" + +msgid "schacPersonalUniqueID" +msgstr "schacPersonalUniqueID" + +msgid "urn:mace:terena.org:attribute-def:schacPersonalUniqueID" +msgstr "urn:mace:terena.org:attribute-def:schacPersonalUniqueID" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.15" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.15" + +msgid "urn:schac:attribute-def:schacPersonalUniqueID" +msgstr "urn:schac:attribute-def:schacPersonalUniqueID" + +msgid "schacPlaceOfBirth" +msgstr "schacPlaceOfBirth" + +msgid "urn:mace:terena.org:attribute-def:schacPlaceOfBirth" +msgstr "urn:mace:terena.org:attribute-def:schacPlaceOfBirth" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.4" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.4" + +msgid "urn:schac:attribute-def:schacPlaceOfBirth" +msgstr "urn:schac:attribute-def:schacPlaceOfBirth" + +msgid "schacProjectMembership" +msgstr "schacProjectMembership" + +msgid "urn:mace:terena.org:attribute-def:schacProjectMembership" +msgstr "urn:mace:terena.org:attribute-def:schacProjectMembership" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.20" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.20" + +msgid "urn:schac:attribute-def:schacProjectMembership" +msgstr "urn:schac:attribute-def:schacProjectMembership" + +msgid "schacProjectSpecificRole" +msgstr "schacProjectSpecificRole" + +msgid "urn:mace:terena.org:attribute-def:schacProjectSpecificRole" +msgstr "urn:mace:terena.org:attribute-def:schacProjectSpecificRole" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.21" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.21" + +msgid "urn:schac:attribute-def:schacProjectSpecificRole" +msgstr "urn:schac:attribute-def:schacProjectSpecificRole" + +msgid "schacSn1" +msgstr "schacSn1" + +msgid "urn:mace:terena.org:attribute-def:schacSn1" +msgstr "urn:mace:terena.org:attribute-def:schacSn1" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.6" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.6" + +msgid "urn:schac:attribute-def:schacSn1" +msgstr "urn:schac:attribute-def:schacSn1" + +msgid "schacSn2" +msgstr "schacSn2" + +msgid "urn:mace:terena.org:attribute-def:schacSn2" +msgstr "urn:mace:terena.org:attribute-def:schacSn2" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.7" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.7" + +msgid "urn:schac:attribute-def:schacSn2" +msgstr "urn:schac:attribute-def:schacSn2" + +msgid "schacUserPresenceID" +msgstr "schacUserPresenceID" + +msgid "urn:mace:terena.org:attribute-def:schacUserPresenceID" +msgstr "urn:mace:terena.org:attribute-def:schacUserPresenceID" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.12" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.12" + +msgid "urn:schac:attribute-def:schacUserPresenceID" +msgstr "urn:schac:attribute-def:schacUserPresenceID" + +# English string: Private information elements +msgid "schacUserPrivateAttribute" +msgstr "Елементи особистої інформації" + +# English string: Private information elements +msgid "urn:mace:terena.org:attribute-def:schacUserPrivateAttribute" +msgstr "Елементи особистої інформації" + +# English string: Private information elements +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.18" +msgstr "Елементи особистої інформації" + +# English string: Private information elements +msgid "urn:schac:attribute-def:schacUserPrivateAttribute" +msgstr "Елементи особистої інформації" + +msgid "schacUserStatus" +msgstr "schacUserStatus" + +msgid "urn:mace:terena.org:attribute-def:schacUserStatus" +msgstr "urn:mace:terena.org:attribute-def:schacUserStatus" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.2.19" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.2.19" + +msgid "urn:schac:attribute-def:schacUserStatus" +msgstr "urn:schac:attribute-def:schacUserStatus" + +msgid "schacYearOfBirth" +msgstr "schacYearOfBirth" + +msgid "urn:mace:terena.org:attribute-def:schacYearOfBirth" +msgstr "urn:mace:terena.org:attribute-def:schacYearOfBirth" + +msgid "urn:oid:1.3.6.1.4.1.25178.1.0.2.3" +msgstr "urn:oid:1.3.6.1.4.1.25178.1.0.2.3" + +msgid "urn:schac:attribute-def:schacYearOfBirth" +msgstr "urn:schac:attribute-def:schacYearOfBirth" + +msgid "searchGuide" +msgstr "searchGuide" + +msgid "urn:mace:dir:attribute-def:searchGuide" +msgstr "urn:mace:dir:attribute-def:searchGuide" + +msgid "urn:oid:2.5.4.14" +msgstr "urn:oid:2.5.4.14" + +msgid "secretary" +msgstr "secretary" + +msgid "urn:mace:dir:attribute-def:secretary" +msgstr "urn:mace:dir:attribute-def:secretary" + +msgid "urn:oid:0.9.2342.19200300.100.1.21" +msgstr "urn:oid:0.9.2342.19200300.100.1.21" + +msgid "seeAlso" +msgstr "seeAlso" + +msgid "urn:mace:dir:attribute-def:seeAlso" +msgstr "urn:mace:dir:attribute-def:seeAlso" + +msgid "urn:oid:2.5.4.34" +msgstr "urn:oid:2.5.4.34" + +msgid "serialNumber" +msgstr "serialNumber" + +msgid "urn:mace:dir:attribute-def:serialNumber" +msgstr "urn:mace:dir:attribute-def:serialNumber" + +msgid "urn:oid:2.5.4.5" +msgstr "urn:oid:2.5.4.5" + +msgid "singleLevelQuality" +msgstr "singleLevelQuality" + +msgid "urn:mace:dir:attribute-def:singleLevelQuality" +msgstr "urn:mace:dir:attribute-def:singleLevelQuality" + +msgid "urn:oid:0.9.2342.19200300.100.1.50" +msgstr "urn:oid:0.9.2342.19200300.100.1.50" + +msgid "sisSchoolGrade" +msgstr "sisSchoolGrade" + +msgid "urn:mace:dir:attribute-def:sisSchoolGrade" +msgstr "urn:mace:dir:attribute-def:sisSchoolGrade" + +msgid "urn:oid:1.2.752.194.10.2.2" +msgstr "urn:oid:1.2.752.194.10.2.2" + +msgid "sisLegalGuardianFor" +msgstr "sisLegalGuardianFor" + +msgid "urn:mace:dir:attribute-def:sisLegalGuardianFor" +msgstr "urn:mace:dir:attribute-def:sisLegalGuardianFor" + +msgid "urn:oid:1.2.752.194.10.2.1" +msgstr "urn:oid:1.2.752.194.10.2.1" + +# English string: Surname +msgid "sn" +msgstr "Прізвище" + +# English string: Surname +msgid "urn:mace:dir:attribute-def:sn" +msgstr "Прізвище" + +# English string: Surname +msgid "urn:oid:2.5.4.4" +msgstr "Прізвище" + +msgid "st" +msgstr "st" + +msgid "urn:mace:dir:attribute-def:st" +msgstr "urn:mace:dir:attribute-def:st" + +msgid "urn:oid:2.5.4.8" +msgstr "urn:oid:2.5.4.8" + +msgid "stateOrProvinceName" +msgstr "stateOrProvinceName" + +msgid "urn:mace:dir:attribute-def:stateOrProvinceName" +msgstr "urn:mace:dir:attribute-def:stateOrProvinceName" + +# English string: Street +msgid "street" +msgstr "Вулиця" + +# English string: Street +msgid "urn:mace:dir:attribute-def:street" +msgstr "Вулиця" + +# English string: Street +msgid "urn:oid:2.5.4.9" +msgstr "Вулиця" + +msgid "streetAddress" +msgstr "streetAddress" + +msgid "urn:mace:dir:attribute-def:streetAddress" +msgstr "urn:mace:dir:attribute-def:streetAddress" + +# English string: Pseudonymous ID at home organization +msgid "subject-id" +msgstr "subject-id" + +# English string: Pseudonymous ID at home organization +msgid "urn:oasis:names:tc:SAML:attribute:subject-id" +msgstr "urn:oasis:names:tc:SAML:attribute:subject-id" + +msgid "subtreeMaximumQuality" +msgstr "subtreeMaximumQuality" + +msgid "urn:mace:dir:attribute-def:subtreeMaximumQuality" +msgstr "urn:mace:dir:attribute-def:subtreeMaximumQuality" + +msgid "urn:oid:0.9.2342.19200300.100.1.52" +msgstr "urn:oid:0.9.2342.19200300.100.1.52" + +msgid "subtreeMinimumQuality" +msgstr "subtreeMinimumQuality" + +msgid "urn:mace:dir:attribute-def:subtreeMinimumQuality" +msgstr "urn:mace:dir:attribute-def:subtreeMinimumQuality" + +msgid "urn:oid:0.9.2342.19200300.100.1.51" +msgstr "urn:oid:0.9.2342.19200300.100.1.51" + +msgid "supportedAlgorithms" +msgstr "supportedAlgorithms" + +msgid "urn:mace:dir:attribute-def:supportedAlgorithms" +msgstr "urn:mace:dir:attribute-def:supportedAlgorithms" + +msgid "urn:oid:2.5.4.52" +msgstr "urn:oid:2.5.4.52" + +msgid "supportedApplicationContext" +msgstr "supportedApplicationContext" + +msgid "urn:mace:dir:attribute-def:supportedApplicationContext" +msgstr "urn:mace:dir:attribute-def:supportedApplicationContext" + +msgid "urn:oid:2.5.4.30" +msgstr "urn:oid:2.5.4.30" + +msgid "surname" +msgstr "surname" + +msgid "urn:mace:dir:attribute-def:surname" +msgstr "urn:mace:dir:attribute-def:surname" + +# English string: Telephone number +msgid "telephoneNumber" +msgstr "Номер телефону" + +# English string: Telephone number +msgid "urn:mace:dir:attribute-def:telephoneNumber" +msgstr "Номер телефону" + +# English string: Telephone number +msgid "urn:oid:2.5.4.20" +msgstr "Номер телефону" + +msgid "teletexTerminalIdentifier" +msgstr "teletexTerminalIdentifier" + +msgid "urn:mace:dir:attribute-def:teletexTerminalIdentifier" +msgstr "urn:mace:dir:attribute-def:teletexTerminalIdentifier" + +msgid "urn:oid:2.5.4.22" +msgstr "urn:oid:2.5.4.22" + +msgid "telexNumber" +msgstr "telexNumber" + +msgid "urn:mace:dir:attribute-def:telexNumber" +msgstr "urn:mace:dir:attribute-def:telexNumber" + +msgid "urn:oid:2.5.4.21" +msgstr "urn:oid:2.5.4.21" + +msgid "textEncodedORAddress" +msgstr "textEncodedORAddress" + +msgid "urn:mace:dir:attribute-def:textEncodedORAddress" +msgstr "urn:mace:dir:attribute-def:textEncodedORAddress" + +msgid "urn:oid:0.9.2342.19200300.100.1.2" +msgstr "urn:oid:0.9.2342.19200300.100.1.2" + +# English string: Title +msgid "title" +msgstr "Заголовок" + +# English string: Title +msgid "urn:mace:dir:attribute-def:title" +msgstr "Заголовок" + +# English string: Title +msgid "urn:oid:2.5.4.12" +msgstr "Заголовок" + +# English string: User ID +msgid "uid" +msgstr "ID користувача" + +# English string: User ID +msgid "urn:mace:dir:attribute-def:uid" +msgstr "ID користувача" + +# English string: User ID +msgid "urn:oid:0.9.2342.19200300.100.1.1" +msgstr "ID користувача" + +msgid "uniqueIdentifier" +msgstr "uniqueIdentifier" + +msgid "urn:mace:dir:attribute-def:uniqueIdentifier" +msgstr "urn:mace:dir:attribute-def:uniqueIdentifier" + +msgid "urn:oid:0.9.2342.19200300.100.1.44" +msgstr "urn:oid:0.9.2342.19200300.100.1.44" + +msgid "uniqueMember" +msgstr "uniqueMember" + +msgid "urn:mace:dir:attribute-def:uniqueMember" +msgstr "urn:mace:dir:attribute-def:uniqueMember" + +msgid "urn:oid:2.5.4.50" +msgstr "urn:oid:2.5.4.50" + +msgid "userCertificate" +msgstr "userCertificate" + +msgid "urn:mace:dir:attribute-def:userCertificate" +msgstr "urn:mace:dir:attribute-def:userCertificate" + +msgid "urn:oid:2.5.4.36" +msgstr "urn:oid:2.5.4.36" + +msgid "userClass" +msgstr "userClass" + +msgid "urn:mace:dir:attribute-def:userClass" +msgstr "urn:mace:dir:attribute-def:userClass" + +msgid "urn:oid:0.9.2342.19200300.100.1.8" +msgstr "urn:oid:0.9.2342.19200300.100.1.8" + +msgid "userPKCS12" +msgstr "userPKCS12" + +msgid "urn:mace:dir:attribute-def:userPKCS12" +msgstr "urn:mace:dir:attribute-def:userPKCS12" + +msgid "urn:oid:2.16.840.1.113730.3.1.216" +msgstr "urn:oid:2.16.840.1.113730.3.1.216" + +# English string: User's password hash +msgid "userPassword" +msgstr "Хеш пароля користувача" + +# English string: User's password hash +msgid "urn:mace:dir:attribute-def:userPassword" +msgstr "Хеш пароля користувача" + +# English string: User's password hash +msgid "urn:oid:2.5.4.35" +msgstr "Хеш пароля користувача" + +msgid "userSMIMECertificate" +msgstr "userSMIMECertificate" + +msgid "urn:mace:dir:attribute-def:userSMIMECertificate" +msgstr "urn:mace:dir:attribute-def:userSMIMECertificate" + +msgid "urn:oid:2.16.840.1.113730.3.1.40" +msgstr "urn:oid:2.16.840.1.113730.3.1.40" + +msgid "userid" +msgstr "userid" + +msgid "urn:mace:dir:attribute-def:userid" +msgstr "urn:mace:dir:attribute-def:userid" + +msgid "x121Address" +msgstr "x121Address" + +msgid "urn:mace:dir:attribute-def:x121Address" +msgstr "urn:mace:dir:attribute-def:x121Address" + +msgid "urn:oid:2.5.4.24" +msgstr "urn:oid:2.5.4.24" + +msgid "x500UniqueIdentifier" +msgstr "x500UniqueIdentifier" + +msgid "urn:mace:dir:attribute-def:x500UniqueIdentifier" +msgstr "urn:mace:dir:attribute-def:x500UniqueIdentifier" + +msgid "urn:oid:2.5.4.45" +msgstr "urn:oid:2.5.4.45" + +# English string: Common name +msgid "facebook_cn" +msgstr "Повне ім'я" + +msgid "http://axschema.org/contact/country/home" +msgstr "http://axschema.org/contact/country/home" + +msgid "openid.sreg.country" +msgstr "openid.sreg.country" + +msgid "facebook.about_me" +msgstr "facebook.about_me" + +msgid "linkedin.summary" +msgstr "linkedin.summary" + +msgid "twitter.description" +msgstr "twitter.description" + +# English string: Display name +msgid "facebook.name" +msgstr "Відображуване ім'я" + +# English string: Display name +msgid "http://axschema.org/namePerson/friendly" +msgstr "Відображуване ім'я" + +# English string: Display name +msgid "openid.sreg.nickname" +msgstr "Відображуване ім'я" + +# English string: Display name +msgid "http://axschema.org/namePerson" +msgstr "Відображуване ім'я" + +# English string: Display name +msgid "openid.sreg.fullname" +msgstr "Відображуване ім'я" + +# English string: Display name +msgid "twitter.name" +msgstr "Відображуване ім'я" + +# English string: Display name +msgid "windowslive.displayName" +msgstr "Відображуване ім'я" + +# English string: Person's principal name at home organization +msgid "facebook_user" +msgstr "Ім'я керівника головної організації" + +# English string: Person's principal name at home organization +msgid "linkedin_user" +msgstr "Ім'я керівника головної організації" + +# English string: Person's principal name at home organization +msgid "twitter_screen_n_realm" +msgstr "Ім'я керівника головної організації" + +# English string: Person's principal name at home organization +msgid "windowslive_user" +msgstr "Ім'я керівника головної організації" + +# English string: Person's principal name at home organization +msgid "windowslive.userPrincipalName" +msgstr "Ім'я керівника головної організації" + +# English string: Persistent pseudonymous ID +msgid "facebook_targetedID" +msgstr "ID постійного псевдоніма" + +# English string: Persistent pseudonymous ID +msgid "linkedin_targetedID" +msgstr "IID постійного псевдоніма" + +# English string: Persistent pseudonymous ID +msgid "twitter_targetedID" +msgstr "ID постійного псевдоніма" + +# English string: Persistent pseudonymous ID +msgid "windowslive_targetedID" +msgstr "ID постійного псевдоніма" + +# English string: Fax number +msgid "http://axschema.org/contact/phone/fax" +msgstr "Номер факсу" + +# English string: Given name +msgid "facebook.first_name" +msgstr "Ім'я" + +# English string: Given name +msgid "linkedin.firstName" +msgstr "Ім'я" + +# English string: Given name +msgid "http://axschema.org/namePerson/first" +msgstr "Ім'я" + +# English string: Given name +msgid "windowslive.FirstName" +msgstr "Ім'я" + +# English string: Given name +msgid "windowslive.givenName" +msgstr "Ім'я" + +# English string: Home telephone +msgid "http://axschema.org/contact/phone/home" +msgstr "Домашній телефон" + +# English string: Locality +msgid "windowslive.Location" +msgstr "Район" + +# English string: Labeled URI +msgid "facebook.profile_url" +msgstr "Маркований URI (LabeledURI)" + +# English string: Labeled URI +msgid "twitter.url" +msgstr "Маркований URI (LabeledURI)" + +# English string: Mail +msgid "facebook.email" +msgstr "Пошта" + +# English string: Mail +msgid "http://axschema.org/contact/email" +msgstr "Пошта" + +# English string: Mail +msgid "openid.sreg.email" +msgstr "Пошта" + +# English string: Mail +msgid "windowslive_mail" +msgstr "Пошта" + +# English string: Mail +msgid "windowslive.mail" +msgstr "Пошта" + +# English string: Mobile +msgid "http://axschema.org/contact/phone/cell" +msgstr "Мобільний" + +# English string: Organization name +msgid "http://axschema.org/company/name" +msgstr "Назва організації" + +msgid "http://axschema.org/namePerson/prefix" +msgstr "http://axschema.org/namePerson/prefix" + +# English string: Postal code +msgid "http://axschema.org/contact/postalCode/home" +msgstr "Поштовий індекс" + +# English string: Postal code +msgid "openid.sreg.postcode" +msgstr "Поштовий індекс" + +# English string: Preferred language +msgid "facebook.locale" +msgstr "Бажана мова" + +# English string: Preferred language +msgid "http://axschema.org/pref/language" +msgstr "Бажана мова" + +# English string: Preferred language +msgid "openid.sreg.language" +msgstr "Бажана мова" + +# English string: Preferred language +msgid "twitter.lang" +msgstr "Бажана мова" + +# English string: Preferred language +msgid "windowslive.preferredLanguage" +msgstr "Бажана мова" + +# English string: Surname +msgid "facebook.last_name" +msgstr "Прізвище" + +# English string: Surname +msgid "linkedin.lastName" +msgstr "Прізвище" + +# English string: Surname +msgid "http://axschema.org/namePerson/last" +msgstr "Прізвище" + +# English string: Surname +msgid "windowslive.LastName" +msgstr "Прізвище" + +# English string: Surname +msgid "windowslive.surname" +msgstr "Прізвище" + +# English string: Telephone number +msgid "http://axschema.org/contact/phone/default" +msgstr "Номер телефону" + +# English string: Telephone number +msgid "http://axschema.org/contact/phone/business" +msgstr "Номер телефону" + +# English string: Title +msgid "linkedin.headline" +msgstr "Заголовок" + +# English string: Title +msgid "http://axschema.org/company/title" +msgstr "Заголовок" + +# English string: User ID +msgid "facebook.username" +msgstr "ID користувача" + +# English string: User ID +msgid "linkedin.id" +msgstr "ID користувача" + +# English string: User ID +msgid "twitter.screen_name" +msgstr "ID користувача" + +# English string: User ID +msgid "windowslive_uid" +msgstr "ID користувача" + +# English string: User ID +msgid "windowslive.id" +msgstr "ID користувача" diff --git a/locales/uk/LC_MESSAGES/messages.po b/locales/uk/LC_MESSAGES/messages.po new file mode 100644 index 0000000000..85d4f685cc --- /dev/null +++ b/locales/uk/LC_MESSAGES/messages.po @@ -0,0 +1,834 @@ +msgid "" +msgstr "" +"Project-Id-Version: SimpleSAMLphp\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Domain: messages\n" +"X-Generator: Poedit 3.7\n" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:189 +msgid "%MESSAGE%" +msgstr "%MESSAGE%" + +msgid "A service has requested you to authenticate yourself. Please enter your username and password in the form below." +msgstr "Служба запросила автентифікацію. Будь ласка, введіть ім’я користувача та пароль у форму нижче." + +msgid "ADFS IdP Metadata" +msgstr "Метадані постачальника ідентифікації IdP ADFS" + +msgid "ADFS Identity Provider (Hosted)" +msgstr "Постачальник ідентифікації ADFS (хостинговий)" + +msgid "ADFS SP Metadata" +msgstr "Метадані постачальника послуг (SP) ADFS" + +msgid "ADFS Service Provider (Remote)" +msgstr "Постачальник сервісів ADFS (віддалений)" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:89 +msgid "Admin password not set to a hashed value" +msgstr "Пароль адміністратора не встановлено у хешованому вигляді" + +msgid "Affiliation" +msgstr "Членство" + +msgid "Affiliation at home organization" +msgstr "Належність до домашньої організації" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:196 +msgid "An error occurred when trying to create the SAML request." +msgstr "Виникла помилка при формуванні SAML-запиту." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:218 +msgid "An error occurred when trying to process the Logout Request." +msgstr "Сталася помилка під час обробки запиту на вихід із системи." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:257 +msgid "An unhandled exception was thrown." +msgstr "Виникла необроблена виняткова ситуація." + +msgid "As you are in debug mode, you get to see the content of the message you are sending:" +msgstr "Оскільки ви перебуваєте в режимі налагодження, ви можете бачити вміст повідомлення, яке надсилаєте:" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:101 +msgid "Authentication aborted" +msgstr "Аутентифікацію перервано" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:191 +msgid "Authentication error in source %AUTHSOURCE%. The reason was: %REASON%" +msgstr "Помилка автентифікації в джерелі %AUTHSOURCE%. Причина: %REASON%" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:203 +msgid "Authentication failed: the certificate your browser sent is invalid or cannot be read" +msgstr "Помилка автентифікації: сертифікат, надісланий вашим браузером, є недійсним або не може бути прочитаний" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:258 +msgid "Authentication failed: the certificate your browser sent is unknown" +msgstr "Помилка автентифікації: сертифікат, надісланий вашим браузером, невідомий" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:230 +msgid "Authentication failed: your browser did not send any certificate" +msgstr "Помилка автентифікації: ваш браузер не надіслав жодного сертифіката" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:72 +msgid "Authentication source error" +msgstr "Помилка джерела автентифікаці" + +msgid "Authentication status" +msgstr "Статус автентифікації" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:73 +msgid "Bad request received" +msgstr "Отримано некоректний запит" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:77 +msgid "Bad request to discovery service" +msgstr "Некоректний запит до служби виявлення" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:74 +msgid "CAS Error" +msgstr "Помилка CAS" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:83 +msgid "Cannot retrieve session data" +msgstr "Не вдалося отримати дані сеансу" + +msgid "Certificates" +msgstr "Сертифікати" + +msgid "Change your home organization" +msgstr "Змініть вашу домашню організацію" + +msgid "Choose home organization" +msgstr "Виберіть домашню організацію" + +msgid "Choose your home organization" +msgstr "Виберить вашу домашню організацію" + +msgid "Common name" +msgstr "Звичайне ім’я" + +msgid "Completed" +msgstr "Виконано" + +msgid "Configuration check" +msgstr "Перевірка конфігурації" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:75 +msgid "Configuration error" +msgstr "Помилка конфігурації" + +msgid "Contact information:" +msgstr "Контактна інформація:" + +msgid "Converted metadata" +msgstr "Перетворені метадані" + +msgid "Copy to clipboard" +msgstr "Скопіювати до буфера обміну" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:78 +msgid "Could not create authentication response" +msgstr "Не вдалося створити відповідь автентифікації" + +msgid "Date of birth" +msgstr "Дата народження" + +msgid "Debug information" +msgstr "Налагоджувальна інформація" + +msgid "Debug information to be used by your support staff" +msgstr "Налагоджувальна інформація для використання вашою службою підтримки" + +msgid "Display name" +msgstr "Відображуване ім’я" + +msgid "Distinguished name (DN) of person's home organization" +msgstr "Унікальне ім’я (DN) домашньої організації особи" + +msgid "Distinguished name (DN) of person's primary Organizational Unit" +msgstr "Визначене ім’я (DN) головного організаційного підрозділу особи" + +msgid "Distinguished name (DN) of the person's home organizational unit" +msgstr "Визначене ім’я (DN) домашнього структурного підрозділу особи" + +msgid "Do you want to logout from all the services above?" +msgstr "Хочете завершити сеанс у всіх вищезгаданих сервісах?" + +msgid "Domain component (DC)" +msgstr "Компонент домену (DC)" + +msgid "Download the X509 certificates as PEM-encoded files." +msgstr "Завантажте сертифікати X509 у форматі файлів з кодуванням PEM." + +msgid "E-mail address:" +msgstr "Адреса електронної пошти:" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:260 +msgid "Either no user with the given username could be found, or the password you gave was wrong. Please check the username and try again." +msgstr "Користувача з вказаним іменем не знайдено, або введений пароль неправильний. Перевірте ім’я користувача та спробуйте ще раз." + +msgid "Enter your username and password" +msgstr "Введіть ім’я користувача та пароль" + +msgid "Entitlement regarding the service" +msgstr "Право на користування сервісом" + +msgid "Error" +msgstr "Помилка" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:76 +msgid "Error creating request" +msgstr "Помилка під час створення запиту" + +msgid "Error in this metadata entry" +msgstr "Помилка в цьому записі метаданих" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:84 +msgid "Error loading metadata" +msgstr "Помилка завантаження метаданих" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:95 +msgid "Error processing request from Service Provider" +msgstr "Помилка обробки запиту від постачальника послуг" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:94 +msgid "Error processing response from Identity Provider" +msgstr "Помилка обробки відповіді від постачальника ідентифікації" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:82 +msgid "Error processing the Logout Request" +msgstr "Помилка обробки запиту на вихід із системи" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:96 +msgid "Error received from Identity Provider" +msgstr "Отримано помилку від постачальника ідентифікації" + +msgid "Error report sent" +msgstr "Звіт про помилку надіслано" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:194 +msgid "Error when communicating with the CAS server." +msgstr "Помилка під час зв'язку з сервером CAS." + +msgid "Explain what you did when this error occurred..." +msgstr "Поясніть, що ви зробили, коли сталася ця помилка..." + +msgid "Fax number" +msgstr "Номер факса" + +msgid "Format" +msgstr "Формат" + +msgid "Given name" +msgstr "Ім'я" + +msgid "Go back to SimpleSAMLphp installation page" +msgstr "Повернутися на сторінку встановлення SimpleSAMLphp" + +msgid "Go back to the file list" +msgstr "Повернутися до списку файлів" + +msgid "Group membership" +msgstr "Членство в групі" + +msgid "Hello, Untranslated World!" +msgstr "Привіт, Untranslated World!" + +msgid "Help desk homepage" +msgstr "Домашня сторінка служби підтримки" + +msgid "Help! I don't remember my password." +msgstr "Допоможіть! Я не пам’ятаю свій пароль." + +msgid "Here is the metadata that SimpleSAMLphp has generated for you. You may send this metadata document to trusted partners to setup a trusted federation." +msgstr "Ось метадані, які SimpleSAMLphp згенерував для вас. Ви можете надіслати цей документ метаданих довіреним партнерам для налаштування довіреної федерації." + +msgid "Hi, this is the status page of SimpleSAMLphp. Here you can see if your session is timed out, how long it lasts until it times out and all the attributes that are attached to your session." +msgstr "Привіт! Це сторінка стану SimpleSAMLphp. Тут ви можете побачити, чи завершено вашу сесію, скільки часу залишилося до її завершення, а також усі атрибути, пов’язані з вашою сесією." + +msgid "Home organization domain name" +msgstr "Доменне ім’я домашньої організації" + +msgid "Home postal address" +msgstr "Домашня поштова адреса" + +msgid "Home telephone" +msgstr "Домашній телефон" + +msgid "How to get help" +msgstr "Як отримати допомогу" + +msgid "Identity assurance profile" +msgstr "Профіль гарантії автентичності особи" + +msgid "Identity number assigned by public authorities" +msgstr "Ідентифікаційний номер, присвоєний державними органами" + +msgid "If you report this error, please also report this tracking number which makes it possible to locate your session in the logs available to the system administrator:" +msgstr "Якщо ви повідомляєте про цю помилку, будь ласка, також вкажіть цей номер відстеження, який дозволяє знайти вашу сесію в журналах, доступних системному адміністратору:" + +msgid "In SAML 2.0 Metadata XML format:" +msgstr "У форматі XML метаданих SAML 2.0:" + +msgid "In SimpleSAMLphp flat file format - use this if you are using a SimpleSAMLphp entity on the other side:" +msgstr "У форматі плоского файлу SimpleSAMLphp — використовуйте це, якщо на іншій стороні використовується сутність SimpleSAMLphp:" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:102 +msgid "Incorrect username or password" +msgstr "Неправильне ім’я користувача або пароль" + +msgid "Incorrect username or password." +msgstr "Неправильне ім’я користувача або пароль." + +msgid "Information about your current session" +msgstr "Інформація про вашу поточну сесію" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:79 +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:93 +msgid "Invalid certificate" +msgstr "Недійсний сертифікат" + +msgid "JPEG Photo" +msgstr "Фотографія в форматі JPEG" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:80 +msgid "LDAP Error" +msgstr "Помилка LDAP" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:206 +msgid "LDAP is the user database, and when you try to login, we need to contact an LDAP database. An error occurred when we tried it this time." +msgstr "LDAP — це база даних користувачів, і коли ви намагаєтесь увійти в систему, нам потрібно звернутися до бази даних LDAP. Цього разу під час спроби виникла помилка." + +msgid "Labeled URI" +msgstr "Маркований URI (LabeledURI)" + +msgid "Language" +msgstr "Мова" + +msgid "Legal name" +msgstr "Офіційна назва" + +msgid "Local identity number" +msgstr "Місцевий ідентифікаційний номер" + +msgid "Locality" +msgstr "Місцевість" + +msgid "Logged out" +msgstr "Вихід виконано" + +msgid "Logging out of the following services:" +msgstr "Вихід із таких сервісів:" + +msgid "Logging out..." +msgstr "Вихід із системи..." + +msgid "Login" +msgstr "Вхід" + +msgid "Login at" +msgstr "Вхід на" + +msgid "Logout" +msgstr "Вийти" + +msgid "Logout failed" +msgstr "Не вдалося вийти із системи" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:81 +msgid "Logout information lost" +msgstr "Інформацію про вихід втрачено" + +msgid "Mail" +msgstr "Пошта" + +msgid "Manager" +msgstr "Управляючий" + +msgid "Message" +msgstr "Повідомлення" + +msgid "Metadata" +msgstr "Метадані" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:85 +msgid "Metadata not found" +msgstr "Метадані не знайдені" + +msgid "Metadata overview" +msgstr "Загальна інформація про метадані" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:90 +msgid "Method not allowed" +msgstr "Цей метод не підтримується" + +msgid "Mobile" +msgstr "Мобільний" + +msgid "Next" +msgstr "Далі" + +msgid "Nickname" +msgstr "Псевдонім" + +msgid "No" +msgstr "Ні" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:88 +msgid "No RelayState" +msgstr "Відсутній параметр RelayState" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:71 +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:97 +msgid "No SAML message provided" +msgstr "Не надано повідомлення SAML" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:98 +msgid "No SAML request provided" +msgstr "Запит SAML не надано" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:70 +msgid "No SAML response provided" +msgstr "Відсутня відповідь SAML" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:86 +msgid "No access" +msgstr "Доступ заборонено" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:87 +msgid "No certificate" +msgstr "Сертифікат відсутній" + +msgid "No errors found." +msgstr "Помилок не виявлено." + +msgid "No identity providers found. Cannot continue." +msgstr "Постачальників ідентифікації не знайдено. Неможливо продовжити." + +msgid "No, cancel" +msgstr "Ні, скасувати" + +msgid "No, only %SP%" +msgstr "Ні, тільки для служби %SP%" + +msgid "Notices" +msgstr "Повідомлення" + +msgid "ORCID researcher identifiers" +msgstr "Унікальні ідентифікатори ORCID для дослідників" + +msgid "On hold" +msgstr "У режимі очікування" + +msgid "One or more of the services you are logged into do not support logout. To ensure that all your sessions are closed, you are encouraged to close your webbrowser." +msgstr "Одна або кілька служб, у які ви увійшли, не підтримують вихід із системи. Щоб переконатися, що всі ваші сеанси завершено, рекомендується закрити ваш браузер." + +msgid "Optional fields" +msgstr "Необов’язкові поля" + +msgid "Optionally enter your email address, for the administrators to be able contact you for further questions about your issue:" +msgstr "За бажанням введіть свою електронну адресу, щоб адміністратори могли зв’язатися з вами для уточнення деталей щодо вашої проблеми:" + +msgid "Options missing from config file" +msgstr "Параметри відсутні у файлі конфігурації" + +msgid "Organization" +msgstr "Організація" + +msgid "Organization name" +msgstr "Назва організації" + +msgid "Organization's legal name" +msgstr "Юридична назва організації" + +msgid "Organizational homepage" +msgstr "Домашня сторінка організації" + +msgid "Organizational number" +msgstr "Номер організації" + +msgid "Organizational unit" +msgstr "Підрозділ організації" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:90 +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:91 +msgid "Page not found" +msgstr "Сторінку не знайдено" + +msgid "Parse" +msgstr "Аналіз" + +msgid "Password" +msgstr "Пароль" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:92 +msgid "Password not set" +msgstr "Пароль не встановлено" + +msgid "Persistent pseudonymous ID" +msgstr "Cтійкий псевдонімний ідентифікатор ID" + +msgid "Person's principal name at home organization" +msgstr "Ім'я керівника головної організації" + +msgid "Please select the identity provider where you want to authenticate:" +msgstr "Будь ласка, виберіть постачальника ідентифікації, через якого ви хочете пройти автентифікацію:" + +msgid "Post office box" +msgstr "Абонентська скринька" + +msgid "Postal address" +msgstr "Поштова адреса" + +msgid "Postal code" +msgstr "Поштовий індекс" + +msgid "Preferred language" +msgstr "Бажана мова" + +msgid "Primary affiliation" +msgstr "Основна приналежність" + +msgid "Private information elements" +msgstr "Елементи особистої інформації" + +msgid "Remember" +msgstr "Запам'ятати" + +msgid "Remember me" +msgstr "Запам'ятати мене" + +msgid "Remember my choice" +msgstr "Запам’ятати мій вибір" + +msgid "Remember my username" +msgstr "Запам’ятати моє ім'я користувача" + +msgid "Report errors" +msgstr "Повідомлення про помилки" + +msgid "Required fields" +msgstr "Обов'язкові поля" + +msgid "Return to service" +msgstr "Повернутися до сервісу" + +msgid "SAML 2.0 Identity Provider (Hosted)" +msgstr "Постачальник ідентифікації SAML 2.0 (хостинговий)" + +msgid "SAML 2.0 Identity Provider (Remote)" +msgstr "Постачальник ідентифікації SAML 2.0 (віддалений)" + +msgid "SAML 2.0 SP Demo Example" +msgstr "Демонстраційний приклад SAML 2.0 SP" + +msgid "SAML 2.0 SP Metadata" +msgstr "Метадані постачальника послуг SAML 2.0" + +msgid "SAML 2.0 Service Provider (Hosted)" +msgstr "Постачальник сервісів SAML 2.0 (хостинговий)" + +msgid "SAML Subject" +msgstr "Об'єкт SAML" + +msgid "Select" +msgstr "Вибрати" + +msgid "Select configuration file to check:" +msgstr "Виберіть файл конфігурації для перевірки:" + +msgid "Select your identity provider" +msgstr "Виберіть свого постачальника ідентифікації" + +msgid "Send e-mail to help desk" +msgstr "Надіслати електронного листа до служби підтримки" + +msgid "Send error report" +msgstr "Надіслати звіт про помилку" + +msgid "Sending message" +msgstr "Надіслати повідомлення" + +msgid "Service Provider" +msgstr "Постачальник послуг" + +msgid "Session size: %SIZE%" +msgstr "Розмір сесії: %SIZE%" + +msgid "Shib 1.3 IdP Metadata" +msgstr "Метадані IdP Shib 1.3" + +msgid "Shib 1.3 Identity Provider (Hosted)" +msgstr "Постачальник ідентифікації Shib 1.3 (хостинговий)" + +msgid "Shib 1.3 Identity Provider (Remote)" +msgstr "Постачальник ідентифікації Shib 1.3 (віддалений)" + +msgid "Shib 1.3 SP Metadata" +msgstr "Метаданные сервис провайдера Shib 1.3 SP" + +msgid "Shib 1.3 Service Provider (Hosted)" +msgstr "Постачальник сервісів Shib 1.3 (хостинговий)" + +msgid "Shib 1.3 Service Provider (Remote)" +msgstr "Постачальник сервісів Shib 1.3 (віддалений)" + +msgid "Shibboleth demo" +msgstr "Тестова версія Shibboleth" + +msgid "SimpleSAMLphp Diagnostics" +msgstr "Діагностика SimpleSAMLphp" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:195 +msgid "SimpleSAMLphp appears to be misconfigured." +msgstr "Здається, SimpleSAMLphp неправильно налаштований." + +msgid "SimpleSAMLphp error" +msgstr "Помилка SimpleSAMLphp" + +msgid "Since your browser does not support Javascript, you must press the button below to proceed." +msgstr "Оскільки ваш браузер не підтримує JavaScript, вам потрібно натиснути кнопку нижче, щоб продовжити." + +msgid "Some error occurred" +msgstr "Виникла помилка" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:89 +msgid "State information lost" +msgstr "Втрачено дані про поточний стан" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:234 +msgid "State information lost, and no way to restart the request" +msgstr "Інформацію про стан втрачено, і немає способу перезапустити запит" + +msgid "Street" +msgstr "Вулиця" + +msgid "Submit message" +msgstr "Надіслати повідомлення" + +msgid "Superfluous options in config file" +msgstr "Зайві параметри у конфігураційному файлі" + +msgid "Surname" +msgstr "Прізвище" + +msgid "Telephone number" +msgstr "Номер телефону" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:246 +msgid "The Identity Provider responded with an error. (The status code in the SAML Response was not success)" +msgstr "Постачальник ідентифікації відповів з помилкою. (Код статусу в SAML-відповіді не є успішним)" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:259 +msgid "The authentication was aborted by the user" +msgstr "Автентифікацію було перервано користувачем" + +msgid "The debug information below may be of interest to the administrator / help desk:" +msgstr "Наведена нижче інформація може зацікавити адміністратора або службу підтримки:" + +msgid "The error report has been sent to the administrators." +msgstr "Звіт про помилку надіслано адміністраторам." + +msgid "The following fields was not recognized" +msgstr "Наступні поля не були розпізнані" + +msgid "The following optional fields was not found" +msgstr "Наступні необов’язкові поля не знайдено" + +msgid "The following required fields was not found" +msgstr "Наступні обов’язкові поля не знайдено" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:235 +msgid "The given page was not found. The URL was: %URL%" +msgstr "Задану сторінку не знайдено. URL-адреса: %URL%" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:236 +msgid "The given page was not found. The reason was: %REASON% The URL was: %URL%" +msgstr "Задану сторінку не знайдено. Причина: %REASON% URL-адреса: %URL%" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:209 +msgid "The information about the current logout operation has been lost. You should return to the service you were trying to log out from and try to log out again. This error can be caused by the logout information expiring. The logout information is stored for a limited amount of time - usually a number of hours. This is longer than any normal logout operation should take, so this error may indicate some other error with the configuration. If the problem persists, contact your service provider." +msgstr "Інформація про поточну операцію виходу була втрачена. Вам слід повернутися до сервісу, з якого ви намагалися вийти, і спробувати вийти ще раз. Ця помилка може бути спричинена закінченням терміну дії інформації про вихід. Інформація про вихід зберігається протягом обмеженого періоду часу - зазвичай кілька годин. Це довше, ніж має тривати будь-яка звичайна операція виходу з системи, тому ця помилка може вказувати на якусь іншу помилку у конфігурації. Якщо проблема не зникає, зверніться до свого постачальника послуг." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:231 +msgid "The initiator of this request did not provide a RelayState parameter indicating where to go next." +msgstr "Ініціатор цього запиту не надав параметр RelayState, який вказує, куди слід перейти далі." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:197 +msgid "The parameters sent to the discovery service were not according to specifications." +msgstr "Параметри, надіслані до служби виявлення, не відповідають технічним специфікаціям." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:194 +msgid "The password in the configuration (auth.adminpassword) is not a hashed value. Full details on how to fix this are supplied at https://github.com/simplesamlphp/simplesamlphp/wiki/Frequently-Asked-Questions-(FAQ)#failed-to-login-to-the-admin-page-with-and-error-message-admin-password-not-set-to-a-hashed-value" +msgstr "Пароль у конфігурації (auth.adminpassword) не є хешованим значенням. Повні інструкції щодо виправлення цієї помилки наведені на сторінці https://github.com/simplesamlphp/simplesamlphp/wiki/Frequently-Asked-Questions-(FAQ)#failed-to-login-to-the-admin-page-with-and-error-message-admin-password-not-set-to-a-hashed-value" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:238 +msgid "The password in the configuration (auth.adminpassword) is not changed from the default value. Please edit the configuration file." +msgstr "Пароль у конфігурації (auth.adminpassword) не змінено зі значення за замовчуванням. Будь ласка, відредагуйте конфігураційний файл." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:193 +msgid "There is an error in the request to this page. The reason was: %REASON%" +msgstr "Сталася помилка у запиті до цієї сторінки. Причина: %REASON%" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:222 +msgid "There is some misconfiguration of your SimpleSAMLphp installation. If you are the administrator of this service, you should make sure your metadata configuration is correctly setup." +msgstr "У вашій інсталяції SimpleSAMLphp є деякі помилки у конфігурації. Якщо ви є адміністратором цього сервісу, переконайтеся, що конфігурація метаданих налаштована правильно." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:243 +msgid "This Identity Provider received an Authentication Request from a Service Provider, but an error occurred when trying to process the request." +msgstr "Цей постачальник Ідентифікації отримав запит на автентифікацію від Постачальника послуг, але під час обробки запиту сталася помилка." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:227 +msgid "This endpoint is not enabled. Check the enable options in your configuration of SimpleSAMLphp." +msgstr "Цю кінцеву точку не увімкнено. Перевірте опції увімкнення у вашій конфігурації SimpleSAMLphp." + +msgid "This error probably is due to some unexpected behaviour or to misconfiguration of SimpleSAMLphp. Contact the administrator of this login service, and send them the error message above." +msgstr "Ця помилка, ймовірно, пов'язана з неочікуваною поведінкою або неправильною конфігурацією SimpleSAMLphp. Зверніться до адміністратора цієї служби входу і надішліть йому повідомлення про помилку, що наведене вище." + +msgid "Title" +msgstr "Заголовок" + +msgid "To look at the details for an SAML entity, click on the SAML entity header." +msgstr "Щоб переглянути деталі SAML-об'єкта, натисніть на заголовок SAML-об'єкта." + +msgid "Tracking number" +msgstr "Номер для відстеження" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:226 +#, php-format +msgid "Unable to locate metadata for %ENTITYID%" +msgstr "Неможливо знайти метадані для %ENTITYID%" + +msgid "Unable to log out of one or more services. To ensure that all your sessions are closed, you are encouraged to close your webbrowser." +msgstr "Не вдалося вийти з одного або кількох сервісів. Щоб переконатися, що всі ваші сеанси завершено, рекомендується закрити веббраузер." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:99 +msgid "Unhandled exception" +msgstr "Необроблений виняток" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:100 +msgid "Unknown certificate" +msgstr "Невідомий сертифікат" + +msgid "User ID" +msgstr "ID користувача" + +msgid "User's password hash" +msgstr "Хеш пароля користувача" + +msgid "Username" +msgstr "Ім'я користувача" + +msgid "WS-Fed SP Demo Example" +msgstr "Демонстраційний приклад WS-Fed SP" + +msgid "WS-Federation Identity Provider (Remote)" +msgstr "Постачальник ідентифікації WS-Federation (віддалений)" + +msgid "WS-Federation Service Provider (Hosted)" +msgstr "Постачальник послуг WS-Federation (хостинговий)" + +msgid "Warning" +msgstr "Увага" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:242 +msgid "We did not accept the response sent from the Identity Provider." +msgstr "Відповідь, надіслана постачальником ідентифікації, не була прийнята." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:200 +msgid "When this identity provider tried to create an authentication response, an error occurred." +msgstr "Виникла помилка під час формування відповіді автентифікації цим постачальником ідентитифікації." + +msgid "Without your username and password you cannot authenticate yourself for access to the service. There may be someone that can help you. Consult the help desk at your organization!" +msgstr "Без вашого імені користувача та пароля ви не зможете пройти автентифікацію для доступу до сервісу. Можливо, хтось зможе вам допомогти. Зверніться до служби підтримки у вашій організації!" + +msgid "World" +msgstr "Світ" + +msgid "XML metadata" +msgstr "XML метадані" + +msgid "Yes, all services" +msgstr "Так, для всіх сервісів" + +msgid "Yes, continue" +msgstr "Так, продовжити" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:187 +msgid "You accessed the Artifact Resolution Service interface, but did not provide a SAML ArtifactResolve message. Please note that this endpoint is not intended to be accessed directly." +msgstr "Ви звернулися до інтерфейсу служби розв’язання артефактів, але не надали повідомлення SAML ArtifactResolve. Зверніть увагу, що ця кінцева точка не призначена для прямого доступу." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:183 +msgid "You accessed the Assertion Consumer Service interface, but did not provide a SAML Authentication Response. Please note that this endpoint is not intended to be accessed directly." +msgstr "Ви звернулися до інтерфейсу служби отримання тверджень (Assertion Consumer Service), але не надали автентифікаційну відповідь SAML. Зверніть увагу, що ця кінцева точка не призначена для прямого доступу." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:253 +msgid "You accessed the Single Sign On Service interface, but did not provide a SAML Authentication Request. Please note that this endpoint is not intended to be accessed directly." +msgstr "Ви звернулися до інтерфейсу служби єдиного входу (Single Sign-On), але не надали автентифікаційний запит SAML. Зверніть увагу, що ця кінцева точка не призначена для прямого доступу." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:249 +msgid "You accessed the SingleLogoutService interface, but did not provide a SAML LogoutRequest or LogoutResponse. Please note that this endpoint is not intended to be accessed directly." +msgstr "Ви звернулися до інтерфейсу служби єдиного виходу (SingleLogoutService), але не надали запит на вихід або відповідь SAML. Зверніть увагу, що ця кінцева точка не призначена для прямого доступу." + +msgid "You are about to send a message. Hit the submit message button to continue." +msgstr "Ви збираєтеся надіслати повідомлення. Натисніть кнопку Надіслати повідомлення, щоб продовжити." + +msgid "You are about to send a message. Hit the submit message link to continue." +msgstr "Ви збираєтесь надіслати повідомлення. Натисніть посилання Надіслати повідомлення, щоб продовжити." + +msgid "You are also logged in on these services:" +msgstr "Ви також авторизовані в наступних сервісах:" + +msgid "You are now accessing a pre-production system. This authentication setup is for testing and pre-production verification only. If someone sent you a link that pointed you here, and you are not a tester you probably got the wrong link, and should not be here." +msgstr "Ви зараз отримали доступ до тестової системи.Ця система автентифікації призначена лише для тестування та перевірки попередньої версії. Якщо хтось надіслав вам посилання, яке привело вас сюди, і ви не є тестувальником, ймовірно, це неправильне посилання, і вам < не слід бути тут." + +msgid "You are now successfully logged out from %SP%." +msgstr "Ви успішно вийшли з системи %SP%." + +msgid "You can get the metadata xml on a dedicated URL:" +msgstr "Ви можете отримати XML-метадані за спеціальним посиланням URL:" + +msgid "You can turn off debug mode in the global SimpleSAMLphp configuration file config/config.php." +msgstr "Ви можете вимкнути режим налагодження у глобальному конфігураційному файлі SimpleSAMLphp -config/config.php." + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:241 +msgid "You did not present a valid certificate." +msgstr "Ви не надали дійсний сертифікат." + +msgid "You have been logged out." +msgstr "Ви вийшли з системи." + +msgid "You have chosen %HOMEORG% as your home organization. If this is wrong you may choose another one." +msgstr "Ви обрали %HOMEORG% як вашу домашню організацію. Якщо це помилка, ви можете вибрати іншу." + +msgid "You have previously chosen to authenticate at" +msgstr "Раніше ви обрали автентифікацію через" + +msgid "You have successfully logged out from all services listed above." +msgstr "Ви успішно вийшли з усіх перелічених вище сервісів." + +msgid "You sent something to the login page, but for some reason the password was not sent. Try again please." +msgstr "Ви надіслали запит на сторінку входу, але з якоїсь причини пароль не був переданий. Спробуйте ще раз, будь ласка." + +msgid "Your attributes" +msgstr "Ваші атрибути" + +#: /home/runner/work/simplesamlphp/simplesamlphp/src/SimpleSAML/Error/ErrorCodes.php:219 +msgid "Your session data cannot be retrieved right now due to technical difficulties. Please try again in a few minutes." +msgstr "Наразі неможливо отримати дані вашої сесії через технічні неполадки. Спробуйте ще раз за кілька хвилин." + +msgid "Your session is valid for %remaining% seconds from now." +msgstr "Ваша сесія буде дійсною ще протягом %remaining% секунд." + +msgid "[Preferred choice]" +msgstr "[Бажаний вибір]" + +msgid "not set" +msgstr "не встановлено" diff --git a/modules/admin/locales/uk/LC_MESSAGES/admin.po b/modules/admin/locales/uk/LC_MESSAGES/admin.po new file mode 100644 index 0000000000..4fb5258a7b --- /dev/null +++ b/modules/admin/locales/uk/LC_MESSAGES/admin.po @@ -0,0 +1,372 @@ +msgid "" +msgstr "" +"Project-Id-Version: SimpleSAMLphp\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Domain: admin\n" +"X-Generator: Poedit 3.7\n" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:422 +msgid "The configuration uses the default secret salt. Make sure to modify the secretsalt option in the SimpleSAMLphp configuration in production environments. Read more about the maintenance of SimpleSAMLphp." +msgstr "Конфігурація використовує типовий secret salt. Переконайтеся, що параметр secretsalt змінено у конфігурації SimpleSAMLphp у робочому середовищі. Дізнатися більше про конфігурацію SimpleSAMLphp." + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:412 +msgid "You are not using HTTPS to protect communications with your users. HTTP works fine for testing purposes, but in a production environment you should use HTTPS. Read more about the maintenance of SimpleSAMLphp." +msgstr "Ви не використовуєте HTTPS для захисту зв’язку з користувачами.для захисту зв’язку з користувачами.HTTP підходить для тестових цілей, але у робочому середовищі слід використовувати HTTPS. Дізнайтеся більше про обслуговування SimpleSAMLphp ]" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:175 +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:176 +msgid "ADFS IdP metadata" +msgstr "ADFS IdP metadata" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:173 +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:174 +msgid "ADFS SP metadata" +msgstr "ADFS SP metadata" + +msgid "An error occurred" +msgstr "Виникла помилка" + +msgid "Authentication data" +msgstr "Дані автентифікації" + +msgid "Back" +msgstr "Назад" + +msgid "Certificates" +msgstr "Сертифікати" + +msgid "Checking your PHP installation" +msgstr "Перевірка інсталяції PHP" + +msgid "Checks" +msgstr "Перевірки" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Menu.php:33 +msgid "Configuration" +msgstr "Налаштування" + +msgid "Content of jpegPhoto attribute" +msgstr "Вміст атрибута jpegPhoto" + +msgid "Converted metadata" +msgstr "Перетворені метадані" + +msgid "Copy to clipboard" +msgstr "Копіювати в буфер обміну" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:228 +msgid "Date/Time Extension" +msgstr "Розширення дати/часу" + +msgid "Deprecated" +msgstr "Застарілий" + +msgid "Details" +msgstr "Деталі" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:133 +msgid "Diagnostics on hostname, port and protocol" +msgstr "Діагностика імені хоста, порту та протоколу" + +msgid "EntityID" +msgstr "Ідентифікатор об'єкта EntityID" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Menu.php:41 +msgid "Federation" +msgstr "Федерація" + +msgid "Format" +msgstr "Формат" + +msgid "Go back to SimpleSAMLphp installation page" +msgstr "Повернутися на сторінку встановлення SimpleSAMLphp" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:234 +msgid "Hashing function" +msgstr "Функція хешування" + +msgid "Hi, this is the status page of SimpleSAMLphp. Here you can see if your session is timed out, how long it lasts until it times out and all the attributes that are attached to your session." +msgstr "Вітаємо! Це сторінка стану SimpleSAMLphp. Тут ви можете перевірити, чи завершено вашу сесію, скільки часу залишилося до її завершення та всі атрибути, що пов’язані з вашою сесією." + +msgid "Hosted IdP metadata present" +msgstr "Метадані розміщеного IdP наявні" + +msgid "Hosted entities" +msgstr "Хостовані об'єкти" + +msgid "In SAML 2.0 Metadata XML format:" +msgstr "У форматі XML метаданих SAML 2.0:" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:137 +msgid "Information on your PHP installation" +msgstr "Інформація про вашу інсталяцію PHP" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:270 +msgid "JSON support" +msgstr "Підтримка JSON" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:314 +msgid "LDAP extension" +msgstr "Розширення LDAP" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:313 +msgid "LDAP extension (required if an LDAP backend is used)" +msgstr "Розширення LDAP (потрібне, якщо використовується бекенд LDAP)" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:184 +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:485 +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:106 +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:149 +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Test.php:156 +msgid "Log out" +msgstr "Вийти" + +msgid "Logged out" +msgstr "Вийшов" + +msgid "Logout" +msgstr "Вийти" + +msgid "Look up metadata for entity:" +msgstr "Переглянути метадані для сутності:" + +msgid "Matching key-pair for signing assertions" +msgstr "Відповідна пара ключів для підписування тверджень" + +msgid "Matching key-pair for signing assertions (rollover key)" +msgstr "Відповідна пара ключів для підписування тверджень (резервна пара ключів)" + +msgid "Matching key-pair for signing metadata" +msgstr "Відповідна пара ключів для підписання метаданих" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:344 +msgid "Memcache or Memcached extension" +msgstr "Розширення Memcache або Memcached" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:341 +msgid "Memcache or Memcached extension (required if the memcache backend is used)" +msgstr "Розширення Memcache або Memcached (потрібне для роботи з бекендом memcache)" + +msgid "Metadata" +msgstr "Метадані" + +msgid "Metadata parser" +msgstr "Аналізатор метаданих" + +msgid "Modules" +msgstr "Модулі" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:282 +msgid "Multibyte String extension" +msgstr "Розширення для роботи з багатобайтовими рядками" + +msgid "No file selected." +msgstr "Файл не вибрано." + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:246 +msgid "OpenSSL" +msgstr "OpenSSL" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:306 +msgid "PDO Extension (required if a database backend is used)" +msgstr "Розширення PDO (необхідне, якщо використовується базовий бекенд бази даних)" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:307 +msgid "PDO extension" +msgstr "Розширення PDO" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:211 +#, no-php-format +msgid "PHP %minimum% or newer is needed. You are running: %current%" +msgstr "Потрібна версія PHP %minimum% або новіша. Поточна версія: %current%" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:264 +msgid "PHP intl extension" +msgstr "Розширення PHP intl" + +msgid "Parse" +msgstr "Аналізувати" + +msgid "Radius extension" +msgstr "Розширення RADIUS" + +msgid "Radius extension (required if a radius backend is used)" +msgstr "Розширення RADIUS (необхідне, якщо використовується бекенд RADIUS)" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:258 +msgid "Regular expression support" +msgstr "Підтримка регулярних виразів" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:171 +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:172 +msgid "SAML 2.0 IdP metadata" +msgstr "Метадані IdP для SAML 2.0" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:169 +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:170 +msgid "SAML 2.0 SP metadata" +msgstr "Метадані SP для SAML 2.0" + +msgid "SAML Metadata" +msgstr "Метадані SAML" + +msgid "SAML Subject" +msgstr "Об'єкт SAML" + +msgid "Search" +msgstr "Пошук" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:300 +msgid "Session extension" +msgstr "Розширення сесії" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:299 +msgid "Session extension (required if PHP sessions are used)" +msgstr "Розширення сесій (необхідне, якщо використовуються PHP-сесії)" + +msgid "SimpleSAMLphp Metadata" +msgstr "Метадані SimpleSAMLphp" + +msgid "SimpleSAMLphp Show Metadata" +msgstr "SimpleSAMLphp — Показати метадані" + +msgid "SimpleSAMLphp installation page" +msgstr "Сторінка інсталяції SimpleSAMLphp" + +msgid "SimpleSAMLphp is installed in:" +msgstr "SimpleSAMLphp встановлюється в:" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:276 +msgid "Standard PHP library (SPL)" +msgstr "Стандартна бібліотека PHP (SPL)" + +msgid "Technical information" +msgstr "Технічна інформація" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Menu.php:37 +msgid "Test" +msgstr "Тест" + +msgid "Test Authentication Sources" +msgstr "Перевірити джерела автентифікації" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:430 +msgid "The \"secretsalt\" configuration option may not contain a `%` sign." +msgstr "Параметр конфігурації 'secretsalt' не може містити символ '%'." + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:366 +msgid "The technicalcontact_email configuration option should be set" +msgstr "Параметр конфігурації technicalcontact_email має бути встановлений" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:372 +msgid "The auth.adminpassword configuration option must be set" +msgstr "Параметр конфігурації auth.adminpassword має бути встановлений" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:437 +msgid "The cURL PHP extension is missing. Cannot check for SimpleSAMLphp updates." +msgstr "Розширення cURL для PHP відсутнє. Неможливо перевірити оновлення SimpleSAMLphp." + +msgid "Tools" +msgstr "Інструменти" + +msgid "Trusted entities" +msgstr "Довірені суб’єкти" + +msgid "Type:" +msgstr "Тип:" + +msgid "Use this if you are using a SimpleSAMLphp entity on the other side:" +msgstr "\"Використовуйте це, якщо на іншій стороні використовується суб’єкт SimpleSAMLphp:" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:252 +msgid "XML DOM" +msgstr "XML DOM" + +msgid "XML metadata" +msgstr "Метадані XML" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Federation.php:164 +msgid "XML to SimpleSAMLphp metadata converter" +msgstr "Конвертер метаданих XML в SimpleSAMLphp" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:462 +msgid "You are running an outdated version of SimpleSAMLphp. Please update to the latest version as soon as possible." +msgstr "\"Ви використовуєте застарілу версію SimpleSAMLphp. Будь ласка, оновіть її до останньої версії якнайшвидше." + +msgid "You are running version %version%." +msgstr "Ви використовуєте версію %version%." + +msgid "You can get the metadata XML on a dedicated URL:" +msgstr "Ви можете отримати XML метаданих за спеціальною URL-адресою:" + +msgid "You have been logged out." +msgstr "Ви вийшли з системи." + +msgid "You have the following modules installed" +msgstr "У вас встановлені наступні модулі" + +msgid "Your attributes" +msgstr "Ваші атрибути" + +msgid "Your session is valid for %remaining% seconds from now." +msgstr "Ваша сесія буде дійсною ще протягом %remaining% секунд." + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:240 +msgid "ZLib" +msgstr "ZLib" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:288 +msgid "cURL (might be required by some modules)" +msgstr "cURL (може знадобитися деяким модулям)" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:291 +msgid "cURL (required if automatic version checks are used, also by some modules)" +msgstr "cURL (потрібен, якщо використовується автоматична перевірка версії; також може знадобитися деяким модулям)" + +msgid "default" +msgstr "за замовчуванням" + +msgid "disabled" +msgstr "вимкнено" + +msgid "enabled" +msgstr "увімкнено" + +msgid "expired" +msgstr "термін дії закінчився" + +msgid "expires" +msgstr "закінчується" + +msgid "means the module is not enabled" +msgstr "означає, що модуль не увімкнено" + +msgid "new" +msgstr "новий" + +msgid "not set" +msgstr "не встановлено" + +msgid "optional" +msgstr "необов’язковий" + +msgid "or select a file:" +msgstr "або вибрати файл:" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:333 +msgid "predis/predis (required if the redis data store is used)" +msgstr "predis/predis (необхідний у разі використання Redis як сховища даних)" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/admin/src/Controller/Config.php:334 +msgid "predis/predis library" +msgstr "predis/predis бібліотека" + +msgid "required" +msgstr "необхідний" diff --git a/modules/admin/src/Controller/Config.php b/modules/admin/src/Controller/Config.php index 235a0ec98f..09b26c031b 100644 --- a/modules/admin/src/Controller/Config.php +++ b/modules/admin/src/Controller/Config.php @@ -5,21 +5,21 @@ namespace SimpleSAML\Module\admin\Controller; use SimpleSAML\Configuration; +use SimpleSAML\Event\Dispatcher\ModuleEventDispatcherFactory; use SimpleSAML\Locale\Translate; +use SimpleSAML\Logger; use SimpleSAML\Module; +use SimpleSAML\Module\admin\Event\ConfigPageEvent; +use SimpleSAML\Module\admin\Event\SanityCheckEvent; use SimpleSAML\Session; use SimpleSAML\Utils; use SimpleSAML\XHTML\Template; -use Symfony\Component\HttpFoundation\{Request, Response, StreamedResponse}; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; -use function curl_close; -use function curl_exec; -use function curl_getinfo; -use function curl_init; -use function curl_setopt; use function explode; use function function_exists; -use function json_decode; use function ltrim; use function phpversion; use function version_compare; @@ -33,9 +33,10 @@ */ class Config { - public const LATEST_VERSION_STATE_KEY = 'core:latest_simplesamlphp_version'; + public const string LATEST_VERSION_STATE_KEY = 'admin:latest_simplesamlphp_version'; + + public const string RELEASES_API = 'https://api.github.com/repos/simplesamlphp/simplesamlphp/releases/latest'; - public const RELEASES_API = 'https://api.github.com/repos/simplesamlphp/simplesamlphp/releases/latest'; /** @var \SimpleSAML\Utils\Auth */ protected Utils\Auth $authUtils; @@ -142,6 +143,10 @@ public function main(/** @scrutinizer ignore-unused */ Request $request): Templa 'modulelist' => $this->getModuleList(), ]; + $eventDispatcher = ModuleEventDispatcherFactory::getInstance(); + /** @var \SimpleSAML\Module\admin\Controller\CronEvent $event */ + $event = $eventDispatcher->dispatch(new ConfigPageEvent($t)); + $t = $event->getTemplate(); Module::callHooks('configpage', $t); $this->menu->addOption('logout', $this->authUtils->getAdminLogoutURL(), Translate::noop('Log out')); return $this->menu->insert($t); @@ -205,11 +210,11 @@ protected function getPrerequisiteChecks(): array 'descr' => [ Translate::noop('PHP %minimum% or newer is needed. You are running: %current%'), [ - '%minimum%' => '8.1', + '%minimum%' => '8.3', '%current%' => explode('-', phpversion())[0], ], ], - 'enabled' => version_compare(phpversion(), '8.1', '>='), + 'enabled' => version_compare(phpversion(), '8.3', '>='), ], ]; $store = $this->config->getOptionalString('store.type', null); @@ -370,8 +375,25 @@ protected function getPrerequisiteChecks(): array // Add module specific checks via the sanitycheck hook that a module can provide. + $eventDispatcher = ModuleEventDispatcherFactory::getInstance(); + /** @var \SimpleSAML\Module\admin\Event\SanityCheckEvent $event */ + $event = $eventDispatcher->dispatch(new SanityCheckEvent()); + $hookinfo = [ 'info' => [], 'errors' => [] ]; Module::callHooks('sanitycheck', $hookinfo); + + // Merge results from the event into $hookinfo. Can be removed when hook infrastructure is removed. + $hookinfo = [ + 'info' => array_merge( + $event->getInfo(), + $hookinfo['info'], + ), + 'errors' => array_merge( + $event->getErrors(), + $hookinfo['errors'], + ), + ]; + foreach (['info', 'errors'] as $resulttype) { foreach ($hookinfo[$resulttype] as $result) { $matrix[] = [ @@ -427,47 +449,37 @@ protected function getWarnings(): array ); } - /* + /** * Check for updates. Store the remote result in the session so we don't need to fetch it on every access to * this page. */ $checkforupdates = $this->config->getOptionalBoolean('admin.checkforupdates', true); if (($checkforupdates === true) && $this->config->getVersion() !== 'master') { - if (!function_exists('curl_init')) { - $warnings[] = Translate::noop( - 'The cURL PHP extension is missing. Cannot check for SimpleSAMLphp updates.', - ); - } else { - $latest = $this->session->getData(self::LATEST_VERSION_STATE_KEY, "version"); - - if (!$latest) { - $ch = curl_init(self::RELEASES_API); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_USERAGENT, 'SimpleSAMLphp'); - curl_setopt($ch, CURLOPT_TIMEOUT, 2); - curl_setopt($ch, CURLOPT_PROXY, $this->config->getOptionalString('proxy', null)); - curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->config->getOptionalValue('proxy.auth', null)); - $response = curl_exec($ch); - - if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) === 200) { - /** @psalm-var string $response */ - $latest = json_decode($response, true); - $this->session->setData(self::LATEST_VERSION_STATE_KEY, 'version', $latest); - } - curl_close($ch); + $latest = $this->session->getData(self::LATEST_VERSION_STATE_KEY, "version"); + + if (!$latest) { + $client = $this->httpUtils->createHttpClient(['timeout' => 3]); + $response = $client->request('GET', self::RELEASES_API); + + try { + $latest = $response->toArray(); + $this->session->setData(self::LATEST_VERSION_STATE_KEY, 'version', $latest); + } catch (ExceptionInterface $e) { + Logger::warning(sprintf("Unable to check for updates; %s", $e->getMessage())); + $warnings[] = Translate::noop("Unable to check for updates; see logs for details."); } + } - if ($latest && version_compare($this->config->getVersion(), ltrim($latest['tag_name'], 'v'), 'lt')) { - $warnings[] = [ - Translate::noop( - 'You are running an outdated version of SimpleSAMLphp. Please update to the latest version as soon as possible.', - ), - [ - '%latest%' => $latest['html_url'], - ], - ]; - } + if ($latest && version_compare($this->config->getVersion(), ltrim($latest['tag_name'], 'v'), 'lt')) { + $warnings[] = [ + Translate::noop( + 'You are running an outdated version of SimpleSAMLphp. Please update to the latest version as soon as possible.', + ), + [ + '%latest%' => $latest['html_url'], + ], + ]; } } diff --git a/modules/admin/src/Controller/Federation.php b/modules/admin/src/Controller/Federation.php index 5c934ec333..f6486e2b32 100644 --- a/modules/admin/src/Controller/Federation.php +++ b/modules/admin/src/Controller/Federation.php @@ -9,6 +9,7 @@ use SimpleSAML\Assert\Assert; use SimpleSAML\Auth; use SimpleSAML\Configuration; +use SimpleSAML\Event\Dispatcher\ModuleEventDispatcherFactory; use SimpleSAML\Locale\Translate; use SimpleSAML\Logger; use SimpleSAML\Metadata\MetaDataStorageHandler; @@ -17,6 +18,7 @@ use SimpleSAML\Metadata\Signer; use SimpleSAML\Module; use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IdP; +use SimpleSAML\Module\admin\Event\FederationPageEvent; use SimpleSAML\Module\saml\IdP\SAML2 as SAML2_IdP; use SimpleSAML\Utils; use SimpleSAML\XHTML\Template; @@ -181,6 +183,11 @@ public function main(/** @scrutinizer ignore-unused */ Request $request): Templa 'logouturl' => $this->authUtils->getAdminLogoutURL(), ]; + $eventDispatcher = ModuleEventDispatcherFactory::getInstance(); + /** @var \SimpleSAML\Module\admin\Event\FederationPageEvent $event */ + $event = $eventDispatcher->dispatch(new FederationPageEvent($t)); + $t = $event->getTemplate(); + Module::callHooks('federationpage', $t); Assert::isInstanceOf($t, Template::class); @@ -211,7 +218,11 @@ private function getHostedIdP(): array $selfHost = $httpUtils->getSelfHostWithPath(); foreach ($idps as $index => $idp) { if (isset($idp['host']) && $idp['host'] !== '__DEFAULT__') { - $mdHostBase = str_replace('://' . $selfHost . '/', '://' . $idp['host'] . '/', $metadataBase); + $mdHostBase = str_replace( + '://' . $selfHost . '/', + '://' . $idp['host'] . '/', + $metadataBase, + ); } else { $mdHostBase = $metadataBase; } diff --git a/modules/admin/src/Controller/Menu.php b/modules/admin/src/Controller/Menu.php index 084ee6c33b..0bf2a344fb 100644 --- a/modules/admin/src/Controller/Menu.php +++ b/modules/admin/src/Controller/Menu.php @@ -5,8 +5,10 @@ namespace SimpleSAML\Module\admin\Controller; use SimpleSAML\Assert\Assert; +use SimpleSAML\Event\Dispatcher\ModuleEventDispatcherFactory; use SimpleSAML\Locale\Translate; use SimpleSAML\Module; +use SimpleSAML\Module\admin\Event\AdminMenuEvent; use SimpleSAML\XHTML\Template; /** @@ -87,6 +89,10 @@ public function addOption(string $id, string $url, string $name): void public function insert(Template $template): Template { $template->data['menu'] = $this->options; + $eventDispatcher = ModuleEventDispatcherFactory::getInstance(); + /** @var \SimpleSAML\Module\admin\Event\AdminMenuEvent $event */ + $event = $eventDispatcher->dispatch(new AdminMenuEvent($template)); + $template = $event->getTemplate(); Module::callHooks('adminmenu', $template); Assert::isInstanceOf($template, Template::class); diff --git a/modules/admin/src/Event/AdminMenuEvent.php b/modules/admin/src/Event/AdminMenuEvent.php new file mode 100644 index 0000000000..8941d323a3 --- /dev/null +++ b/modules/admin/src/Event/AdminMenuEvent.php @@ -0,0 +1,24 @@ +template = $template; + } + + + public function getTemplate(): XHTML\Template + { + return $this->template; + } +} diff --git a/modules/admin/src/Event/ConfigPageEvent.php b/modules/admin/src/Event/ConfigPageEvent.php new file mode 100644 index 0000000000..9f651e1d1b --- /dev/null +++ b/modules/admin/src/Event/ConfigPageEvent.php @@ -0,0 +1,21 @@ +template; + } +} diff --git a/modules/admin/src/Event/FederationPageEvent.php b/modules/admin/src/Event/FederationPageEvent.php new file mode 100644 index 0000000000..156ba5f2ab --- /dev/null +++ b/modules/admin/src/Event/FederationPageEvent.php @@ -0,0 +1,21 @@ +template; + } +} diff --git a/modules/admin/src/Event/SanityCheckEvent.php b/modules/admin/src/Event/SanityCheckEvent.php new file mode 100644 index 0000000000..58486f5e9f --- /dev/null +++ b/modules/admin/src/Event/SanityCheckEvent.php @@ -0,0 +1,41 @@ +info[] = $message; + } + + + public function addError(string $message): void + { + $this->errors[] = $message; + } + + + public function getInfo(): array + { + return $this->info; + } + + + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/modules/admin/templates/federation.twig b/modules/admin/templates/federation.twig index 017a5f6b0f..dae9211d04 100644 --- a/modules/admin/templates/federation.twig +++ b/modules/admin/templates/federation.twig @@ -56,7 +56,7 @@ - + {{ set.url }} diff --git a/modules/core/docs/authproc_attributeconditionaladd.md b/modules/core/docs/authproc_attributeconditionaladd.md new file mode 100644 index 0000000000..d74d561a2d --- /dev/null +++ b/modules/core/docs/authproc_attributeconditionaladd.md @@ -0,0 +1,312 @@ +# `core:AttributeConditionalAdd` + +Filter that adds attributes to the user, optionally only doing so if certain attributes and values already exist the attribute list. + +## Configuration Format + +: `class` + declares that this block will configure an AttributeConditionalAdd authproc. It is required. + +: `%` +allows for optional (zero or more) flags to be specified. These start with a "%" character (eg `%replace`). See the **Flags** section below for more details. + +: `attributes` +defines a list of one or more attributes to add. It is required. + +: `conditions` +is optional. If it is specified, it contains a list of conditions that determine whether the `attributes` will be added or not. If the `conditions` section is not specified (or is an empty list), the `attributes` will be added. See the **Conditions** section below for more details. + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + '%', + 'attributes' => [ + 'attribute' => ['value'], + [...], + ], + 'conditions' => [], + ], + ], + +## Conditions + +Conditions are optional. If there are one or more conditions specified, the attributes will only be added if all of the conditions evaluate to true. By default, all `conditions` must evaluate to true, unless the `%anycondition` flag is specified, in which case the attributes will be added if any of the `conditions` are true. + +### attrExistsAny + +If the current attributes includes any of the listed attribute names, the new attributes in the `attributes` section will be added. + +In the below example, if there is an attribute named **either** `customerId` OR `supplierId` (or both), then the `isExternalUser => ['true']` attribute will be added. + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'conditions' => [ + 'attrExistsAny' => [ + 'customerId', + 'supplierId', + ], + ], + 'attributes' => [ + 'isExternalUser' => ['true'], + ], + ], + ], + +### attrExistsAll + +If the current attributes includes all of the listed attribute names, the new attributes in the `attributes` section will be added. + +In the below example, if there is an attribute named `customerId` AND there is an attribute named `companyName`, then the `isCompanyUser => ['true']` attribute will be added. + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'conditions' => [ + 'attrExistsAll' => [ + 'customerId', + 'companyName', + ], + ], + 'attributes' => [ + 'isCompanyUser' => ['true'], + ], + ], + ], + +### attrExistsRegexAny + +If the current attributes includes any attribute names that match any of the regular expressions listed, the new attributes in the `attributes` section will be added. + +In the below example, if there is an attribute named **either** starting with `cust` OR ending with `PhoneNumber`, then the `isCustomer => ['true']` attribute will be added. + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'conditions' => [ + 'attrExistsRegexAny' => [ + '/^cust/', + '/PhoneNumber$/', + ], + ], + 'attributes' => [ + 'isCustomer' => ['true'], + ], + ], + ], + +### attrExistsRegexAll + +If each of the regular expressions listed matches at least one of the current attributes, the new attributes in the `attributes` section will be added. + +In the below example, if there is an attribute name starting with `email` AND there is an attribute name starting with `member`, then the `isCustomer => ['true']` attribute will be added. + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'conditions' => [ + 'attrExistsRegexAll' => [ + '/^email/', + '/^member/', + ], + ], + 'attributes' => [ + 'isCustomer' => ['true'], + ], + ], + ], + +### attrValueIsAny + +If the current attributes includes any of the listed attribute names, and at least one of those has any of the listed values for that attribute, the new attributes in the `attributes` section will be added. + +In the below example, if the user has the `departmentName` attribute set, and one of the values of that attribute is `Physics` or `Chemistry`, or they have a `managementRole` attribute set, and that attribute includes the value of `Vice Chancellor`, then the `newSystemPilotUser => ['true']` attribute will be added. + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'conditions' => [ + 'attrValueIsAny' => [ // any of the attributes listed below has any of the values listed + 'departmentName' => ['Physics', Chemistry], + 'managementRole' => ['Vice Chancellor'], + ], + ], + 'attributes' => [ + 'newSystemPilotUser' => ['true'], + ], + ], + ], + +### attrValueIsAll + +If the current attributes includes all of the listed attribute names, and each of those attributes include all of the listed values for that attribute, the new attributes in the `attributes` section will be added. + +Note: this does not mean the values listed are the only values present. They are a subset of the values present. + +In the below example, only the Dean of the Physics department will have the `newSystemPilotUser => ['true']` attribute added. + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'conditions' => [ + 'attrValueIsAll' => [ // all of the attributes listed below has all of the values listed + 'departmentName' => ['Physics'], + 'managementRole' => ['Dean'], + ], + ], + 'attributes' => [ + 'newSystemPilotUser' => ['true'], + ], + ], + ], + +### attrValueIsRegexAny + +If the current attributes includes any of the listed attribute names, and has at least one existing value for those attributes that matches any of the listed regular expressions for that attribute, the new attributes in the `attributes` section will be added. + +In the below example, if the user has the `qualifications` attribute set, and one of the values of that attributes starts with `Certified` or ends with `Assessor`, then the `qualifiedTradie => ['true']` attribute will be added. + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'conditions' => [ + 'attrValueIsRegexAny' => [ // any of the attributes listed below has a value that matches any of the regex values listed + 'qualifications' => ['/^Certfied/', '/Assessor$/'], + ], + ], + 'attributes' => [ + 'qualifiedTradie' => ['true'], + ], + ], + ], + +### attrValueIsRegexAll + +If the current attributes includes all of the listed attribute names, and all of the existing values for those attributes matches any of the listed regular expressions for that attribute, the new attributes in the `attributes` section will be added. + +In the below example, if the user has the `email` attribute set, and all email addresses listed as values in that attribue end with either `/@staff.example.edu$/` OR `/@student.example.edu$/`, then the `'internalUser' => ['true']` attribute will be added. + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'conditions' => [ + 'attrValueIsRegexAll' => [ // all of the attributes listed below, every value matches one of the regex values listed + 'email' => ['/@staff.example.edu$/', '/@student.example.edu$/'], + ], + ], + 'attributes' => [ + 'internalUser' => ['true'], + ], + ], + ], + +## Flags + +`%replace` +: can be used to replace all existing values in target with new ones (any existing values will be lost) + +`%nodupe` +: removes all duplicate values from the `attributes` being added. Without this flag being specified, the default behaviour is that if a value already exists that we're appending to the attribute, a duplicate is added. Note that if there are pre-existing duplicate values in the attributes being added to, those values will also be de-duplicated. + +`%anycondition` +: if there are multiple `conditions`, any of those conditions evaluating to true will cause the `attributes` to be added. Without this flag being specified, the default behaviour is that all those conditions must be true for the values in the `attributes` section to be added. + +## Examples + +The most basic case - unconditionally add a SAML attribute named `source`: + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'attributes' => [ + 'source' => ['myidp'], + ], + ], + ], + +You can specify multiple attributes, and attributes can have multiple values: + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'attributes' => [ + 'eduPersonPrimaryAffiliation' => 'student', + 'eduPersonAffiliation' => ['student', 'employee', 'members'], + ], + ], + ], + +Append to an existing attribute if a condition is satisfied, removing duplicate values (if they already have the 'management' value in the 'groups' attribute, don't add it again): + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + '%nodupe', + 'conditions' => [ + 'attrValueIsAny' => [ + 'role' => ['Manager', 'Director'] + ], + ], + 'attributes' => [ + 'groups' => ['management'], + ], + ], + ], + +Replace an existing attribute if a given attribute satisfies a condition: + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + '%replace', + 'conditions' => [ + 'attrValueIsAll' => [ + 'userType' => ['Customer'], + 'onStopSupply' => ['true'], + ], + ], + 'attributes' => [ + 'uid' => ['guest'], + ], + ], + ], + +Multiple conditions, where all must be true: + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + 'conditions' => [ + 'attrExistsAny' => [ + 'staffId', + ], + 'attrValueIsAny' => [ + 'departmentName' => ['Physics'], + ], + ], + 'attributes' => [ + 'groups' => ['StaffPhysics'], + ], + ], + ], + +Multiple conditions, where any can be true. In the below case, the user must either have a `supplierId` attribute, or have the "staff" role and be in the "Procurement" department to receive the `'allowedSystems' => ['procurement']` attribute: + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeConditionalAdd', + '%anycondition', + 'conditions' => [ + 'attrExistsAny' => [ + 'supplierId', + ], + 'attrValueIsAll' => [ + 'role' => ['Staff'], + 'departmentName' => ['Procurement'], + ], + ], + 'attributes' => [ + 'allowedSystems' => ['procurement'], + ], + ], + ], diff --git a/modules/core/docs/authproc_attributedump.md b/modules/core/docs/authproc_attributedump.md new file mode 100644 index 0000000000..36596831a4 --- /dev/null +++ b/modules/core/docs/authproc_attributedump.md @@ -0,0 +1,90 @@ +`core:AttributeDump` +=================== + +Filter that outputs to the system log file attributes and their values that match a given criteria. + +This is particularly useful for adding debug points in your list of authproc filters as you are configuring your SimpleSAMLphp. + +Parameters +---------- + +`class` (required) +: This is the name of the filter. + It must be `core:AttributeDump`. + +`attributes` +: An array of attribute names that are to be output to the SimpleSAMLphp logs. + If not specified, and `attributesRegex` is not specified, all attributes will be output. + +`attributesRegex` +: An array of regular expressions. Any attribute name that matches any of the regular expressions + in this list are to be output to the SimpleSAMLphp logs. + If not specified, and `attributes` is also not specified, all attributes will be output. + +`logPrefix` +: A string to prefix each log line to be outputted. + Defaults to "AttributeDump". + +`logLevel` +: The level to log at. For the message to appear in the SimpleSAMLphp log files it needs to be at a level + equal to or higher than the log value you've configured in your `config.php`. + Valid values are: "emergency", "critical", "alert", "error", "warning", "notice", "info" or "debug". + +Examples +-------- + +If no attribute list or list of attribute regular expressions is provided, it will simply dump all attributes: + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeDump', + ], + ], + +This will output the `uid` and `groups` attributes only to the logs: + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeDump', + 'attributes' => ['uid', 'groups'], + ], + ], + +This will output any attribute that ends in the letter `n` (eg. `fn`, `sn`, `cn`): + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeDump', + 'attributesRegex' => ['/n$/'], + ], + ], + +This will output the `uid` and `groups` attributes, as well as any attribute that ends in the letter `n` (eg. `fn`, `sn`, `cn`) to the logs: + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeDump', + 'attributes' => ['uid', 'groups'], + 'attributesRegex' => ['/n$/'], + ], + ], + +Optionally, you can specify a prefix to the log message and a log level to log at: + + 'authproc' => [ + 49 => [ + 'class' => 'core:AttributeAdd', + [...] + ], + + 50 => [ + 'class' => 'core:AttributeDump', + 'logPrefix' => 'After running AttributeAdd but before applying AttributeLimit filter', + 'logLevel' => 'debug', + ], + + 51 => [ + 'class' => 'core:AttributeLimit', + [...] + ], + ], diff --git a/modules/core/docs/authproc_attributevaluemap.md b/modules/core/docs/authproc_attributevaluemap.md index d4e6d684c5..94b3f0a5ba 100644 --- a/modules/core/docs/authproc_attributevaluemap.md +++ b/modules/core/docs/authproc_attributevaluemap.md @@ -5,7 +5,8 @@ Filter that creates a target attribute based on one or more value(s) in source a Besides the mapping of source values to target values, the filter has the following options: * `%replace` can be used to replace all existing values in target with new ones (any existing values will be lost) -* `%keep` can be used to keep the source attribute, otherwise it will be removed. +* `%keep` can be used to keep the source attribute, otherwise it will be removed (regardless of whether there is a match or not). +* `%regex` can be used to evaluate the values as regular expressions instead of plain strings. **Examples**: @@ -84,3 +85,25 @@ Replace any existing `affiliation` attribute values and keep the `groups` attrib ], ], ], + +## Regular expressions + +Will add eduPersonAffiliation containing value `student` if the `memberOf` attribute contains +some value matching `/^cn=student,o=[a-z]+,o=organization,dc=org` +(eg. `cn=student,o=some,o=organization,dc=org`). +The `memberOf` attribute will be removed (use `%keep`, to keep it) and existing values in +`eduPersonAffiliation` will be merged (use `%replace` to replace them). + + 'authproc' => [ + 50 => [ + 'class' => 'core:AttributeValueMap', + 'sourceattribute' => 'memberOf', + 'targetattribute' => 'eduPersonAffiliation', + '%regex', + 'values' => [ + 'student' => [ + '/^cn=student,o=[a-z]+,o=organization,dc=org$/', + ], + ], + ], + ], diff --git a/modules/core/locales/uk/LC_MESSAGES/core.po b/modules/core/locales/uk/LC_MESSAGES/core.po new file mode 100644 index 0000000000..e3725e0a29 --- /dev/null +++ b/modules/core/locales/uk/LC_MESSAGES/core.po @@ -0,0 +1,211 @@ +msgid "" +msgstr "" +"Project-Id-Version: SimpleSAMLphp\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Domain: core\n" +"X-Generator: Poedit 3.7\n" + +msgid "A service has requested you to authenticate yourself. Please enter your username and password in the form below." +msgstr "Сервіс запитав вас про автентифікацію. Будь ласка, введіть своє ім’я користувача та пароль у форму нижче." + +msgid "An error has occurred" +msgstr "Сталася помилка" + +msgid "Check that the link you used to access the web site is correct." +msgstr "Переконайтеся, що ви використовуєте правильне посилання для доступу до сайту." + +msgid "Close the web browser, and try again." +msgstr "Закрийте веб-браузер і спробуйте ще раз." + +msgid "Continue" +msgstr "Продовжити" + +msgid "Cookies may be disabled in the web browser." +msgstr "Можливо, у вашому браузері вимкнено файли cookie." + +msgid "Do you want to logout from all the services above?" +msgstr "Ви хочете вийти з усіх вищезазначених сервісів?" + +msgid "Enter your username and password" +msgstr "Введіть своє ім'я користувача та пароль" + +msgid "Error report sent" +msgstr "Повідомлення про помилку надіслано" + +msgid "Go back to the previous page and try again." +msgstr "Поверніться на попередню сторінку і спробуйте ще раз." + +msgid "Help! I don't remember my password." +msgstr "Допоможіть! Я не пам’ятаю свій пароль." + +msgid "Hosted Shibboleth 1.3 Identity Provider Metadata (automatically generated)" +msgstr "Метадані постачальника ідентифікації Shibboleth 1.3 (згенеровано автоматично)" + +msgid "Hosted Shibboleth 1.3 Service Provider Metadata (automatically generated)" +msgstr "Метадані постачальника послуг Shibboleth 1.3 (згенеровано автоматично)" + +msgid "If this problem persists, you can report it to the system administrators." +msgstr "Якщо проблема не зникає, ви можете повідомити про це системним адміністраторам." + +msgid "If you are a developer who is deploying a single sign-on solution, you have a problem with the metadata configuration. Verify that metadata is configured correctly on both the identity provider and service provider." +msgstr "Якщо ви розробник, який впроваджує рішення єдиного входу (SSO), у вас виникла проблема з конфігурацією метаданих. Перевірте, чи правильно налаштовані метадані як на стороні постачальника ідентифікації, так і на стороні постачальника послуг." + +msgid "If you are a developer who is deploying a single sign-on solution, you have are trying to reach an endpoint using the wrong HTTP-method." +msgstr "Якщо ви розробник, який впроваджує рішення єдиного входу (SSO), ви намагаєтесь звернутися до кінцевої точки, використовуючи неправильний HTTP-метод." + +msgid "If you are a user who received this error after following a link on a site, you should report this error to the owner of that site." +msgstr "Якщо ви користувач, який отримав цю помилку після переходу за посиланням на сайті, вам слід повідомити про це власнику цього сайту." + +msgid "If you are the administrator of this installation, please refer to the SimpleSAMLphp documentation for how to configure and interact with the software." +msgstr "Якщо ви адміністратор цієї інсталяції, зверніться до документації SimpleSAMLphp, щоб дізнатися, як налаштовувати та взаємодіяти з програмним забезпеченням." + +msgid "If you report this error, please also report this tracking number which makes it possible to locate your session in the logs available to the system administrator:" +msgstr "Якщо ви повідомляєте про цю помилку, будь ласка, також надайте цей ідентифікаційний номер, який дозволяє знайти вашу сесію в журналах, доступних системному адміністратору:" + +msgid "Incorrect Attributes" +msgstr "Неправильні атрибути" + +msgid "Logging out..." +msgstr "Вихід із системи..." + +msgid "Login" +msgstr "Увійти" + +msgid "Logout" +msgstr "Вийти" + +msgid "Missing cookie" +msgstr "Відсутній файл cookie" + +msgid "No" +msgstr "Ні" + +msgid "No, only %SP%" +msgstr "Ні, тільки%SP%" + +msgid "One or more of the attributes supplied by your identity provider did not contain the expected number of values." +msgstr "Один або кілька атрибутів, наданих вашим постачальником ідентифікації, не містили очікуваної кількості значень." + +msgid "Opened the web browser with tabs saved from the previous session." +msgstr "Веб-браузер відкрито з вкладками, збереженими з попереднього сеансу." + +msgid "Organization" +msgstr "Організація" + +msgid "Password" +msgstr "Пароль" + +msgid "Processing..." +msgstr "Обробка..." + +msgid "Remember me" +msgstr "Запам’ятати мене" + +msgid "Remember my organization" +msgstr "Запам’ятати мою організацію" + +msgid "Remember my username" +msgstr "Запам’ятати моє ім'я користувача" + +msgid "Report this error" +msgstr "Повідомити про цю помилку" + +msgid "Retry" +msgstr "Повторити" + +msgid "Retry login" +msgstr "Повторити вхід" + +msgid "Shibboleth 1.3 SP example - test logging in through your Shib IdP" +msgstr "Приклад Shibboleth 1.3 SP — тестування входу через ваш Shib IdP" + +msgid "SimpleSAMLphp" +msgstr "SimpleSAMLphp" + +msgid "State information lost" +msgstr "Інформацію про стан втрачено" + +msgid "Suggestions for resolving this problem:" +msgstr "Пропозиції щодо вирішення цієї проблеми:" + +msgid "The authentication procesd has failed." +msgstr "Процес автентифікації завершився невдало." + +msgid "The authorization procesd has failed." +msgstr "Процес авторизації завершився невдало." + +msgid "The error report has been sent to the administrators." +msgstr "Звіт про помилку надіслано адміністраторам." + +msgid "The identification procesd has failed." +msgstr "Процес ідентифікації завершився невдало." + +msgid "The link used to get here was bad, perhaps a bookmark." +msgstr "\"Посилання, за яким ви перейшли, було некоректним — можливо, це закладка." + +msgid "The problematic attribute(s) are:" +msgstr "Проблемні атрибут(и):" + +msgid "There was an issue while signing you in." +msgstr "Виникла проблема під час входу в систему" + +msgid "This error may be caused by:" +msgstr "Ця помилка може бути спричинена наступним:" + +msgid "This is most likely a configuration problem on either the service provider or identity provider." +msgstr "Ймовірно, це проблема конфігурації на стороні постачальника сервісів або постачальника ідентифікації." + +msgid "This is the front page of the SimpleSAMLphp authentication software. There's not much to see here." +msgstr "Це головна сторінка програмного забезпечення автентифікації SimpleSAMLphp. Тут особливо нічого немає." + +msgid "Too short interval between single sign on events." +msgstr "Занадто короткий інтервал між подіями єдиного входу." + +msgid "Unable to log out of one or more services. To ensure that all your sessions are closed, you are encouraged to close your webbrowser." +msgstr "Не вдалося вийти з одного або кількох сервісів. Щоб переконатися, що всі ваші сесії завершено, рекомендується закрити веббраузер." + +msgid "Username" +msgstr "Ім’я користувача" + +msgid "Using the back and forward buttons in the web browser." +msgstr "Використовуйте клавіші «Назад» і «Вперед» у веббраузері." + +msgid "We have detected that there is only a few seconds since you last authenticated with this service provider, and therefore assume that there is a problem with this SP." +msgstr "Ми виявили, що з моменту вашої останньої автентифікації з цим постачальником сервісів пройшло лише кілька секунд, тому припускаємо, що виникла проблема з цим постачальником послуг SP." + +msgid "We were unable to locate the state information for the current request." +msgstr "Нам не вдалося знайти інформацію про стан для поточного запиту." + +msgid "Welcome" +msgstr "Ласкаво просимо" + +msgid "Without your username and password you cannot authenticate yourself for access to the service. There may be someone that can help you. Consult the help desk at your organization!" +msgstr "Без вашого імені користувача та пароля ви не можете пройти автентифікацію для доступу до сервісу. Можливо, хтось зможе вам допомогти. Зверніться до служби підтримки у вашій організації!" + +msgid "Yes, all services" +msgstr "Так, всі сервіси" + +msgid "You appear to have disabled cookies in your browser. Please check the settings in your browser, and try again." +msgstr "Здається, ви вимкнули файли cookie у своєму браузері. Перевірте налаштування браузера та спробуйте ще раз." + +msgid "You are also logged in on these services:" +msgstr "Ви також увійшли до наступних сервісів:" + +msgid "You are now accessing a pre-production system. This authentication setup is for testing and pre-production verification only. If someone sent you a link that pointed you here, and you are not a tester you probably got the wrong link, and should not be here." +msgstr "Ви зараз отримали доступ до тестової (pre-production) системи. Ця автентифікація призначена лише для тестування та попередньої перевірки. Якщо хтось надіслав вам посилання, яке привело вас сюди, і ви не тестувальник, то, ймовірно, це неправильне посилання, і вам не слід бути тут." + +msgid "You are now successfully logged out from %SP%." +msgstr "Ви успішно вийшли з системи %SP%." + +msgid "got %got% values, want %want%" +msgstr "отримано %got% значень, потрібно %want%" + +msgid "logout is not supported" +msgstr "вихід із системи не підтримується" diff --git a/modules/core/src/Auth/Process/AttributeConditionalAdd.php b/modules/core/src/Auth/Process/AttributeConditionalAdd.php new file mode 100644 index 0000000000..db2f1a5c7d --- /dev/null +++ b/modules/core/src/Auth/Process/AttributeConditionalAdd.php @@ -0,0 +1,574 @@ + $topLevelValues) { + if (is_int($topLevelName)) { + if ($topLevelValues === '%replace') { + $this->replace = true; + } elseif ($topLevelValues === '%nodupe') { + $this->nodupe = true; + } elseif ($topLevelValues === '%anycondition') { + $this->anycondition = true; + } else { + throw new Exception('Unknown flag: ' . var_export($topLevelValues, true)); + } + continue; + } + + if ($topLevelName === 'attributes') { + $this->attributes = $this->constructAttributes($topLevelValues); + } elseif ($topLevelName === "conditions") { + if (!is_array($topLevelValues)) { + throw new Exception('Configuration for "conditions" must be array.'); + } + + foreach ($topLevelValues as $conditionName => $conditionValues) { + // We'll call a function called set for each condition found. + $setterToCall = 'set' . ucfirst($conditionName); + if (!method_exists($this, $setterToCall)) { + throw new Exception('Unknown condition option: ' . var_export($conditionName, true)); + } + $this->$setterToCall($conditionValues); + } + } else { + throw new Exception('Unknown configuration option: ' . var_export($topLevelName, true)); + } + } + + if ($this->attributes === []) { + throw new Exception('No attributes specified to add.'); + } + } + + + private function constructAttributes(array $attributesConfig): array + { + $attributes = []; + + foreach ($attributesConfig as $name => $value) { + if (is_int($name)) { + throw new Exception( + 'Invalid value for "attributes": value must be an associative array of "name"=> ["value", ...].', + ); + } + if (is_string($value)) { + $attributes[$name] = [$value]; + } elseif ( + is_array($value) && + array_is_list($value) && + count($value) === count(array_filter($value, 'is_string')) + ) { + $attributes[$name] = $value; + } else { + throw new Exception( + 'Invalid value for attribute "' . $name . '": value must be a string or an array of strings.', + ); + } + } + + return $attributes; + } + + + private function setAttrExistsAny(array $attrExistsAny): void + { + if (!array_is_list($attrExistsAny)) { + throw new Exception( + 'Invalid value for "attrExistsAny": value must be a list of attribute names.', + ); + } + $this->attrExistsAny = $attrExistsAny; + } + + + private function isConfiguredAttrExistsAny(): bool + { + return $this->attrExistsAny !== []; + } + + + private function processConditionalAttrExistsAny(array $attributes): bool + { + // No attributes to check, so condition is met. + if ($this->attrExistsAny === []) { + return true; + } + + foreach ($this->attrExistsAny as $attrName) { + if (array_key_exists($attrName, $attributes)) { + return true; + } + } + + return false; + } + + + private function setAttrExistsAll(array $attrExistsAll): void + { + if (!array_is_list($attrExistsAll)) { + throw new Exception( + 'Invalid value for "attrExistsAll": value must be a list of attribute names.', + ); + } + $this->attrExistsAll = $attrExistsAll; + } + + + private function isConfiguredAttrExistsAll(): bool + { + return $this->attrExistsAll !== []; + } + + + private function processConditionalAttrExistsAll(array $attributes): bool + { + // No attributes to check, so condition is met. + if ($this->attrExistsAll === []) { + return true; + } + + foreach ($this->attrExistsAll as $attrName) { + if (!array_key_exists($attrName, $attributes)) { + return false; + } + } + + return true; + } + + + private function setAttrExistsRegexAny(array $attrExistsRegexAny): void + { + if (!array_is_list($attrExistsRegexAny)) { + throw new Exception( + 'Invalid value for "attrExistsRegexAny": value must be a list of regular expressions.', + ); + } + $this->attrExistsRegexAny = $attrExistsRegexAny; + } + + + private function isConfiguredAttrExistsRegexAny(): bool + { + return $this->attrExistsRegexAny !== []; + } + + + private function processConditionalAttrExistsRegexAny(array $attributes): bool + { + // No attributes to check, so condition is met. + if ($this->attrExistsRegexAny === []) { + return true; + } + + foreach ($this->attrExistsRegexAny as $attrNameRegex) { + foreach (array_keys($attributes) as $attrName) { + if (preg_match($attrNameRegex, $attrName) === 1) { + return true; + } + } + } + + return false; + } + + + private function setAttrExistsRegexAll(array $attrExistsRegexAll): void + { + if (!array_is_list($attrExistsRegexAll)) { + throw new Exception( + 'Invalid value for "attrExistsRegexAll": value must be a list of regular expressions.', + ); + } + $this->attrExistsRegexAll = $attrExistsRegexAll; + } + + + private function isConfiguredAttrExistsRegexAll(): bool + { + return $this->attrExistsRegexAll !== []; + } + + + private function processConditionalAttrExistsRegexAll(array $attributes): bool + { + // No attributes to check, so condition is met. + if ($this->attrExistsRegexAll === []) { + return true; + } + + foreach ($this->attrExistsRegexAll as $attrNameRegex) { + $found = false; + foreach (array_keys($attributes) as $attrName) { + if (preg_match($attrNameRegex, $attrName) === 1) { + $found = true; + break; + } + } + if (!$found) { + return false; + } + } + + return true; + } + + + private function validateValueConditional(array $attrValue, string $conditionalName): void + { + // Validate that the input is an associative array + if (array_is_list($attrValue)) { + throw new Exception( + 'Invalid value for "' . + $conditionalName . + '": value must be an associative array of "attribute_name" => "attribute_value".', + ); + } + + // Validate that each value in the associative array is a string or an array of strings + foreach ($attrValue as $attrName => $attrValue) { + // Throw an exception if the $attrValue is not a string or an array of strings + if ( + !is_array($attrValue) || + !array_is_list($attrValue) || + count(array_filter($attrValue, 'is_string')) !== count($attrValue) + ) { + throw new Exception( + 'Invalid attribute value in "' . + $conditionalName . + '" for attribute "' . + $attrName . + '": value must be an array of strings.', + ); + } + } + } + + + private function setAttrValueIsAny(array $attrValueIsAny): void + { + $this->validateValueConditional($attrValueIsAny, 'attrValueIsAny'); + $this->attrValueIsAny = $attrValueIsAny; + } + + + private function isConfiguredAttrValueIsAny(): bool + { + return $this->attrValueIsAny !== []; + } + + + private function processConditionalAttrValueIsAny(array $attributes): bool + { + // No attributes to check, so condition is met. + if ($this->attrValueIsAny === []) { + return true; + } + + foreach ($this->attrValueIsAny as $attrName => $attrValue) { + if (array_key_exists($attrName, $attributes)) { + if (is_array($attrValue)) { + foreach ($attrValue as $singleAttrValue) { + if (in_array($singleAttrValue, $attributes[$attrName], true)) { + return true; + } + } + } elseif (in_array($attrValue, $attributes[$attrName], true)) { + return true; + } + } + } + + return false; + } + + + private function setAttrValueIsAll(array $attrValueIsAll): void + { + $this->validateValueConditional($attrValueIsAll, 'attrValueIsAll'); + $this->attrValueIsAll = $attrValueIsAll; + } + + + private function isConfiguredAttrValueIsAll(): bool + { + return $this->attrValueIsAll !== []; + } + + + private function processConditionalAttrValueIsAll(array $attributes): bool + { + // No attributes to check, so condition is met. + if ($this->attrValueIsAll === []) { + return true; + } + + foreach ($this->attrValueIsAll as $attrName => $attrValue) { + if (!array_key_exists($attrName, $attributes)) { + return false; + } + foreach ($attrValue as $singleAttrValue) { + if (!in_array($singleAttrValue, $attributes[$attrName], true)) { + return false; + } + } + } + + return true; + } + + + private function setAttrValueIsRegexAny(array $attrValueIsRegexAny): void + { + $this->validateValueConditional($attrValueIsRegexAny, 'attrValueIsRegexAny'); + $this->attrValueIsRegexAny = $attrValueIsRegexAny; + } + + + private function isConfiguredAttrValueIsRegexAny(): bool + { + return $this->attrValueIsRegexAny !== []; + } + + + private function processConditionalAttrValueIsRegexAny(array $attributes): bool + { + // No attributes to check, so condition is met. + if ($this->attrValueIsRegexAny === []) { + return true; + } + + foreach ($this->attrValueIsRegexAny as $attrName => $attrValueRegexList) { + if (array_key_exists($attrName, $attributes)) { + foreach ($attributes[$attrName] as $attrValue) { + foreach ($attrValueRegexList as $attrValueRegex) { + if (preg_match($attrValueRegex, $attrValue) === 1) { + return true; + } + } + } + } + } + + return false; + } + + + private function setAttrValueIsRegexAll(array $attrValueIsRegexAll): void + { + $this->validateValueConditional($attrValueIsRegexAll, 'attrValueIsRegexAll'); + $this->attrValueIsRegexAll = $attrValueIsRegexAll; + } + + + private function isConfiguredAttrValueIsRegexAll(): bool + { + return $this->attrValueIsRegexAll !== []; + } + + + private function processConditionalAttrValueIsRegexAll(array $attributes): bool + { + // No attributes to check, so condition is met. + if ($this->attrValueIsRegexAll === []) { + return true; + } + + // foreach attribute to check we need to ensure that all regexes match at least one value + foreach ($this->attrValueIsRegexAll as $attrName => $attrValueRegexList) { + if (!array_key_exists($attrName, $attributes)) { + return false; + } + + // Foreach existing attribute value, ensure it matches at least one regex + foreach ($attributes[$attrName] as $existingAttrValue) { + $matched = false; + foreach ($attrValueRegexList as $attrValueRegex) { + if (preg_match($attrValueRegex, $existingAttrValue) === 1) { + $matched = true; + break; + } + } + if (!$matched) { + return false; + } + } + } + + return true; + } + + + /** + * Apply filter to add or replace attributes. + * + * Add or replace existing attributes with the configured values. + * + * @param array &$state The current request + */ + #[\Override] + public function process(array &$state): void + { + Assert::keyExists($state, 'Attributes'); + + $attributes = &$state['Attributes']; + + // Check conditions first. If all conditions succeed, add/append the attributes. + $numConditionsConfigured = 0; + $numConditionsMet = 0; + foreach (get_class_methods($this) as $methodName) { + if (str_starts_with($methodName, 'processConditional')) { + // Only process conditions that are configured. + $isConfiguredMethod = 'isConfigured' . substr($methodName, strlen('processConditional')); + if ($this->$isConfiguredMethod()) { + $numConditionsConfigured++; + if ($this->$methodName($attributes) === true) { + $numConditionsMet++; + } + } + } + } + + // If there are conditions configured, and they are not met, return without adding attributes. + // The anycondition flag indicates whether any condition being met is sufficient. The default is + // that all conditions must be met. + // Note that if no conditions are configured, we always add the attributes. + if ( + $numConditionsConfigured > 0 && (($this->anycondition === true && $numConditionsMet === 0) || + ($this->anycondition === false && $numConditionsMet < $numConditionsConfigured)) + ) { + return; + } + + foreach ($this->attributes as $name => $values) { + if ($this->replace === true || !array_key_exists($name, $attributes)) { + $attributes[$name] = $values; + } elseif ($this->nodupe === true) { + $attributes[$name] = array_unique(array_merge($attributes[$name], $values)); + } else { + $attributes[$name] = array_merge($attributes[$name], $values); + } + } + } +} diff --git a/modules/core/src/Auth/Process/AttributeDump.php b/modules/core/src/Auth/Process/AttributeDump.php new file mode 100644 index 0000000000..0bfbcc0788 --- /dev/null +++ b/modules/core/src/Auth/Process/AttributeDump.php @@ -0,0 +1,133 @@ + $values) { + if ($name === 'attributes') { + if (!is_array($values)) { + throw new Exception('The "attributes" configuration option must be an array of strings.'); + } + foreach ($values as $attribute) { + if (!is_string($attribute)) { + throw new Exception('Attribute name must be a string: ' . var_export($attribute, true)); + } + $this->attributes[] = $attribute; + } + } elseif ($name === 'attributesRegex') { + if (!is_array($values)) { + throw new Exception('The "attributesRegex" configuration option must be an array.'); + } + foreach ($values as $regex) { + if (!is_string($regex)) { + throw new Exception('Attribute regex must be a string: ' . var_export($regex, true)); + } + $this->attributesRegex[] = $regex; + } + } elseif ($name === 'logLevel') { + if (!is_string($values) || !method_exists(Logger::class, $values)) { + throw new Exception( + 'The "logLevel" configuration option must be a string (eg. "debug", "info", "notice", etc).', + ); + } + $this->logLevel = $values; + } elseif ($name === 'logPrefix') { + if (!is_string($values)) { + throw new Exception('The "logPrefix" configuration option must be a string.'); + } + $this->logPrefix = $values; + } else { + throw new Exception('Unknown configuration option: ' . var_export($name, true)); + } + } + } + + /** + * Process the attributes. + * + * @param array &$state The state array containing the attributes to process. + */ + #[\Override] + public function process(array &$state): void + { + Assert::keyExists($state, 'Attributes'); + $attributesPassedIn = &$state['Attributes']; + + $matches = []; + + if (empty($this->attributes) && empty($this->attributesRegex)) { + $matches = $attributesPassedIn; + } else { + foreach ($attributesPassedIn as $attribute => $values) { + foreach ($this->attributes as $attributeToMatch) { + if ($attribute === $attributeToMatch) { + $matches[$attribute] = $values; + continue 2; + } + } + foreach ($this->attributesRegex as $regex) { + if (preg_match($regex, $attribute)) { + $matches[$attribute] = $values; + continue 2; + } + } + } + } + + Logger::{$this->logLevel}($this->logPrefix . ': ' . var_export($matches, true)); + } +} diff --git a/modules/core/src/Auth/Process/AttributeLimit.php b/modules/core/src/Auth/Process/AttributeLimit.php index 244262a020..a846cb8cca 100644 --- a/modules/core/src/Auth/Process/AttributeLimit.php +++ b/modules/core/src/Auth/Process/AttributeLimit.php @@ -247,7 +247,7 @@ private function filterAttributeValues(array $values, ?array $allowedConfigValue * @param array|null Array with regular expressions to test against. null is equivalent to an empty array. * @return string|null Regular expression that matched, or null if no match. */ - private static function matchAnyRegex(string $needle, ?array $regexps = null): string | null + private static function matchAnyRegex(string $needle, ?array $regexps = null): string|null { if ($regexps !== null) { foreach ($regexps as $x => $y) { diff --git a/modules/core/src/Auth/Process/AttributeValueMap.php b/modules/core/src/Auth/Process/AttributeValueMap.php index 6c635ea56f..a7de2e40ac 100644 --- a/modules/core/src/Auth/Process/AttributeValueMap.php +++ b/modules/core/src/Auth/Process/AttributeValueMap.php @@ -46,6 +46,12 @@ class AttributeValueMap extends Auth\ProcessingFilter */ private bool $replace = false; + /** + * Whether $sourceattribute values should be treated as regular expressions or not. + * @var bool + */ + private bool $regex = false; + /** * Initialize the filter. @@ -66,6 +72,8 @@ public function __construct(array &$config, $reserved) $this->replace = true; } elseif ($value === '%keep') { $this->keep = true; + } elseif ($value === '%regex') { + $this->regex = true; } else { // unknown configuration option, log it and ignore the error Logger::warning( @@ -129,7 +137,19 @@ public function process(array &$state): void if (!is_array($values)) { $values = [$values]; } - if (count(array_intersect($values, $sourceattribute)) > 0) { + if ($this->regex) { + foreach ($sourceattribute as $sourcevalue) { + foreach ($values as $pattern) { + if (preg_match($pattern, $sourcevalue) === 1) { + Logger::debug("AttributeValueMap: regex match for '$value'"); + $targetvalues[] = $value; + // no need to check other patterns for this sourceattribute value + break 2; + } + } + } + continue; + } elseif (count(array_intersect($values, $sourceattribute)) > 0) { Logger::debug("AttributeValueMap: intersect match for '$value'"); $targetvalues[] = $value; } diff --git a/modules/core/src/Auth/Source/RequestedAuthnContextSelector.php b/modules/core/src/Auth/Source/RequestedAuthnContextSelector.php index 411e6868d8..c62d783523 100644 --- a/modules/core/src/Auth/Source/RequestedAuthnContextSelector.php +++ b/modules/core/src/Auth/Source/RequestedAuthnContextSelector.php @@ -23,17 +23,17 @@ class RequestedAuthnContextSelector extends AbstractSourceSelector /** * The key of the AuthId field in the state. */ - public const AUTHID = '\SimpleSAML\Module\core\Auth\Source\RequestedAuthnContextSelector.AuthId'; + public const string AUTHID = '\SimpleSAML\Module\core\Auth\Source\RequestedAuthnContextSelector.AuthId'; /** * The string used to identify our states. */ - public const STAGEID = '\SimpleSAML\Module\core\Auth\Source\RequestedAuthnContextSelector.StageId'; + public const string STAGEID = '\SimpleSAML\Module\core\Auth\Source\RequestedAuthnContextSelector.StageId'; /** * The key where the sources is saved in the state. */ - public const SOURCESID = '\SimpleSAML\Module\core\Auth\Source\RequestedAuthnContextSelector.SourceId'; + public const string SOURCESID = '\SimpleSAML\Module\core\Auth\Source\RequestedAuthnContextSelector.SourceId'; /** diff --git a/modules/core/src/Auth/Source/SourceIPSelector.php b/modules/core/src/Auth/Source/SourceIPSelector.php index 6f42e2d037..45bddaf772 100644 --- a/modules/core/src/Auth/Source/SourceIPSelector.php +++ b/modules/core/src/Auth/Source/SourceIPSelector.php @@ -4,9 +4,11 @@ namespace SimpleSAML\Module\core\Auth\Source; -use SimpleSAML\{Error, Logger}; use SimpleSAML\Assert\Assert; -use Symfony\Component\HttpFoundation\{IpUtils, Request}; +use SimpleSAML\Error; +use SimpleSAML\Logger; +use Symfony\Component\HttpFoundation\IpUtils; +use Symfony\Component\HttpFoundation\Request; use function array_key_exists; use function sprintf; @@ -22,17 +24,18 @@ class SourceIPSelector extends AbstractSourceSelector /** * The key of the AuthId field in the state. */ - public const AUTHID = '\SimpleSAML\Module\core\Auth\Source\SourceIPSelector.AuthId'; + public const string AUTHID = '\SimpleSAML\Module\core\Auth\Source\SourceIPSelector.AuthId'; /** * The string used to identify our states. */ - public const STAGEID = '\SimpleSAML\Module\core\Auth\Source\SourceIPSelector.StageId'; + public const string STAGEID = '\SimpleSAML\Module\core\Auth\Source\SourceIPSelector.StageId'; /** * The key where the sources is saved in the state. */ - public const SOURCESID = '\SimpleSAML\Module\core\Auth\Source\SourceIPSelector.SourceId'; + public const string SOURCESID = '\SimpleSAML\Module\core\Auth\Source\SourceIPSelector.SourceId'; + /** * @param string|null The default authentication source to use when none of the zones match diff --git a/modules/core/src/Auth/UserPassBase.php b/modules/core/src/Auth/UserPassBase.php index e60bad3ea3..f96bb22e65 100644 --- a/modules/core/src/Auth/UserPassBase.php +++ b/modules/core/src/Auth/UserPassBase.php @@ -26,12 +26,13 @@ abstract class UserPassBase extends Auth\Source /** * The string used to identify our states. */ - public const STAGEID = '\SimpleSAML\Module\core\Auth\UserPassBase.state'; + public const string STAGEID = '\SimpleSAML\Module\core\Auth\UserPassBase.state'; /** * The key of the AuthId field in the state. */ - public const AUTHID = '\SimpleSAML\Module\core\Auth\UserPassBase.AuthId'; + public const string AUTHID = '\SimpleSAML\Module\core\Auth\UserPassBase.AuthId'; + /** * Username we should force. @@ -136,6 +137,7 @@ public function setForcedUsername(?string $forcedUsername): void $this->forcedUsername = $forcedUsername; } + /** * Return login links from configuration * @return string[] @@ -250,7 +252,7 @@ public function authenticate(array &$state): void $httpUtils->redirectTrustedURL($url, $params); // The previous function never returns, so this code is never executed. - assert::true(false); + Assert::true(false); } diff --git a/modules/core/src/Auth/UserPassOrgBase.php b/modules/core/src/Auth/UserPassOrgBase.php index be440c1f6c..1f52b850f9 100644 --- a/modules/core/src/Auth/UserPassOrgBase.php +++ b/modules/core/src/Auth/UserPassOrgBase.php @@ -26,17 +26,18 @@ abstract class UserPassOrgBase extends Auth\Source /** * The string used to identify our states. */ - public const STAGEID = '\SimpleSAML\Module\core\Auth\UserPassOrgBase.state'; + public const string STAGEID = '\SimpleSAML\Module\core\Auth\UserPassOrgBase.state'; /** * The key of the AuthId field in the state. */ - public const AUTHID = '\SimpleSAML\Module\core\Auth\UserPassOrgBase.AuthId'; + public const string AUTHID = '\SimpleSAML\Module\core\Auth\UserPassOrgBase.AuthId'; /** * The key of the OrgId field in the state, identifies which org was selected. */ - public const ORGID = '\SimpleSAML\Module\core\Auth\UserPassOrgBase.SelectedOrg'; + public const string ORGID = '\SimpleSAML\Module\core\Auth\UserPassOrgBase.SelectedOrg'; + /** * What way do we handle the organization as part of the username. diff --git a/modules/core/src/Controller/ErrorReport.php b/modules/core/src/Controller/ErrorReport.php index c3fd76f650..a5a9d6ea9b 100644 --- a/modules/core/src/Controller/ErrorReport.php +++ b/modules/core/src/Controller/ErrorReport.php @@ -5,8 +5,12 @@ namespace SimpleSAML\Module\core\Controller; use Exception as BuiltinException; -use SimpleSAML\{Configuration, Error, Logger, Session, Utils}; +use SimpleSAML\Configuration; +use SimpleSAML\Error; use SimpleSAML\HTTP\RunnableResponse; +use SimpleSAML\Logger; +use SimpleSAML\Session; +use SimpleSAML\Utils; use SimpleSAML\XHTML\Template; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/modules/core/src/Controller/Exception.php b/modules/core/src/Controller/Exception.php index 385799bca8..a9fcfc5c53 100644 --- a/modules/core/src/Controller/Exception.php +++ b/modules/core/src/Controller/Exception.php @@ -6,9 +6,16 @@ use DateTimeInterface; use SimpleSAML\Assert\Assert; -use SimpleSAML\{Auth, Configuration, Error, Logger, Module, Session, Utils}; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; +use SimpleSAML\Error; +use SimpleSAML\Logger; +use SimpleSAML\Module; +use SimpleSAML\Session; +use SimpleSAML\Utils; use SimpleSAML\XHTML\Template; -use Symfony\Component\HttpFoundation\{Request, Response}; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use function array_keys; use function date; @@ -26,7 +33,12 @@ */ class Exception { - public const CODES = ['IDENTIFICATION_FAILURE', 'AUTHENTICATION_FAILURE', 'AUTHORIZATION_FAILURE', 'OTHER_ERROR']; + public const array CODES = [ + 'IDENTIFICATION_FAILURE', + 'AUTHENTICATION_FAILURE', + 'AUTHORIZATION_FAILURE', + 'OTHER_ERROR', + ]; /** @@ -34,8 +46,8 @@ class Exception * * It initializes the global configuration and auth source configuration for the controllers implemented here. * - * @param \SimpleSAML\Configuration $config The configuration to use by the controllers. - * @param \SimpleSAML\Session $session The session to use by the controllers. + * @param \SimpleSAML\Configuration $config The configuration to use by the controllers. + * @param \SimpleSAML\Session $session The session to use by the controllers. * * @throws \Exception */ @@ -49,7 +61,7 @@ public function __construct( /** * Show Service Provider error. * - * @param Request $request The request that lead to this login operation. + * @param \Symfony\Component\HttpFoundation\Request $request The request that lead to this login operation. * @param string $code The error code * @return \SimpleSAML\XHTML\Template An HTML template */ @@ -108,7 +120,7 @@ public function error(Request $request, string $code): Response /** * Show cardinality error. * - * @param Request $request The request that lead to this login operation. + * @param \Symfony\Component\HttpFoundation\Request $request The request that lead to this login operation. * @throws \SimpleSAML\Error\BadRequest * @return \SimpleSAML\XHTML\Template|\Symfony\Component\HttpFoundation\RedirectResponse * An HTML template or a redirection if we are not authenticated. @@ -142,7 +154,7 @@ public function cardinality(Request $request): Response /** * Show missing cookie error. * - * @param Request $request The request that lead to this login operation. + * @param \Symfony\Component\HttpFoundation\Request $request The request that lead to this login operation. * @return \SimpleSAML\XHTML\Template|\Symfony\Component\HttpFoundation\RedirectResponse * An HTML template or a redirection if we are not authenticated. */ @@ -165,10 +177,13 @@ public function nocookie(Request $request): Response * Show a warning to an user about the SP requesting SSO a short time after * doing it previously. * - * @param Request $request The request that lead to this login operation. + * @param \Symfony\Component\HttpFoundation\Request $request The request that lead to this login operation. * - * @return \SimpleSAML\XHTML\Template|\SimpleSAML\HTTP\RunnableResponse|\Symfony\Component\HttpFoundation\RedirectResponse - * An HTML template, a redirect or a "runnable" response. + * @return ( + * \SimpleSAML\XHTML\Template| + * \SimpleSAML\HTTP\RunnableResponse| + * \Symfony\Component\HttpFoundation\RedirectResponse + * ) An HTML template, a redirect or a "runnable" response. * * @throws \SimpleSAML\Error\BadRequest */ diff --git a/modules/core/src/Controller/Login.php b/modules/core/src/Controller/Login.php index 8d9811f43a..4bb708883b 100644 --- a/modules/core/src/Controller/Login.php +++ b/modules/core/src/Controller/Login.php @@ -4,13 +4,18 @@ namespace SimpleSAML\Module\core\Controller; -use SimpleSAML\{Auth, Configuration, Error, Module, Utils}; -use SimpleSAML\Module\core\Auth\{UserPassBase, UserPassOrgBase}; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; +use SimpleSAML\Error; +use SimpleSAML\Error\ErrorCodes; +use SimpleSAML\Module; +use SimpleSAML\Module\core\Auth\UserPassBase; +use SimpleSAML\Module\core\Auth\UserPassOrgBase; +use SimpleSAML\Utils; use SimpleSAML\XHTML\Template; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use SimpleSAML\Error\ErrorCodes; use function array_key_exists; use function substr; @@ -44,13 +49,12 @@ class Login protected static array $registeredErrorCodeClasses = []; - /** * Controller constructor. * * It initializes the global configuration for the controllers implemented here. * - * @param \SimpleSAML\Configuration $config The configuration to use by the controllers. + * @param \SimpleSAML\Configuration $config The configuration to use by the controllers. * * @throws \Exception */ @@ -136,6 +140,7 @@ public static function registerErrorCodeClass(ErrorCodes $ecc): void } } + /** * This method handles the generic part for both loginuserpass and loginuserpassorg * @@ -150,7 +155,7 @@ private function handleLogin(Request $request, UserPassBase|UserPassOrgBase $sou $organizations = $organization = null; if ($source instanceof UserPassOrgBase) { - $organizations = UserPassOrgBase::listOrganizations($authStateId); + $organizations = $source::listOrganizations($authStateId); $organization = $this->getOrganizationFromRequest($request, $source, $state); } @@ -204,7 +209,7 @@ private function handleLogin(Request $request, UserPassBase|UserPassOrgBase $sou if (($source instanceof UserPassBase) && $source->isRememberMeEnabled()) { if ($request->request->has('remember_me') && ($request->request->get('remember_me') === 'Yes')) { $state['RememberMe'] = true; - $authStateId = Auth\State::saveState($state, UserPassBase::STAGEID); + $authStateId = Auth\State::saveState($state, $source::STAGEID); } } @@ -233,9 +238,9 @@ private function handleLogin(Request $request, UserPassBase|UserPassOrgBase $sou try { if ($source instanceof UserPassOrgBase) { - UserPassOrgBase::handleLogin($authStateId, $username, $password, $organization); + $source::handleLogin($authStateId, $username, $password, $organization); } else { - UserPassBase::handleLogin($authStateId, $username, $password); + $source::handleLogin($authStateId, $username, $password); } } catch (Error\Error $e) { // Login failed. Extract error code and parameters, to display the error @@ -500,7 +505,7 @@ private function getReturnPath(Request $request): string /** * This clears the user's IdP discovery choices. * - * @param Request $request The request that lead to this login operation. + * @param \Symfony\Component\HttpFoundation\Request $request The request that lead to this login operation. */ public function cleardiscochoices(Request $request): void { diff --git a/modules/core/src/Controller/Logout.php b/modules/core/src/Controller/Logout.php index a31b26d136..aef2fa3645 100644 --- a/modules/core/src/Controller/Logout.php +++ b/modules/core/src/Controller/Logout.php @@ -7,10 +7,16 @@ use Exception as BuiltinException; use SAML2\Binding; use SAML2\Constants; -use SimpleSAML\{Auth, Configuration, Error, IdP, Logger, Stats, Utils}; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; +use SimpleSAML\Error; use SimpleSAML\HTTP\RunnableResponse; +use SimpleSAML\IdP; +use SimpleSAML\Logger; use SimpleSAML\Metadata\MetaDataStorageHandler; use SimpleSAML\Module\saml\Message; +use SimpleSAML\Stats; +use SimpleSAML\Utils; use SimpleSAML\XHTML\Template; use Symfony\Component\HttpFoundation\Request; @@ -66,7 +72,7 @@ public function setAuthState(Auth\State $authState): void /** * Log the user out of a given authentication source. * - * @param Request $request The request that lead to this logout operation. + * @param \Symfony\Components\HttpFoundation\Request $request The request that lead to this logout operation. * @param string $as The name of the auth source. * * @return \SimpleSAML\HTTP\RunnableResponse A runnable response which will actually perform logout. @@ -106,7 +112,7 @@ private function getReturnPath(Request $request): string /** - * @param Request $request The request that lead to this logout operation. + * @param \Symfony\Component\HttpFoundation\Request $request The request that lead to this logout operation. * @return \SimpleSAML\HTTP\RunnableResponse */ public function logoutIframeDone(Request $request): RunnableResponse @@ -173,7 +179,7 @@ public function logoutIframeDone(Request $request): RunnableResponse /** - * @param Request $request The request that lead to this logout operation. + * @param \Symfony\Component\HttpFoundation\Request $request The request that lead to this logout operation. * @return \SimpleSAML\HTTP\RunnableResponse */ public function logoutIframePost(Request $request): RunnableResponse @@ -239,7 +245,7 @@ public function logoutIframePost(Request $request): RunnableResponse /** - * @param Request $request The request that lead to this logout operation. + * @param \Symfony\Component\HttpFoundation\Request $request The request that lead to this logout operation. * @return \SimpleSAML\XHTML\Template */ public function logoutIframe(Request $request): Template @@ -385,7 +391,7 @@ public function logoutIframe(Request $request): Template /** - * @param Request $request The request that lead to this logout operation. + * @param \Symfony\Component\HttpFoundation\Request $request The request that lead to this logout operation. * @return \SimpleSAML\HTTP\RunnableResponse */ public function resumeLogout(Request $request): RunnableResponse diff --git a/modules/core/src/Controller/Redirection.php b/modules/core/src/Controller/Redirection.php index 9ad8414e99..f2e2690c8c 100644 --- a/modules/core/src/Controller/Redirection.php +++ b/modules/core/src/Controller/Redirection.php @@ -13,7 +13,8 @@ use SimpleSAML\XMLSecurity\Alg\Encryption\AES; use SimpleSAML\XMLSecurity\Constants as C; use SimpleSAML\XMLSecurity\Key\SymmetricKey; -use Symfony\Component\HttpFoundation\{Request, Response}; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * Controller class for the core module. @@ -44,7 +45,7 @@ public function __construct( /** * This controller provides a way to create a redirect to a POST request * - * @param Request $request The request that lead to this login operation. + * @param \Symfony\Component\HttpFoundation\Request $request The request that lead to this login operation. * @throws \SimpleSAML\Error\BadRequest * @return \SimpleSAML\XHTML\Template|\Symfony\Component\HttpFoundation\RedirectResponse * An HTML template or a redirection if we are not authenticated. diff --git a/modules/core/src/Storage/SQLPermanentStorage.php b/modules/core/src/Storage/SQLPermanentStorage.php index 61095c9baf..0dacb5aa5d 100644 --- a/modules/core/src/Storage/SQLPermanentStorage.php +++ b/modules/core/src/Storage/SQLPermanentStorage.php @@ -53,6 +53,8 @@ public function __construct(string $name, ?Configuration $config = null) $this->db = new PDO($dbfile); if ($this->db) { $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + $this->db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + $q = @$this->db->query('SELECT key1 FROM data LIMIT 1'); if ($q === false) { $this->db->exec(' @@ -165,6 +167,7 @@ public function get(?string $type = null, ?string $key1 = null, ?string $key2 = return $res; } + /** * Return the value directly (not in a container) * @@ -206,7 +209,7 @@ public function exists(string $type, string $key1, string $key2): bool * @param string|null $key2 * @return array|false */ - public function getList(?string $type = null, ?string $key1 = null, ?string $key2 = null) + public function getList(?string $type = null, ?string $key1 = null, ?string $key2 = null): array|false { $conditions = $this->getCondition($type, $key1, $key2); $query = 'SELECT * FROM data WHERE ' . $conditions; @@ -291,6 +294,7 @@ public function removeExpired(): int return $prepared->rowCount(); } + /** * Create a SQL condition statement based on parameters * diff --git a/modules/core/templates/logout-iframe.twig b/modules/core/templates/logout-iframe.twig index 1cd8b2d54c..5c96865c2f 100644 --- a/modules/core/templates/logout-iframe.twig +++ b/modules/core/templates/logout-iframe.twig @@ -37,11 +37,11 @@ {%- for key, sp in remaining_services %} {%- set timeout = 5 %} {%- set name = sp['metadata']|entityDisplayName %} - {%- set icon = 'circle-o-notch' %} + {%- set icon = 'circle-notch' %} {%- if sp['status'] == 'completed' %} - {%- set icon = 'check-circle' %} + {%- set icon = 'circle-check' %} {%- elseif sp['status'] == 'failed' %} - {%- set icon = 'exclamation-circle' %} + {%- set icon = 'circle-exclamation' %} {%- set failed = true %} {%- elseif (sp['status'] == 'onhold' or sp['status'] == 'inprogress') %} {%- set remaining = remaining + 1 %} diff --git a/modules/cron/hooks/hook_configpage.php b/modules/cron/hooks/hook_configpage.php deleted file mode 100644 index 0442547aff..0000000000 --- a/modules/cron/hooks/hook_configpage.php +++ /dev/null @@ -1,22 +0,0 @@ -data['links'][] = [ - 'href' => Module::getModuleURL('cron/info'), - 'text' => Translate::noop('Cron module information page'), - ]; - - $template->getLocalization()->addModuleDomain('cron'); -} diff --git a/modules/cron/hooks/hook_cron.php b/modules/cron/hooks/hook_cron.php deleted file mode 100644 index 3f07679e94..0000000000 --- a/modules/cron/hooks/hook_cron.php +++ /dev/null @@ -1,23 +0,0 @@ -getOptionalBoolean('debug_message', true)) { - $croninfo['summary'][] = 'Cron did run tag [' . $croninfo['tag'] . '] at ' . date(DATE_RFC822); - } -} diff --git a/modules/cron/src/Cron.php b/modules/cron/src/Cron.php index 0cffbc296c..0435e47fcb 100644 --- a/modules/cron/src/Cron.php +++ b/modules/cron/src/Cron.php @@ -7,8 +7,10 @@ use Exception; use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; +use SimpleSAML\Event\Dispatcher\ModuleEventDispatcherFactory; use SimpleSAML\Logger; use SimpleSAML\Module; +use SimpleSAML\Module\cron\Event\CronEvent; /** * Handles interactions with SSP's cron system/hooks. @@ -34,6 +36,7 @@ public function __construct(?Configuration $cronconfig = null) $this->cronconfig = $cronconfig; } + /** * Invoke the cron hook for the given tag * @param string $tag The tag to use. Must be valid in the cronConfig @@ -52,7 +55,17 @@ public function runTag(string $tag): array 'tag' => $tag, ]; + // DEPRECATED: call the hook infrastructure Module::callHooks('cron', $croninfo); + // NEW: dispatch the cron event + $eventDispatcher = ModuleEventDispatcherFactory::getInstance(); + /** @var \SimpleSAML\Module\cron\Event\CronEvent $event */ + $event = $eventDispatcher->dispatch(new CronEvent($tag)); + // merge results from the event into $croninfo. Can be removed when hook infrastructure is removed. + $croninfo['summary'] = array_merge($croninfo['summary'], array_map( + fn ($result) => $result['message'], + $event->getResults(), + )); Assert::isArray($croninfo); foreach ($summary as $s) { @@ -63,6 +76,7 @@ public function runTag(string $tag): array return $croninfo; } + /** * @param string $tag * @return bool diff --git a/modules/cron/src/Event/CronEvent.php b/modules/cron/src/Event/CronEvent.php new file mode 100644 index 0000000000..32ab0cd361 --- /dev/null +++ b/modules/cron/src/Event/CronEvent.php @@ -0,0 +1,48 @@ +tag; + } + + + public function addResult(string $taskName, bool $success, string $message = ''): void + { + $this->results[$taskName] = [ + 'success' => $success, + 'message' => $message, + ]; + } + + + public function getResults(): array + { + return $this->results; + } + + + public function hasFailures(): bool + { + foreach ($this->results as $result) { + if (!$result['success']) { + return true; + } + } + return false; + } +} diff --git a/modules/cron/src/Event/Listener/ConfigPageListener.php b/modules/cron/src/Event/Listener/ConfigPageListener.php new file mode 100644 index 0000000000..37fb84e378 --- /dev/null +++ b/modules/cron/src/Event/Listener/ConfigPageListener.php @@ -0,0 +1,22 @@ +getTemplate(); + + $template->data['links'][] = [ + 'href' => \SimpleSAML\Module::getModuleURL('cron/info'), + 'text' => \SimpleSAML\Locale\Translate::noop('Cron module information page'), + ]; + + $template->getLocalization()->addModuleDomain('cron'); + } +} diff --git a/modules/cron/src/Event/Listener/CronEventListener.php b/modules/cron/src/Event/Listener/CronEventListener.php new file mode 100644 index 0000000000..800cbdc000 --- /dev/null +++ b/modules/cron/src/Event/Listener/CronEventListener.php @@ -0,0 +1,20 @@ +getOptionalBoolean('debug_message', true)) { + $event->addResult('cron info', true, 'Cron did run tag [' . $event->getTag() . '] at ' . date(DATE_RFC822)); + } + } +} diff --git a/modules/debugsp/locales/uk/LC_MESSAGES/debugsp.po b/modules/debugsp/locales/uk/LC_MESSAGES/debugsp.po new file mode 100644 index 0000000000..02d132fd7d --- /dev/null +++ b/modules/debugsp/locales/uk/LC_MESSAGES/debugsp.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: SimpleSAMLphp\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Domain: debugsp\n" +"X-Generator: Poedit 3.7\n" + +msgid "Logged out" +msgstr "Вийти із системи" + +msgid "SimpleSAMLphp installation page" +msgstr "Сторінка встановлення SimpleSAMLphp" + +msgid "Test SP login" +msgstr "Тестовий вхід через постачальника послуг SP" + +msgid "Test another SP login for this site" +msgstr "Протестувати вхід через інший постачальника послуг SP для цього сайту" + +msgid "You have been logged out." +msgstr "Ви вийшли з системи." diff --git a/modules/debugsp/src/Controller/Test.php b/modules/debugsp/src/Controller/Test.php index bb3823059a..9d3dc3cf11 100644 --- a/modules/debugsp/src/Controller/Test.php +++ b/modules/debugsp/src/Controller/Test.php @@ -38,7 +38,6 @@ class Test protected $authState = Auth\State::class; - /** * TestController constructor. * @@ -51,6 +50,7 @@ public function __construct( ) { } + /** * Inject the \SimpleSAML\Auth\Simple dependency. * @@ -61,6 +61,7 @@ public function setAuthSimple(Auth\Simple $authSimple): void $this->authSimple = $authSimple; } + /** * Inject the \SimpleSAML\Auth\State dependency. * @@ -71,6 +72,7 @@ public function setAuthState(Auth\State $authState): void $this->authState = $authState; } + /** * Create a page listing the SPs that can be tested * @@ -94,6 +96,7 @@ private function makeSPList(Request $request, ?string $as = null): Response return $t; } + /** * Display the list of available authsources. * diff --git a/modules/debugsp/templates/status.twig b/modules/debugsp/templates/status.twig index d56608be29..f2f516deda 100644 --- a/modules/debugsp/templates/status.twig +++ b/modules/debugsp/templates/status.twig @@ -1,5 +1,4 @@ {% set pagetitle = 'SimpleSAMLphp installation page'|trans %} {% set frontpage_section = 'test' %} -{% set trackid = '' %} {%- include "auth_status.twig" %} diff --git a/modules/exampleauth/public/assets/css/default.css b/modules/exampleauth/public/assets/css/default.css new file mode 100644 index 0000000000..58bce21a0b --- /dev/null +++ b/modules/exampleauth/public/assets/css/default.css @@ -0,0 +1,23 @@ +h2 { + margin-bottom: 0; +} + +h2:hover { + margin-bottom: 0; +} + +a { + text-decoration: unset; +} + +a:hover { + color: unset; +} + +table.profile { + font-size: smaller; +} + +.profile td.key { + padding-right: 10px; +} diff --git a/modules/exampleauth/routing/routes/routes.yml b/modules/exampleauth/routing/routes/routes.yml index 2c181e8796..769591b8ce 100644 --- a/modules/exampleauth/routing/routes/routes.yml +++ b/modules/exampleauth/routing/routes/routes.yml @@ -20,3 +20,10 @@ exampleauth-resume: _controller: 'SimpleSAML\Module\exampleauth\Controller\ExampleAuth::resume' } methods: [GET] + +profileauth-login: + path: /profileauth + defaults: { + _controller: 'SimpleSAML\Module\exampleauth\Controller\ProfileAuth::login' + } + methods: [GET, POST] diff --git a/modules/exampleauth/src/Auth/Source/External.php b/modules/exampleauth/src/Auth/Source/External.php index 4bbc9c865d..a18b290e5a 100644 --- a/modules/exampleauth/src/Auth/Source/External.php +++ b/modules/exampleauth/src/Auth/Source/External.php @@ -36,7 +36,7 @@ class External extends Auth\Source /** * The key of the AuthId field in the state. */ - public const AUTHID = 'SimpleSAML\Module\exampleauth\Auth\Source\External.AuthId'; + public const string AUTHID = 'SimpleSAML\Module\exampleauth\Auth\Source\External.AuthId'; /** diff --git a/modules/exampleauth/src/Auth/Source/UserClick.php b/modules/exampleauth/src/Auth/Source/UserClick.php new file mode 100644 index 0000000000..1c6ac4e4f8 --- /dev/null +++ b/modules/exampleauth/src/Auth/Source/UserClick.php @@ -0,0 +1,215 @@ +", + * while the value of each element is a new array with the attributes for each user. + * + * @var array + */ + public array $users = []; + + + /** + * @param array $config + * @return array + */ + protected function getUsers(array $config): array + { + $users = []; + $attrUtils = new Utils\Attributes(); + + // Validate and parse our configuration + foreach ($config as $id => $attributes) { + try { + $attributes = $attrUtils->normalizeAttributesArray($attributes); + } catch (Exception $e) { + throw new Exception(sprintf( + 'Invalid attributes for user %s in authentication source %s: %s', + $id, + $this->authId, + $e->getMessage(), + )); + } + + $users[$id] = $attributes; + } + + return $users; + } + + + /** + * Constructor for this authentication source. + * + * @param array $info Information about this authentication source. + * @param array $config Configuration. + */ + public function __construct(array $info, array $config) + { + // Call the parent constructor first, as required by the interface + parent::__construct($info, $config); + + if (array_key_exists('users', $config)) { + $users = $config['users']; + } else { + Logger::warning( + "Module exampleauth:UserClick misconfigured. Put users in 'users' key in your authsource.", + ); + + throw new Error\Error(Error\ErrorCodes::WRONGUSERPASS); + } + + $this->users = $this->getUsers($users); + } + + + /** + * Initialize login. + * + * This function saves the information about the login, and redirects to a + * login page. + * + * @param array &$state Information about the current authentication. + */ + public function authenticate(array &$state): never + { + /* + * Save the identifier of this authentication source, so that we can + * retrieve it later. This allows us to call the login()-function on + * the current object. + */ + $state[self::AUTHID] = $this->authId; + + // Save the $state-array, so that we can restore it after a redirect + $id = Auth\State::saveState($state, self::STAGEID); + + /* + * If there is only one user configured, skip the persona chooser + */ + if (count($this->users) === 1) { + $this->handleLogin($id, 0); + } + + /* + * Redirect to the login form. We include the identifier of the saved + * state array as a parameter to the login form. + */ + $url = Module::getModuleURL('exampleauth/profileauth'); + $params = ['AuthState' => $id]; + $httpUtils = new Utils\HTTP(); + $httpUtils->redirectTrustedURL($url, $params); + } + + + /** + * Attempt to log in using the given user id. + * + * On a successful login, this function should return the users attributes. On failure, + * it should throw an exception. If the error was caused by the user entering the wrong + * username or password, a \SimpleSAML\Error\Error(\SimpleSAML\Error\ErrorCodes::WRONGUSERPASS) should be thrown. + * + * Note that both the username and the password are UTF-8 encoded. + * + * @param int $id The userprofile the user clicked on. + * @return array Associative array with the users attributes. + */ + protected function login(int $id): array + { + if (!array_key_exists($id, $this->users)) { + throw new Error\Error(Error\ErrorCodes::WRONGUSERPASS); + } + + return $this->users[$id]; + } + + + /** + * Handle login request. + * + * This function is used by the login form (exampleauth/login) when the user + * enters a username and password. On success, it will not return. On wrong + * username/password failure, and other errors, it will throw an exception. + * + * @param string $authStateId The identifier of the authentication state. + * @param string $id The username the user wrote. + */ + public static function handleLogin(string $authStateId, int $id): void + { + // Here we retrieve the state array we saved in the authenticate-function. + $state = Auth\State::loadState($authStateId, self::STAGEID); + + // Retrieve the authentication source we are executing. + Assert::keyExists($state, self::AUTHID); + + /** @var \SimpleSAML\Module\exampleauth\Auth\Source\UserClick|null $source */ + $source = Auth\Source::getById($state[self::AUTHID]); + if ($source === null) { + throw new Exception('Could not find authentication source with id ' . $state[self::AUTHID]); + } + + /* + * $source now contains the authentication source on which authenticate() + * was called. We should call login() on the same authentication source. + */ + + // Attempt to log in + try { + $attributes = $source->login($id); + } catch (Exception $e) { + Logger::stats(sprintf( + 'Unsuccessful login attempt from %s.', + $_SERVER['REMOTE_ADDR'], + )); + throw $e; + } + + Logger::stats(sprintf( + "User '%s' successfully authenticated from %s", + $id, + $_SERVER['REMOTE_ADDR'], + )); + + // Save the attributes we received from the login-function in the $state-array + $state['Attributes'] = $attributes; + + // Return control to SimpleSAMLphp after successful authentication. + Auth\Source::completeAuth($state); + } +} diff --git a/modules/exampleauth/src/Controller/ExampleAuth.php b/modules/exampleauth/src/Controller/ExampleAuth.php index 102e5c92d3..561816e9f1 100644 --- a/modules/exampleauth/src/Controller/ExampleAuth.php +++ b/modules/exampleauth/src/Controller/ExampleAuth.php @@ -12,7 +12,8 @@ use SimpleSAML\Session; use SimpleSAML\Utils; use SimpleSAML\XHTML\Template; -use Symfony\Component\HttpFoundation\{Request, Response}; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Session as SymfonySession; use function preg_match; diff --git a/modules/exampleauth/src/Controller/ProfileAuth.php b/modules/exampleauth/src/Controller/ProfileAuth.php new file mode 100644 index 0000000000..6e0c6f5d98 --- /dev/null +++ b/modules/exampleauth/src/Controller/ProfileAuth.php @@ -0,0 +1,172 @@ +query->has('AuthState')) { + throw new Error\BadRequest('Missing AuthState parameter.'); + } + + $authStateId = $request->query->get('AuthState'); + $this->authState::validateStateId($authStateId); + + $state = $this->authState::loadState($authStateId, UserClick::STAGEID); + + /** @var \SimpleSAML\Module\exampleauth\Auth\UserClick|null $source */ + $source = $this->authSource::getById($state[UserClick::AUTHID]); + if ($source === null) { + throw new BuiltinException( + 'Could not find authentication source with id ' . $state[UserClick::AUTHID], + ); + } + + return $this->handleLogin($request, $source, $state); + } + + + /** + * This method handles the generic part for both login and loginuserpassorg + * + * @param \Symfony\Component\HttpFoundation\Request $request + * @param \SimpleSAML\Module\exampleauth\Auth\UserClick $source + * @param array $state + */ + private function handleLogin(Request $request, UserClick $source, array $state): Response + { + $authStateId = $request->query->get('AuthState'); + $this->authState::validateStateId($authStateId); + + $id = $this->getIDFromRequest($request, $source, $state); + + $errorCode = null; + $errorParams = null; + + if (isset($state['error'])) { + $errorCode = $state['error']['code']; + $errorParams = $state['error']['params']; + } + + if ($id !== '') { + try { + UserClick::handleLogin($authStateId, (int)$id); + } catch (Error\Error $e) { + // Login failed. Extract error code and parameters, to display the error + $errorCode = $e->getErrorCode(); + $errorParams = $e->getParameters(); + $state['error'] = [ + 'code' => $errorCode, + 'params' => $errorParams, + ]; + $authStateId = Auth\State::saveState($state, $source::STAGEID); + } + + if (isset($state['error'])) { + unset($state['error']); + } + } + + $t = new Template($this->config, 'exampleauth:userclick.twig'); + + $t->data['users'] = $source->users; + $userlinks = []; + foreach ($source->users as $id => $attr) { + $userlinks[$id] = Module::getModuleURL('exampleauth/profileauth', ['AuthState' => $authStateId, + "id" => $id ]); + } + $t->data['usersLinks'] = $userlinks; + + $t->data['errorcode'] = $errorCode; + $t->data['errorcodes'] = Error\ErrorCodes::getAllErrorCodeMessages(); + $t->data['errorparams'] = $errorParams; + + if (isset($state['SPMetadata'])) { + $t->data['SPMetadata'] = $state['SPMetadata']; + } else { + $t->data['SPMetadata'] = null; + } + + return $t; + } + + + /** + * Retrieve the username from the request, a cookie or the state + * + * @param \Symfony\Component\HttpFoundation\Request $request + * @param \SimpleSAML\Auth\Source $source + * @param array $state + * @return string + */ + private function getIDFromRequest(Request $request, Auth\Source $source, array $state): string + { + $id = ''; + + if ($request->query->has('id')) { + $id = trim($request->query->get('id')); + } elseif (isset($state['exampleauth:id'])) { + $id = strval($state['exampleauth:id']); + } + + return $id; + } +} diff --git a/modules/exampleauth/templates/userclick.twig b/modules/exampleauth/templates/userclick.twig new file mode 100644 index 0000000000..2fa61ac179 --- /dev/null +++ b/modules/exampleauth/templates/userclick.twig @@ -0,0 +1,63 @@ +{% set pagetitle = 'Continue as persona' %} + +{% extends "@core/base.twig" %} + +{% block preload %} + +{% endblock %} + +{% block content %} + {%- if not isProduction %} + +
+ {% trans %}You are now accessing a pre-production system. This authentication setup is for testing and pre-production verification only. If someone sent you a link that pointed you here, and you are not a tester you probably got the wrong link, and should not be here.{% endtrans %} +
+ {% endif -%} + {% if errorcode -%} +
+
+
+ + {% set errtitles = errorcodes['title'] %} + {% set errtitle = errtitles[errorcode] %} + +

{{ errtitle|trans(errorparams) }}

+ + {% set errdescs = errorcodes['descr'] %} + {% set errdesc = errdescs[errorcode] %} + +

{{ errdesc|trans(errorparams) }}

+ +
+
+
+ {%- endif %} + +

{{ 'Continue as persona' }}

+ + {% for id, attributes in users %} + +
+

+ {{ attributes['displayName'][0] }} +

+ + + {%- for key, values in attributes %} + + + + {%- endfor %} + + +
+ {{ key }} + + {%- for value in values %} + {{ value }}
+ {%- endfor %} +
+
+ {%- endfor %} + +{% endblock %} diff --git a/modules/multiauth/locales/uk/LC_MESSAGES/multiauth.po b/modules/multiauth/locales/uk/LC_MESSAGES/multiauth.po new file mode 100644 index 0000000000..b775409d82 --- /dev/null +++ b/modules/multiauth/locales/uk/LC_MESSAGES/multiauth.po @@ -0,0 +1,19 @@ +msgid "" +msgstr "" +"Project-Id-Version: SimpleSAMLphp\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Domain: multiauth\n" +"X-Generator: Poedit 3.7\n" + +msgid "Select an authentication source" +msgstr "Виберіть джерело автентифікації" + +msgid "The selected authentication source will be used to authenticate you and to create a valid session." +msgstr "Обране джерело автентифікації буде використано для підтвердження вашої особи та створення дійсної сесії." diff --git a/modules/multiauth/src/Auth/Source/MultiAuth.php b/modules/multiauth/src/Auth/Source/MultiAuth.php index 8e5a0f9e1f..fd6c45304d 100644 --- a/modules/multiauth/src/Auth/Source/MultiAuth.php +++ b/modules/multiauth/src/Auth/Source/MultiAuth.php @@ -6,7 +6,6 @@ use Exception; use SAML2\Exception\Protocol\NoAuthnContextException; -use SimpleSAML\Assert\Assert; use SimpleSAML\Auth; use SimpleSAML\Configuration; use SimpleSAML\Error; @@ -26,22 +25,23 @@ class MultiAuth extends Auth\Source /** * The key of the AuthId field in the state. */ - public const AUTHID = '\SimpleSAML\Module\multiauth\Auth\Source\MultiAuth.AuthId'; + public const string AUTHID = '\SimpleSAML\Module\multiauth\Auth\Source\MultiAuth.AuthId'; /** * The string used to identify our states. */ - public const STAGEID = '\SimpleSAML\Module\multiauth\Auth\Source\MultiAuth.StageId'; + public const string STAGEID = '\SimpleSAML\Module\multiauth\Auth\Source\MultiAuth.StageId'; /** * The key where the sources is saved in the state. */ - public const SOURCESID = '\SimpleSAML\Module\multiauth\Auth\Source\MultiAuth.SourceId'; + public const string SOURCESID = '\SimpleSAML\Module\multiauth\Auth\Source\MultiAuth.SourceId'; /** * The key where the selected source is saved in the session. */ - public const SESSION_SOURCE = 'multiauth:selectedSource'; + public const string SESSION_SOURCE = 'multiauth:selectedSource'; + /** * Array of sources we let the user chooses among. @@ -94,7 +94,7 @@ public function __construct(array $info, array $config) * * @param array &$state Information about the current authentication. */ - public function authenticate(array &$state): void + public function authenticate(array &$state): never { $state[self::AUTHID] = $this->authId; $state[self::SOURCESID] = $this->sources; @@ -145,9 +145,6 @@ public function authenticate(array &$state): void $httpUtils = new Utils\HTTP(); $httpUtils->redirectTrustedURL($url, $params); - - // The previous function never returns, so this code is never executed - Assert::true(false); } diff --git a/modules/saml/docs/sp.md b/modules/saml/docs/sp.md index 4bf7c5e7bc..06762700c7 100644 --- a/modules/saml/docs/sp.md +++ b/modules/saml/docs/sp.md @@ -479,9 +479,9 @@ Here we will list some examples for this authentication source. ### Using samlp:Extensions - $dom = \SAML2\DOMDocumentFactory::create(); + $dom = \SimpleSAML\XML\DOMDocumentFactory::create(); $ce = $dom->createElementNS('http://www.example.com/XFoo', 'xfoo:test', 'Test data!'); - $ext[] = new \SAML2\XML\Chunk($ce); + $ext[] = new \SimpleSAML\XML\Chunk($ce); $auth = new \SimpleSAML\Auth\Simple('default-sp'); $auth->login([ diff --git a/modules/saml/hooks/hook_sanitycheck.php b/modules/saml/hooks/hook_sanitycheck.php deleted file mode 100644 index 15d9ba3622..0000000000 --- a/modules/saml/hooks/hook_sanitycheck.php +++ /dev/null @@ -1,59 +0,0 @@ -getOptionalBoolean('enable.saml20-idp', false) !== false) { - $handler = MetaDataStorageHandler::getMetadataHandler(); - try { - $metadata = $handler->getMetaDataCurrent('saml20-idp-hosted'); - } catch (Exception $e) { - $hookinfo['errors'][] = MODID . Translate::noop('Hosted IdP metadata present'); - } - - if (isset($metadata)) { - $metadata_config = Configuration::loadfromArray($metadata); - $private = $cryptoUtils->loadPrivateKey($metadata_config, false); - $public = $cryptoUtils->loadPublicKey($metadata_config, false); - - $matches = matchingKeyPair($public['PEM'], $private['PEM'], $private['password']); - $hookinfo[$matches ? 'info' : 'errors'][] = MODID . - Translate::noop('Matching key-pair for signing assertions'); - - $private = $cryptoUtils->loadPrivateKey($metadata_config, false, 'new_'); - if ($private !== null) { - $public = $cryptoUtils->loadPublicKey($metadata_config, false, 'new_'); - $matches = matchingKeyPair($public['PEM'], $private['PEM'], $private['password']); - $hookinfo[$matches ? 'info' : 'errors'][] = MODID . - Translate::noop('Matching key-pair for signing assertions (rollover key)'); - } - } - } - - if ($config->getOptionalBoolean('metadata.sign.enable', false) !== false) { - $private = $cryptoUtils->loadPrivateKey($config, true, 'metadata.sign.'); - $public = $cryptoUtils->loadPublicKey($config, true, 'metadata.sign.'); - $matches = matchingKeyPair($public['PEM'], $private['PEM'], $private['password']); - $hookinfo[$matches ? 'info' : 'errors'][] = - MODID . Translate::noop('Matching key-pair for signing metadata'); - } -} - -function matchingKeyPair(string $publicKey, string $privateKey, ?string $password = null): bool -{ - return openssl_x509_check_private_key($publicKey, [$privateKey, $password]); -} diff --git a/modules/saml/locales/uk/LC_MESSAGES/saml.po b/modules/saml/locales/uk/LC_MESSAGES/saml.po new file mode 100644 index 0000000000..ece98a773c --- /dev/null +++ b/modules/saml/locales/uk/LC_MESSAGES/saml.po @@ -0,0 +1,50 @@ +msgid "" +msgstr "" +"Project-Id-Version: SimpleSAMLphp\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Domain: saml\n" +"X-Generator: Poedit 3.7\n" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/saml/hooks/hook_sanitycheck.php:25 +msgid "Hosted IdP metadata present" +msgstr "Метадані розміщеного IdP наявні" + +msgid "Invalid Identity Provider" +msgstr "Недійсний постачальник ідентифікації" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/saml/hooks/hook_sanitycheck.php:35 +msgid "Matching key-pair for signing assertions" +msgstr "Відповідна пара ключів для підпису тверджень" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/saml/hooks/hook_sanitycheck.php:42 +msgid "Matching key-pair for signing assertions (rollover key)" +msgstr "Відповідна пара ключів для підписання тверджень (ключ, що замінюється)" + +#: /home/runner/work/simplesamlphp/simplesamlphp/modules/saml/hooks/hook_sanitycheck.php:52 +msgid "Matching key-pair for signing metadata" +msgstr "Відповідна пара ключів для підпису метаданих" + +msgid "No, cancel" +msgstr "Ні, скасувати" + +msgid "SimpleSAMLphp" +msgstr "SimpleSAMLphp" + +msgid "Wrong authentication context" +msgstr "Неправильний контекст автентифікації" + +msgid "Yes, continue" +msgstr "Так, продовжити" + +msgid "You already have a valid session with an identity provider (%IDP%) that is not accepted by %SP%. Would you like to log out from your existing session and log in again with another identity provider?" +msgstr "У вас уже є активна сесія з постачальником ідентифікації (%IDP%), який не приймається постачальником послуг (%SP%). Бажаєте вийти з поточної сесії та увійти з іншим постачальником ідентифікації?" + +msgid "Your authentication context is not accepted at this service. Probably too weak or not two-factor." +msgstr "Контекст автентифікації не приймається цим сервісом. Ймовірно, він надто слабкий або не містить двофакторної автентифікації." diff --git a/modules/saml/src/Auth/Process/ExpectedAuthnContextClassRef.php b/modules/saml/src/Auth/Process/ExpectedAuthnContextClassRef.php index 05ded98294..b287cbf5b0 100644 --- a/modules/saml/src/Auth/Process/ExpectedAuthnContextClassRef.php +++ b/modules/saml/src/Auth/Process/ExpectedAuthnContextClassRef.php @@ -36,7 +36,6 @@ class ExpectedAuthnContextClassRef extends ProcessingFilter */ private array $accepted; - /** * AuthnContextClassRef of the assertion * @var string|null diff --git a/modules/saml/src/Auth/Process/FilterScopes.php b/modules/saml/src/Auth/Process/FilterScopes.php index 4d4222c89a..7a32f0aae0 100644 --- a/modules/saml/src/Auth/Process/FilterScopes.php +++ b/modules/saml/src/Auth/Process/FilterScopes.php @@ -24,6 +24,7 @@ class FilterScopes extends ProcessingFilter 'eduPersonPrincipalName', ]; + /** * Constructor for the processing filter. * @@ -39,6 +40,7 @@ public function __construct(array &$config, $reserved) } } + /** * This method applies the filter, removing any values * diff --git a/modules/saml/src/Auth/Process/NameIDAttribute.php b/modules/saml/src/Auth/Process/NameIDAttribute.php index 0037d04562..735313beea 100644 --- a/modules/saml/src/Auth/Process/NameIDAttribute.php +++ b/modules/saml/src/Auth/Process/NameIDAttribute.php @@ -24,7 +24,6 @@ class NameIDAttribute extends ProcessingFilter */ private string $attribute; - /** * The format of the NameID in the attribute. * diff --git a/modules/saml/src/Auth/Process/PairwiseID.php b/modules/saml/src/Auth/Process/PairwiseID.php index 104d20333c..166bc01684 100644 --- a/modules/saml/src/Auth/Process/PairwiseID.php +++ b/modules/saml/src/Auth/Process/PairwiseID.php @@ -5,7 +5,6 @@ namespace SimpleSAML\Module\saml\Auth\Process; use SAML2\Constants; -use SimpleSAML\{Auth, Utils}; use function strtolower; @@ -39,7 +38,7 @@ class PairwiseID extends SubjectID /** * The name for this class */ - public const NAME = 'PairwiseID'; + public const string NAME = 'PairwiseID'; /** diff --git a/modules/saml/src/Auth/Process/PersistentNameID2TargetedID.php b/modules/saml/src/Auth/Process/PersistentNameID2TargetedID.php index 03bbf0e397..ad1fe9f27f 100644 --- a/modules/saml/src/Auth/Process/PersistentNameID2TargetedID.php +++ b/modules/saml/src/Auth/Process/PersistentNameID2TargetedID.php @@ -23,7 +23,6 @@ class PersistentNameID2TargetedID extends ProcessingFilter */ private string $attribute; - /** * Whether we should insert it as an saml:NameID element. * diff --git a/modules/saml/src/Auth/Process/ScopedIssuer.php b/modules/saml/src/Auth/Process/ScopedIssuer.php index 5261232605..c101b8ee54 100644 --- a/modules/saml/src/Auth/Process/ScopedIssuer.php +++ b/modules/saml/src/Auth/Process/ScopedIssuer.php @@ -5,13 +5,13 @@ namespace SimpleSAML\Module\saml\Auth\Process; use SAML2\Exception\ProtocolViolationException; -use SimpleSAML\{Auth, Utils}; use SimpleSAML\Assert\Assert; +use SimpleSAML\Auth; use function array_key_exists; use function explode; -use function strpos; use function sprintf; +use function strpos; /** * Filter to generate a saml:issuer dynamically based on an input attribute. @@ -42,10 +42,9 @@ class ScopedIssuer extends Auth\ProcessingFilter { /** * The regular expression to match the scope - * - * @var string */ - public const SCOPE_PATTERN = '/^[a-z0-9][a-z0-9.-]{0,126}$/Di'; + public const string SCOPE_PATTERN = '/^[a-z0-9][a-z0-9.-]{0,126}$/Di'; + /** * The attribute we should use for the scope of the saml:Issuer. @@ -131,7 +130,10 @@ public function process(array &$state): void */ protected function getScopedAttribute(array $state): ?string { - if (!array_key_exists('Attributes', $state) || !array_key_exists($this->scopedAttribute, $state['Attributes'])) { + if ( + !array_key_exists('Attributes', $state) + || !array_key_exists($this->scopedAttribute, $state['Attributes']) + ) { return null; } diff --git a/modules/saml/src/Auth/Process/SubjectID.php b/modules/saml/src/Auth/Process/SubjectID.php index 3d4401c899..3122d32b10 100644 --- a/modules/saml/src/Auth/Process/SubjectID.php +++ b/modules/saml/src/Auth/Process/SubjectID.php @@ -6,16 +6,18 @@ use SAML2\Constants; use SAML2\Exception\ProtocolViolationException; -use SimpleSAML\{Auth, Logger, Utils}; use SimpleSAML\Assert\Assert; +use SimpleSAML\Auth; +use SimpleSAML\Logger; +use SimpleSAML\Utils; use function array_key_exists; use function explode; use function hash_hmac; use function preg_match; +use function sprintf; use function strpos; use function strtolower; -use function sprintf; /** * Filter to generate the subject ID attribute. @@ -47,28 +49,26 @@ class SubjectID extends Auth\ProcessingFilter /** * The name for this class */ - public const NAME = 'SubjectID'; + public const string NAME = 'SubjectID'; /** * The regular expression to match the scope * - * @var string */ - public const SCOPE_PATTERN = '/^[a-z0-9][a-z0-9.-]{0,126}$/Di'; + public const string SCOPE_PATTERN = '/^[a-z0-9][a-z0-9.-]{0,126}$/Di'; /** * The regular expression to match the specifications * - * @var string */ - public const SPEC_PATTERN = '/^[a-z0-9][a-z0-9=-]{0,126}@[a-z0-9][a-z0-9.-]{0,126}$/Di'; + public const string SPEC_PATTERN = '/^[a-z0-9][a-z0-9=-]{0,126}@[a-z0-9][a-z0-9.-]{0,126}$/Di'; /** * The regular expression to match worrisome identifiers that need to raise a warning * - * @var string */ - public const WARN_PATTERN = '/^[a-z0-9][a-z0-9=-]{3,}@[a-z0-9][a-z0-9.-]+\.[a-z]{2,}$/Di'; + public const string WARN_PATTERN = '/^[a-z0-9][a-z0-9=-]{3,}@[a-z0-9][a-z0-9.-]+\.[a-z]{2,}$/Di'; + /** * The attribute we should generate the subject id from. diff --git a/modules/saml/src/Auth/Source/SP.php b/modules/saml/src/Auth/Source/SP.php index dc6e513a3a..12efdc3231 100644 --- a/modules/saml/src/Auth/Source/SP.php +++ b/modules/saml/src/Auth/Source/SP.php @@ -22,12 +22,15 @@ use SimpleSAML\Metadata\MetaDataStorageHandler; use SimpleSAML\Module; use SimpleSAML\Module\saml\Error\ProxyCountExceeded; +use SimpleSAML\SAML2\XML\samlp\AuthnContextComparisonTypeEnum; use SimpleSAML\Session; use SimpleSAML\Store; use SimpleSAML\Store\StoreFactory; use SimpleSAML\Utils; -class SP extends \SimpleSAML\Auth\Source +use function array_column; + +class SP extends Auth\Source { /** * The entity ID of this SP. @@ -448,6 +451,7 @@ private function getSLOEndpoints(): array return $endpoints; } + /** * Get the DiscoveryResponse endpoint available for a given local SP. */ @@ -461,6 +465,7 @@ private function getDiscoveryResponseEndpoints(): array ] ]; } + /** * Determine if the Request Initiator Protocol is enabled * @@ -505,17 +510,16 @@ private function startSSO2(Configuration $idpMetadata, array $state): void } if ($accr !== null) { - $comp = Constants::COMPARISON_EXACT; + $comp = AuthnContextComparisonTypeEnum::Exact->value; if ($idpMetadata->getOptionalString('AuthnContextComparison', null) !== null) { $comp = $idpMetadata->getString('AuthnContextComparison'); } elseif ( isset($state['saml:AuthnContextComparison']) - && in_array($state['saml:AuthnContextComparison'], [ - Constants::COMPARISON_EXACT, - Constants::COMPARISON_MINIMUM, - Constants::COMPARISON_MAXIMUM, - Constants::COMPARISON_BETTER, - ], true) + && in_array( + $state['saml:AuthnContextComparison'], + array_column(AuthnContextComparisonTypeEnum::cases(), 'value'), + true, + ) ) { $comp = $state['saml:AuthnContextComparison']; } @@ -529,12 +533,7 @@ private function startSSO2(Configuration $idpMetadata, array $state): void isset($state['saml:RequestedAuthnContext']['Comparison']) && in_array( $state['saml:RequestedAuthnContext']['Comparison'], - [ - Constants::COMPARISON_EXACT, - Constants::COMPARISON_MINIMUM, - Constants::COMPARISON_MAXIMUM, - Constants::COMPARISON_BETTER, - ], + array_column(AuthnContextComparisonTypeEnum::cases(), 'value'), true, ) ) { @@ -594,8 +593,11 @@ private function startSSO2(Configuration $idpMetadata, array $state): void if (!empty($state['saml:NameIDPolicy'])) { $ar->setNameIdPolicy($state['saml:NameIDPolicy']); + } else { + $ar->setNameIdPolicy($this->metadata->getOptionalArray('NameIDPolicy', [])); } + $requesterID = []; /* Only check for real info for Scoping element if we are going to send Scoping element */ @@ -699,12 +701,11 @@ private function startSSO2(Configuration $idpMetadata, array $state): void * This function does not return. * * @param \SAML2\Binding $binding The binding. - * @param \SAML2\AuthnRequest $ar The authentication request. + * @param \SAML2\AuthnRequest $ar The authentication request. */ - public function sendSAML2AuthnRequest(Binding $binding, AuthnRequest $ar): void + public function sendSAML2AuthnRequest(Binding $binding, AuthnRequest $ar): never { $binding->send($ar); - Assert::true(false); } @@ -714,12 +715,11 @@ public function sendSAML2AuthnRequest(Binding $binding, AuthnRequest $ar): void * This function does not return. * * @param \SAML2\Binding $binding The binding. - * @param \SAML2\LogoutRequest $ar The logout request. + * @param \SAML2\LogoutRequest $ar The logout request. */ - public function sendSAML2LogoutRequest(Binding $binding, LogoutRequest $lr): void + public function sendSAML2LogoutRequest(Binding $binding, LogoutRequest $lr): never { $binding->send($lr); - Assert::true(false); } @@ -729,7 +729,7 @@ public function sendSAML2LogoutRequest(Binding $binding, LogoutRequest $lr): voi * @param string $idp The entity ID of the IdP. * @param array $state The state array for the current authentication. */ - public function startSSO(string $idp, array $state): void + public function startSSO(string $idp, array $state): never { $idpMetadata = $this->getIdPMetadata($idp); @@ -737,7 +737,6 @@ public function startSSO(string $idp, array $state): void Assert::oneOf($type, ['saml20-idp-remote']); $this->startSSO2($idpMetadata, $state); - Assert::true(false); // Should not return } @@ -746,7 +745,7 @@ public function startSSO(string $idp, array $state): void * * @param array $state The state array. */ - private function startDisco(array $state): void + private function startDisco(array $state): never { $id = Auth\State::saveState($state, 'saml:sp:sso'); @@ -784,7 +783,7 @@ private function startDisco(array $state): void * * @param array &$state Information about the current authentication. */ - public function authenticate(array &$state): void + public function authenticate(array &$state): never { // We are going to need the authId in order to retrieve this authentication source later $state['saml:sp:AuthId'] = $this->authId; @@ -822,11 +821,9 @@ public function authenticate(array &$state): void if ($idp === null) { $this->startDisco($state); - Assert::true(false); + } else { + $this->startSSO($idp, $state); } - - $this->startSSO($idp, $state); - Assert::true(false); } @@ -914,7 +911,7 @@ public function reauthenticate(array &$state): void if ( $this->passAuthnContextClassRef && isset($state['saml:RequestedAuthnContext']) - && $state['saml:RequestedAuthnContext']['Comparison'] === Constants::COMPARISON_EXACT + && $state['saml:RequestedAuthnContext']['Comparison'] === AuthnContextComparisonTypeEnum::Exact->value && isset($data['saml:sp:AuthnContext']) && $state['saml:RequestedAuthnContext']['AuthnContextClassRef'][0] !== $data['saml:sp:AuthnContext'] ) { @@ -949,7 +946,7 @@ public function reauthenticate(array &$state): void * * @throws \SAML2\Exception\Protocol\NoPassiveException In case the authentication request was passive. */ - public static function askForIdPChange(array &$state): void + public static function askForIdPChange(array &$state): never { Assert::keyExists($state, 'saml:sp:IdPMetadata'); Assert::keyExists($state, 'saml:sp:AuthId'); @@ -969,7 +966,6 @@ public static function askForIdPChange(array &$state): void $httpUtils = new Utils\HTTP(); $httpUtils->redirectTrustedURL($url, ['AuthState' => $id]); - Assert::true(false); } @@ -985,7 +981,7 @@ public static function askForIdPChange(array &$state): void * - 'saml:sp:AuthId': the identifier of the current authentication source. * @throws \SAML2\Exception\Protocol\NoPassiveException In case the authentication request was passive. */ - public static function tryStepUpAuth(array &$state): void + public static function tryStepUpAuth(array &$state): never { Assert::keyExists($state, 'saml:idp'); Assert::keyExists($state, 'saml:sp:AuthId'); @@ -1000,7 +996,6 @@ public static function tryStepUpAuth(array &$state): void /** @var \SimpleSAML\Module\saml\Auth\Source\SP $as */ $as = new Auth\Simple($state['saml:sp:AuthId']); $as->login($state); - Assert::true(false); } @@ -1011,7 +1006,7 @@ public static function tryStepUpAuth(array &$state): void * * @param array $state The state array. */ - public static function reauthLogout(array $state): void + public static function reauthLogout(array $state): never { Logger::debug('Proxy: logging the user out before re-authentication.'); @@ -1022,7 +1017,6 @@ public static function reauthLogout(array $state): void $idp = IdP::getByState($state); $idp->handleLogoutRequest($state, null); - Assert::true(false); } @@ -1031,7 +1025,7 @@ public static function reauthLogout(array $state): void * * @param array $state The authentication state. */ - public static function reauthPostLogin(array $state): void + public static function reauthPostLogin(array $state): never { Assert::keyExists($state, 'ReturnCallback'); @@ -1042,7 +1036,6 @@ public static function reauthPostLogin(array $state): void // resume the login process call_user_func($state['ReturnCallback'], $state); - Assert::true(false); } @@ -1054,7 +1047,7 @@ public static function reauthPostLogin(array $state): void * @param \SimpleSAML\IdP $idp The IdP we are logging out from. * @param array &$state The state array with the state during logout. */ - public static function reauthPostLogout(IdP $idp, array $state): void + public static function reauthPostLogout(IdP $idp, array $state): never { Assert::keyExists($state, 'saml:sp:AuthId'); @@ -1069,7 +1062,6 @@ public static function reauthPostLogout(IdP $idp, array $state): void Logger::debug('Proxy: logging in again.'); $sp->authenticate($state); - Assert::true(false); } @@ -1160,7 +1152,7 @@ public function logout(array &$state): void * @param string $idp The entity id of the IdP. * @param array $attributes The attributes. */ - public function handleResponse(array $state, string $idp, array $attributes): void + public function handleResponse(array $state, string $idp, array $attributes): never { Assert::keyExists($state, 'LogoutState'); Assert::keyExists($state['LogoutState'], 'saml:logout:Type'); @@ -1187,6 +1179,7 @@ public function handleResponse(array $state, string $idp, array $attributes): vo if (isset($state['saml:sp:NameID'])) { $authProcState['saml:sp:NameID'] = $state['saml:sp:NameID']; } + if (isset($state['saml:sp:SessionIndex'])) { $authProcState['saml:sp:SessionIndex'] = $state['saml:sp:SessionIndex']; } @@ -1203,7 +1196,7 @@ public function handleResponse(array $state, string $idp, array $attributes): vo * * @param string $idpEntityId The entity ID of the IdP. */ - public function handleLogout(string $idpEntityId): void + public function handleLogout(string $idpEntityId): never { /* Call the logout callback we registered in onProcessingCompleted(). */ $this->callLogoutCallback($idpEntityId); @@ -1224,7 +1217,7 @@ public function handleLogout(string $idpEntityId): void * manually check the URL on beforehand. Please refer to the 'trusted.url.domains' * configuration directive for more information about allowing (or disallowing) URLs. */ - public static function handleUnsolicitedAuth(string $authId, array $state, string $redirectTo): void + public static function handleUnsolicitedAuth(string $authId, array $state, string $redirectTo): never { $session = Session::getSessionFromRequest(); $session->doLogin($authId, Auth\State::getPersistentAuthData($state)); @@ -1239,7 +1232,7 @@ public static function handleUnsolicitedAuth(string $authId, array $state, strin * * @param array $authProcState The processing chain state. */ - public static function onProcessingCompleted(array $authProcState): void + public static function onProcessingCompleted(array $authProcState): never { Assert::keyExists($authProcState, 'saml:sp:IdP'); Assert::keyExists($authProcState, 'saml:sp:State'); diff --git a/modules/saml/src/BaseNameIDGenerator.php b/modules/saml/src/BaseNameIDGenerator.php index c0206fd806..94688527d0 100644 --- a/modules/saml/src/BaseNameIDGenerator.php +++ b/modules/saml/src/BaseNameIDGenerator.php @@ -27,7 +27,6 @@ abstract class BaseNameIDGenerator extends ProcessingFilter */ private string|bool $nameQualifier; - /** * What SPNameQualifier should be used. * Can be one of: @@ -39,7 +38,6 @@ abstract class BaseNameIDGenerator extends ProcessingFilter */ private string|bool $spNameQualifier; - /** * The format of this NameID. * diff --git a/modules/saml/src/Controller/Exception.php b/modules/saml/src/Controller/Exception.php index 39d112f8a9..0da8dcba0c 100644 --- a/modules/saml/src/Controller/Exception.php +++ b/modules/saml/src/Controller/Exception.php @@ -4,7 +4,8 @@ namespace SimpleSAML\Module\saml\Controller; -use SimpleSAML\{Configuration, Logger}; +use SimpleSAML\Configuration; +use SimpleSAML\Logger; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; diff --git a/modules/saml/src/Controller/Metadata.php b/modules/saml/src/Controller/Metadata.php index 8bb14ab93f..e9766dea97 100644 --- a/modules/saml/src/Controller/Metadata.php +++ b/modules/saml/src/Controller/Metadata.php @@ -31,6 +31,7 @@ class Metadata /** @var \SimpleSAML\Metadata\MetaDataStorageHandler */ protected MetadataStorageHandler $mdHandler; + /** * Controller constructor. * @@ -45,6 +46,7 @@ public function __construct( $this->mdHandler = MetaDataStorageHandler::getMetadataHandler(); } + /** * Inject the \SimpleSAML\Utils\Auth dependency. * @@ -55,6 +57,7 @@ public function setAuthUtils(Utils\Auth $authUtils): void $this->authUtils = $authUtils; } + /** * Inject the \SimpleSAML\Metadata\MetadataStorageHandler dependency. */ @@ -63,6 +66,7 @@ public function setMetadataStorageHandler(MetadataStorageHandler $mdHandler): vo $this->mdHandler = $mdHandler; } + /** * This endpoint will offer the SAML 2.0 IdP metadata. * diff --git a/modules/saml/src/Controller/Proxy.php b/modules/saml/src/Controller/Proxy.php index 9b726c907d..73ff5c5903 100644 --- a/modules/saml/src/Controller/Proxy.php +++ b/modules/saml/src/Controller/Proxy.php @@ -14,7 +14,8 @@ use SimpleSAML\Module\saml\Auth\Source\SP; use SimpleSAML\Module\saml\Error\NoAvailableIDP; use SimpleSAML\XHTML\Template; -use Symfony\Component\HttpFoundation\{Request, Response}; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; /** * Controller class for the saml module. diff --git a/modules/saml/src/Controller/ServiceProvider.php b/modules/saml/src/Controller/ServiceProvider.php index d79673b331..91b2c38e2e 100644 --- a/modules/saml/src/Controller/ServiceProvider.php +++ b/modules/saml/src/Controller/ServiceProvider.php @@ -30,7 +30,8 @@ use SimpleSAML\Store\StoreFactory; use SimpleSAML\Utils; use SimpleSAML\XHTML\Template; -use Symfony\Component\HttpFoundation\{Request, Response}; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use function array_merge; use function count; @@ -65,8 +66,8 @@ class ServiceProvider * * It initializes the global configuration for the controllers implemented here. * - * @param Configuration $config The configuration to use by the controllers. - * @param Session $session The Session to use by the controllers. + * @param \SimpleSAML\Configuration $config The configuration to use by the controllers. + * @param \SimpleSAML\Session $session The Session to use by the controllers. */ public function __construct( protected Configuration $config, @@ -98,15 +99,15 @@ public function setAuthUtils(Utils\Auth $authUtils): void } - /** - * Start single sign-on for an SP identified with the specified Authsource ID - * - * @param \Symfony\Component\HttpFoundation\Request $request - * @param string $sourceId - * - * @return \SimpleSAML\HTTP\RunnableResponse - * @throws Error\Exception - */ + /** + * Start single sign-on for an SP identified with the specified Authsource ID + * + * @param \Symfony\Component\HttpFoundation\Request $request + * @param string $sourceId + * + * @return \SimpleSAML\HTTP\RunnableResponse + * @throws \SimpleSAML\Error\Exception + */ public function login(Request $request, string $sourceId): RunnableResponse { // Initialize all the dependencies @@ -125,15 +126,16 @@ public function login(Request $request, string $sourceId): RunnableResponse return new RunnableResponse([$httpUtils, 'redirectTrustedURL'], [$returnTo]); } + /** - * @param Request $request - * @param Auth\Simple $authSource - * @param Auth\Source $spSource - * @param Utils\HTTP $httpUtils + * @param \Symfony\Component\HttpFoundation\Request $request + * @param \SimpleSAML\Auth\Simple $authSource + * @param \SimpleSAML\Auth\Source $spSource + * @param \SimpleSAML\Utils\HTTP $httpUtils * * @return string * @throws \SimpleSAML\Error\BadRequest - * @throws Error\Exception + * @throws \SimpleSAML\Error\Exception */ protected function loginHandler( Request $request, diff --git a/modules/saml/src/Controller/WebBrowserSingleSignOn.php b/modules/saml/src/Controller/WebBrowserSingleSignOn.php index 0021c839c8..0fbb2dc625 100644 --- a/modules/saml/src/Controller/WebBrowserSingleSignOn.php +++ b/modules/saml/src/Controller/WebBrowserSingleSignOn.php @@ -7,7 +7,6 @@ use Exception; use SAML2\ArtifactResolve; use SAML2\ArtifactResponse; -use SAML2\DOMDocumentFactory; use SAML2\Exception\Protocol\UnsupportedBindingException; use SAML2\SOAP; use SAML2\XML\saml\Issuer; @@ -20,6 +19,7 @@ use SimpleSAML\Metadata; use SimpleSAML\Module; use SimpleSAML\Store\StoreFactory; +use SimpleSAML\XML\DOMDocumentFactory; /** * Controller class for the Web Browser Single Sign On profile. diff --git a/modules/saml/src/Error.php b/modules/saml/src/Error.php index 8bf5df581b..19b4751913 100644 --- a/modules/saml/src/Error.php +++ b/modules/saml/src/Error.php @@ -4,7 +4,10 @@ namespace SimpleSAML\Module\saml; -use SAML2\Constants; +use SimpleSAML\Error as SSPError; +use SimpleSAML\Module\saml\Error as SAMLError; +use SimpleSAML\Module\saml\Error\NoPassive; +use SimpleSAML\SAML2\Constants; use Throwable; /** @@ -13,7 +16,7 @@ * @package SimpleSAMLphp */ -class Error extends \SimpleSAML\Error\Exception +class Error extends SSPError\Exception { /** * Create a SAML 2 error. @@ -84,9 +87,9 @@ public function getStatusMessage(): ?string * @param \Throwable $e The original exception. * @return \SimpleSAML\Error\Exception The new exception. */ - public static function fromException(Throwable $e): \SimpleSAML\Error\Exception + public static function fromException(Throwable $e): SSPError\Exception { - if ($e instanceof \SimpleSAML\Module\saml\Error) { + if ($e instanceof SAMLError) { // Return the original exception unchanged return $e; } else { @@ -113,7 +116,7 @@ public static function fromException(Throwable $e): \SimpleSAML\Error\Exception * * @return \SimpleSAML\Error\Exception An exception representing this error. */ - public function toException(): \SimpleSAML\Error\Exception + public function toException(): SSPError\Exception { $e = null; @@ -121,7 +124,7 @@ public function toException(): \SimpleSAML\Error\Exception case Constants::STATUS_RESPONDER: switch ($this->subStatus) { case Constants::STATUS_NO_PASSIVE: - $e = new \SimpleSAML\Module\saml\Error\NoPassive( + $e = new NoPassive( Constants::STATUS_RESPONDER, $this->statusMessage, ); diff --git a/modules/saml/src/Event/Listener/SamlSanityCheckListener.php b/modules/saml/src/Event/Listener/SamlSanityCheckListener.php new file mode 100644 index 0000000000..94f006c356 --- /dev/null +++ b/modules/saml/src/Event/Listener/SamlSanityCheckListener.php @@ -0,0 +1,84 @@ +getOptionalBoolean('enable.saml20-idp', false) !== false) { + $handler = MetaDataStorageHandler::getMetadataHandler($config); + try { + $metadata = $handler->getMetaDataCurrent('saml20-idp-hosted'); + } catch (Exception $e) { + $event->addError(MODID . Translate::noop('Hosted IdP metadata present')); + } + + if (isset($metadata)) { + $metadata_config = Configuration::loadfromArray($metadata); + $private = $cryptoUtils->loadPrivateKey($metadata_config, false); + $public = $cryptoUtils->loadPublicKey($metadata_config, false); + + $matches = $this->matchingKeyPair($public['PEM'], $private['PEM'], $private['password']); + $message = MODID . + Translate::noop('Matching key-pair for signing assertions'); + if ($matches) { + $event->addInfo($message); + } else { + $event->addError($message); + } + + $private = $cryptoUtils->loadPrivateKey($metadata_config, false, 'new_'); + if ($private !== null) { + $public = $cryptoUtils->loadPublicKey($metadata_config, false, 'new_'); + $matches = $this->matchingKeyPair($public['PEM'], $private['PEM'], $private['password']); + $message = MODID . + Translate::noop('Matching key-pair for signing assertions (rollover key)'); + if ($matches) { + $event->addInfo($message); + } else { + $event->addError($message); + } + } + } + } + + if ($config->getOptionalBoolean('metadata.sign.enable', false) !== false) { + $private = $cryptoUtils->loadPrivateKey($config, true, 'metadata.sign.'); + $public = $cryptoUtils->loadPublicKey($config, true, 'metadata.sign.'); + $matches = $this->matchingKeyPair($public['PEM'], $private['PEM'], $private['password']); + $message = MODID . Translate::noop('Matching key-pair for signing metadata'); + if ($matches) { + $event->addInfo($message); + } else { + $event->addError($message); + } + } + } + + /* Previously there was no visibility modifier on this method, but it is only used within this class, + * so we can make it private. + */ + private function matchingKeyPair( + string $publicKey, + string $privateKey, + #[\SensitiveParameter] + ?string $password = null, + ): bool { + return openssl_x509_check_private_key($publicKey, [$privateKey, $password]); + } +} diff --git a/modules/saml/src/IdP/SAML2.php b/modules/saml/src/IdP/SAML2.php index 72c15ae52c..dbab0a355e 100644 --- a/modules/saml/src/IdP/SAML2.php +++ b/modules/saml/src/IdP/SAML2.php @@ -11,7 +11,6 @@ use SAML2\AuthnRequest; use SAML2\Binding; use SAML2\Constants; -use SAML2\DOMDocumentFactory; use SAML2\EncryptedAssertion; use SAML2\Exception\Protocol\UnsupportedBindingException; use SAML2\HTTPRedirect; @@ -37,6 +36,7 @@ use SimpleSAML\Module\saml\Message; use SimpleSAML\Stats; use SimpleSAML\Utils; +use SimpleSAML\XML\DOMDocumentFactory; /** * IdP implementation for SAML 2.0 protocol. @@ -399,6 +399,10 @@ public static function receiveAuthnRequest(IdP $idp): void if (isset($_REQUEST['username'])) { $username = (string) $_REQUEST['username']; + } elseif (isset($_REQUEST['login_hint'])) { + $username = (string) $_REQUEST['login_hint']; + } elseif (isset($_REQUEST['LoginHint'])) { + $username = (string) $_REQUEST['LoginHint']; } $issuer = $request->getIssuer(); @@ -761,7 +765,7 @@ public static function getAssociationConfig(IdP $idp, array $association): Confi * Retrieve the metadata of a hosted SAML 2 IdP. * * @param string $entityid The entity ID of the hosted SAML 2 IdP whose metadata we want. - * @param MetaDataStorageHandler|null $handler Optionally the metadata storage to use, + * @param \SimpleSAML\Metadata\MetaDataStorageHandler|null $handler Optionally the metadata storage to use, * if omitted the configured handler will be used. * * @return array @@ -1267,6 +1271,7 @@ private static function buildAssertion( return $a; } + /** * Helper for buildAssertion to decide on an NameID to set */ @@ -1328,6 +1333,7 @@ private static function generateNameId( return $nameId; } + /** * Encrypt an assertion. * diff --git a/modules/saml/src/IdP/SQLNameID.php b/modules/saml/src/IdP/SQLNameID.php index afbcf8d8d9..a6b68347ab 100644 --- a/modules/saml/src/IdP/SQLNameID.php +++ b/modules/saml/src/IdP/SQLNameID.php @@ -22,9 +22,11 @@ */ class SQLNameID { - public const TABLE_VERSION = 1; - public const DEFAULT_TABLE_PREFIX = ''; - public const TABLE_SUFFIX = '_saml_PersistentNameID'; + public const int TABLE_VERSION = 1; + + public const string DEFAULT_TABLE_PREFIX = ''; + + public const string TABLE_SUFFIX = '_saml_PersistentNameID'; /** @@ -53,7 +55,7 @@ private static function read(string $query, array $params = [], array $config = * @param array $config * @return int|false The number of rows affected by the query or false on error. */ - private static function write(string $query, array $params = [], array $config = []) + private static function write(string $query, array $params = [], array $config = []): int|false { if (!empty($config)) { $database = Database::getInstance(Configuration::loadFromArray($config)); @@ -82,6 +84,7 @@ private static function tableName(array $config = []): string return $table; } + /** * @param array $config */ @@ -121,7 +124,7 @@ private static function createAndRead(string $query, array $params = [], array $ * @param array $config * @return int|false The number of rows affected by the query or false on error. */ - private static function createAndWrite(string $query, array $params = [], array $config = []) + private static function createAndWrite(string $query, array $params = [], array $config = []): int|false { self::create($config); return self::write($query, $params, $config); diff --git a/modules/saml/src/Message.php b/modules/saml/src/Message.php index 3c4d1856c0..f8beb913c1 100644 --- a/modules/saml/src/Message.php +++ b/modules/saml/src/Message.php @@ -7,10 +7,10 @@ use RobRichards\XMLSecLibs\XMLSecurityKey; use SAML2\Assertion; use SAML2\AuthnRequest; -use SAML2\Constants; use SAML2\EncryptedAssertion; use SAML2\LogoutRequest; use SAML2\LogoutResponse; +use SAML2\Message as SAMLMessage; use SAML2\Response; use SAML2\SignedElement; use SAML2\StatusResponse; @@ -21,9 +21,15 @@ use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Error as SSP_Error; +use SimpleSAML\Error\ErrorCodes; use SimpleSAML\Logger; +use SimpleSAML\Module\saml\Error as SAMLError; +use SimpleSAML\SAML2\Constants; +use SimpleSAML\SAML2\XML\samlp\AuthnContextComparisonTypeEnum; use SimpleSAML\Utils; +use function array_column; + /** * Common code for building SAML 2 messages based on the available metadata. * @@ -93,7 +99,7 @@ public static function addSign( private static function addRedirectSign( Configuration $srcMetadata, Configuration $dstMetadata, - \SAML2\Message $message, + SAMLMessage $message, ): void { $signingEnabled = null; if ($message instanceof LogoutRequest || $message instanceof LogoutResponse) { @@ -172,17 +178,30 @@ public static function checkSign(Configuration $srcMetadata, SignedElement $elem } Logger::debug('Validation with key #' . $i . ' failed without exception.'); } catch (\Exception $e) { + Logger::debug('Check Signature for ' . get_class($element) . ' element'); + Logger::debug('EntityID: ' . $srcMetadata->getString('entityid')); Logger::debug('Validation with key #' . $i . ' failed with exception: ' . $e->getMessage()); - $lastException = $e; + + // Clone the exception and improve the message + $lastException = new SSP_Error\Error( + [ + ErrorCodes::NOTVALIDCERTSIGNATURE, + '%MESSAGE%' => (new ErrorCodes())->getMessage(ErrorCodes::NOTVALIDCERTSIGNATURE), + '%ELEMENT%' => get_class($element), + '%ISSUER%' => $element->getIssuer()->getValue(), + '%ENTITYID%' => $srcMetadata->getString('entityid'), + ], + $e->getPrevious(), + ); } } // we were unable to validate the signature with any of our keys if ($lastException !== null) { throw $lastException; - } else { - return false; } + + return false; } @@ -199,7 +218,7 @@ public static function checkSign(Configuration $srcMetadata, SignedElement $elem public static function validateMessage( Configuration $srcMetadata, Configuration $dstMetadata, - \SAML2\Message $message, + SAMLMessage $message, ): bool { $enabled = null; if ($message instanceof LogoutRequest || $message instanceof LogoutResponse) { @@ -252,7 +271,7 @@ public static function validateMessage( public static function getDecryptionKeys( Configuration $srcMetadata, Configuration $dstMetadata, - $encryptionMethod = null, + ?string $encryptionMethod = null, ): array { $sharedKey = $srcMetadata->getOptionalString('sharedkey', null); if ($sharedKey !== null) { @@ -402,7 +421,6 @@ private static function decryptAssertion( * @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient (SP). * @param \SAML2\Assertion|\SAML2\Assertion $assertion The assertion containing any possibly encrypted attributes. * - * * @throws \SimpleSAML\Error\Exception if we cannot get the decryption keys or decryption fails. */ private static function decryptAttributes( @@ -446,10 +464,10 @@ private static function decryptAttributes( * * @return \SimpleSAML\Module\saml\Error The error. */ - public static function getResponseError(StatusResponse $response): \SimpleSAML\Module\saml\Error + public static function getResponseError(StatusResponse $response): SAMLError { $status = $response->getStatus(); - return new \SimpleSAML\Module\saml\Error($status['Code'], $status['SubCode'], $status['Message']); + return new SAMLError($status['Code'], $status['SubCode'], $status['Message']); } @@ -504,12 +522,11 @@ public static function buildAuthnRequest( if ($spMetadata->hasValue('AuthnContextClassRef')) { $accr = $spMetadata->getArrayizeString('AuthnContextClassRef'); - $comp = $spMetadata->getOptionalValueValidate('AuthnContextComparison', [ - Constants::COMPARISON_EXACT, - Constants::COMPARISON_MINIMUM, - Constants::COMPARISON_MAXIMUM, - Constants::COMPARISON_BETTER, - ], Constants::COMPARISON_EXACT); + $comp = $spMetadata->getOptionalValueValidate( + 'AuthnContextComparison', + array_column(AuthnContextComparisonTypeEnum::cases(), 'value'), + AuthnContextComparisonTypeEnum::Exact->value, + ); $ar->setRequestedAuthnContext(['AuthnContextClassRef' => $accr, 'Comparison' => $comp]); } @@ -589,7 +606,7 @@ public static function processResponse( } // validate Response-element destination - $httpUtils = new \SimpleSAML\Utils\HTTP(); + $httpUtils = new Utils\HTTP(); $currentURL = $httpUtils->getSelfURLNoQuery(); $msgDestination = $response->getDestination(); if ($msgDestination !== null && $msgDestination !== $currentURL) { diff --git a/modules/saml/src/SP/LogoutStore.php b/modules/saml/src/SP/LogoutStore.php index 6b8d151138..1ebe671861 100644 --- a/modules/saml/src/SP/LogoutStore.php +++ b/modules/saml/src/SP/LogoutStore.php @@ -52,9 +52,11 @@ private static function createLogoutTable(Store\SQLStore $store): void * NOTE: We get the name of the index by looking for the only unique index with a default name. */ $update = [ + // phpcs:disable Generic.Files.LineLength.TooLong 'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore DROP INDEX IF EXISTS SELECT CONSTRAINT_NAME ' . 'FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_NAME=' . $store->prefix . '_saml_LogoutStore ' . 'AND CONSTRAINT_NAME LIKE \'UQ__%"\'', + // phpcs:enable Generic.Files.LineLength.TooLong 'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore ADD CONSTRAINT _authSource ' . 'PRIMARY KEY CLUSTERED (_authSource, _nameId, _sessionIndex)', ]; @@ -322,7 +324,7 @@ public static function addSession(string $authId, NameID $nameId, ?string $sessi * @param array $sessionIndexes The SessionIndexes we should log out of. Logs out of all if this is empty. * @return int|false Number of sessions logged out, or FALSE if not supported. */ - public static function logoutSessions(string $authId, NameID $nameId, array $sessionIndexes) + public static function logoutSessions(string $authId, NameID $nameId, array $sessionIndexes): int|false { $config = Configuration::getInstance(); $storeType = $config->getOptionalString('store.type', 'phpsession'); diff --git a/phpstan-dev.neon b/phpstan-dev.neon new file mode 100644 index 0000000000..37375608cc --- /dev/null +++ b/phpstan-dev.neon @@ -0,0 +1,6 @@ +parameters: + level: 3 + paths: + - tests +#includes: +# - phpstan-baseline-dev.neon diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000000..b29cf81faf --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: 3 + paths: + - src +#includes: +# - phpstan-baseline.neon diff --git a/psalm-dev.xml b/psalm-dev.xml deleted file mode 100644 index d9e4d0b261..0000000000 --- a/psalm-dev.xml +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 51153742bc..0000000000 --- a/psalm.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/routing/services/simplesamlphp.yml b/routing/services/simplesamlphp.yml index 80c1fe9062..27a81a0d2c 100644 --- a/routing/services/simplesamlphp.yml +++ b/routing/services/simplesamlphp.yml @@ -16,3 +16,6 @@ services: arguments: - '@SimpleSAML\Configuration' - '@SimpleSAML\Session' + + SimpleSAML\Utils\HTTP: + class: SimpleSAML\Utils\HTTP diff --git a/src/SimpleSAML/Auth/ProcessingChain.php b/src/SimpleSAML/Auth/ProcessingChain.php index 02cab9e8b7..b2997429a1 100644 --- a/src/SimpleSAML/Auth/ProcessingChain.php +++ b/src/SimpleSAML/Auth/ProcessingChain.php @@ -38,20 +38,18 @@ class ProcessingChain /** * The list of remaining filters which should be applied to the state. */ - public const FILTERS_INDEX = '\SimpleSAML\Auth\ProcessingChain.filters'; - + public const string FILTERS_INDEX = '\SimpleSAML\Auth\ProcessingChain.filters'; /** * The stage we use for completed requests. */ - public const COMPLETED_STAGE = '\SimpleSAML\Auth\ProcessingChain.completed'; - + public const string COMPLETED_STAGE = '\SimpleSAML\Auth\ProcessingChain.completed'; /** * The request parameter we will use to pass the state identifier when we redirect after * having completed processing of the state. */ - public const AUTHPARAM = 'AuthProcId'; + public const string AUTHPARAM = 'AuthProcId'; /** @@ -328,8 +326,7 @@ public static function fetchProcessedState(string $id): ?array /** * @param array $state - * @psalm-param array{"\\\SimpleSAML\\\Auth\\\ProcessingChain.filters": array} $state - * @param ProcessingFilter[] $authProcs + * @param \SimpleSAML\Auth\ProcessingFilter[] $authProcs */ public static function insertFilters(array &$state, array $authProcs): void { @@ -349,7 +346,6 @@ public static function insertFilters(array &$state, array $authProcs): void /** * @param array $state - * @psalm-param array{"\\\SimpleSAML\\\Auth\\\ProcessingChain.filters": array} $state * @param array $authProcConfigs * @return \SimpleSAML\Auth\ProcessingFilter[] */ diff --git a/src/SimpleSAML/Auth/ProcessingFilter.php b/src/SimpleSAML/Auth/ProcessingFilter.php index e920a68d62..54039581ba 100644 --- a/src/SimpleSAML/Auth/ProcessingFilter.php +++ b/src/SimpleSAML/Auth/ProcessingFilter.php @@ -50,6 +50,7 @@ abstract class ProcessingFilter * * @param array &$config Configuration for this filter. * @param mixed $reserved For future use. + * @phpstan-ignore constructor.unusedParameter */ public function __construct(array &$config, /** @scrutinizer ignore-unused */ $reserved) { diff --git a/src/SimpleSAML/Auth/Source.php b/src/SimpleSAML/Auth/Source.php index 81af7a2842..d9f1ca4478 100644 --- a/src/SimpleSAML/Auth/Source.php +++ b/src/SimpleSAML/Auth/Source.php @@ -39,6 +39,7 @@ abstract class Source * * @param array $info Information about this authentication source. * @param array &$config Configuration for this authentication source. + * @phpstan-ignore constructor.unusedParameter */ public function __construct(array $info, array &$config) { @@ -53,7 +54,7 @@ public function __construct(array $info, array &$config) * * @param string $type The type of the authentication source. * - * @return Source[] Array of \SimpleSAML\Auth\Source objects of the specified type. + * @return \SimpleSAML\Auth\Source[] Array of \SimpleSAML\Auth\Source objects of the specified type. * @throws \Exception If the authentication source is invalid. */ public static function getSourcesOfType(string $type): array @@ -305,7 +306,7 @@ private static function parseAuthSource(string $authId, array $config): Source '\SimpleSAML\Auth\SourceFactory', ); - /** @var SourceFactory $factory */ + /** @var \SimpleSAML\Auth\SourceFactory $factory */ $factory = new $factoryClass(); $authSource = $factory->create($info, $config); } catch (\Exception $e) { diff --git a/src/SimpleSAML/Auth/State.php b/src/SimpleSAML/Auth/State.php index 55399563c3..ce4df664b6 100644 --- a/src/SimpleSAML/Auth/State.php +++ b/src/SimpleSAML/Auth/State.php @@ -48,51 +48,50 @@ class State /** * The index in the state array which contains the identifier. */ - public const ID = '\SimpleSAML\Auth\State.id'; + public const string ID = '\SimpleSAML\Auth\State.id'; /** * The index in the cloned state array which contains the identifier of the * original state. */ - public const CLONE_ORIGINAL_ID = '\SimpleSAML\Auth\State.cloneOriginalId'; + public const string CLONE_ORIGINAL_ID = '\SimpleSAML\Auth\State.cloneOriginalId'; /** * The index in the state array which contains the current stage. */ - public const STAGE = '\SimpleSAML\Auth\State.stage'; + public const string STAGE = '\SimpleSAML\Auth\State.stage'; /** * The index in the state array which contains the restart URL. */ - public const RESTART = '\SimpleSAML\Auth\State.restartURL'; + public const string RESTART = '\SimpleSAML\Auth\State.restartURL'; /** * The index in the state array which contains the exception handler URL. */ - public const EXCEPTION_HANDLER_URL = '\SimpleSAML\Auth\State.exceptionURL'; + public const string EXCEPTION_HANDLER_URL = '\SimpleSAML\Auth\State.exceptionURL'; /** * The index in the state array which contains the exception handler function. */ - public const EXCEPTION_HANDLER_FUNC = '\SimpleSAML\Auth\State.exceptionFunc'; + public const string EXCEPTION_HANDLER_FUNC = '\SimpleSAML\Auth\State.exceptionFunc'; /** * The index in the state array which contains the exception data. */ - public const EXCEPTION_DATA = '\SimpleSAML\Auth\State.exceptionData'; + public const string EXCEPTION_DATA = '\SimpleSAML\Auth\State.exceptionData'; /** * The stage of a state with an exception. */ - public const EXCEPTION_STAGE = '\SimpleSAML\Auth\State.exceptionStage'; - + public const string EXCEPTION_STAGE = '\SimpleSAML\Auth\State.exceptionStage'; /** * The URL parameter which contains the exception state id. * Note that this does not contain a "." since it's used in the * _REQUEST superglobal that does not allow dots. */ - public const EXCEPTION_PARAM = '\SimpleSAML\Auth\State_exceptionId'; + public const string EXCEPTION_PARAM = '\SimpleSAML\Auth\State_exceptionId'; /** @@ -168,6 +167,7 @@ public static function getStateId(array &$state, bool $rawId = false): string return $id . ':' . $state[self::RESTART]; } + /** * Perform syntactic validation of an incoming state ID. * @@ -185,6 +185,7 @@ public static function validateStateId(string $stateId): void } } + /** * Retrieve state timeout. * @@ -272,8 +273,8 @@ public static function cloneState(array $state): array * @throws \SimpleSAML\Error\NoState If we couldn't find the state and there's no URL defined to redirect to. * @throws \Exception If the stage of the state is invalid and there's no URL defined to redirect to. * - * @return array|null State information, or NULL if the state is missing and $allowMissing is true. - * @psalm-return ($allowMissing is true ? array|null : array) + * @return ($allowMissing is true ? array|null : array) + * State information, or NULL if the state is missing and $allowMissing is true. */ public static function loadState(string $id, string $stage, bool $allowMissing = false): ?array { diff --git a/src/SimpleSAML/Command/RouterDebugCommand.php b/src/SimpleSAML/Command/RouterDebugCommand.php index 34110908c8..037b515fb5 100644 --- a/src/SimpleSAML/Command/RouterDebugCommand.php +++ b/src/SimpleSAML/Command/RouterDebugCommand.php @@ -5,6 +5,7 @@ namespace SimpleSAML\Command; use Closure; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; @@ -12,14 +13,12 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Routing\RouterInterface; +#[AsCommand( + name: 'debug:router', + description: 'Displays the current routes for a module.', +)] class RouterDebugCommand extends Command { - /** - * @var string|null - */ - protected static $defaultName = 'debug:router'; - - /** * @param \Symfony\Component\Routing\RouterInterface $router * @@ -37,7 +36,6 @@ public function __construct( protected function configure(): void { $this - ->setDescription('Displays current routes for a module') ->setHelp( <<<'EOF' The %command.name% displays the configured routes for a module: diff --git a/src/SimpleSAML/Command/SspCacheClearCommand.php b/src/SimpleSAML/Command/SspCacheClearCommand.php index 174f3b5b9a..272249dc94 100644 --- a/src/SimpleSAML/Command/SspCacheClearCommand.php +++ b/src/SimpleSAML/Command/SspCacheClearCommand.php @@ -7,6 +7,7 @@ use SimpleSAML\Configuration; use SimpleSAML\Error\CriticalConfigurationError; use SimpleSAML\Kernel; +use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; @@ -20,6 +21,8 @@ use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; +use Symfony\Component\HttpKernel\Kernel as BaseKernel; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\RebootableInterface; #[AsCommand( @@ -28,19 +31,50 @@ )] class SspCacheClearCommand extends Command { - private CacheClearerInterface $cacheClearer; + /** @var \Symfony\Component\Filesystem\Filesystem */ private Filesystem $filesystem; + /** @var array */ private array $enabledModules; - public function __construct(CacheClearerInterface $cacheClearer, ?Filesystem $filesystem = null) - { - parent::__construct(); + /** @var \Symfony\Component\HttpKernel\KernelInterface */ + private KernelInterface $temporaryKernel; - $this->cacheClearer = $cacheClearer; + + /** + * @param \Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface $cacheClearer + * @param \Symfony\Component\Filesystem\Filesystem $filesystem + */ + public function __construct( + private CacheClearerInterface $cacheClearer, + ?Filesystem $filesystem = null, + ) { + parent::__construct(); $this->filesystem = $filesystem ?? new Filesystem(); + + $this->temporaryKernel = new class ('test', true) extends BaseKernel { + public function registerBundles(): iterable + { + return []; + } + + + public function registerContainerConfiguration(LoaderInterface $loader): void + { + } + + + public function getContainerClass(): string + { + return parent::getContainerClass(); + } + }; } + + /** + * @return void + */ protected function configure(): void { $this @@ -58,7 +92,11 @@ protected function configure(): void EOF,); } + /** + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * * @throws \Symfony\Component\Console\Exception\ExceptionInterface */ protected function execute(InputInterface $input, OutputInterface $output): int @@ -238,6 +276,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } + + /** + * @param string $dir + */ private function isNfs(string $dir): bool { static $mounts = null; @@ -266,10 +308,15 @@ private function isNfs(string $dir): bool return false; } + + /** + * @param string $warmupDir + * @param string $realBuildDir + */ private function warmup(string $warmupDir, string $realBuildDir): void { // create a temporary kernel - $kernel = $this->getApplication()->getKernel(); + $kernel = $this->temporaryKernel; if (!$kernel instanceof RebootableInterface) { $throwMessage = 'Calling "cache:clear" with a kernel that does not implement ' . '"Symfony\Component\HttpKernel\RebootableInterface" is not supported.'; @@ -278,9 +325,15 @@ private function warmup(string $warmupDir, string $realBuildDir): void $kernel->reboot($warmupDir); } + + /** + * @param string $cacheDir + * @param string $warmupDir + * @param \Symfony\Component\Console\Style\SymfonyStyle $io + */ private function warmupOptionals(string $cacheDir, string $warmupDir, SymfonyStyle $io): void { - $kernel = $this->getApplication()->getKernel(); + $kernel = $this->temporaryKernel; $warmer = $kernel->getContainer()->get('cache_warmer'); // non optional warmers already ran during container compilation $warmer->enableOnlyOptionalWarmers(); diff --git a/src/SimpleSAML/Command/UnusedTranslatableStringsCommand.php b/src/SimpleSAML/Command/UnusedTranslatableStringsCommand.php index a2a0d65965..46f3884cd0 100644 --- a/src/SimpleSAML/Command/UnusedTranslatableStringsCommand.php +++ b/src/SimpleSAML/Command/UnusedTranslatableStringsCommand.php @@ -12,10 +12,12 @@ use SimpleSAML\Module; use SimpleSAML\TestUtils\ArrayLogger; use SimpleSAML\Utils; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Finder\Exception\DirectoryNotFoundException; use Symfony\Component\Finder\Finder; use function array_diff; @@ -28,13 +30,14 @@ use function in_array; use function sprintf; +#[AsCommand( + name: 'translations:unused', + description: 'Generates a list of translations that are no longer in used in PHP or Twig files.', +)] class UnusedTranslatableStringsCommand extends Command { - /** @var string|null */ - protected static $defaultName = 'translations:unused'; - - /** + * @return void */ protected function configure(): void { @@ -48,7 +51,6 @@ protected function configure(): void 'simplesaml', ); - $this->setDescription('Generates a list of translations that are no longer in used in PHP or Twig files'); $this->addOption( 'module', null, @@ -84,7 +86,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (in_array('all', $inputModules) || $inputModules === []) { $modules = array_merge([''], $registeredModules); } elseif (in_array('main', $inputModules)) { - $modules = array_merge([''], ['core', 'admin', 'cron', 'exampleauth', 'multiauth', 'saml']); + $modules = array_merge([''], ['core', 'admin', 'cron', 'debugsp', 'exampleauth', 'multiauth', 'saml']); } else { $known = array_intersect($registeredModules, $inputModules); $unknown = array_diff($inputModules, $registeredModules); @@ -171,7 +173,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $domain = $domain ?: 'messages'; $finder = new Finder(); - foreach ($finder->files()->in($moduleLocalesDir . '**/LC_MESSAGES/')->name("{$domain}.po") as $poFile) { + try { + $poFiles = $finder->files()->in($moduleLocalesDir . '**/LC_MESSAGES/')->name("{$domain}.po"); + } catch (DirectoryNotFoundException $e) { + $output->writeln($e->getMessage() . "; skipping."); + continue; + } + + foreach ($poFiles as $poFile) { $current = $loader->loadFile($poFile->getPathName()); foreach ($current->getTranslations() as $t) { if (!$template->find(null, $t->getOriginal())) { diff --git a/src/SimpleSAML/Command/UpdateTranslatableStringsCommand.php b/src/SimpleSAML/Command/UpdateTranslatableStringsCommand.php index e711f425df..7f44f7018c 100644 --- a/src/SimpleSAML/Command/UpdateTranslatableStringsCommand.php +++ b/src/SimpleSAML/Command/UpdateTranslatableStringsCommand.php @@ -4,6 +4,7 @@ namespace SimpleSAML\Command; +use ArrayIterator; use Gettext\Generator\PoGenerator; use Gettext\Loader\PoLoader; use Gettext\Merge; @@ -14,6 +15,7 @@ use SimpleSAML\Module; use SimpleSAML\TestUtils\ArrayLogger; use SimpleSAML\Utils; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -32,15 +34,14 @@ use function ksort; use function sprintf; +#[AsCommand( + name: 'translations:update:translatable', + description: 'Generates fresh .po translation files based on the translatable strings from PHP and Twig files.', +)] class UpdateTranslatableStringsCommand extends Command { /** - * @var string|null - */ - protected static $defaultName = 'translations:update:translatable'; - - - /** + * @return void */ protected function configure(): void { @@ -54,9 +55,6 @@ protected function configure(): void 'simplesaml', ); - $this->setDescription( - 'Generates fresh .po translation files based on the translatable strings from PHP and Twig files', - ); $this->addOption( 'module', null, @@ -78,16 +76,17 @@ protected function configure(): void ); } + /** * Clone the entries from $iterator into the passed Translations object. * It is expected that $iterator was made by getIterator() on Translations. * This can be useful as the entries are cloned in the iterator order. * * @param \Gettext\Translations $ret - * @param iterable $iterator - * @return $ret + * @param \ArrayIterator $iterator + * @return \Gettext\Translations $ret */ - protected function cloneIteratorToTranslations(Translations $ret, iterable $iterator): Translations + protected function cloneIteratorToTranslations(Translations $ret, ArrayIterator $iterator): Translations { while ($iterator->valid()) { $ret->addOrMerge( @@ -99,6 +98,7 @@ protected function cloneIteratorToTranslations(Translations $ret, iterable $iter return $ret; } + /** * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface $output @@ -112,7 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (in_array('all', $inputModules) || $inputModules === []) { $modules = array_merge([''], $registeredModules); } elseif (in_array('main', $inputModules)) { - $modules = array_merge([''], ['core', 'admin', 'cron', 'exampleauth', 'multiauth', 'saml']); + $modules = array_merge([''], ['core', 'admin', 'cron', 'debugsp', 'exampleauth', 'multiauth', 'saml']); } else { $known = array_intersect($registeredModules, $inputModules); $unknown = array_diff($inputModules, $registeredModules); @@ -199,38 +199,44 @@ protected function execute(InputInterface $input, OutputInterface $output): int $moduleLocalesDir = $moduleDir . '/locales/'; $domain = $domain ?: 'messages'; - $finder = new Finder(); - foreach ($finder->files()->in($moduleLocalesDir . '**/LC_MESSAGES/')->name("{$domain}.po") as $poFile) { - $current = $loader->loadFile($poFile->getPathName()); - - $merged = $template->mergeWith( - $current, - Merge::TRANSLATIONS_OVERRIDE - | Merge::COMMENTS_OURS - | Merge::HEADERS_OURS - | Merge::REFERENCES_THEIRS - | Merge::EXTRACTED_COMMENTS_OURS, - ); - $merged->setDomain($domain); - - // - // Sort the translations in a predictable way - // - $iter = $merged->getIterator(); - $iter->ksort(); - $merged = $this->cloneIteratorToTranslations( - Translations::create($merged->getDomain(), $merged->getLanguage()), - $iter, - ); - - $language = basename(dirname($poFile->getPath())); - $merged->getHeaders() - ->set('Project-Id-Version', 'SimpleSAMLphp') - ->set('MIME-Version', '1.0') - ->set('Content-Type', 'text/plain; charset=UTF-8') - ->set('Content-Transfer-Encoding', '8bit') - ->setLanguage($language); - $poGenerator->generateFile($merged, $poFile->getPathName()); + if (file_exists($moduleLocalesDir)) { + // The module contains translations - if not we skip it. + + $finder = new Finder(); + $poFiles = $finder->files()->in($moduleLocalesDir . '**/LC_MESSAGES/')->name("{$domain}.po"); + foreach ($poFiles as $poFile) { + $current = $loader->loadFile($poFile->getPathName()); + + $merged = $template->mergeWith( + $current, + Merge::TRANSLATIONS_OVERRIDE + | Merge::COMMENTS_OURS + | Merge::HEADERS_OURS + | Merge::REFERENCES_THEIRS + | Merge::EXTRACTED_COMMENTS_OURS, + ); + $merged->setDomain($domain); + + // + // Sort the translations in a predictable way + // + /** @var \ArrayIterator $iter */ + $iter = $merged->getIterator(); + $iter->ksort(); + $merged = $this->cloneIteratorToTranslations( + Translations::create($merged->getDomain(), $merged->getLanguage()), + $iter, + ); + + $language = basename(dirname($poFile->getPath())); + $merged->getHeaders() + ->set('Project-Id-Version', 'SimpleSAMLphp') + ->set('MIME-Version', '1.0') + ->set('Content-Type', 'text/plain; charset=UTF-8') + ->set('Content-Transfer-Encoding', '8bit') + ->setLanguage($language); + $poGenerator->generateFile($merged, $poFile->getPathName()); + } } } } diff --git a/src/SimpleSAML/Compat/SspContainer.php b/src/SimpleSAML/Compat/SspContainer.php index 2718ef2a72..224b6c8898 100644 --- a/src/SimpleSAML/Compat/SspContainer.php +++ b/src/SimpleSAML/Compat/SspContainer.php @@ -13,6 +13,7 @@ class SspContainer extends AbstractContainer /** @var \Psr\Log\LoggerInterface */ protected LoggerInterface $logger; + /** * Create a new SimpleSAMLphp compatible container. */ @@ -93,7 +94,7 @@ public function getTempDir(): string /** * {@inheritdoc} * @param string $filename - * @param string $date + * @param string $data * @param int|null $mode */ public function writeFile(string $filename, string $data, ?int $mode = null): void diff --git a/src/SimpleSAML/Configuration.php b/src/SimpleSAML/Configuration.php index 8959b67ff0..887a555c0f 100644 --- a/src/SimpleSAML/Configuration.php +++ b/src/SimpleSAML/Configuration.php @@ -41,19 +41,17 @@ class Configuration implements Utils\ClearableState /** * The release version of this package */ - public const VERSION = '2.4.2'; + public const string VERSION = '2.5.0'; /** * A default value which means that the given option is required. - * - * @var string */ - public const REQUIRED_OPTION = '___REQUIRED_OPTION___'; + public const string REQUIRED_OPTION = '___REQUIRED_OPTION___'; /** * The default security-headers to be sent on responses. */ - public const DEFAULT_SECURITY_HEADERS = [ + public const array DEFAULT_SECURITY_HEADERS = [ 'Content-Security-Policy' => "default-src 'none'; " . "frame-ancestors 'self'; " . @@ -70,6 +68,7 @@ class Configuration implements Utils\ClearableState 'Referrer-Policy' => 'origin-when-cross-origin', ]; + /** * Associative array with mappings from instance-names to configuration objects. * @@ -117,6 +116,7 @@ class Configuration implements Utils\ClearableState */ private ?string $filename = null; + /** * Initializes a configuration from the given array. * @@ -363,7 +363,6 @@ public static function getInstance(string $instancename = 'simplesaml'): Configu if (array_key_exists($instancename, self::$instance)) { return self::$instance[$instancename]; } - if ($instancename === 'simplesaml') { try { return self::getConfig(); @@ -1011,7 +1010,7 @@ public function getArrayize(string $name): array * @return array|null The option with the given name. * @psalm-return ($default is null ? array|null : array) */ - public function getOptionalArrayize(string $name, $default): ?array + public function getOptionalArrayize(string $name, ?array $default): ?array { $ret = $this->getOptionalValue($name, $default); @@ -1069,7 +1068,7 @@ public function getOptionalArrayizeString(string $name, ?array $default): ?array { $ret = $this->getOptionalArrayize($name, $default); - Assert::nullOrAllString( + Assert::allNullOrString( $ret, sprintf( '%s: The option %s must be null, a string or an array of strings.', @@ -1103,7 +1102,6 @@ public function getConfigItem(string $name): Configuration } - /** * Retrieve an optional array as a \SimpleSAML\Configuration object. * @@ -1189,6 +1187,7 @@ private function getDefaultBinding(string $endpointType): string } } + /** * Helper function for dealing with metadata endpoints. * @@ -1343,7 +1342,6 @@ public function getDefaultEndpoint( * The default language returned is always 'en'. * * @param string $name The name of the option. - * @param array $default The default value. * * @return array Associative array with language => string pairs. * diff --git a/src/SimpleSAML/Database.php b/src/SimpleSAML/Database.php index 588b1b4eb9..e6e46a2fc1 100644 --- a/src/SimpleSAML/Database.php +++ b/src/SimpleSAML/Database.php @@ -169,6 +169,7 @@ private function connect(string $dsn, ?string $username = null, ?string $passwor try { $db = new PDO($dsn, $username, $password, $options); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); return $db; } catch (PDOException $e) { diff --git a/src/SimpleSAML/Error/CannotSetCookie.php b/src/SimpleSAML/Error/CannotSetCookie.php index 48a2079950..3478ddeee1 100644 --- a/src/SimpleSAML/Error/CannotSetCookie.php +++ b/src/SimpleSAML/Error/CannotSetCookie.php @@ -14,23 +14,17 @@ class CannotSetCookie extends Exception { /** * The exception was thrown for unknown reasons. - * - * @var int */ - public const UNKNOWN = 0; + public const int UNKNOWN = 0; /** * The exception was due to the HTTP headers being already sent, and therefore we cannot send additional headers to * set the cookie. - * - * @var int */ - public const HEADERS_SENT = 1; + public const int HEADERS_SENT = 1; /** * The exception was due to trying to set a secure cookie over an insecure channel. - * - * @var int */ - public const SECURE_COOKIE = 2; + public const int SECURE_COOKIE = 2; } diff --git a/src/SimpleSAML/Error/ConfigurationError.php b/src/SimpleSAML/Error/ConfigurationError.php index b9bd562d3c..4215964f36 100644 --- a/src/SimpleSAML/Error/ConfigurationError.php +++ b/src/SimpleSAML/Error/ConfigurationError.php @@ -33,6 +33,7 @@ class ConfigurationError extends Error * @param string|null $reason The reason for this exception. * @param string|null $file The configuration file that originated this error. * @param array|null $config The configuration array that led to this problem. + * @phpstan-ignore constructor.unusedParameter */ public function __construct(?string $reason = null, ?string $file = null, ?array $config = null) { diff --git a/src/SimpleSAML/Error/Error.php b/src/SimpleSAML/Error/Error.php index 7bfbcdbe6d..21a3b0eb66 100644 --- a/src/SimpleSAML/Error/Error.php +++ b/src/SimpleSAML/Error/Error.php @@ -5,7 +5,11 @@ namespace SimpleSAML\Error; use SimpleSAML\Assert\Assert; -use SimpleSAML\{Configuration, Logger, Module, Session, Utils}; +use SimpleSAML\Configuration; +use SimpleSAML\Logger; +use SimpleSAML\Module; +use SimpleSAML\Session; +use SimpleSAML\Utils; use SimpleSAML\XHTML\Template; use Throwable; @@ -74,7 +78,7 @@ class Error extends Exception * (with index 0), is the error code, while the other elements are replacements for the error text. * * @param string|array $errorCode One of the error codes defined in the errors dictionary. - * @param Throwable|null $cause The exception which caused this fatal error (if any). Optional. + * @param \Throwable|null $cause The exception which caused this fatal error (if any). Optional. * @param int|null $httpCode The HTTP response code to use. Optional. */ public function __construct( @@ -101,21 +105,15 @@ public function __construct( $this->dictDescr = $errorCodes->getDescription($this->errorCode); if (!empty($this->parameters)) { - $msg = $this->errorCode . '('; - foreach ($this->parameters as $k => $v) { - if ($k === 0) { - continue; - } - - $msg .= var_export($k, true) . ' => ' . var_export($v, true) . ', '; - } - $msg = substr($msg, 0, -2) . ')'; + $msgData = ['errorCode' => $this->errorCode] + $this->parameters; + $msg = json_encode($msgData); } else { $msg = $this->errorCode; } parent::__construct($msg, -1, $cause); } + /** * Retrieve the ErrorCodes instance to use for resolving dictionary title and description tags. * @@ -124,7 +122,7 @@ public function __construct( * This has to be public to allow Login to get an object * containing custom error codes if they in use. * - * @return ErrorCodes + * @return \SimpleSAML\Error\ErrorCodes */ public function getErrorCodes(): ErrorCodes { diff --git a/src/SimpleSAML/Error/ErrorCodes.php b/src/SimpleSAML/Error/ErrorCodes.php index c343b6dda2..009d0deeb8 100644 --- a/src/SimpleSAML/Error/ErrorCodes.php +++ b/src/SimpleSAML/Error/ErrorCodes.php @@ -16,6 +16,85 @@ */ class ErrorCodes { + final public const string ACSPARAMS = 'ACSPARAMS'; + + final public const string ADMINNOTHASHED = 'ADMINNOTHASHED'; + + final public const string ARSPARAMS = 'ARSPARAMS'; + + final public const string AUTHSOURCEERROR = 'AUTHSOURCEERROR'; + + final public const string BADREQUEST = 'BADREQUEST'; + + final public const string CASERROR = 'CASERROR'; + + final public const string CONFIG = 'CONFIG'; + + final public const string CREATEREQUEST = 'CREATEREQUEST'; + + final public const string DISCOPARAMS = 'DISCOPARAMS'; + + final public const string GENERATEAUTHNRESPONSE = 'GENERATEAUTHNRESPONSE'; + + final public const string INVALIDCERT = 'INVALIDCERT'; + + final public const string LDAPERROR = 'LDAPERROR'; + + final public const string LOGOUTINFOLOST = 'LOGOUTINFOLOST'; + + final public const string LOGOUTREQUEST = 'LOGOUTREQUEST'; + + final public const string MEMCACHEDOWN = 'MEMCACHEDOWN'; + + final public const string METADATA = 'METADATA'; + + final public const string METADATANOTFOUND = 'METADATANOTFOUND'; + + final public const string METHODNOTALLOWED = 'METHODNOTALLOWED'; + + final public const string NOACCESS = 'NOACCESS'; + + final public const string NOCERT = 'NOCERT'; + + final public const string NORELAYSTATE = 'NORELAYSTATE'; + + final public const string NOSTATE = 'NOSTATE'; + + final public const string NOTFOUND = 'NOTFOUND'; + + final public const string NOTFOUNDREASON = 'NOTFOUNDREASON'; + + final public const string NOTSET = 'NOTSET'; + + final public const string NOTVALIDCERT = 'NOTVALIDCERT'; + + final public const string NOTVALIDCERTSIGNATURE = 'NOTVALIDCERTSIGNATURE'; + + final public const string PROCESSASSERTION = 'PROCESSASSERTION'; + + final public const string PROCESSAUTHNREQUEST = 'PROCESSAUTHNREQUEST'; + + final public const string RESPONSESTATUSNOSUCCESS = 'RESPONSESTATUSNOSUCCESS'; + + final public const string SLOSERVICEPARAMS = 'SLOSERVICEPARAMS'; + + final public const string SSOPARAMS = 'SSOPARAMS'; + + final public const string UNHANDLEDEXCEPTION = 'UNHANDLEDEXCEPTION'; + + final public const string UNKNOWNCERT = 'UNKNOWNCERT'; + + final public const string USERABORTED = 'USERABORTED'; + + final public const string WRONGUSERPASS = 'WRONGUSERPASS'; + + final public const string KEY_TITLE = 'title'; + + final public const string KEY_DESCRIPTION = 'descr'; + + + /** + */ public function __construct() { // Automatically register instances of subclasses with Login to allow @@ -23,46 +102,6 @@ public function __construct() Login::registerErrorCodeClass($this); } - // TODO PHPv8.1 - Consider moving to final consts for these default error codes to prevent overrides. - public const ACSPARAMS = 'ACSPARAMS'; - public const ARSPARAMS = 'ARSPARAMS'; - public const AUTHSOURCEERROR = 'AUTHSOURCEERROR'; - public const BADREQUEST = 'BADREQUEST'; - public const CASERROR = 'CASERROR'; - public const CONFIG = 'CONFIG'; - public const CREATEREQUEST = 'CREATEREQUEST'; - public const DISCOPARAMS = 'DISCOPARAMS'; - public const GENERATEAUTHNRESPONSE = 'GENERATEAUTHNRESPONSE'; - public const INVALIDCERT = 'INVALIDCERT'; - public const LDAPERROR = 'LDAPERROR'; - public const LOGOUTINFOLOST = 'LOGOUTINFOLOST'; - public const LOGOUTREQUEST = 'LOGOUTREQUEST'; - public const MEMCACHEDOWN = 'MEMCACHEDOWN'; - public const METADATA = 'METADATA'; - public const METADATANOTFOUND = 'METADATANOTFOUND'; - public const METHODNOTALLOWED = 'METHODNOTALLOWED'; - public const NOACCESS = 'NOACCESS'; - public const NOCERT = 'NOCERT'; - public const NORELAYSTATE = 'NORELAYSTATE'; - public const NOSTATE = 'NOSTATE'; - public const NOTFOUND = 'NOTFOUND'; - public const NOTFOUNDREASON = 'NOTFOUNDREASON'; - public const NOTSET = 'NOTSET'; - public const ADMINNOTHASHED = 'ADMINNOTHASHED'; - public const NOTVALIDCERT = 'NOTVALIDCERT'; - public const PROCESSASSERTION = 'PROCESSASSERTION'; - public const PROCESSAUTHNREQUEST = 'PROCESSAUTHNREQUEST'; - public const RESPONSESTATUSNOSUCCESS = 'RESPONSESTATUSNOSUCCESS'; - public const SLOSERVICEPARAMS = 'SLOSERVICEPARAMS'; - public const SSOPARAMS = 'SSOPARAMS'; - public const UNHANDLEDEXCEPTION = 'UNHANDLEDEXCEPTION'; - public const UNKNOWNCERT = 'UNKNOWNCERT'; - public const USERABORTED = 'USERABORTED'; - public const WRONGUSERPASS = 'WRONGUSERPASS'; - - public const KEY_TITLE = 'title'; - public const KEY_DESCRIPTION = 'descr'; - /** * Fetch all default translation strings for error code titles. @@ -77,6 +116,7 @@ final public static function defaultGetAllErrorCodeTitles(): array { return [ self::ACSPARAMS => Translate::noop('No SAML response provided'), + self::ADMINNOTHASHED => Translate::noop('Admin password not set to a hashed value'), self::ARSPARAMS => Translate::noop('No SAML message provided'), self::AUTHSOURCEERROR => Translate::noop('Authentication source error'), self::BADREQUEST => Translate::noop('Bad request received'), @@ -100,8 +140,8 @@ final public static function defaultGetAllErrorCodeTitles(): array self::NOTFOUND => Translate::noop('Page not found'), self::NOTFOUNDREASON => Translate::noop('Page not found'), self::NOTSET => Translate::noop('Password not set'), - self::ADMINNOTHASHED => Translate::noop('Admin password not set to a hashed value'), self::NOTVALIDCERT => Translate::noop('Invalid certificate'), + self::NOTVALIDCERTSIGNATURE => Translate::noop('Invalid certificate signature'), self::PROCESSASSERTION => Translate::noop('Error processing response from Identity Provider'), self::PROCESSAUTHNREQUEST => Translate::noop('Error processing request from Service Provider'), self::RESPONSESTATUSNOSUCCESS => Translate::noop('Error received from Identity Provider'), @@ -114,6 +154,7 @@ final public static function defaultGetAllErrorCodeTitles(): array ]; } + /** * Fetch all default translation strings for error code titles. * @@ -125,6 +166,7 @@ final public function getDefaultTitles(): array return self::defaultGetAllErrorCodeTitles(); } + /** * Fetch all title translation strings for custom error codes. * @@ -141,6 +183,7 @@ public static function getCustomErrorCodeTitles(): array return []; } + /** * Fetch all title translation strings for custom error codes. * @@ -168,6 +211,7 @@ public static function getAllErrorCodeTitles(): array return array_merge(self::defaultGetAllErrorCodeTitles(), static::getCustomErrorCodeTitles()); } + /** * Fetch all translation strings for error code titles. * @@ -201,7 +245,8 @@ final public static function defaultGetAllErrorCodeDescriptions(): array " not intended to be accessed directly."), self::AUTHSOURCEERROR => Translate::noop("" . 'Authentication error in source %AUTHSOURCE%. The reason was: %REASON%'), - self::BADREQUEST => Translate::noop('There is an error in the request to this page. The reason was: %REASON%'), + self::BADREQUEST => + Translate::noop('There is an error in the request to this page. The reason was: %REASON%'), self::CASERROR => Translate::noop('Error when communicating with the CAS server.'), self::CONFIG => Translate::noop('SimpleSAMLphp appears to be misconfigured.'), self::CREATEREQUEST => Translate::noop("An error occurred when trying to create the SAML request."), @@ -258,7 +303,9 @@ final public static function defaultGetAllErrorCodeDescriptions(): array "admin-page-with-and-error-message-admin-password-" . "not-set-to-a-hashed-value"), self::NOTVALIDCERT => Translate::noop('You did not present a valid certificate.'), - self::PROCESSASSERTION => Translate::noop('We did not accept the response sent from the Identity Provider.'), + self::NOTVALIDCERTSIGNATURE => Translate::noop('Unable to validate certificate signature.'), + self::PROCESSASSERTION => + Translate::noop('We did not accept the response sent from the Identity Provider.'), self::PROCESSAUTHNREQUEST => Translate::noop("" . "This Identity Provider received an Authentication Request from a Service " . "Provider, but an error occurred when trying to process the request."), @@ -282,6 +329,7 @@ final public static function defaultGetAllErrorCodeDescriptions(): array ]; } + /** * Fetch all default translation strings for error code descriptions. * @@ -293,6 +341,7 @@ final public function getDefaultDescriptions(): array return self::defaultGetAllErrorCodeDescriptions(); } + /** * Fetch all description translation strings for custom error codes. * @@ -309,6 +358,7 @@ public static function getCustomErrorCodeDescriptions(): array return []; } + /** * Fetch all description translation strings for custom error codes. * @@ -321,6 +371,7 @@ public function getCustomDescriptions(): array return []; } + /** * Fetch all translation strings for error code descriptions. * @@ -335,6 +386,10 @@ public static function getAllErrorCodeDescriptions(): array return array_merge(self::defaultGetAllErrorCodeDescriptions(), static::getCustomErrorCodeDescriptions()); } + + /** + * @return array + */ public function getAllDescriptions(): array { return array_merge($this->getDefaultDescriptions(), $this->getCustomDescriptions()); @@ -359,6 +414,7 @@ public static function getAllErrorCodeMessages(): array ]; } + /** * Get a map of both errorcode titles and descriptions * @@ -396,6 +452,7 @@ public static function getErrorCodeTitle(string $errorCode): string } } + /** * Fetch a translation string for a title for a given error code. * @@ -408,6 +465,7 @@ public function getTitle(string $errorCode): string return (string)($this->getAllTitles()[$errorCode] ?? Translate::addTagPrefix($errorCode, 'title_')); } + /** * Fetch a translation string for a description for a given error code. * @@ -429,6 +487,7 @@ public static function getErrorCodeDescription(string $errorCode): string } } + /** * Fetch a translation string for a description for a given error code. * @@ -441,6 +500,7 @@ public function getDescription(string $errorCode): string return (string)($this->getAllDescriptions()[$errorCode] ?? Translate::addTagPrefix($errorCode, 'descr_')); } + /** * Get both title and description for a specific error code * @@ -462,6 +522,7 @@ public static function getErrorCodeMessage(string $errorCode): array ]; } + /** * Get both title and description for a specific error code * diff --git a/src/SimpleSAML/Error/ErrorHandler.php b/src/SimpleSAML/Error/ErrorHandler.php index a889c7205f..d53f94e617 100644 --- a/src/SimpleSAML/Error/ErrorHandler.php +++ b/src/SimpleSAML/Error/ErrorHandler.php @@ -20,7 +20,6 @@ class ErrorHandler * @param string $errstr * @param string|null $errfile * @param int $errline - * @param string|null $errcontext * @return false */ public function customErrorHandler( @@ -28,7 +27,7 @@ public function customErrorHandler( string $errstr, ?string $errfile = null, int $errline = 0, - ): bool { + ): false { if (Logger::isErrorMasked($errno)) { // masked error return false; diff --git a/src/SimpleSAML/Error/ExceptionHandler.php b/src/SimpleSAML/Error/ExceptionHandler.php index e73302ea27..6720715631 100644 --- a/src/SimpleSAML/Error/ExceptionHandler.php +++ b/src/SimpleSAML/Error/ExceptionHandler.php @@ -6,7 +6,10 @@ use Error as BuiltinError; use Exception as BuiltinException; -use SimpleSAML\{Logger, Module}; +use SimpleSAML\Event\Dispatcher\ModuleEventDispatcherFactory; +use SimpleSAML\Event\ExceptionHandlerEvent; +use SimpleSAML\Logger; +use SimpleSAML\Module; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Throwable; @@ -25,6 +28,11 @@ class ExceptionHandler */ public function customExceptionHandler(Throwable $exception): void { + $eventDispatcher = ModuleEventDispatcherFactory::getInstance(); + /** @var \SimpleSAML\Event\ExceptionHandlerEvent $event */ + $event = $eventDispatcher->dispatch(new ExceptionHandlerEvent($exception)); + $exception = $event->getException(); + Module::callHooks('exception_handler', $exception); if ($exception instanceof MethodNotAllowedHttpException) { diff --git a/src/SimpleSAML/Event/Dispatcher/EventDispatcher.php b/src/SimpleSAML/Event/Dispatcher/EventDispatcher.php new file mode 100644 index 0000000000..a28d2cb38e --- /dev/null +++ b/src/SimpleSAML/Event/Dispatcher/EventDispatcher.php @@ -0,0 +1,49 @@ +isPropagationStopped()) { + return $event; + } + + /** @var iterable $listeners */ + $listeners = $this->listenerProvider->getListenersForEvent($event); + + foreach ($listeners as $listener) { + try { + // Call the listener + $listener($event); + } catch (\Throwable $e) { + // Log the error + \SimpleSAML\Logger::error('Error in event listener: ' . $e->getMessage()); + + // Rethrow the exception according to PSR-14 + throw $e; + } + + // Check if propagation should stop + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + break; + } + } + + return $event; + } +} diff --git a/src/SimpleSAML/Event/Dispatcher/ModuleEventDispatcherFactory.php b/src/SimpleSAML/Event/Dispatcher/ModuleEventDispatcherFactory.php new file mode 100644 index 0000000000..64468b4a70 --- /dev/null +++ b/src/SimpleSAML/Event/Dispatcher/ModuleEventDispatcherFactory.php @@ -0,0 +1,31 @@ +exception = $exception; + } + + + public function getException(): \Throwable + { + return $this->exception; + } +} diff --git a/src/SimpleSAML/Event/Provider/ModuleListenerProvider.php b/src/SimpleSAML/Event/Provider/ModuleListenerProvider.php new file mode 100644 index 0000000000..db5cacfcc7 --- /dev/null +++ b/src/SimpleSAML/Event/Provider/ModuleListenerProvider.php @@ -0,0 +1,137 @@ +> */ + private array $listeners = []; + + + public function __construct() + { + $configuration = Configuration::getInstance(); + $enabledModules = $configuration->getOptionalArray('module.enable', Module::$core_modules); + $this->discoverListeners($enabledModules); + } + + + private function discoverListeners(array $enabledModules): void + { + foreach ($enabledModules as $moduleName => $enabled) { + if (!$enabled) { + continue; + } + + $listenerDir = dirname(__DIR__, 4) . "/modules/$moduleName/src/Event/Listener"; + + if (!is_dir($listenerDir)) { + continue; + } + + foreach (glob("{$listenerDir}/*.php") as $file) { + $className = $this->getClassNameFromFile($file, $moduleName); + + if (!$className || !class_exists($className)) { + continue; + } + + $this->registerListenerClass($className); + } + } + } + + + private function registerListenerClass(string $className): void + { + $reflection = new \ReflectionClass($className); + + // support named methods with event type hints + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->getName() === '__construct') { + continue; + } + + $params = $method->getParameters(); + if (count($params) > 0) { + /** @var \ReflectionNamedType $eventType */ + $eventType = $params[0]->getType(); + if ($eventType && !$eventType->isBuiltin()) { + $eventClass = $eventType->getName(); + $instance = new $className(); + $this->addListener($eventClass, [$instance, $method->getName()]); + } + } + } + } + + + public function addListener(string $eventClass, callable $listener, int $priority = 0): void + { + if (!isset($this->listeners[$eventClass])) { + $this->listeners[$eventClass] = []; + } + + $this->listeners[$eventClass][] = [ + 'callable' => $listener, + 'priority' => $priority, + ]; + + // Sort listeners by priority (higher priority first) + usort($this->listeners[$eventClass], function ($a, $b) { + return $b['priority'] <=> $a['priority']; + }); + } + + + /** + * Get the listeners for a specific event type. + * + * @param object $event The event object. + * + * @return iterable The listeners for the event type. + */ + public function getListenersForEvent(object $event): iterable + { + $eventClass = get_class($event); + + // check for exact class match + if (isset($this->listeners[$eventClass])) { + foreach ($this->listeners[$eventClass] as $listenerData) { + yield $listenerData['callable']; + } + } + + // check for parent classes + foreach (class_parents($event) as $parentClass) { + if (isset($this->listeners[$parentClass])) { + foreach ($this->listeners[$parentClass] as $listenerData) { + yield $listenerData['callable']; + } + } + } + + // check for implemented interfaces + foreach (class_implements($event) as $interface) { + if (isset($this->listeners[$interface])) { + foreach ($this->listeners[$interface] as $listenerData) { + yield $listenerData['callable']; + } + } + } + } + + + private function getClassNameFromFile(string $file, string $moduleName): ?string + { + $basename = basename($file, '.php'); + $className = "SimpleSAML\\Module\\{$moduleName}\\Event\\Listener\\{$basename}"; + return $className; + } +} diff --git a/src/SimpleSAML/HTTP/RunnableResponse.php b/src/SimpleSAML/HTTP/RunnableResponse.php index 738acf0431..19618464fd 100644 --- a/src/SimpleSAML/HTTP/RunnableResponse.php +++ b/src/SimpleSAML/HTTP/RunnableResponse.php @@ -24,7 +24,7 @@ class RunnableResponse extends Response * RunnableResponse constructor. * * @param callable $callable A callable that we should run as part of this response. - * @param array $args An array of arguments to be passed to the callable. Note that each element of the array + * @param array $arguments An array of arguments to be passed to the callable. Note that each element of the array */ public function __construct( callable $callable, diff --git a/src/SimpleSAML/IdP.php b/src/SimpleSAML/IdP.php index a587f997db..97da6a5ed3 100644 --- a/src/SimpleSAML/IdP.php +++ b/src/SimpleSAML/IdP.php @@ -161,7 +161,7 @@ public static function getByState(array &$state): IdP /** * Retrieve the configuration for this IdP. * - * @return Configuration The configuration object. + * @return \SimpleSAML\Configuration The configuration object. */ public function getConfig(): Configuration { @@ -437,7 +437,6 @@ public function getLogoutHandler(): LogoutHandlerInterface throw new Error\Exception('Unknown logout handler: ' . var_export($logouttype, true)); } - /** @var \SimpleSAML\IdP\LogoutHandlerInterface */ return new $handler($this); } @@ -542,8 +541,8 @@ public function doLogoutRedirect(string $url): void * * This function never returns. * - * @param IdP $idp Deprecated. Will be removed. - * @param array &$state The logout state from doLogoutRedirect(). + * @param \SimpleSAML\IdP $idp Deprecated. Will be removed. + * @param array &$state The logout state from doLogoutRedirect(). */ public static function finishLogoutRedirect(IdP $idp, array $state): void { diff --git a/src/SimpleSAML/Kernel.php b/src/SimpleSAML/Kernel.php index bb001642b3..32e0b48b4a 100644 --- a/src/SimpleSAML/Kernel.php +++ b/src/SimpleSAML/Kernel.php @@ -28,7 +28,9 @@ class Kernel extends BaseKernel { use MicroKernelTrait; - public const CONFIG_EXTS = '.{php,xml,yaml,yml}'; + + public const string CONFIG_EXTS = '.{php,xml,yaml,yml}'; + /** @var string */ private string $module; @@ -113,8 +115,8 @@ public function getModule(): string /** * Configures the container. * - * @param ContainerBuilder $container - * @param LoaderInterface $loader + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param \Symfony\Component\Config\Loader\LoaderInterface $loader */ protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void { @@ -137,7 +139,7 @@ protected function configureContainer(ContainerBuilder $container, LoaderInterfa /** * Import routes. * - * @param RoutingConfigurator $routes + * @param \Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator $routes */ protected function configureRoutes(RoutingConfigurator $routes): void { @@ -152,7 +154,7 @@ protected function configureRoutes(RoutingConfigurator $routes): void /** - * @param ContainerBuilder $container + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container */ private function registerModuleControllers(ContainerBuilder $container): void { diff --git a/src/SimpleSAML/Locale/Language.php b/src/SimpleSAML/Locale/Language.php index b4e2952571..c8dcc9863c 100644 --- a/src/SimpleSAML/Locale/Language.php +++ b/src/SimpleSAML/Locale/Language.php @@ -10,11 +10,21 @@ namespace SimpleSAML\Locale; -use SimpleSAML\{Configuration, Logger, Utils}; +use SimpleSAML\Configuration; +use SimpleSAML\Logger; +use SimpleSAML\Utils; use Symfony\Component\Intl\Locales; +use function sprintf; + class Language { + /** + * The final fallback language to use when no current or default available + */ + public const string FALLBACKLANGUAGE = 'en'; + + /** * This is the default language map. It is used to map languages codes from the user agent to other language codes. * @var array @@ -42,13 +52,6 @@ class Language */ private string $defaultLanguage; - /** - * The final fallback language to use when no current or default available - * - * @var string - */ - public const FALLBACKLANGUAGE = 'en'; - /** * An array holding a list of languages that are written from right to left. * @@ -194,7 +197,10 @@ private function getInstalledLanguages(): array $availableLanguages[] = $code; } else { /* The configured language code can't be found in Symfony's list of known locales */ - Logger::error("Locale \"$code\" is not known to the translation system. Check language settings in your config."); + Logger::error(sprintf( + "Locale \"%s\" is not known to the translation system. Check language settings in your config.", + $code, + )); } } @@ -399,6 +405,7 @@ public function isLanguageRTL(): bool return in_array($this->getLanguage(), $this->rtlLanguages, true); } + /** * Returns the list of languages in order of preference. This is useful * to search e.g. an array of entity names for first the current language, @@ -410,6 +417,7 @@ public function getPreferredLanguages(): array return array_unique([0 => $curLanguage, 1 => $this->defaultLanguage, 2 => self::FALLBACKLANGUAGE]); } + /** * Retrieve the user-selected language from a cookie. * @@ -431,6 +439,7 @@ public static function getLanguageCookie(): ?string return null; } + /** * This method will attempt to set the user-selected language in a cookie. It will do nothing if the language * specified is not in the list of available languages, or the headers have already been sent to the browser. diff --git a/src/SimpleSAML/Locale/Localization.php b/src/SimpleSAML/Locale/Localization.php index c110d49662..a9945d425c 100644 --- a/src/SimpleSAML/Locale/Localization.php +++ b/src/SimpleSAML/Locale/Localization.php @@ -13,9 +13,11 @@ use Exception; use Gettext\Generator\ArrayGenerator; use Gettext\Loader\PoLoader; -use Gettext\{Translations, Translator, TranslatorFunctions}; -use SimpleSAML\{Configuration, Logger}; +use Gettext\Translator; +use Gettext\TranslatorFunctions; +use SimpleSAML\Configuration; use SimpleSAML\Locale\Translate; +use SimpleSAML\Logger; use Symfony\Component\HttpFoundation\File\File; use function explode; @@ -24,11 +26,11 @@ class Localization { /** * The default gettext domain. - * - * @var string */ - public const DEFAULT_DOMAIN = 'messages'; - public const CORE_DOMAIN = 'core'; + public const string DEFAULT_DOMAIN = 'messages'; + + public const string CORE_DOMAIN = 'core'; + /** * The default locale directory @@ -80,6 +82,7 @@ public function __construct( $this->language = new Language($configuration); $this->langcode = $this->language->getPosixLanguage($this->language->getLanguage()); $this->setupL10N(); + $this->addAttributeDomains(); } @@ -94,8 +97,6 @@ public function getTranslator(): Translator /** * Dump the default locale directory - * - * @return string */ public function getLocaleDir(): string { @@ -107,8 +108,6 @@ public function getLocaleDir(): string * Get the default locale dir for a specific module aka. domain * * @param string $domain Name of module/domain - * - * @return string */ public function getDomainLocaleDir(string $domain): string { @@ -136,6 +135,9 @@ public function addModuleDomain(string $module, ?string $localeDir = null, ?stri } + /** + * @param string $domain + */ public function defaultDomain(string $domain): self { $this->translator->defaultDomain($domain); @@ -164,8 +166,6 @@ public function addDomain(string $localeDir, string $domain): void * * @param string $domain Name of localization domain * @throws \Exception If the path does not exist even for the default, fallback language - * - * @return string */ public function getLangPath(string $domain = self::DEFAULT_DOMAIN): string { @@ -300,14 +300,13 @@ private function setupL10N(): void /** * Show which domains are registered - * - * @return array */ public function getRegisteredDomains(): array { return $this->localeDomainMap; } + /** * Add translation domains specifically used for translating attributes names: * the default in attributes.po and any attributes.po in the enabled theme. diff --git a/src/SimpleSAML/Locale/Translate.php b/src/SimpleSAML/Locale/Translate.php index cb8db8e0f1..eab4a838d9 100644 --- a/src/SimpleSAML/Locale/Translate.php +++ b/src/SimpleSAML/Locale/Translate.php @@ -11,7 +11,7 @@ namespace SimpleSAML\Locale; use Gettext\TranslatorFunctions; -use SimpleSAML\{Configuration, Logger}; +use SimpleSAML\Configuration; class Translate { @@ -27,6 +27,7 @@ class Translate */ private static array $defaultDomains = []; + /** * Constructor * @@ -62,11 +63,16 @@ public static function noop(string $tag): string return $tag; } + + /** + * @param string $domain + */ public static function addDefaultDomain(string $domain): void { array_push(self::$defaultDomains, $domain); } + /** * Translate a singular text. * @@ -77,11 +83,6 @@ public static function addDefaultDomain(string $domain): void * which will pass the following arguments. * The $id will match $original above but there are other arguments which may also be used in this method. * - * @param string $id - * @param array $parameters - * @param string|null $domain - * @param string|null $locale - * * @return string The translated string. */ public static function translateSingularGettext(?string $original): string @@ -105,7 +106,7 @@ public static function translateSingularGettext(?string $original): string // try attributes.po if ($text === $original) { // @TODO: Fix this to be compatible with PHP 8.4 - domain cannot be an empty string - $text = TranslatorFunctions::getTranslator()->dgettext("", $original); + $text = TranslatorFunctions::getTranslator()->dgettext("attributes", $original); } } } @@ -173,6 +174,7 @@ public static function translateFromArray(?array $context, ?array $translations) return null; } + /** * Prefix tag * diff --git a/src/SimpleSAML/Logger.php b/src/SimpleSAML/Logger.php index 7458374bd3..a318751c9a 100644 --- a/src/SimpleSAML/Logger.php +++ b/src/SimpleSAML/Logger.php @@ -18,6 +18,29 @@ */ class Logger { + /** + * This constant defines the string we set the track ID to while we are fetching the track ID from the session + * class. This is used to prevent infinite recursion. + */ + public const string NO_TRACKID = '_NOTRACKIDYET_'; + + public const int EMERG = 0; + + public const int ALERT = 1; + + public const int CRIT = 2; + + public const int ERR = 3; + + public const int WARNING = 4; + + public const int NOTICE = 5; + + public const int INFO = 6; + + public const int DEBUG = 7; + + /** * @var \SimpleSAML\Logger\LoggingHandlerInterface|null */ @@ -68,14 +91,6 @@ class Logger */ private static int $logMask = 0; - /** - * This constant defines the string we set the track ID to while we are fetching the track ID from the session - * class. This is used to prevent infinite recursion. - * - * @var string - */ - public const NO_TRACKID = '_NOTRACKIDYET_'; - /** * This variable holds the track ID we have retrieved from the session class. It can also be NULL, in which case * we haven't fetched the track ID yet, or self::NO_TRACKID, which means that we are fetching the track ID now. @@ -128,30 +143,6 @@ class Logger */ private static bool $shuttingDown = false; - /** @var int */ - public const EMERG = 0; - - /** @var int */ - public const ALERT = 1; - - /** @var int */ - public const CRIT = 2; - - /** @var int */ - public const ERR = 3; - - /** @var int */ - public const WARNING = 4; - - /** @var int */ - public const NOTICE = 5; - - /** @var int */ - public const INFO = 6; - - /** @var int */ - public const DEBUG = 7; - /** * Log an emergency message. @@ -207,6 +198,7 @@ public static function warning(string $string): void self::log(self::WARNING, $string); } + /** * Log a warning about deprecated code. * @@ -217,6 +209,7 @@ public static function deprecated(string $string): void self::log(self::WARNING, 'DEPRECATION WARNING: ' . $string); } + /** * We reserve the notice level for statistics, so do not use this level for other kind of log messages. * @@ -388,7 +381,7 @@ public static function popErrorMask(): void /** * Returns the current logging handler * - * @return LoggingHandlerInterface + * @return \SimpleSAML\Logger\LoggingHandlerInterface */ public static function getLoggingHandler(): ?LoggingHandlerInterface { @@ -399,7 +392,7 @@ public static function getLoggingHandler(): ?LoggingHandlerInterface /** * Sets the current logging handler * - * @param LoggingHandlerInterface|null $loggingHandler The logging handler to set + * @param \SimpleSAML\Logger\LoggingHandlerInterface|null $loggingHandler The logging handler to set */ public static function setLoggingHandler(?LoggingHandlerInterface $loggingHandler): void { @@ -407,6 +400,7 @@ public static function setLoggingHandler(?LoggingHandlerInterface $loggingHandle self::$loggingHandler = $loggingHandler; } + /** * Sets the log level. * @@ -417,6 +411,7 @@ public static function setLogLevel(int $level): void self::$logLevel = $level; } + /** * Defer a message for later logging. * @@ -484,7 +479,6 @@ private static function createLoggingHandler(?string $handler = null): void self::$format = $config->getOptionalString('logging.format', self::$format); try { - /** @var \SimpleSAML\Logger\LoggingHandlerInterface */ self::$loggingHandler = new $handler($config); self::$loggingHandler->setLogFormat(self::$format); self::$initializing = false; diff --git a/src/SimpleSAML/Logger/FileLoggingHandler.php b/src/SimpleSAML/Logger/FileLoggingHandler.php index 75703c38c6..27771120a0 100644 --- a/src/SimpleSAML/Logger/FileLoggingHandler.php +++ b/src/SimpleSAML/Logger/FileLoggingHandler.php @@ -5,7 +5,9 @@ namespace SimpleSAML\Logger; use DateTimeImmutable; -use SimpleSAML\{Configuration, Logger, Utils}; +use SimpleSAML\Configuration; +use SimpleSAML\Logger; +use SimpleSAML\Utils; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException; use Symfony\Component\HttpFoundation\File\File; diff --git a/src/SimpleSAML/Memcache.php b/src/SimpleSAML/Memcache.php index d31b6c96ba..96fb3c4a76 100644 --- a/src/SimpleSAML/Memcache.php +++ b/src/SimpleSAML/Memcache.php @@ -274,7 +274,7 @@ private static function addMemcacheServer(Memcached $memcache, array $server): v * * @throws \Exception If the servers configuration is invalid. */ - private static function loadMemcacheServerGroup(array $group, $index = null) + private static function loadMemcacheServerGroup(array $group, $index = null): Memcached { if (is_string($index)) { $memcache = new Memcached($index); diff --git a/src/SimpleSAML/Metadata/MetaDataStorageHandler.php b/src/SimpleSAML/Metadata/MetaDataStorageHandler.php index c930397957..ccd7627c87 100644 --- a/src/SimpleSAML/Metadata/MetaDataStorageHandler.php +++ b/src/SimpleSAML/Metadata/MetaDataStorageHandler.php @@ -27,8 +27,7 @@ class MetaDataStorageHandler implements ClearableState * * @var \SimpleSAML\Metadata\MetaDataStorageHandler|null */ - private static ?MetadataStorageHandler $metadataHandler = null; - + private static ?MetaDataStorageHandler $metadataHandler = null; /** * This is a list of all the metadata sources we have in our metadata @@ -44,7 +43,7 @@ class MetaDataStorageHandler implements ClearableState * The metadata handler will be instantiated if this is the first call * to this function. * - * @return MetaDataStorageHandler The current metadata handler instance. + * @return \SimpleSAML\Metadata\MetaDataStorageHandler The current metadata handler instance. */ public static function getMetadataHandler(): MetaDataStorageHandler { @@ -302,6 +301,7 @@ public function getMetaDataForEntities(array $entityIds, string $set): array return $result; } + /** * This function looks up the metadata for the given entity id in the given set. It will throw an * exception if it is unable to locate the metadata. diff --git a/src/SimpleSAML/Metadata/MetaDataStorageHandlerDirectory.php b/src/SimpleSAML/Metadata/MetaDataStorageHandlerDirectory.php index 9246446ba8..db93e28d3a 100644 --- a/src/SimpleSAML/Metadata/MetaDataStorageHandlerDirectory.php +++ b/src/SimpleSAML/Metadata/MetaDataStorageHandlerDirectory.php @@ -31,7 +31,6 @@ class MetaDataStorageHandlerDirectory extends MetaDataStorageSource */ private string $directory = '/'; - /** * This is an associative array which stores the different metadata sets we have loaded. * diff --git a/src/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php b/src/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php index 0743373552..f314f26378 100644 --- a/src/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php +++ b/src/SimpleSAML/Metadata/MetaDataStorageHandlerFlatFile.php @@ -29,7 +29,6 @@ class MetaDataStorageHandlerFlatFile extends MetaDataStorageSource */ private string $directory = '/'; - /** * Optional explicit file path to load metadata from * diff --git a/src/SimpleSAML/Metadata/MetaDataStorageHandlerPdo.php b/src/SimpleSAML/Metadata/MetaDataStorageHandlerPdo.php index 55d7799aea..3c851fd9ed 100644 --- a/src/SimpleSAML/Metadata/MetaDataStorageHandlerPdo.php +++ b/src/SimpleSAML/Metadata/MetaDataStorageHandlerPdo.php @@ -58,6 +58,7 @@ class MetaDataStorageHandlerPdo extends MetaDataStorageSource * - 'password': Password for the database user. * * @param array $config An associative array with the configuration for this handler. + * @phpstan-ignore constructor.unusedParameter */ public function __construct(/** @scrutinizer ignore-unused */ array $config) { diff --git a/src/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php b/src/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php index 8844b9dbf9..cf975379df 100644 --- a/src/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php +++ b/src/SimpleSAML/Metadata/MetaDataStorageHandlerSerialize.php @@ -30,10 +30,8 @@ class MetaDataStorageHandlerSerialize extends MetaDataStorageSource { /** * The file extension we use for our metadata files. - * - * @var string */ - public const EXTENSION = '.serialized'; + public const string EXTENSION = '.serialized'; /** diff --git a/src/SimpleSAML/Metadata/MetaDataStorageHandlerXML.php b/src/SimpleSAML/Metadata/MetaDataStorageHandlerXML.php index ba5ebf7566..dd29bd04b4 100644 --- a/src/SimpleSAML/Metadata/MetaDataStorageHandlerXML.php +++ b/src/SimpleSAML/Metadata/MetaDataStorageHandlerXML.php @@ -4,8 +4,11 @@ namespace SimpleSAML\Metadata; +use Exception; use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; +use SimpleSAML\Utils; +use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; /** * This class implements a metadata source which loads metadata from XML files. @@ -43,30 +46,33 @@ protected function __construct(array $config) // get the configuration $globalConfig = Configuration::getInstance(); $src = $globalConfig->resolvePath($config['file']); + $srcXml = file_get_contents($src); } elseif (array_key_exists('url', $config)) { $src = $config['url']; if (array_key_exists('context', $config)) { Assert::isArray($config['context']); $context = $config['context']; } + $httpUtils = new Utils\HTTP(); + $client = $httpUtils->createHttpClient($context); + $response = $client->request('GET', $src); + + try { + $response->getHeaders(); + $srcXML = $response->getContent(); + } catch (ExceptionInterface $e) { + } } elseif (array_key_exists('xml', $config)) { $srcXml = $config['xml']; } else { - throw new \Exception("Missing one of 'file', 'url' and 'xml' in XML metadata source configuration."); + throw new Exception("Missing one of 'file', 'url' and 'xml' in XML metadata source configuration."); } - $SP20 = []; $IdP20 = []; $AAD = []; - if (isset($src)) { - $entities = SAMLParser::parseDescriptorsFile($src, $context); - } elseif (isset($srcXml)) { - $entities = SAMLParser::parseDescriptorsString($srcXml); - } else { - throw new \Exception("Neither source file path/URI nor string data provided"); - } + $entities = SAMLParser::parseDescriptorsString($srcXml); foreach ($entities as $entityId => $entity) { $md = $entity->getMetadata20SP(); if ($md !== null) { diff --git a/src/SimpleSAML/Metadata/SAMLBuilder.php b/src/SimpleSAML/Metadata/SAMLBuilder.php index 1132ed6225..f5db175835 100644 --- a/src/SimpleSAML/Metadata/SAMLBuilder.php +++ b/src/SimpleSAML/Metadata/SAMLBuilder.php @@ -27,8 +27,11 @@ use SAML2\XML\saml\Attribute; use SAML2\XML\saml\AttributeValue; use SAML2\XML\shibmd\Scope; -use SimpleSAML\{Configuration, Module, Logger, Utils}; -use SimpleSAML\Assert\{Assert, AssertionFailedException}; +use SimpleSAML\Assert\Assert; +use SimpleSAML\Configuration; +use SimpleSAML\Logger; +use SimpleSAML\Module; +use SimpleSAML\Utils; /** * Class for generating SAML 2.0 metadata from SimpleSAMLphp metadata arrays. diff --git a/src/SimpleSAML/Metadata/SAMLParser.php b/src/SimpleSAML/Metadata/SAMLParser.php index eaa819018b..736350d4ff 100644 --- a/src/SimpleSAML/Metadata/SAMLParser.php +++ b/src/SimpleSAML/Metadata/SAMLParser.php @@ -8,8 +8,6 @@ use DOMElement; use Exception; use RobRichards\XMLSecLibs\XMLSecurityKey; -use SAML2\Constants; -use SAML2\DOMDocumentFactory; use SAML2\SignedElementHelper; use SAML2\XML\ds\X509Certificate; use SAML2\XML\ds\X509Data; @@ -36,8 +34,11 @@ use SAML2\XML\shibmd\Scope; use SimpleSAML\Assert\Assert; use SimpleSAML\Logger; +use SimpleSAML\SAML2\Constants; use SimpleSAML\Utils; +use SimpleSAML\XML\DOMDocumentFactory; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use function array_diff; use function array_intersect; @@ -226,17 +227,23 @@ private function __construct( * * @param string $file The path to the file which contains the metadata. * - * @return SAMLParser An instance of this class with the metadata loaded. + * @return \SimpleSAML\Metadata\SAMLParser An instance of this class with the metadata loaded. * @throws \Exception If the file does not parse as XML. */ + #[\Deprecated('Will be removed in 3.0. No replacement was suggested', '16-12-2025')] public static function parseFile(string $file): SAMLParser { - /** @var string $data */ $httpUtils = new Utils\HTTP(); - $data = $httpUtils->fetch($file); + $client = $httpUtils->createHttpClient(); + $response = $client->request('GET', $file); try { + $response->getHeaders(); + /** @var string $data */ + $data = $response->getContent(); $doc = DOMDocumentFactory::fromString($data); + } catch (ExceptionInterface $e) { + throw new Exception('Failed to read XML from file: ' . $file); } catch (Exception $e) { throw new Exception('Failed to read XML from file: ' . $file); } @@ -250,7 +257,7 @@ public static function parseFile(string $file): SAMLParser * * @param string $metadata A string which contains XML encoded metadata. * - * @return SAMLParser An instance of this class with the metadata loaded. + * @return \SimpleSAML\Metadata\SAMLParser An instance of this class with the metadata loaded. * @throws \Exception If the string does not parse as XML. */ public static function parseString(string $metadata): SAMLParser @@ -270,7 +277,7 @@ public static function parseString(string $metadata): SAMLParser * * @param \DOMDocument $document The \DOMDocument which contains the EntityDescriptor element. * - * @return SAMLParser An instance of this class with the metadata loaded. + * @return \SimpleSAML\Metadata\SAMLParser An instance of this class with the metadata loaded. */ public static function parseDocument(DOMDocument $document): SAMLParser { @@ -286,7 +293,7 @@ public static function parseDocument(DOMDocument $document): SAMLParser * @param \SAML2\XML\md\EntityDescriptor $entityElement A \SAML2\XML\md\EntityDescriptor object which represents a * EntityDescriptor element. * - * @return SAMLParser An instance of this class with the metadata loaded. + * @return \SimpleSAML\Metadata\SAMLParser An instance of this class with the metadata loaded. */ public static function parseElement(EntityDescriptor $entityElement): SAMLParser { @@ -303,21 +310,27 @@ public static function parseElement(EntityDescriptor $entityElement): SAMLParser * @param string $file The path to the file which contains the EntityDescriptor or EntitiesDescriptor element. * @param array $context The connection context to pass to file_get_contents() * - * @return SAMLParser[] An array of SAMLParser instances. + * @return \SimpleSAML\Metadata\SAMLParser[] An array of SAMLParser instances. * @throws \Exception If the file does not parse as XML. */ + #[\Deprecated('Will be removed in 3.0. No replacement was suggested', '16-12-2025')] public static function parseDescriptorsFile(string $file, array $context = []): array { if (empty($file)) { throw new Exception('Cannot open file; file name not specified.'); } - /** @var string $data */ $httpUtils = new Utils\HTTP(); - $data = $httpUtils->fetch($file, $context); + $client = $httpUtils->createHttpClient($context); + $response = $client->request('GET', $file); try { + $response->getHeaders(); + /** @var string $data */ + $data = $response->getContent(); $doc = DOMDocumentFactory::fromString($data); + } catch (ExceptionInterface $e) { + throw new Exception('Failed to read XML from file: ' . $file); } catch (Exception $e) { throw new Exception('Failed to read XML from file: ' . $file); } @@ -333,8 +346,8 @@ public static function parseDescriptorsFile(string $file, array $context = []): * * @param string $string The string with XML data. * - * @return SAMLParser[] An associative array of SAMLParser instances. The key of the array will - * be the entity id. + * @return \SimpleSAML\Metadata\SAMLParser[] An associative array of SAMLParser instances. + * The key of the array will be the entity id. * @throws \Exception If the string does not parse as XML. */ public static function parseDescriptorsString(string $string): array @@ -356,8 +369,8 @@ public static function parseDescriptorsString(string $string): array * @param \DOMElement|null $element The DOMElement which contains the EntityDescriptor element or the * EntitiesDescriptor element. * - * @return SAMLParser[] An associative array of SAMLParser instances. The key of the array will - * be the entity id. + * @return \SimpleSAML\Metadata\SAMLParser[] An associative array of SAMLParser instances. + * The key of the array will be the entity id. * @throws \Exception if the document is empty or the root is an unexpected node. */ public static function parseDescriptorsElement(?DOMElement $element = null): array @@ -384,7 +397,7 @@ public static function parseDescriptorsElement(?DOMElement $element = null): arr * @param array $validators The parent-elements that may be signed. * @param array $parentExtensions An optional array of extensions from the parent element. * - * @return SAMLParser[] Array of SAMLParser instances. + * @return \SimpleSAML\Metadata\SAMLParser[] Array of SAMLParser instances. */ private static function processDescriptorsElement( SignedElementHelper $element, @@ -395,7 +408,7 @@ private static function processDescriptorsElement( if ($element instanceof EntityDescriptor) { $ret = new SAMLParser($element, $maxExpireTime, $validators, $parentExtensions); $ret = [$ret->getEntityId() => $ret]; - /** @var SAMLParser[] $ret */ + /** @var \SimpleSAML\Metadata\SAMLParser[] $ret */ return $ret; } @@ -424,8 +437,8 @@ private static function processDescriptorsElement( * @param mixed $element The element we should determine the expiry time of. * @param int|null $maxExpireTime The maximum expiration time. * - * @return int|null The unix timestamp for when the element should expire. Will be NULL if no - * limit is set for the element. + * @return int|null The unix timestamp for when the element should expire. + * Will be NULL if no limit is set for the element. */ private static function getExpireTime(mixed $element, ?int $maxExpireTime): ?int { @@ -1075,7 +1088,16 @@ private static function parseAttributeConsumerService(AttributeConsumingService $sp['attributes.required'] = []; foreach ($element->getRequestedAttribute() as $child) { $attrname = $child->getName(); - $sp['attributes'][] = $attrname; + $attrvalue = $child->getAttributeValue(); + if (empty($attrvalue)) { + $sp['attributes'][] = $attrname; + } else { + $values = []; + foreach ($attrvalue as $attrval) { + $values[] = $attrval->getString(); + } + $sp['attributes'][$attrname] = $values; + } if ($child->getIsRequired() === true) { $sp['attributes.required'][] = $attrname; diff --git a/src/SimpleSAML/Metadata/Signer.php b/src/SimpleSAML/Metadata/Signer.php index f78d15f937..3271af07dd 100644 --- a/src/SimpleSAML/Metadata/Signer.php +++ b/src/SimpleSAML/Metadata/Signer.php @@ -7,10 +7,10 @@ use Exception; use RobRichards\XMLSecLibs\XMLSecurityDSig; use RobRichards\XMLSecLibs\XMLSecurityKey; -use SAML2\DOMDocumentFactory; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\Utils; +use SimpleSAML\XML\DOMDocumentFactory; use function array_key_exists; use function hash; diff --git a/src/SimpleSAML/Metadata/Sources/MDQ.php b/src/SimpleSAML/Metadata/Sources/MDQ.php index 23ab985859..76f8ccb8b3 100644 --- a/src/SimpleSAML/Metadata/Sources/MDQ.php +++ b/src/SimpleSAML/Metadata/Sources/MDQ.php @@ -12,6 +12,7 @@ use SimpleSAML\Metadata\SAMLParser; use SimpleSAML\Utils; use Symfony\Component\HttpFoundation\File\File; +use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use function array_key_exists; use function error_get_last; @@ -61,6 +62,7 @@ class MDQ extends MetaDataStorageSource */ private int $cacheLength; + /** * This function initializes the dynamic XML metadata source. * @@ -296,31 +298,25 @@ public function getMetaData(string $entityId, string $set): ?array // look at Metadata Query Protocol: https://github.com/iay/md-query/blob/master/draft-young-md-query.txt $mdq_url = $this->server . '/entities/' . urlencode($entityId); - Logger::debug(sprintf('%s: downloading metadata for "%s" from [%s]', __CLASS__, $entityId, $mdq_url)); $httpUtils = new Utils\HTTP(); - $context = [ - 'http' => [ - 'header' => 'Accept: application/samlmetadata+xml', - ], - ]; - try { - $xmldata = $httpUtils->fetch($mdq_url, $context); - } catch (Exception $e) { - // Avoid propagating the exception, make sure we can handle the error later - $xmldata = false; - } + $client = $httpUtils->createHttpClient([ + 'headers' => ['Accept', 'application/samlmetadata+xml'], + ]); - if (empty($xmldata)) { - $error = error_get_last(); - Logger::info(sprintf( - 'Unable to fetch metadata for "%s" from %s: %s', - $entityId, - $mdq_url, - (is_array($error) ? $error['message'] : 'no error available'), - )); + Logger::debug(sprintf('%s: downloading metadata for "%s" from [%s]', __CLASS__, $entityId, $mdq_url)); + $response = $client->request('GET', $mdq_url); + + try { + $response->getHeaders(); + } catch (ExceptionInterface $e) { + Logger::info( + sprintf('Unable to fetch metadata for "%s" from %s: %s', $entityId, $mdq_url, $e->getMessage()), + ); return null; } + $xmldata = $response->getContent(); + /** @var string $xmldata */ $entity = SAMLParser::parseString($xmldata); Logger::debug(sprintf('%s: completed parsing of [%s]', __CLASS__, $mdq_url)); @@ -350,6 +346,7 @@ public function getMetaData(string $entityId, string $set): ?array return $data; } + /** * This function loads the metadata for entity IDs in $entityIds. It is returned as an associative array * where the key is the entity id. An empty array may be returned if no matching entities were found diff --git a/src/SimpleSAML/Module.php b/src/SimpleSAML/Module.php index 55d7809654..e32b20381d 100644 --- a/src/SimpleSAML/Module.php +++ b/src/SimpleSAML/Module.php @@ -102,6 +102,7 @@ class Module */ public static array $core_modules = [ 'core' => true, + 'admin' => true, 'saml' => true, ]; @@ -156,11 +157,16 @@ public static function isModuleEnabled(string $module): bool * configuration and the actual request, it will run a PHP script and exit, or return a Response produced either * by another controller or by a static file. * - * @param \Symfony\Component\HttpFoundation\Request|null $request The request to process. Defaults to the current one. + * @param \Symfony\Component\HttpFoundation\Request|null $request + * The request to process. Defaults to the current one. + * @return ( + * \Symfony\Component\HttpFoundation\Response| + * \Symfony\Component\HttpFoundation\BinaryFileResponse + * ) Returns a Response object that can be sent to the browser. * - * @return Response|BinaryFileResponse Returns a Response object that can be sent to the browser. - * @throws Error\BadRequest In case the request URI is malformed. - * @throws Error\NotFound In case the request URI is invalid or the resource it points to cannot be found. + * @throws \SimpleSAML\Error\BadRequest In case the request URI is malformed. + * @throws \SimpleSAML\Error\NotFound + * In case the request URI is invalid or the resource it points to cannot be found. */ public static function process(?Request $request = null): Response { @@ -480,7 +486,7 @@ public static function resolveClass(string $id, string $type, ?string $subclass /** * Create an object of a class returned by resolveNonModuleClass() or resolveClass(). * - * @param string The classname. + * @param string $className The classname. * @param string|null $subclass The class should be a subclass of this class. Optional. * * @return object The new object @@ -499,6 +505,7 @@ public static function createObject(string $className, ?string $subclass = null) return $obj; } + /** * Get absolute URL to a specified module resource. * @@ -569,7 +576,7 @@ public static function getModuleHooks(string $module): array public static function callHooks(string $hook, mixed &$data = null): void { $modules = self::getModules(); - $config = Configuration::getOptionalConfig()->getOptionalArray('module.enable', []); + $config = Configuration::getOptionalConfig()->getOptionalArray('module.enable', self::$core_modules); sort($modules); foreach ($modules as $module) { if (!self::isModuleEnabledWithConf($module, $config)) { @@ -604,9 +611,9 @@ public static function callHooks(string $hook, mixed &$data = null): void * * This method add the trailing slash and redirects to the resulting URL. * - * @param Request $request The request to process by this controller method. - * - * @return RedirectResponse A redirection to the URI specified in the request, but with a trailing slash. + * @param \Symfony\Component\HttpFoundation\Request $request The request to process by this controller method. + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * A redirection to the URI specified in the request, but with a trailing slash. */ public static function addTrailingSlash(Request $request): RedirectResponse { diff --git a/src/SimpleSAML/Session.php b/src/SimpleSAML/Session.php index 012d3a8584..3f4820c738 100644 --- a/src/SimpleSAML/Session.php +++ b/src/SimpleSAML/Session.php @@ -33,7 +33,8 @@ class Session implements Utils\ClearableState * This is a timeout value for setData, which indicates that the data * should never be deleted, i.e. lasts the whole session lifetime. */ - public const DATA_TIMEOUT_SESSION_END = 'sessionEndTimeout'; + public const string DATA_TIMEOUT_SESSION_END = 'sessionEndTimeout'; + /** * The list of loaded session objects. @@ -194,7 +195,7 @@ private function __construct(bool $transient = false) /** * Set the configuration we should use. * - * @param Configuration $config + * @param \SimpleSAML\Configuration $config */ public function setConfiguration(Configuration $config): void { @@ -223,7 +224,7 @@ public function __serialize(): array * * @param array $serialized The serialized representation of a session that we want to restore. */ - public function __unserialize($serialized): void + public function __unserialize(array $serialized): void { foreach ($serialized as $k => $v) { $this->$k = $v; @@ -250,7 +251,7 @@ public function __unserialize($serialized): void /** * Retrieves the current session. Creates a new session if there's not one. * - * @return Session The current session. + * @return \SimpleSAML\Session The current session. * @throws \Exception When session couldn't be initialized and the session fallback is disabled by configuration. */ public static function getSessionFromRequest(): Session @@ -498,7 +499,6 @@ public function cleanup(): void * Mark this session as dirty. * * This method will register a callback to save the session right before any output is sent to the browser. - * */ public function markDirty(): void { @@ -598,7 +598,7 @@ public function setRememberMeExpire(?int $lifetime = null): void * @param string $authority The authority the user logged in with. * @param array $data The authentication data for this authority. * - * @throws Error\CannotSetCookie If the authentication token cannot be set for some reason. + * @throws \SimpleSAML\Error\CannotSetCookie If the authentication token cannot be set for some reason. */ public function doLogin(string $authority, array $data = []): void { diff --git a/src/SimpleSAML/SessionHandlerCookie.php b/src/SimpleSAML/SessionHandlerCookie.php index eeb54239c9..cf409864ea 100644 --- a/src/SimpleSAML/SessionHandlerCookie.php +++ b/src/SimpleSAML/SessionHandlerCookie.php @@ -25,7 +25,6 @@ abstract class SessionHandlerCookie extends SessionHandler */ private ?string $session_id = null; - /** * This variable contains the session cookie name. * diff --git a/src/SimpleSAML/SessionHandlerPHP.php b/src/SimpleSAML/SessionHandlerPHP.php index 04553c3a76..dac685d496 100644 --- a/src/SimpleSAML/SessionHandlerPHP.php +++ b/src/SimpleSAML/SessionHandlerPHP.php @@ -165,6 +165,7 @@ public function newSessionId(): string return $sessionId; } + /** * Retrieve the session ID saved in the session cookie, if there's one. * diff --git a/src/SimpleSAML/Stats.php b/src/SimpleSAML/Stats.php index 7a375f4803..317ad4c755 100644 --- a/src/SimpleSAML/Stats.php +++ b/src/SimpleSAML/Stats.php @@ -23,7 +23,6 @@ class Stats */ private static bool $initialized = false; - /** * The statistics output callbacks. * diff --git a/src/SimpleSAML/Stats/Output.php b/src/SimpleSAML/Stats/Output.php index 75c3e6455a..1dfc818f7d 100644 --- a/src/SimpleSAML/Stats/Output.php +++ b/src/SimpleSAML/Stats/Output.php @@ -11,13 +11,13 @@ * * @package SimpleSAMLphp */ - abstract class Output { /** * Initialize the output. * * @param \SimpleSAML\Configuration $config The configuration for this output. + * @phpstan-ignore constructor.unusedParameter */ public function __construct(/** @scrutinizer ignore-unused */ Configuration $config) { diff --git a/src/SimpleSAML/Store/RedisStore.php b/src/SimpleSAML/Store/RedisStore.php index 3dab9bfa54..96b02960b1 100644 --- a/src/SimpleSAML/Store/RedisStore.php +++ b/src/SimpleSAML/Store/RedisStore.php @@ -6,7 +6,9 @@ use Predis\Client; use SimpleSAML\Assert\Assert; -use SimpleSAML\{Configuration, Error, Utils}; +use SimpleSAML\Configuration; +use SimpleSAML\Error; +use SimpleSAML\Utils; use function class_exists; use function serialize; @@ -98,6 +100,7 @@ public function __construct(?Client $redis = null) 'service' => $mastergroup, 'prefix' => $prefix, 'parameters' => [ + 'scheme' => $scheme, 'database' => $database, ] + (!empty($ssl) ? ['ssl' => $ssl] : []) diff --git a/src/SimpleSAML/Store/SQLStore.php b/src/SimpleSAML/Store/SQLStore.php index e472cad669..ecefac6ae3 100644 --- a/src/SimpleSAML/Store/SQLStore.php +++ b/src/SimpleSAML/Store/SQLStore.php @@ -8,7 +8,9 @@ use PDO; use PDOException; use SimpleSAML\Assert\Assert; -use SimpleSAML\{Configuration, Logger, Utils}; +use SimpleSAML\Configuration; +use SimpleSAML\Logger; +use SimpleSAML\Utils; use function array_keys; use function count; @@ -17,12 +19,12 @@ use function in_array; use function intval; use function rand; +use function rawurlencode; use function serialize; use function sha1; use function strlen; use function unserialize; use function urldecode; -use function rawurlencode; /** * A data store using a RDBMS to keep the data. @@ -78,6 +80,7 @@ public function __construct() throw new Exception("Database error: " . $e->getMessage()); } $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $this->driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME); @@ -196,6 +199,7 @@ private function initTableVersionTable(): void } } + /** * Initialize key-value table. */ diff --git a/src/SimpleSAML/Store/StoreFactory.php b/src/SimpleSAML/Store/StoreFactory.php index 5f3c18b5f2..9009aed076 100644 --- a/src/SimpleSAML/Store/StoreFactory.php +++ b/src/SimpleSAML/Store/StoreFactory.php @@ -24,7 +24,7 @@ abstract class StoreFactory implements Utils\ClearableState * * @var \SimpleSAML\Store\StoreInterface|false|null */ - private static $instance = null; + private static StoreInterface|false|null $instance = null; /** @@ -35,7 +35,7 @@ abstract class StoreFactory implements Utils\ClearableState * * @throws \SimpleSAML\Error\CriticalConfigurationError */ - public static function getInstance(string $storeType) + public static function getInstance(string $storeType): StoreInterface|false { if (self::$instance !== null) { return self::$instance; @@ -69,7 +69,7 @@ public static function getInstance(string $storeType) $c, ); } - /** @var \SimpleSAML\Store\StoreInterface|false */ + self::$instance = new $className(); } diff --git a/src/SimpleSAML/Utils/Config.php b/src/SimpleSAML/Utils/Config.php index e89200da41..7d4efb5e2a 100644 --- a/src/SimpleSAML/Utils/Config.php +++ b/src/SimpleSAML/Utils/Config.php @@ -63,6 +63,7 @@ public function getSecretSalt(): string return $secretSalt; } + /** * Returns the path to the config dir * diff --git a/src/SimpleSAML/Utils/Config/Metadata.php b/src/SimpleSAML/Utils/Config/Metadata.php index 0aa2e01b52..021ad5b60e 100644 --- a/src/SimpleSAML/Utils/Config/Metadata.php +++ b/src/SimpleSAML/Utils/Config/Metadata.php @@ -22,7 +22,6 @@ class Metadata */ public static string $ENTITY_CATEGORY = 'http://macedir.org/entity-category'; - /** * The string the identifies the REFEDS "Hide From Discovery" Entity Category. * @@ -30,7 +29,6 @@ class Metadata */ public static string $HIDE_FROM_DISCOVERY = 'http://refeds.org/category/hide-from-discovery'; - /** * Valid options for the ContactPerson element * @@ -250,7 +248,7 @@ public static function parseNameIdPolicy(?array $nameIdPolicy = null): array { if ($nameIdPolicy === null) { // when NameIDPolicy is unset or set to null, default to transient - return ['Format' => Constants::NAMEID_TRANSIENT, 'AllowCreate' => true]; + return ['Format' => Constants::NAMEID_TRANSIENT, 'AllowCreate' => false]; } if ($nameIdPolicy === []) { @@ -260,9 +258,17 @@ public static function parseNameIdPolicy(?array $nameIdPolicy = null): array // handle configurations specifying an array in the NameIDPolicy config option $nameIdPolicy_cf = Configuration::loadFromArray($nameIdPolicy); + $format = $nameIdPolicy_cf->getOptionalString('Format', Constants::NAMEID_TRANSIENT); + $allowCreate = $nameIdPolicy_cf->getOptionalBoolean('AllowCreate', true); + // SAML Version 2.0 Errata 05 lines 252-255 (pg 12) + if ($format === Constants::NAMEID_TRANSIENT) { + if ($allowCreate) { + $allowCreate = false; + } + } $policy = [ - 'Format' => $nameIdPolicy_cf->getOptionalString('Format', Constants::NAMEID_TRANSIENT), - 'AllowCreate' => $nameIdPolicy_cf->getOptionalBoolean('AllowCreate', true), + 'Format' => $format, + 'AllowCreate' => $allowCreate, ]; $spNameQualifier = $nameIdPolicy_cf->getOptionalString('SPNameQualifier', null); if ($spNameQualifier !== null) { diff --git a/src/SimpleSAML/Utils/Crypto.php b/src/SimpleSAML/Utils/Crypto.php index 64e8933d00..15bc9f6766 100644 --- a/src/SimpleSAML/Utils/Crypto.php +++ b/src/SimpleSAML/Utils/Crypto.php @@ -27,7 +27,7 @@ class Crypto * * @return string The decrypted data. * @throws \InvalidArgumentException If $ciphertext is not a string. - * @throws Error\Exception If the openssl module is not loaded. + * @throws \SimpleSAML\Error\Exception If the openssl module is not loaded. * * @see \SimpleSAML\Utils\Crypto::aesDecrypt() */ @@ -79,7 +79,7 @@ private function aesDecryptInternal(string $ciphertext, string $secret): string * * @return string The decrypted data. * @throws \InvalidArgumentException If $ciphertext is not a string. - * @throws Error\Exception If the openssl module is not loaded. + * @throws \SimpleSAML\Error\Exception If the openssl module is not loaded. * * @deprecated - Possibly use xml-security library */ @@ -102,7 +102,7 @@ public function aesDecrypt(string $ciphertext, ?string $secret = null): string * * @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated. * @throws \InvalidArgumentException If $data is not a string. - * @throws Error\Exception If the openssl module is not loaded. + * @throws \SimpleSAML\Error\Exception If the openssl module is not loaded. * * @see \SimpleSAML\Utils\Crypto::aesEncrypt() */ @@ -145,7 +145,7 @@ private function aesEncryptInternal(string $data, string $secret): string * * @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated. * @throws \InvalidArgumentException If $data is not a string. - * @throws Error\Exception If the openssl module is not loaded. + * @throws \SimpleSAML\Error\Exception If the openssl module is not loaded. * * @deprecated - Possibly use xml-security library */ @@ -197,8 +197,8 @@ public function der2pem(string $der, string $type = 'CERTIFICATE'): string * * @return array|null Extracted private key, or NULL if no private key is present. * @throws \InvalidArgumentException If $required is not boolean or $prefix is not a string. - * @throws Error\Exception If no private key is found in the metadata, or it was not possible to load - * it. + * @throws \SimpleSAML\Error\Exception If no private key is found in the metadata, + * or it was not possible to load it. * */ public function loadPrivateKey( @@ -254,8 +254,8 @@ public function loadPrivateKey( * @return array|null Public key or certificate data, or NULL if no public key or certificate was found. * @throws \InvalidArgumentException If $metadata is not an instance of \SimpleSAML\Configuration, $required is not * boolean or $prefix is not a string. - * @throws Error\Exception If no public key is found in the metadata, or it was not possible to load - * it. + * @throws \SimpleSAML\Error\Exception If no public key is found in the metadata, + * or it was not possible to load it. * */ public function loadPublicKey(Configuration $metadata, bool $required = false, string $prefix = ''): ?array @@ -333,7 +333,7 @@ public function pem2der(string $pem): string * * @return string The hashed password. * @throws \Exception If the algorithm is not known ti PHP. - * @throws Error\Exception If the algorithm specified is not supported. + * @throws \SimpleSAML\Error\Exception If the algorithm specified is not supported. * * @see hash_algos() * @deprecated Use Symfony NativePasswordHasher::hash instead @@ -372,7 +372,7 @@ public function secureCompare(string $known, string $user): bool * * @return boolean True if the hash corresponds with the given password, false otherwise. * @throws \InvalidArgumentException If the input parameters are not strings. - * @throws Error\Exception If the algorithm specified is not supported. + * @throws \SimpleSAML\Error\Exception If the algorithm specified is not supported. * * @deprecated Use Symfony NativePasswordHasher::verify instead */ @@ -387,6 +387,7 @@ public function pwValid(string $hash, string $password): bool return $hash === $password; } + /** * Retrieve a certificate or private key from specified storage location * @@ -463,6 +464,7 @@ private function retrieveCertOrKey(string $data_type, string $location, bool $fu return($data); } + /** * Public wrapper around retrieveCertOrKey to retrieve a certificate * @@ -479,6 +481,7 @@ public function retrieveCertificate(string $location, bool $full_path = false): return $this->retrieveCertOrKey('certificate', $location, $full_path); } + /** * Public wrapper around retrieveCertOrKey to retrieve a private key * diff --git a/src/SimpleSAML/Utils/EMail.php b/src/SimpleSAML/Utils/EMail.php index b141f0d725..a10872105f 100644 --- a/src/SimpleSAML/Utils/EMail.php +++ b/src/SimpleSAML/Utils/EMail.php @@ -230,8 +230,8 @@ public function setTransportMethod(string $transportMethod, array $transportOpti /** * Initializes the provided EMail object with the configuration provided from the SimpleSAMLphp configuration. * - * @param EMail $EMail - * @return EMail + * @param \SimpleSAML\Utils\EMail $EMail + * @return \SimpleSAML\Utils\EMail * @throws \Exception */ public function initFromConfig(EMail $EMail): EMail diff --git a/src/SimpleSAML/Utils/HTTP.php b/src/SimpleSAML/Utils/HTTP.php index c992015591..0cf39c7578 100644 --- a/src/SimpleSAML/Utils/HTTP.php +++ b/src/SimpleSAML/Utils/HTTP.php @@ -4,6 +4,7 @@ namespace SimpleSAML\Utils; +use Exception; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\Logger; @@ -13,6 +14,12 @@ use SimpleSAML\XMLSecurity\Alg\Encryption\AES; use SimpleSAML\XMLSecurity\Constants as C; use SimpleSAML\XMLSecurity\Key\SymmetricKey; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +use function array_merge; +use function parse_url; +use function str_replace; /** * HTTP-related utility methods. @@ -21,6 +28,37 @@ */ class HTTP { + protected Configuration $config; + + + /** + * Instantiate an HTTP Client + * + * https://github.com/symfony/symfony/blob/d1ebc450128b626d4b9822f6baf97f530eb3b4d1/src/Symfony/Contracts/HttpClient/HttpClientInterface.php#L26 + * + * @param array $options See Symfony\Contracts\HttpClient\HttpClientInterface::OPTIONS_DEFAULTS for possible values + */ + public function createHttpClient(array $options = []): HttpClientInterface + { + $config = Configuration::getInstance(); + $proxy = $config->getOptionalString('proxy', null); + + if ($proxy !== null) { + $proxyAuth = $config->getOptionalString('proxy.auth', null); + $scheme = parse_url($proxy, PHP_URL_SCHEME); + + if ($proxyAuth !== null) { + $proxy = ['proxy' => str_replace($scheme . '://', $scheme . '://' . $proxyAuth . '@', $proxy)]; + } elseif ($proxy !== null) { + $proxy = ['proxy' => str_replace($scheme . '://', 'http://', $proxy)]; + } + $options = array_merge($proxy, $options); + } + + return HttpClient::create($options); + } + + /** * Determine if the user agent can support cookies being sent with SameSite equal to "None". * Browsers without support may drop the cookie and or treat it as stricter setting @@ -66,13 +104,14 @@ public function canSetSameSiteNone(): bool return true; } + /** * Obtain a URL where we can redirect to securely post a form with the given data to a specific destination. * * @param string $destination The destination URL. * @param array $data An associative array containing the data to be posted to $destination. * - * @throws Error\Exception If the current session is transient. + * @throws \SimpleSAML\Error\Exception If the current session is transient. * @return string A URL which allows to securely post a form to $destination. * */ @@ -199,10 +238,7 @@ public function isValidURL(string $url): bool /** - * This function redirects the user to the specified address. - * - * This function will use the "HTTP 303 See Other" redirection if the current request used the POST method and the - * HTTP version is 1.1. Otherwise, a "HTTP 302 Found" redirection will be used. + * This function redirects the user to the specified address using the "HTTP 303 See Other" redirection. * * The function will also generate a simple web page with a clickable link to the target page. * @@ -265,7 +301,9 @@ private function redirect(string $url, array $parameters = []): void echo ''; // end script execution - exit; + if (!defined('SIMPLESAMLPHP_TEST_NOEXIT')) { + exit; + } } @@ -360,13 +398,13 @@ public function checkSessionCookie(?string $retryURL = null): void * Check if a URL is valid and is in our list of allowed URLs. * * @param string $url The URL to check. - * @param string[]|null $trustedSites An optional white list of domains. If none specified, the 'trusted.url.domains' + * @param string[]|null $trustedSites An optional whitelist of domains. If none specified, the 'trusted.url.domains' * configuration directive will be used. * * @return string The normalized URL itself if it is allowed. An empty string if the $url parameter is empty as * defined by the empty() function. * @throws \InvalidArgumentException If the URL is malformed. - * @throws Error\Exception If the URL is not allowed by configuration. + * @throws \SimpleSAML\Error\Exception If the URL is not allowed by configuration. * */ public function checkURLAllowed(string $url, ?array $trustedSites = null): string @@ -455,9 +493,9 @@ public function checkURLAllowed(string $url, ?array $trustedSites = null): strin * @return string|array An array if $getHeaders is set, containing the data and the headers respectively; string * otherwise. * @throws \InvalidArgumentException If the input parameters are invalid. - * @throws Error\Exception If the file or URL cannot be retrieved. - * + * @throws \SimpleSAML\Error\Exception If the file or URL cannot be retrieved. */ + #[\Deprecated('Use an HTTP client instead (see createHttpClient method)', '16-12-2025')] public function fetch(string $url, array $context = [], bool $getHeaders = false) { $config = Configuration::getInstance(); @@ -500,37 +538,23 @@ public function fetch(string $url, array $context = [], bool $getHeaders = false } } - $context = stream_context_create($context); - $data = @file_get_contents($url, false, $context); - if ($data === false) { - $error = error_get_last(); - throw new Error\Exception('Error fetching ' . var_export($url, true) . ':' . - (is_array($error) ? $error['message'] : 'no error available')); - } + $client = $this->createHttpClient($context); + $response = $client->request('GET', $url); - // data and headers - if ($getHeaders) { - if (!empty($http_response_header)) { - $headers = []; - foreach ($http_response_header as $h) { - if (preg_match('@^HTTP/1\.[01]\s+\d{3}\s+@', $h)) { - $headers = []; // reset - $headers[0] = $h; - continue; - } - $bits = explode(':', $h, 2); - if (count($bits) === 2) { - $headers[strtolower($bits[0])] = trim($bits[1]); - } - } - } else { - // no HTTP headers, probably a different protocol, e.g. file - $headers = null; + try { + $headers = $response->getHeaders(); + /** @var string $data */ + $data = $response->getContent(); + + // data and headers + if ($getHeaders) { + return [$data, $headers]; } - return [$data, $headers]; - } - return $data; + return $data; + } catch (Exception $e) { + throw new Error\Exception('Error fetching ' . var_export($url, true) . ':' . $e->getMessage()); + } } @@ -717,7 +741,6 @@ public function getPOSTRedirectURL(string $destination, array $data): string * E.g. www.example.com * * @return string The current host. - * */ public function getSelfHost(): string { @@ -734,13 +757,12 @@ public function getSelfHost(): string * * @return string The current host, followed by a colon and the port number, in case the port is not standard for * the protocol. - * */ public function getSelfHostWithNonStandardPort(): string { $url = $this->getBaseURL(); - /** @var int $colon getBaseURL() will always return a valid URL */ + /** @var int<0, max>|false $colon getBaseURL() will always return a valid URL */ $colon = strpos($url, '://'); $start = $colon + 3; $length = strcspn($url, '/', $start); @@ -754,7 +776,6 @@ public function getSelfHostWithNonStandardPort(): string * current SP, as defined in the global configuration. * * @return string The current host (with non-default ports included) plus the URL path. - * */ public function getSelfHostWithPath(): string { @@ -775,7 +796,6 @@ public function getSelfHostWithPath(): string * Note that this method does NOT make use of the HTTP X-Forwarded-* set of headers. * * @return string The current URL, including query parameters. - * */ public function getSelfURL(): string { @@ -784,12 +804,19 @@ public function getSelfURL(): string $cur_path = realpath($_SERVER['SCRIPT_FILENAME']); // make sure we got a string from realpath() $cur_path = is_string($cur_path) ? $cur_path : ''; + // find the path to the current script relative to the public/ directory of SimpleSAMLphp $rel_path = str_replace($baseDir . 'public' . DIRECTORY_SEPARATOR, '', $cur_path); - // convert that relative path to an HTTP query + $url_path = str_replace(DIRECTORY_SEPARATOR, '/', $rel_path); - // find where the relative path starts in the current request URI - $uri_pos = (!empty($url_path)) ? strpos($_SERVER['REQUEST_URI'] ?? '', $url_path) : false; + + $requestUri = (string)($_SERVER['REQUEST_URI'] ?? ''); + $requestPath = (string)parse_url($requestUri, PHP_URL_PATH); + $requestQuery = (string)parse_url($requestUri, PHP_URL_QUERY); + $requestFragment = (string)parse_url($requestUri, PHP_URL_FRAGMENT); + + // Match script-relative path only against the path part of the request + $uri_pos = (!empty($url_path)) ? strpos($requestPath, $url_path) : false; if ($cur_path == $rel_path || $uri_pos === false) { /* @@ -798,12 +825,13 @@ public function getSelfURL(): string * - $_SERVER['SCRIPT_FILENAME'] points to a script that doesn't exist. E.g. functional testing. In this * case, realpath() returns false and str_replace an empty string, so we compare them loosely. * - * - The URI requested does not belong to a script in the public/ directory of SimpleSAMLphp. In that case, - * removing SimpleSAMLphp's base dir from the current path yields the same path, so $cur_path and + * - The script is not located under the public/ directory of SimpleSAMLphp. In that case, removing + * SimpleSAMLphp's base dir and public/ from the current path yields the same path, so $cur_path and * $rel_path are equal. * - * - The request URI does not match the current script. Even if the current script is located in the - * public/ directory of SimpleSAMLphp, the URI does not contain its relative path, and $uri_pos is false. + * - The request path does not match the current script. Even if the current script is located in the + * public/ directory of SimpleSAMLphp, the request path (without query string) does not contain its + * relative path, and $uri_pos is false. * * It doesn't matter which one of those cases we have. We just know we can't apply our base URL to the * current URI, so we need to build it back from the PHP environment, unless we have a base URL specified @@ -813,20 +841,33 @@ public function getSelfURL(): string $appurl = ($appcfg !== null) ? $appcfg->getOptionalString('baseURL', null) : null; if (!empty($appurl)) { - $protocol = parse_url($appurl, PHP_URL_SCHEME); - $hostname = parse_url($appurl, PHP_URL_HOST); - $port = parse_url($appurl, PHP_URL_PORT); - $port = !empty($port) ? ':' . $port : ''; + $protocol = (string)parse_url($appurl, PHP_URL_SCHEME); + $hostname = (string)parse_url($appurl, PHP_URL_HOST); + $portNum = parse_url($appurl, PHP_URL_PORT); + $port = !empty($portNum) ? ':' . $portNum : ''; } else { // no base URL specified for app, just use the current URL $protocol = $this->getServerHTTPS() ? 'https' : 'http'; $hostname = $this->getServerHost(); $port = $this->getServerPort(); } + return $protocol . '://' . $hostname . $port . $_SERVER['REQUEST_URI']; } - return $this->getBaseURL() . $url_path . substr($_SERVER['REQUEST_URI'], $uri_pos + strlen($url_path)); + // Normal case: baseURL + script-relative path + remaining path, plus query if present + $suffix = substr($requestPath, $uri_pos + strlen($url_path)); + $url = $this->getBaseURL() . $url_path . $suffix; + + if ($requestQuery !== '') { + $url .= '?' . $requestQuery; + } + + if ($requestFragment !== '') { + $url .= '#' . $requestFragment; + } + + return $url; } @@ -835,13 +876,12 @@ public function getSelfURL(): string * optionally, the port number. * * @return string The current URL without path or query parameters. - * */ public function getSelfURLHost(): string { $url = $this->getSelfURL(); - /** @var int $colon getBaseURL() will always return a valid URL */ + /** @var int<0, max>|false $colon getBaseURL() will always return a valid URL */ $colon = strpos($url, '://'); $start = $colon + 3; $length = strcspn($url, '/', $start) + $start; @@ -853,7 +893,6 @@ public function getSelfURLHost(): string * Retrieve the current URL using the base URL in the configuration, without the query parameters. * * @return string The current URL, not including query parameters. - * */ public function getSelfURLNoQuery(): string { @@ -870,7 +909,6 @@ public function getSelfURLNoQuery(): string * This function checks if we are using HTTPS as protocol. * * @return boolean True if the HTTPS is used, false otherwise. - * */ public function isHTTPS(): bool { @@ -886,7 +924,6 @@ public function isHTTPS(): bool * * @return string An absolute URL for the given relative URL. * @throws \InvalidArgumentException If $url is not a string or a valid URL. - * */ public function normalizeURL(string $url): string { @@ -913,7 +950,6 @@ public function normalizeURL(string $url): string * * @return array The query string as an associative array. * @throws \InvalidArgumentException If $query_string is not a string. - * */ public function parseQueryString(string $query_string): array { @@ -954,7 +990,6 @@ public function parseQueryString(string $query_string): array * name, without a value. * * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. - * */ public function redirectTrustedURL(string $url, array $parameters = []): void { @@ -980,7 +1015,6 @@ public function redirectTrustedURL(string $url, array $parameters = []): void * name, without a value. * * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. - * */ public function redirectUntrustedURL(string $url, array $parameters = []): void { @@ -1006,7 +1040,6 @@ public function redirectUntrustedURL(string $url, array $parameters = []): void * @return string An absolute URL for the given relative URL. * @throws \InvalidArgumentException If the base URL cannot be parsed into a valid URL, or the given parameters * are not strings. - * */ public function resolveURL(string $url, ?string $base = null): string { @@ -1080,8 +1113,6 @@ public function resolveURL(string $url, ?string $base = null): string * * @throws \InvalidArgumentException If any parameter has an incorrect type. * @throws \SimpleSAML\Error\CannotSetCookie If the headers were already sent and the cookie cannot be set. - * - * */ public function setCookie(string $name, ?string $value, ?array $params = null, bool $throw = true): void { @@ -1110,7 +1141,9 @@ public function setCookie(string $name, ?string $value, ?array $params = null, b Error\CannotSetCookie::SECURE_COOKIE, ); } - Logger::warning('Error setting cookie: setting secure cookie on plain HTTP (except on localhost) is not allowed.'); + Logger::warning( + 'Error setting cookie: setting secure cookie on plain HTTP (except on localhost) is not allowed.', + ); return; } @@ -1188,8 +1221,6 @@ public function isSecureCookieAllowed(): bool * * @throws \InvalidArgumentException If $destination is not a string or $data is not an array. * @throws \SimpleSAML\Error\Exception If $destination is not a valid HTTP URL. - * - * */ public function submitPOSTData(string $destination, array $data): void { @@ -1203,12 +1234,23 @@ public function submitPOSTData(string $destination, array $data): void if ($allowed && preg_match("#^http:#", $destination) && $this->isHTTPS()) { // we need to post the data to HTTP $this->redirect($this->getSecurePOSTRedirectURL($destination, $data)); + return; } $p = new Template($config, 'post.twig'); $p->data['destination'] = $destination; $p->data['post'] = $data; + + // Read optional config override; default to 30s, ensure non-negative integer + $delay = $config->getOptionalInteger('slow_post_delay_ms', 30000); + if ($delay < 0) { + $delay = 30000; + } + $p->data['slow_post_delay_ms'] = $delay; + $p->send(); - exit(0); + if (!defined('SIMPLESAMLPHP_TEST_NOEXIT')) { + exit(0); + } } } diff --git a/src/SimpleSAML/Utils/Net.php b/src/SimpleSAML/Utils/Net.php index a656425f6e..ac196856ea 100644 --- a/src/SimpleSAML/Utils/Net.php +++ b/src/SimpleSAML/Utils/Net.php @@ -4,7 +4,8 @@ namespace SimpleSAML\Utils; -use Symfony\Component\HttpFoundation\{IpUtils, Request}; +use Symfony\Component\HttpFoundation\IpUtils; +use Symfony\Component\HttpFoundation\Request; /** * Net-related utility methods. @@ -18,11 +19,10 @@ class Net * Check whether an IP address is part of a CIDR. * * @param string|array $cidr The network CIDR address. - * @param string|null $ip The IP address to check. Optional. Current remote address will be used if none specified. Do - * not rely on default parameter if running behind load balancers. + * @param string|null $ip The optional IP address to check. Current remote address will be used if none specified. + * Do not rely on the default parameter if running behind load balancers. * * @return boolean True if the IP address belongs to the specified CIDR, false otherwise. - * */ public function ipCIDRcheck(string|array $cidr, ?string $ip = null): bool { diff --git a/src/SimpleSAML/Utils/Random.php b/src/SimpleSAML/Utils/Random.php index f0b303c1a4..c9a2a515ba 100644 --- a/src/SimpleSAML/Utils/Random.php +++ b/src/SimpleSAML/Utils/Random.php @@ -14,13 +14,13 @@ class Random /** * The fixed length of random identifiers. */ - public const ID_LENGTH = 43; + public const int ID_LENGTH = 43; + /** * Generate a random identifier, ID_LENGTH bytes long. * * @return string A ID_LENGTH-bytes long string with a random, hex-encoded string. - * */ public function generateID(): string { diff --git a/src/SimpleSAML/Utils/System.php b/src/SimpleSAML/Utils/System.php index 6a3300910b..e41d301b34 100644 --- a/src/SimpleSAML/Utils/System.php +++ b/src/SimpleSAML/Utils/System.php @@ -12,17 +12,23 @@ * * @package SimpleSAMLphp */ - class System { - public const WINDOWS = 1; - public const LINUX = 2; - public const OSX = 3; - public const HPUX = 4; - public const UNIX = 5; - public const BSD = 6; - public const IRIX = 7; - public const SUNOS = 8; + public const int WINDOWS = 1; + + public const int LINUX = 2; + + public const int OSX = 3; + + public const int HPUX = 4; + + public const int UNIX = 5; + + public const int BSD = 6; + + public const int IRIX = 7; + + public const int SUNOS = 8; /** @@ -30,7 +36,6 @@ class System * * @return int|false A predefined constant identifying the OS we are running on. * False if we are unable to determine it. - * */ public function getOS(): int|false { @@ -66,9 +71,9 @@ public function getOS(): int|false * This function retrieves the path to a directory where temporary files can be saved. * * @return string Path to a temporary directory, without a trailing directory separator. - * @throws Error\Exception If the temporary directory cannot be created or it exists and cannot be written + * @throws \SimpleSAML\Error\Exception + * If the temporary directory cannot be created or it exists and cannot be written * to by the current user. - * */ public function getTempDir(): string { @@ -121,7 +126,6 @@ public function getTempDir(): string * SimpleSAMLphp installation. * * @return string An absolute path referring to $path. - * */ public function resolvePath(string $path, ?string $base = null): string { @@ -183,9 +187,8 @@ public function resolvePath(string $path, ?string $base = null): string * * * @throws \InvalidArgumentException If any of the input parameters doesn't have the proper types. - * @throws Error\Exception If the file cannot be saved, permissions cannot be changed or it is not + * @throws \SimpleSAML\Error\Exception If the file cannot be saved, permissions cannot be changed or it is not * possible to write to the target file. - * */ public function writeFile(string $filename, string $data, int $mode = 0600): void { @@ -233,7 +236,6 @@ public function writeFile(string $filename, string $data, int $mode = 0600): voi * Check if the supplied path is an absolute path. * * @param string $path - * * @return bool */ public function isAbsolutePath(string $path): bool @@ -246,7 +248,6 @@ public function isAbsolutePath(string $path): bool * Check if the supplied path contains a Windows-style drive letter. * * @param string $path - * * @return bool */ private function pathContainsDriveLetter(string $path): bool @@ -256,8 +257,10 @@ private function pathContainsDriveLetter(string $path): bool && $letterAsciiValue >= 65 && $letterAsciiValue <= 90; } + /** * Check if the supplied path contains a stream wrapper + * * @param string $path * @return bool */ diff --git a/src/SimpleSAML/Utils/Translate.php b/src/SimpleSAML/Utils/Translate.php index d53aed7a64..229aa6f9cf 100644 --- a/src/SimpleSAML/Utils/Translate.php +++ b/src/SimpleSAML/Utils/Translate.php @@ -28,6 +28,9 @@ class Translate protected string $baseDir; + /** + * @param \SimpleSAML\Configuration $configuration + */ public function __construct( protected Configuration $configuration, ) { @@ -35,6 +38,11 @@ public function __construct( } + /** + * @param string $module + * @param \Gettext\Scanner\PhpScanner $phpScanner + * @return \Gettext\Scanner\PhpScanner + */ public function getTranslationsFromPhp(string $module, PhpScanner $phpScanner): PhpScanner { $moduleDir = $this->baseDir . ($module === '' ? '' : 'modules/' . $module . '/'); @@ -54,6 +62,10 @@ public function getTranslationsFromPhp(string $module, PhpScanner $phpScanner): } + /** + * @param string $module + * @param bool $includeThemes + */ public function getTranslationsFromTwig(string $module, bool $includeThemes = false): array { $twigTranslations = []; diff --git a/src/SimpleSAML/Utils/XML.php b/src/SimpleSAML/Utils/XML.php index 20f6ebb9ec..3646cc462f 100644 --- a/src/SimpleSAML/Utils/XML.php +++ b/src/SimpleSAML/Utils/XML.php @@ -17,12 +17,13 @@ use DOMNode; use DOMText; use Exception; -use SAML2\Constants as C; -use SAML2\DOMDocumentFactory; +use InvalidArgumentException; use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\Logger; +use SimpleSAML\SAML2\Constants as C; +use SimpleSAML\XML\DOMDocumentFactory; use SimpleSAML\XML\Errors; class XML @@ -39,14 +40,12 @@ class XML * @throws \InvalidArgumentException If $message is not a string or $type is not a string containing one of the * values allowed. * @throws \SimpleSAML\Error\Exception If $message contains a doctype declaration. - * - * */ public function checkSAMLMessage(string $message, string $type): void { $allowed_types = ['saml20', 'saml-meta']; if (!in_array($type, $allowed_types, true)) { - throw new \InvalidArgumentException('Invalid input parameters.'); + throw new InvalidArgumentException('Invalid input parameters.'); } // a SAML message should not contain a doctype-declaration @@ -91,8 +90,6 @@ public function checkSAMLMessage(string $message, string $type): void * - 'encrypt': for encrypted messages. * * @throws \InvalidArgumentException If $type is not a string or $message is neither a string nor a \DOMElement. - * - * */ public function debugSAMLMessage(string|DOMElement $message, string $type): void { @@ -146,8 +143,6 @@ public function debugSAMLMessage(string|DOMElement $message, string $type): void * string. * * @throws \InvalidArgumentException If $root is not a DOMElement or $indentBase is not a string. - * - * */ public function formatDOMElement(DOMNode $root, string $indentBase = ''): void { @@ -232,7 +227,6 @@ public function formatDOMElement(DOMNode $root, string $indentBase = ''): void * @return string The formatted string. * @throws \InvalidArgumentException If the parameters are not strings. * @throws \DOMException If the input does not parse correctly as an XML string. - * */ public function formatXMLString(string $xml, string $indentBase = ''): string { @@ -266,7 +260,6 @@ public function formatXMLString(string $xml, string $indentBase = ''): string * * @return boolean True if both namespace and local name matches, false otherwise. * @throws \InvalidArgumentException If the namespace shortcut is unknown. - * */ public function isDOMNodeOfType(DOMNode $element, string $name, string $nsURI): bool { @@ -279,7 +272,7 @@ public function isDOMNodeOfType(DOMNode $element, string $name, string $nsURI): if ($nsURI[0] === '@') { // the defined shortcuts $shortcuts = [ - '@ds' => 'http://www.w3.org/2000/09/xmldsig#', + '@ds' => C::NS_XDSIG, '@md' => C::NS_MD, '@saml2' => C::NS_SAML, '@saml2p' => C::NS_SAMLP, @@ -287,7 +280,7 @@ public function isDOMNodeOfType(DOMNode $element, string $name, string $nsURI): // check if it is a valid shortcut if (!array_key_exists($nsURI, $shortcuts)) { - throw new \InvalidArgumentException('Unknown namespace shortcut: ' . $nsURI); + throw new InvalidArgumentException('Unknown namespace shortcut: ' . $nsURI); } // expand the shortcut @@ -315,9 +308,8 @@ public function isDOMNodeOfType(DOMNode $element, string $name, string $nsURI): * * @return bool|string Returns a string with errors found if validation fails. True if validation passes ok. * @throws \InvalidArgumentException If $schema is not a string, or $xml is neither a string nor a \DOMDocument. - * */ - public function isValid(string|DOMDocument $xml, string $schema) + public function isValid(string|DOMDocument $xml, string $schema): bool|string { Errors::begin(); @@ -336,7 +328,9 @@ public function isValid(string|DOMDocument $xml, string $schema) if ($res === true) { $config = Configuration::getInstance(); $sysUtils = new System(); - $schemaFile = $sysUtils->resolvePath($config->getVendorDir() . 'simplesamlphp/saml2/resources/schemas/' . $schema); + $schemaFile = $sysUtils->resolvePath( + $config->getVendorDir() . 'simplesamlphp/saml2/resources/schemas/' . $schema, + ); libxml_set_external_entity_loader( /** @@ -345,7 +339,12 @@ public function isValid(string|DOMDocument $xml, string $schema) * @param array $context * @return string|null */ - function (?string $public = null, string $system = '', /** @scrutinizer ignore-unused */ array $context = []) { + function ( + ?string $public = null, + string $system = '', + /** @scrutinizer ignore-unused */ + array $context = [], + ) { if (filter_var($system, FILTER_VALIDATE_URL) === $system) { return null; } @@ -353,7 +352,7 @@ function (?string $public = null, string $system = '', /** @scrutinizer ignore-u }, ); - /** @psalm-suppress PossiblyUndefinedVariable */ + /** @phpstan-ignore variable.undefined */ $res = $dom->schemaValidate($schemaFile); if ($res) { Errors::end(); diff --git a/src/SimpleSAML/XHTML/IdPDisco.php b/src/SimpleSAML/XHTML/IdPDisco.php index 8aa0c5c0c2..7e82f007ae 100644 --- a/src/SimpleSAML/XHTML/IdPDisco.php +++ b/src/SimpleSAML/XHTML/IdPDisco.php @@ -33,7 +33,6 @@ * * @package SimpleSAMLphp */ - class IdPDisco { /** @@ -505,7 +504,6 @@ protected function filterList(array $list): array /** * Check if an IdP is set or if the request is passive, and redirect accordingly. - * */ protected function start(): void { @@ -561,10 +559,11 @@ public function handleRequest(): void if (sizeof($idpList) === 1) { $selectedIdP = array_key_first($idpList); - $this->log( - 'One candidate IdP, not showing discovery [' . $selectedIdP . '] (Redirecting the user back. returnIDParam=' . - $this->returnIdParam . ')', - ); + $this->log(sprintf( + 'One candidate IdP, not showing discovery [%s] (Redirecting the user back. returnIDParam=%s', + $selectedIdP, + $this->returnIdParam, + )); $httpUtils->redirectTrustedURL( $this->returnURL, [$this->returnIdParam => $selectedIdP], diff --git a/src/SimpleSAML/XHTML/Template.php b/src/SimpleSAML/XHTML/Template.php index 0b609a6a07..9bc6039a17 100644 --- a/src/SimpleSAML/XHTML/Template.php +++ b/src/SimpleSAML/XHTML/Template.php @@ -126,7 +126,7 @@ class Template extends Response * Constructor * * @param \SimpleSAML\Configuration $configuration Configuration object - * @param string $template Which template file to load + * @param string $template Which template file to load */ public function __construct( private Configuration $configuration, @@ -154,7 +154,6 @@ public function __construct( class_exists($controller) && in_array(TemplateControllerInterface::class, class_implements($controller)) ) { - /** @var \SimpleSAML\XHTML\TemplateControllerInterface $this->controller */ $this->controller = new $controller(); } else { throw new Error\ConfigurationError( @@ -251,7 +250,7 @@ private function normalizeTemplateName(string $templateName): string /** * Set up the places where twig can look for templates. * - * @return TemplateLoader The twig template loader or false if the template does not exist. + * @return \SimpleSAML\XHTML\TemplateLoader The twig template loader or false if the template does not exist. * @throws \Twig\Error\LoaderError In case a failure occurs. */ private function setupTwigTemplatepaths(): TemplateLoader @@ -536,6 +535,7 @@ private function twigDefaultContext(): void $this->data['header'] = $this->configuration->getOptionalString('theme.header', 'SimpleSAMLphp'); } + /** * Helper function for locale extraction: just compile but not display * this template. This is not generally useful, getContents() will normally @@ -546,6 +546,7 @@ public function compile(): void $this->twig->load($this->twig_template); } + /** * Get the contents produced by this template. * @@ -574,10 +575,10 @@ public function getContents(): string * @return $this This response. * @throws \Exception if the template cannot be found. */ - public function send(): static + public function send(bool $flush = true): static { $this->content = $this->getContents(); - return parent::send(); + return parent::send($flush); } @@ -649,6 +650,7 @@ private function isLanguageRTL(): bool return $this->translator->getLanguage()->isLanguageRTL(); } + /** * Search through entity metadata to find the best display name for this * entity. It will search in order for the current language, default @@ -674,6 +676,7 @@ public function getEntityDisplayName(array $data): string return $data['entityid']; } + /** * Search through entity metadata to find the best value for a * specific property. It will search in order for the current language, default diff --git a/src/SimpleSAML/XHTML/TemplateControllerInterface.php b/src/SimpleSAML/XHTML/TemplateControllerInterface.php index dbda8a6923..2d4161ef3b 100644 --- a/src/SimpleSAML/XHTML/TemplateControllerInterface.php +++ b/src/SimpleSAML/XHTML/TemplateControllerInterface.php @@ -17,7 +17,6 @@ interface TemplateControllerInterface * Implement to modify the twig environment after its initialization (e.g. add filters or extensions). * * @param \Twig\Environment $twig The current twig environment. - * */ public function setUpTwig(Environment &$twig): void; @@ -28,7 +27,6 @@ public function setUpTwig(Environment &$twig): void; * This method will be called right before displaying the template. * * @param array $data The current data used by the template. - * */ public function display(array &$data): void; } diff --git a/src/SimpleSAML/XHTML/TemplateLoader.php b/src/SimpleSAML/XHTML/TemplateLoader.php index 86a6170b78..df1298c4e9 100644 --- a/src/SimpleSAML/XHTML/TemplateLoader.php +++ b/src/SimpleSAML/XHTML/TemplateLoader.php @@ -11,8 +11,6 @@ * when the main template is not part of a module (or the same one). * * @package simplesamlphp/simplesamlphp - * - * @psalm-suppress DeprecatedInterface This suppress may be removed when Twig 3.0 becomes the default */ class TemplateLoader extends \Twig\Loader\FilesystemLoader { diff --git a/src/SimpleSAML/XML/Parser.php b/src/SimpleSAML/XML/Parser.php index 6a02f27ac3..164818816e 100644 --- a/src/SimpleSAML/XML/Parser.php +++ b/src/SimpleSAML/XML/Parser.php @@ -17,6 +17,7 @@ class Parser /** @var \SimpleXMLElement */ public SimpleXMLElement $simplexml; + /** * @param string $xml */ diff --git a/src/SimpleSAML/XML/Signer.php b/src/SimpleSAML/XML/Signer.php index 3e1dfe9d49..7c5c3e7ea8 100644 --- a/src/SimpleSAML/XML/Signer.php +++ b/src/SimpleSAML/XML/Signer.php @@ -35,7 +35,7 @@ class Signer /** * @var \RobRichards\XMLSecLibs\XMLSecurityKey|false The private key (as an XMLSecurityKey). */ - private $privateKey = false; + private XMLSecurityKey|false $privateKey = false; /** * @var string The certificate (as text). diff --git a/templates/_table.twig b/templates/_table.twig index df05e0d43e..c963b9ce6f 100644 --- a/templates/_table.twig +++ b/templates/_table.twig @@ -1,23 +1,21 @@ - {% for name, values in items %} - {% block namecol -%} + {%~ block namecol -%} {%- endblock %} + + {%- if loop.length>1 %}{% endif -%} + {%- if loop.length>1 and loop.last %}{% endif %} + {%- endfor -%} + {% endfor %}
{{ name }} + {%- for value in values -%} + {% if loop.length>1 and loop.first %}
    {% endif -%} + {%- if loop.length>1 %}
  • {% endif -%} -
- {% for value in values %} - {% if loop.length>1 and loop.first %}
    {% endif -%} - {%- if loop.length>1 %}
  • {% endif -%} - - {%- block value %}{% endblock -%} + {%- block value %}{% endblock -%} - {%- if loop.length>1 %}
  • {% endif -%} - {%- if loop.length>1 and loop.last %}
{% endif %} - {% endfor %} -

diff --git a/templates/auth_status.twig b/templates/auth_status.twig index 731ef1f87c..20353f4637 100644 --- a/templates/auth_status.twig +++ b/templates/auth_status.twig @@ -18,9 +18,9 @@ {% embed '_table.twig' -%} - {% block namecol -%} + {% block namecol %} {% set translated = name|trans %} - {% if translated != name %} {{ translated }}
{% endif %} {{ name }} + {% if translated != name %}{{ translated }}
{% endif %}{{ name }} {% endblock %} @@ -28,7 +28,7 @@ {% if name =='jpegPhoto'-%} {% else %}{{ value }}{% endif -%} - {% endblock %} + {% endblock -%} {%- endembed %} diff --git a/templates/post.twig b/templates/post.twig index 5072e8ddab..ca03d49372 100644 --- a/templates/post.twig +++ b/templates/post.twig @@ -1,40 +1,84 @@ - - + + + {% trans %}Sending message{% endtrans %} - - - + {% block preload %}{% endblock %} + + +{% block content %} +

{% trans %}Sending message{% endtrans %}

- {#- We need to add this element and call the click method, because calling submit() on the form causes failed + {#- We need to add this element and call the click method, because calling submit() on the form causes failed submissions if the form has another element with name or id of submit. See: https://developer.mozilla.org/en/DOM/form.submit#Specification - #} + #} - - {%- for name, value in post %} - {%- if value is iterable %} - {%- for index, item in value %} + + {%- for name, value in post %} + {%- if value is iterable %} + {%- for index, item in value %} - - {%- endfor %} - {%- else %} + + {%- endfor %} + {%- else %} - - {%- endif %} - {%- endfor %} + + {%- endif %} + {%- endfor %} - +
- + + + +{% endblock %} + + +{% block postload %}{% endblock %} + diff --git a/tests/SigningTestCase.php b/tests/SigningTestCase.php index 1dbb858839..3d1b8fce77 100644 --- a/tests/SigningTestCase.php +++ b/tests/SigningTestCase.php @@ -167,12 +167,18 @@ class SigningTestCase extends TestCase /** @var \SimpleSAML\Configuration */ protected Configuration $config; - protected const ROOTDIRNAME = 'testdir'; - protected const DEFAULTCERTDIR = 'certdir'; - protected const CA_PRIVATE_KEY = 'ca.key.pem'; - protected const CA_CERTIFICATE = 'ca.cert.pem'; - protected const GOOD_PRIVATE_KEY = 'good.key.pem'; - protected const GOOD_CERTIFICATE = 'good.cert.pem'; + + protected const string ROOTDIRNAME = 'testdir'; + + protected const string DEFAULTCERTDIR = 'certdir'; + + protected const string CA_PRIVATE_KEY = 'ca.key.pem'; + + protected const string CA_CERTIFICATE = 'ca.cert.pem'; + + protected const string GOOD_PRIVATE_KEY = 'good.key.pem'; + + protected const string GOOD_CERTIFICATE = 'good.cert.pem'; /** diff --git a/tests/Utils/SpTester.php b/tests/Utils/SpTester.php index 8a0ea18b3f..11892b1751 100644 --- a/tests/Utils/SpTester.php +++ b/tests/Utils/SpTester.php @@ -42,7 +42,7 @@ public function startSSO2Test(Configuration $idpMetadata, array $state): void /** * override the method that sends the request to avoid sending anything */ - public function sendSAML2AuthnRequest(Binding $binding, AuthnRequest $ar): void + public function sendSAML2AuthnRequest(Binding $binding, AuthnRequest $ar): never { // Exit test. Continuing would mean running into a assert(FALSE) throw new ExitTestException( @@ -57,7 +57,7 @@ public function sendSAML2AuthnRequest(Binding $binding, AuthnRequest $ar): void /** * override the method that sends the request to avoid sending anything */ - public function sendSAML2LogoutRequest(Binding $binding, LogoutRequest $lr): void + public function sendSAML2LogoutRequest(Binding $binding, LogoutRequest $lr): never { // Exit test. Continuing would mean running into a assert(FALSE) throw new ExitTestException( diff --git a/tests/modules/admin/src/Controller/ConfigTest.php b/tests/modules/admin/src/Controller/ConfigTest.php index 384e30633a..80053bc606 100644 --- a/tests/modules/admin/src/Controller/ConfigTest.php +++ b/tests/modules/admin/src/Controller/ConfigTest.php @@ -5,6 +5,7 @@ namespace SimpleSAML\Test\Module\admin\Controller; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use SimpleSAML\Configuration; use SimpleSAML\Module\admin\Controller; @@ -26,8 +27,8 @@ class ConfigTest extends TestCase /** @var \SimpleSAML\Utils\Auth */ protected Utils\Auth $authUtils; - /** @var \SimpleSAML\Session */ - protected Session $session; + /** @var \SimpleSAML\Session&\PHPUnit\Framework\MockObject\MockObject */ + protected Session&MockObject $session; /** @@ -69,7 +70,6 @@ public function requireAdmin(): void $session = $this->createMock(Session::class); $session->method('getData')->willReturn(['tag_name' => 'v1.18.7', 'html_url' => 'https://example.org']); - /** @var \SimpleSAML\Session $session */ $this->session = $session; } diff --git a/tests/modules/admin/src/Controller/FederationTest.php b/tests/modules/admin/src/Controller/FederationTest.php index f7229039f9..163e19dc85 100644 --- a/tests/modules/admin/src/Controller/FederationTest.php +++ b/tests/modules/admin/src/Controller/FederationTest.php @@ -23,20 +23,16 @@ #[CoversClass(Controller\Federation::class)] class FederationTest extends TestCase { - /** @var string */ - private const SECURITY = 'vendor/simplesamlphp/xml-security/resources'; + private const string SECURITY = 'vendor/simplesamlphp/xml-security/resources'; - /** @var string */ - private const FRAMEWORK = 'vendor/simplesamlphp/simplesamlphp-test-framework'; + private const string FRAMEWORK = 'vendor/simplesamlphp/simplesamlphp-test-framework'; - /** @var string */ - private const LIBRARY = 'vendor/simplesamlphp/saml2/tests/resources'; + private const string LIBRARY = 'vendor/simplesamlphp/saml2/tests/resources'; - /** @var string */ - public const CERT_KEY = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.key'; + public const string CERT_KEY = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.key'; + + public const string CERT_PUBLIC = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.crt'; - /** @var string */ - public const CERT_PUBLIC = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.crt'; /** @var \SimpleSAML\Configuration */ protected Configuration $config; @@ -56,6 +52,7 @@ class FederationTest extends TestCase /** @var string */ private string $ssp_metadata = self::FRAMEWORK . '/metadata/simplesamlphp/saml20-idp-remote_cert_selfsigned.php'; + /** * Set up for each test. */ @@ -110,6 +107,7 @@ public function __construct() { } + public function getList(string $set = 'saml20-idp-remote', bool $showExpired = false): array { if ($set === 'saml20-idp-hosted') { @@ -144,11 +142,13 @@ public function __construct() // stub } + public function authenticate(array &$state): void { // stub } + public static function getSourcesOfType(string $type): array { return [ @@ -227,6 +227,7 @@ public function testMetadataConverterData(): void $this->assertNull($response->data['error']); } + /** */ public function testMetadataConverterSkipsExpires(): void @@ -308,11 +309,13 @@ public function __construct() // stub } + public function authenticate(array &$state): void { // stub } + public function getMetadata(): Configuration { return Configuration::loadFromArray( @@ -322,6 +325,7 @@ public function getMetadata(): Configuration ); } + public static function getById(string $authId, ?string $type = null): ?Auth\Source { return new static(); @@ -356,6 +360,7 @@ public function __construct() { } + public function getMetaDataConfig(string $entityId, string $set): Configuration { return Configuration::loadFromArray([ @@ -392,6 +397,7 @@ public function __construct() { } + public function getMetaData(?string $entityId, string $set): array { return []; diff --git a/tests/modules/admin/src/Controller/TestTest.php b/tests/modules/admin/src/Controller/TestTest.php index b224229bfe..9c153c4ef0 100644 --- a/tests/modules/admin/src/Controller/TestTest.php +++ b/tests/modules/admin/src/Controller/TestTest.php @@ -181,6 +181,7 @@ public function isAuthenticated(): bool return false; } + public function login(array $params = []): void { // stub @@ -212,6 +213,7 @@ public function isAuthenticated(): bool return true; } + public function getAttributes(): array { $nameId = new NameID(); @@ -251,11 +253,13 @@ public function getAttributes(): array ]; } + public function getAuthDataArray(): ?array { return []; } + public function getAuthData(string $name): mixed { $nameId = new NameID(); diff --git a/tests/modules/core/src/Auth/Process/AttributeConditionalAddTest.php b/tests/modules/core/src/Auth/Process/AttributeConditionalAddTest.php new file mode 100644 index 0000000000..a172113716 --- /dev/null +++ b/tests/modules/core/src/Auth/Process/AttributeConditionalAddTest.php @@ -0,0 +1,1739 @@ +process($request); + return $request; + } + + + /** + * Test the most basic functionality - no conditions at all (unconditional add) + */ + public function testUnconditionalAdd(): void + { + $config = [ + 'attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $request = [ + 'Attributes' => [ + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + } + + + /** + * Test single string as attribute value. + */ + public function testStringValue(): void + { + $config = [ + 'attributes' => [ + 'test' => 'value', + ], + ]; + $request = [ + 'Attributes' => [], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('test', $attributes); + $this->assertEquals($attributes['test'], ['value']); + } + + + /** + * Test no conditions at all on existing attributes (unconditional append) + */ + public function testUnconditionalAppend(): void + { + $config = [ + 'attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['admins', 'users']); + } + + + /** + * Test appending an existing value creates duplicates without %nodupe (duplicates allowed) + */ + public function testUnconditionalAppendExistingElement(): void + { + $config = [ + 'attributes' => [ + 'memberOf' => ['users'], + 'anotherAttribute' => ['value1'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'users']); + $this->assertArrayHasKey('anotherAttribute', $attributes); + $this->assertEquals($attributes['anotherAttribute'], ['value1']); + } + + + /** + * Test appending an existing value suppresses duplicates with %nodupe enabled + */ + public function testUnconditionalNoDupeAppendExistingElement(): void + { + $config = [ + '%nodupe', + 'attributes' => [ + 'memberOf' => ['users'], + 'anotherAttribute' => ['value1'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('anotherAttribute', $attributes); + $this->assertEquals($attributes['anotherAttribute'], ['value1']); + } + + + /** + * Test no conditions at all on existing attributes (unconditional replace) + */ + public function testUnconditionalReplace(): void + { + $config = [ + '%replace', + 'attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + } + + + /** + * Test no attributes specified (should error) + */ + public function testNoAttributesSpecified(): void + { + $config = [ + '%replace', + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * Test attributes in configuration not being an associative array (should error) + */ + public function testAttibuteArrayNotAssociative(): void + { + $config = [ + '%replace', + 'attributes' => [ + 'users', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * Test attributes value not being a string or array of strings (should error) + */ + public function testAttibuteArrayValueTypeIncorrect(): void + { + $config = [ + '%replace', + 'attributes' => [ + 'memberOf' => 1, + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * Test attributes value not being a string or array of strings (should error) + */ + public function testAttibuteArrayValueTypeIncorrect2(): void + { + $config = [ + '%replace', + 'attributes' => [ + 'memberOf' => ['test', 1], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * Test AttrExistsAny single attribute add + */ + public function testAttrExistsAnySingleAdd(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAny' => ['memberOf'], + ], + 'attributes' => [ + 'newAttribute' => 'testValue', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('newAttribute', $attributes); + $this->assertEquals($attributes['newAttribute'], ['testValue']); + } + + + /** + * Test AttrExistsAny single attribute append + */ + public function testAttrExistsAnySingleAppend(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAny' => ['memberOf'], + ], + 'attributes' => [ + 'memberOf' => 'admins', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + } + + + /** + * Test AttrExistsAny single attribute type error handling + */ + public function testAttrExistsAnySingleAddWrongType(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAny' => ['memberOf'], + ], + 'attributes' => [ + 'newAttribute' => 1, + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * Test AttrExistsAny where test attribute does not exist + */ + public function testAttrExistsAnyAttributeDoesNotExist(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAny' => ['nonExistentAttribute'], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + } + + + /** + * Test AttrExistsAny where one of the test attributes exists + */ + public function testAttrExistsAnyOneOfManyAttributesExists(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAny' => ['nonExistentAttribute', 'memberOf'], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + } + + + /** + * Test attrExistsAll single attribute add + */ + public function testAttrExistsAllSingleAdd(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAll' => ['memberOf'], + ], + 'attributes' => [ + 'newAttribute' => 'testValue', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('newAttribute', $attributes); + $this->assertEquals($attributes['newAttribute'], ['testValue']); + } + + + /** + * Test attrExistsAll single attribute append + */ + public function testAttrExistsAllSingleAppend(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAll' => ['memberOf'], + ], + 'attributes' => [ + 'memberOf' => 'admins', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + } + + + /** + * Test attrExistsAll single attribute type error handling + */ + public function testAttrExistsAllSingleAddWrongType(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAll' => ['memberOf'], + ], + 'attributes' => [ + 'newAttribute' => 1, + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * Test attrExistsAll where test attribute does not exist + */ + public function testAttrExistsAllAttributeDoesNotExist(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAll' => ['nonExistentAttribute'], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + } + + + /** + * Test attrExistsAll where one of the test attributes exists + */ + public function testAttrExistsAllOneOfManyAttributesExists(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAll' => ['nonExistentAttribute', 'memberOf'], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + } + + + /** + * Test attrExistsAll where one of the test attributes exists + */ + public function testAttrExistsAllAttributesExists(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAll' => ['email', 'memberOf'], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com']); + } + + /////// + /** + * Test attrExistsRegexAny single attribute add + */ + public function testAttrExistsRegexAnySingleAdd(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAny' => ['/^member/'], + ], + 'attributes' => [ + 'newAttribute' => 'testValue', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('newAttribute', $attributes); + $this->assertEquals($attributes['newAttribute'], ['testValue']); + } + + + /** + * Test attrExistsRegexAny single attribute append + */ + public function testAttrExistsRegexAnySingleAppend(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAny' => ['/^member/'], + ], + 'attributes' => [ + 'memberOf' => 'admins', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + } + + + /** + * Test attrExistsRegexAny single attribute type error handling + */ + public function testAttrExistsRegexAnySingleAddWrongType(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAny' => ['/^member/'], + ], + 'attributes' => [ + 'newAttribute' => 1, + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * Test attrExistsRegexAny where test attribute does not exist + */ + public function testAttrExistsRegexAnyAttributeDoesNotExist(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAny' => ['/^nonExistentAttribute/'], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + } + + + /** + * Test attrExistsRegexAny where one of the test attributes exists + */ + public function testAttrExistsRegexAnyOneOfManyAttributesExists(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAny' => ['/^nonExistentAttribute/', '/^member/'], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + } + + + /** + * Test attrExistsRegexAll single attribute add + */ + public function testAttrExistsRegexAllSingleAdd(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAll' => ['/^member/'], + ], + 'attributes' => [ + 'newAttribute' => 'testValue', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('newAttribute', $attributes); + $this->assertEquals($attributes['newAttribute'], ['testValue']); + } + + + /** + * Test attrExistsRegexAll single attribute append + */ + public function testAttrExistsRegexAllSingleAppend(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAll' => ['/^member/'], + ], + 'attributes' => [ + 'memberOf' => 'admins', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + } + + + /** + * Test attrExistsRegexAll single attribute type error handling + */ + public function testAttrExistsRegexAllSingleAddWrongType(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAll' => ['/^member/'], + ], + 'attributes' => [ + 'newAttribute' => 1, + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * Test attrExistsRegexAll where test attribute does not exist + */ + public function testAttrExistsRegexAllAttributeDoesNotExist(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAll' => ['/^nonExistentAttribute/'], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + } + + + /** + * Test attrExistsRegexAll where one of the test attributes exists + */ + public function testAttrExistsRegexAllOneOfManyAttributesExists(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAll' => ['/^nonExistentAttribute/', '/^member/'], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + } + + + /** + * Test attrExistsRegexAll where one of the test attributes exists + */ + public function testAttrExistsRegexAllAttributesExists(): void + { + $config = [ + 'conditions' => [ + 'attrExistsRegexAll' => ['/^email/', '/^member/'], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com']); + } + + + /** + * attrValueIsAny test cases where at least one attribute value exists + */ + public function testAttrValueIsAnyAttributesExists(): void + { + // All of these test cases should pass + $tests = [ + [ + // Single value in array + 'email' => ['bob@example.com'], + ], + [ + // Multiple values, one passes + 'email' => ['bob@example.com', 'alice@example.com'], + ], + [ + // memberOf passes + 'email' => ['alice@example.com'], + 'memberOf' => ['users'], + ], + [ + // one of the emails passes + 'email' => ['alice@example.com', 'bob@example.com'], + 'memberOf' => ['nogroup'], + ], + ]; + + foreach ($tests as $test) { + $config = [ + 'conditions' => [ + 'attrValueIsAny' => $test, + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com']); + } + } + + + /** + * attrValueIsAny multiple values, all exist + */ + public function testAttrValueIsAnyMultipleAttributesExists(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsAny' => [ + 'email' => ['alice@example.com', 'bob@example.com'], + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com', 'alice@example.com']); + } + + + /** + * attrValueIsAny test cases where no attribute values exist + */ + public function testAttrValueIsAnyNoAttributesExist(): void + { + // All of these test cases should fail + $tests = [ + [ + 'email' => ['trudy@example.com'], + ], + [ + 'email' => ['trudy@example.com', 'dennis@example.com'], + ], + [ + 'email' => ['trudy@example.com'], + 'memberOf' => ['nogroup'], + ], + [ + 'email' => ['trudy@example.com', 'dennis@example.com'], + 'memberOf' => ['nogroup'], + ], + ]; + + foreach ($tests as $test) { + $config = [ + 'conditions' => [ + 'attrValueIsAny' => $test, + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com', 'alice@example.com']); + $this->assertArrayNotHasKey('admins', $attributes['memberOf']); + } + } + + + /** + * attrValueIsAny value is a plain string, not an array + */ + public function testAttrValueIsAnyValueNotInArray(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsAny' => [ + 'email' => 'bob@example.com', + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * attrValueIsAny test cases where attribute values are of the wrong type + */ + public function testAttrValueIsAnyValueOfWrongTypeArray(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsAny' => [ + 'email' => [1], + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * attrValueIsAny test cases where attribute values are of the wrong type + */ + public function testAttrValueIsAnyOneValueOfWrongType(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsAny' => [ + 'email' => ['trudy@example.com'], + 'memberOf' => [1], + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * attrValueIsAll test cases where all attribute values exist + */ + public function testAttrValueIsAllAttributesExists(): void + { + // All of these test cases should pass + $tests = [ + [ + // Single value in array + 'email' => ['bob@example.com'], + ], + [ + // memberOf passes + 'email' => ['bob@example.com'], + 'memberOf' => ['users'], + ], + ]; + + foreach ($tests as $test) { + $config = [ + 'conditions' => [ + 'attrValueIsAll' => $test, + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com']); + } + } + + + /** + * attrValueIsAll multiple values, all exist + */ + public function testAttrValueIsAllMultipleAttributesExists(): void + { + // All of these test cases should pass + $tests = [ + [ + // Single value in array + 'email' => ['bob@example.com'], + ], + [ + // Single value in array + 'email' => ['bob@example.com', 'alice@example.com'], + ], + [ + // memberOf passes + 'email' => ['bob@example.com'], + 'memberOf' => ['users'], + ], + [ + // memberOf passes + 'email' => ['bob@example.com', 'alice@example.com'], + 'memberOf' => ['users'], + ], + ]; + + foreach ($tests as $test) { + $config = [ + 'conditions' => [ + 'attrValueIsAll' => $test, + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com', 'alice@example.com']); + } + } + + + /** + * attrValueIsAny test cases which should fail + */ + public function testAttrValueIsAllFailCases(): void + { + // All of these test cases should fail + $tests = [ + [ + 'email' => ['trudy@example.com'], + ], + [ + 'email' => ['trudy@example.com', 'bob@example.com'], + ], + [ + 'email' => ['trudy@example.com'], + 'memberOf' => ['users'], + ], + [ + 'email' => ['alice@example.com', 'bob@example.com'], + 'memberOf' => ['nogroup'], + ], + ]; + + foreach ($tests as $test) { + $config = [ + 'conditions' => [ + 'attrValueIsAll' => $test, + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com', 'alice@example.com']); + $this->assertArrayNotHasKey('admins', $attributes['memberOf']); + } + } + + + /** + * attrValueIsAll value is a plain string, not an array + */ + public function testAttrValueIsAllValueNotInArray(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsAll' => [ + 'email' => 'bob@example.com', + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * attrValueIsAll test cases where attribute values are of the wrong type + */ + public function testAttrValueIsAllValueOfWrongTypeSingleValue(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsAll' => [ + 'email' => 1, + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * attrValueIsAll test cases where attribute values are of the wrong type + */ + public function testAttrValueIsAllValueOfWrongTypeArray(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsAll' => [ + 'email' => [1], + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * attrValueIsAll test cases where attribute values are of the wrong type + */ + public function testAttrValueIsAllValueOfWrongType(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsAll' => [ + 'email' => ['trudy@example.com'], + 'memberOf' => [1], + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * attrValueIsRegexAny test cases that should pass + */ + public function testAttrValueIsRegexAnySuccessful(): void + { + // All of these test cases should pass + $tests = [ + [ + 'email' => ['/@example.com$/'], + ], + [ + 'email' => ['/@example.com$/', '/@something.com$/'], + ], + [ + 'email' => ['/@example.com$/'], + 'memberOf' => ['/^nogroup$/'], + ], + [ + 'email' => ['/@something.com$/', '/@example.com$/'], + 'memberOf' => ['/^nogroup$/'], + ], + [ + 'email' => ['/@something.com$/', '/@example.com$/'], + 'memberOf' => ['/^nogroup$/'], + 'noneExistentAttr' => ['/^somevalue$/'], + ], + ]; + + foreach ($tests as $test) { + $config = [ + 'conditions' => [ + 'attrValueIsRegexAny' => $test, + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@anotherdomain.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com', 'alice@anotherdomain.com']); + } + } + + + /** + * attrValueIsRegexAny test cases that should fail + */ + public function testAttrValueIsRegexAnyFail(): void + { + // All of these test cases should fail + $tests = [ + [ + 'email' => ['/@example.net$/'], + ], + [ + 'email' => ['/@example.net$/', '/@something.com$/'], + ], + [ + 'email' => ['/@example.net$/'], + 'memberOf' => ['/^nogroup$/'], + ], + [ + 'email' => ['/@something.com$/', '/@example.net$/'], + 'memberOf' => ['/^nogroup$/'], + ], + [ + 'email' => ['/@something.com$/', '/@example.net$/'], + 'memberOf' => ['/^nogroup$/'], + 'noneExistentAttr' => ['/^somevalue$/'], + ], + ]; + + foreach ($tests as $test) { + $config = [ + 'conditions' => [ + 'attrValueIsRegexAny' => $test, + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@anotherdomain.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com', 'alice@anotherdomain.com']); + $this->assertArrayNotHasKey('admins', $attributes['memberOf']); + } + } + + + /** + * attrValueIsRegexAny value is a plain regex, not in an array + */ + public function testAttrValueIsRegexAnyValueNotArray(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsRegexAny' => [ + // should be an array of regex strings + 'email' => '/@example.com$/', + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * attrValueIsRegexAny value is an array, but one item is not a regex + */ + public function testAttrValueIsRegexAnyValueNotRegex(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsRegexAny' => [ + // should be an array of regex strings + 'email' => ['/@example.com$/', 1], + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * attrValueIsRegexAll test cases that should pass + */ + public function testAttrValueIsRegexAllSuccessful(): void + { + // All of these test cases should pass + $tests = [ + [ + 'email' => ['/@example.com$/', '/@anotherdomain.com$/'], + ], + [ + 'email' => ['/@example.com$/', '/@anotherdomain.com$/', '/@yetanother.com$/'], + ], + [ + 'email' => ['/@example.com$/', '/@anotherdomain.com$/'], + 'memberOf' => ['/^users$/'], + ], + ]; + + foreach ($tests as $test) { + $config = [ + 'conditions' => [ + 'attrValueIsRegexAll' => $test, + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@anotherdomain.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users', 'admins']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com', 'alice@anotherdomain.com']); + } + } + + + /** + * attrValueIsRegexAll test cases that should fail + */ + public function testAttrValueIsRegexAllFail(): void + { + // All of these test cases should fail + $tests = [ + [ + 'email' => ['/@example.com$/'], + ], + [ + 'email' => ['/@example.com$/', '/@something.com$/'], + ], + [ + 'email' => ['/@example.com$/', '/@anotherdomain.com$/'], + 'memberOf' => ['/^nogroup$/'], + ], + [ + 'email' => ['/@something.com$/', '/@example.net$/'], + 'memberOf' => ['/^users$/'], + ], + [ + 'email' => ['/@example.com$/', '/@anotherdomain.com$/'], + 'noneExistentAttr' => ['/^somevalue$/'], + ], + ]; + + foreach ($tests as $test) { + $config = [ + 'conditions' => [ + 'attrValueIsRegexAll' => $test, + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com', 'alice@anotherdomain.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com', 'alice@anotherdomain.com']); + $this->assertArrayNotHasKey('admins', $attributes['memberOf']); + } + } + + + /** + * attrValueIsRegexAll value is a plain regex, not in an array + */ + public function testAttrValueIsRegexAllValueNotArray(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsRegexAll' => [ + // should be an array of regex strings + 'email' => '/@example.com$/', + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * attrValueIsRegexAll value is an array, but one item is not a regex + */ + public function testAttrValueIsRegexAllValueNotRegex(): void + { + $config = [ + 'conditions' => [ + 'attrValueIsRegexAll' => [ + // should be an array of regex strings + 'email' => ['/@example.com$/', 1], + ], + ], + 'attributes' => [ + 'memberOf' => ['admins'], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $this->expectException(Exception::class); + self::processFilter($config, $request); + $this->fail('Expected exception was not thrown.'); + } + + + /** + * Test multiple conditions being met (no %anycondition) + */ + public function testMultipleConditionsSuccess(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAny' => ['memberOf'], + 'attrValueIsRegexAll' => [ + 'email' => ['/@example.com$/'], + ], + ], + 'attributes' => [ + 'newAttribute' => 'testValue', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com']); + $this->assertArrayHasKey('newAttribute', $attributes); + $this->assertEquals($attributes['newAttribute'], ['testValue']); + } + + + /** + * Test multiple conditions being met (no %anycondition) + */ + public function testMultipleConditionsFailure(): void + { + $config = [ + 'conditions' => [ + 'attrExistsAny' => ['memberOf'], + 'attrValueIsRegexAll' => [ + 'email' => ['/@example.org$/'], + ], + ], + 'attributes' => [ + 'newAttribute' => 'testValue', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com']); + $this->assertArrayNotHasKey('newAttribute', $attributes); + } + + + /** + * Test multiple conditions being met (with %anycondition) + */ + public function testMultipleConditionsAnyConditionSuccess(): void + { + $testConditions = [ + [ + 'attrExistsAny' => ['memberOf'], + 'attrValueIsRegexAll' => [ + 'email' => ['/@example.com$/'], + ], + ], + [ + 'attrExistsAny' => ['memberOf'], + 'attrValueIsRegexAll' => [ + 'email' => ['/@wrongdomain.com$/'], + ], + ], + ]; + + foreach ($testConditions as $conditions) { + $config = [ + '%anycondition', + 'conditions' => $conditions, + 'attributes' => [ + 'newAttribute' => 'testValue', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com']); + $this->assertArrayHasKey('newAttribute', $attributes); + $this->assertEquals($attributes['newAttribute'], ['testValue']); + } + } + + + /** + * Test multiple conditions being met (with %anycondition) + */ + public function testMultipleConditionsAnyConditionFailure(): void + { + $config = [ + '%anycondition', + 'conditions' => [ + 'attrExistsAny' => ['nonExistentAttribute'], + 'attrValueIsRegexAll' => [ + 'email' => ['/@wrongdomain.com$/'], + ], + ], + 'attributes' => [ + 'newAttribute' => 'testValue', + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['users'], + 'email' => ['bob@example.com'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayHasKey('memberOf', $attributes); + $this->assertEquals($attributes['memberOf'], ['users']); + $this->assertArrayHasKey('email', $attributes); + $this->assertEquals($attributes['email'], ['bob@example.com']); + $this->assertArrayNotHasKey('newAttribute', $attributes); + } +} diff --git a/tests/modules/core/src/Auth/Process/AttributeDumpTest.php b/tests/modules/core/src/Auth/Process/AttributeDumpTest.php new file mode 100644 index 0000000000..8a7f9a0c22 --- /dev/null +++ b/tests/modules/core/src/Auth/Process/AttributeDumpTest.php @@ -0,0 +1,240 @@ + ArrayLogger::class, + 'errorreporting' => false, + 'module.enable' => ['core' => true], + ], + '[ARRAY]', + 'simplesaml', + ); + + Configuration::setPreLoadedConfig(self::$config, 'config.php'); + } + + + /** + * Clean up after each test + */ + #[\Override] + protected function tearDown(): void + { + Logger::clearCapturedLog(); + } + + + /** + * Helper function to run the filter with a given configuration. + * + * @param array $config The filter configuration. + * @param array $request The request state. + * @return array The state array after processing. + */ + private function processFilter(array $config, array $request, string $expected): array + { + Logger::setCaptureLog(); + $filter = new AttributeDump($config, null); + $filter->process($request); + Logger::setCaptureLog(false); + + // Strip off the timestamp from the beginning of the log message, since those are not deterministic. + $log = preg_replace('~^\S+\s~', '', Logger::getCapturedLog()[0]); + $this->assertEquals($expected, $log); + + return $request; + } + + + /** + * Test the most basic functionality. + */ + public function testBasic(): void + { + $authProcConfig = [ + ]; + $request = [ + 'Attributes' => [ + 'test' => ['value1', 'value2'], + ], + ]; + + $expectedLog = 'AttributeDump: ' . var_export(['test' => ['value1', 'value2']], true); + + $this->processFilter( + $authProcConfig, + $request, + $expectedLog, + ); + } + + + /** + * Test the most basic functionality. + */ + public function testSingleAttribute(): void + { + $authProcConfig = [ + // ArrayLogger doesn't actually do anything with the log level, + // but we want to test that it is accepted in the configuration. + 'logLevel' => 'debug', + 'logPrefix' => 'TestPrefix', + 'attributes' => ['test'], + ]; + $request = [ + 'Attributes' => [ + 'test' => ['value1', 'value2'], + 'test_drop' => ['value3'], + ], + ]; + + $expectedLog = 'TestPrefix: ' . var_export(['test' => ['value1', 'value2']], true); + + $this->processFilter( + $authProcConfig, + $request, + $expectedLog, + ); + } + + + /** + * Test the most basic functionality. + */ + public function testMultipleAttributes(): void + { + $authProcConfig = [ + 'logPrefix' => 'TestPrefix', + 'attributes' => ['test', 'test_include'], + ]; + $request = [ + 'Attributes' => [ + 'test' => ['value1', 'value2'], + 'test_drop' => ['value3'], + 'test_include' => ['value4'], + ], + ]; + + $expectedLog = 'TestPrefix: ' . + var_export(['test' => ['value1', 'value2'], 'test_include' => ['value4']], true); + + $this->processFilter( + $authProcConfig, + $request, + $expectedLog, + ); + } + + + /** + * Test the most basic functionality. + */ + public function testSingleRegex(): void + { + $authProcConfig = [ + 'attributesRegex' => ['/^test_/'], + ]; + $request = [ + 'Attributes' => [ + 'test' => ['value1', 'value2'], + 'test_next' => ['value3'], + 'test_include' => ['value4'], + ], + ]; + + $expectedLog = 'AttributeDump: ' . + var_export(['test_next' => ['value3'], 'test_include' => ['value4']], true); + + $this->processFilter( + $authProcConfig, + $request, + $expectedLog, + ); + } + + + /** + * Test the most basic functionality. + */ + public function testMultipleRegex(): void + { + $authProcConfig = [ + 'attributesRegex' => ['/^test_o/', '/^test_i/'], + ]; + $request = [ + 'Attributes' => [ + 'test' => ['value1', 'value2'], + 'test_drop' => ['value3'], + 'test_other' => ['value5'], + 'test_include' => ['value4'], + ], + ]; + + $expectedLog = 'AttributeDump: ' . + var_export(['test_other' => ['value5'], 'test_include' => ['value4']], true); + + $this->processFilter( + $authProcConfig, + $request, + $expectedLog, + ); + } + + + /** + * Test the most basic functionality. + */ + public function testAttributeAndRegex(): void + { + $authProcConfig = [ + 'attributes' => ['test'], + 'attributesRegex' => ['/^test_i/'], + ]; + $request = [ + 'Attributes' => [ + 'test' => ['value1', 'value2'], + 'test_drop' => ['value3'], + 'test_other' => ['value5'], + 'test_include' => ['value4'], + ], + ]; + + $expectedLog = 'AttributeDump: ' . + var_export(['test' => ['value1', 'value2'], 'test_include' => ['value4']], true); + + $this->processFilter( + $authProcConfig, + $request, + $expectedLog, + ); + } +} diff --git a/tests/modules/core/src/Auth/Process/AttributeValueMapTest.php b/tests/modules/core/src/Auth/Process/AttributeValueMapTest.php index 38ff6177ea..d8cd94a717 100644 --- a/tests/modules/core/src/Auth/Process/AttributeValueMapTest.php +++ b/tests/modules/core/src/Auth/Process/AttributeValueMapTest.php @@ -59,6 +59,34 @@ public function testBasic(): void } + /** + * The most basic test of no match. + * + * @throws \SimpleSAML\Error\Exception + */ + public function testBasicNoMatch(): void + { + $config = [ + 'sourceattribute' => 'memberOf', + 'targetattribute' => 'eduPersonAffiliation', + 'values' => [ + 'member' => [ + 'nomatch', + ], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['theGroup'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayNotHasKey('memberOf', $attributes); + $this->assertArrayNotHasKey('eduPersonAffiliation', $attributes); + } + + /** * Test basic functionality, remove duplicates * @@ -225,4 +253,63 @@ public function testMissingTargetAttribute(): void ]; self::processFilter($config, $request); } + + + /** + * Basic regular expression functionality test. + * + * @throws \SimpleSAML\Error\Exception + */ + public function testBasicRegex(): void + { + $config = [ + 'sourceattribute' => 'memberOf', + 'targetattribute' => 'eduPersonAffiliation', + '%regex', + 'values' => [ + 'member' => [ + '/^the/', + ], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['theGroup'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayNotHasKey('memberOf', $attributes); + $this->assertArrayHasKey('eduPersonAffiliation', $attributes); + $this->assertEquals($attributes['eduPersonAffiliation'], ['member']); + } + + + /** + * Basic regular expression functionality test of a "no match". + * + * @throws \SimpleSAML\Error\Exception + */ + public function testBasicRegexNoMatch(): void + { + $config = [ + 'sourceattribute' => 'memberOf', + 'targetattribute' => 'eduPersonAffiliation', + '%regex', + 'values' => [ + 'member' => [ + '/^nomatch$/', + ], + ], + ]; + $request = [ + 'Attributes' => [ + 'memberOf' => ['theGroup'], + ], + ]; + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $this->assertArrayNotHasKey('memberOf', $attributes); + $this->assertArrayNotHasKey('eduPersonAffiliation', $attributes); + } } diff --git a/tests/modules/core/src/Auth/Process/CardinalitySingleTest.php b/tests/modules/core/src/Auth/Process/CardinalitySingleTest.php index b0da5d3f37..ac5b6678c6 100644 --- a/tests/modules/core/src/Auth/Process/CardinalitySingleTest.php +++ b/tests/modules/core/src/Auth/Process/CardinalitySingleTest.php @@ -4,7 +4,6 @@ namespace SimpleSAML\Test\Module\core\Auth\Process; -use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\TestCase; use SimpleSAML\Configuration; use SimpleSAML\Module\core\Auth\Process\CardinalitySingle; @@ -47,7 +46,7 @@ private function processFilter(array $config, array $request): array protected function setUp(): void { Configuration::loadFromArray([], '[ARRAY]', 'simplesaml'); - $this->httpUtils = $this->createStub(Utils\HTTP::class); + $this->httpUtils = $this->getMockBuilder(Utils\HTTP::class)->getMock(); } @@ -154,7 +153,6 @@ public function testFlattenUnchanged(): void /** * Test abort */ - #[DoesNotPerformAssertions] public function testAbort(): void { $config = [ diff --git a/tests/modules/core/src/Auth/Process/CardinalityTest.php b/tests/modules/core/src/Auth/Process/CardinalityTest.php index 86eda1a26f..f4f141bd7e 100644 --- a/tests/modules/core/src/Auth/Process/CardinalityTest.php +++ b/tests/modules/core/src/Auth/Process/CardinalityTest.php @@ -4,7 +4,6 @@ namespace SimpleSAML\Test\Module\core\Auth\Process; -use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\TestCase; use SimpleSAML\Configuration; use SimpleSAML\Error\Exception as SspException; @@ -48,7 +47,7 @@ private function processFilter(array $config, array $request): array protected function setUp(): void { Configuration::loadFromArray([], '[ARRAY]', 'simplesaml'); - $this->httpUtils = $this->createStub(Utils\HTTP::class); + $this->httpUtils = $this->getMockBuilder(Utils\HTTP::class)->getMock(); } @@ -115,7 +114,6 @@ public function testMaxMin(): void /** * Test maximum is out of bounds results in redirect */ - #[DoesNotPerformAssertions] public function testMaxOutOfBounds(): void { $config = [ @@ -138,7 +136,6 @@ public function testMaxOutOfBounds(): void /** * Test minimum is out of bounds results in redirect */ - #[DoesNotPerformAssertions] public function testMinOutOfBounds(): void { $config = [ @@ -161,7 +158,6 @@ public function testMinOutOfBounds(): void /** * Test missing attribute results in redirect */ - #[DoesNotPerformAssertions] public function testMissingAttribute(): void { $config = [ diff --git a/tests/modules/core/src/Auth/Process/TargetedIDTest.php b/tests/modules/core/src/Auth/Process/TargetedIDTest.php index 688ef3c4d0..9d895a1255 100644 --- a/tests/modules/core/src/Auth/Process/TargetedIDTest.php +++ b/tests/modules/core/src/Auth/Process/TargetedIDTest.php @@ -25,6 +25,7 @@ class TargetedIDTest extends TestCase /** @var \SimpleSAML\Utils\Config */ protected static Utils\Config $configUtils; + /** * Set up for each test. */ diff --git a/tests/modules/core/src/Auth/Source/SourceIPSelectorTest.php b/tests/modules/core/src/Auth/Source/SourceIPSelectorTest.php index f244236ee6..f86c720dad 100644 --- a/tests/modules/core/src/Auth/Source/SourceIPSelectorTest.php +++ b/tests/modules/core/src/Auth/Source/SourceIPSelectorTest.php @@ -122,6 +122,7 @@ public static function doAuthentication(Auth\Source $as, array &$state): void // Dummy } + /** * @param array &$state * @return void diff --git a/tests/modules/core/src/Auth/UserPassBaseTest.php b/tests/modules/core/src/Auth/UserPassBaseTest.php index d5e398a657..2d13591e53 100644 --- a/tests/modules/core/src/Auth/UserPassBaseTest.php +++ b/tests/modules/core/src/Auth/UserPassBaseTest.php @@ -31,12 +31,12 @@ public function testAuthenticateECPCallsLoginAndSetsAttributes(): void $stub = $this->getMockBuilder(UserPassBase::class) ->disableOriginalConstructor() ->onlyMethods(['login']) - ->getMockForAbstractClass(); + ->getMock(); $stub->expects($this->once()) ->method('login') ->with($username, $password) - ->will($this->returnValue($attributes)); + ->willReturn($attributes); /** @var \SimpleSAML\Module\core\Auth\UserPassBase $stub */ $stub->authenticate($state); @@ -61,7 +61,8 @@ public function testAuthenticateECPMissingUsername(): void $stub = $this->getMockBuilder(UserPassBase::class) ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->onlyMethods(['login']) + ->getMock(); /** @var \SimpleSAML\Module\core\Auth\UserPassBase $stub */ $stub->authenticate($state); @@ -84,7 +85,8 @@ public function testAuthenticateECPMissingPassword(): void $stub = $this->getMockBuilder(UserPassBase::class) ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->onlyMethods(['login']) + ->getMock(); /** @var \SimpleSAML\Module\core\Auth\UserPassBase $stub */ $stub->authenticate($state); @@ -113,7 +115,7 @@ public function testAuthenticateECPCallsLoginWithForcedUsername(): void $stub->expects($this->once()) ->method('login') ->with($forcedUsername, $password) - ->will($this->returnValue($attributes)); + ->willReturn($attributes); /** @var \SimpleSAML\Module\core\Auth\UserPassBase $stub */ $stub->setForcedUsername($forcedUsername); diff --git a/tests/modules/core/src/Controller/ExceptionTest.php b/tests/modules/core/src/Controller/ExceptionTest.php index 5b56fd56a0..91db3aefee 100644 --- a/tests/modules/core/src/Controller/ExceptionTest.php +++ b/tests/modules/core/src/Controller/ExceptionTest.php @@ -8,8 +8,10 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use SimpleSAML\Assert\AssertionFailedException; -use SimpleSAML\{Configuration, Logger, Session}; +use SimpleSAML\Configuration; +use SimpleSAML\Logger; use SimpleSAML\Module\core\Controller; +use SimpleSAML\Session; use SimpleSAML\TestUtils\ArrayLogger; use SimpleSAML\XHTML\Template; use Symfony\Component\HttpFoundation\Request; @@ -84,7 +86,7 @@ public function testErrorURL(string $code, string $ts, string $rp, string $tid, Logger::setCaptureLog(false); $log = Logger::getCapturedLog(); - self::assertCount(7, $log); + self::assertCount(9, $log); self::assertStringContainsString( "A Service Provider reported the following error during authentication: " diff --git a/tests/modules/core/src/Controller/LoginTest.php b/tests/modules/core/src/Controller/LoginTest.php index 56f6dde1f4..a2676770bb 100644 --- a/tests/modules/core/src/Controller/LoginTest.php +++ b/tests/modules/core/src/Controller/LoginTest.php @@ -5,9 +5,11 @@ namespace SimpleSAML\Test\Module\core\Controller; use PHPUnit\Framework\Attributes\CoversClass; -use SimpleSAML\{Auth, Configuration, Error}; -use SimpleSAML\Module\core\Controller; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; +use SimpleSAML\Error; use SimpleSAML\Module\core\Auth\UserPassBase; +use SimpleSAML\Module\core\Controller; use SimpleSAML\TestUtils\ClearStateTestCase; use SimpleSAML\XHTML\Template; use Symfony\Component\HttpFoundation\Request; @@ -111,16 +113,19 @@ public function __construct() // stub } + public function authenticate(array &$state): void { // stub } + public static function getById(string $authId, ?string $type = null): ?UserPassBase { return new static(); } + protected function login(string $username, string $password): array { return ['mail' => 'noreply@simplesamlphp.org']; diff --git a/tests/modules/core/src/Controller/LogoutTest.php b/tests/modules/core/src/Controller/LogoutTest.php index c1657486f2..0b045a1002 100644 --- a/tests/modules/core/src/Controller/LogoutTest.php +++ b/tests/modules/core/src/Controller/LogoutTest.php @@ -5,7 +5,9 @@ namespace SimpleSAML\Test\Module\core\Controller; use PHPUnit\Framework\Attributes\CoversClass; -use SimpleSAML\{Auth, Configuration, Error}; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; +use SimpleSAML\Error; use SimpleSAML\HTTP\RunnableResponse; use SimpleSAML\Module\core\Controller; use SimpleSAML\TestUtils\ClearStateTestCase; @@ -66,7 +68,7 @@ public function testLogout(): void $this->assertInstanceOf(RunnableResponse::class, $response); $this->assertTrue($response->isSuccessful()); - /** @psalm-var array $callable */ + /** @var callable $callable */ $callable = $response->getCallable(); $this->assertInstanceOf(Auth\Simple::class, $callable[0]); $this->assertEquals('logout', $callable[1]); diff --git a/tests/modules/cron/src/Controller/CronTest.php b/tests/modules/cron/src/Controller/CronTest.php index 6e0e878780..ef7b685ee3 100644 --- a/tests/modules/cron/src/Controller/CronTest.php +++ b/tests/modules/cron/src/Controller/CronTest.php @@ -9,6 +9,7 @@ use PHPUnit\Framework\TestCase; use SimpleSAML\Configuration; use SimpleSAML\Error; +use SimpleSAML\Event\Dispatcher\ModuleEventDispatcherFactory; use SimpleSAML\Module\cron\Controller; use SimpleSAML\Session; use SimpleSAML\Utils; @@ -65,6 +66,8 @@ public function requireAdmin(): void 'key' => 'verysecret', 'allowed_tags' => ['daily'], 'sendemail' => false, + 'module.enable' => ['cron' => true], + 'debug_message' => true, ], '[ARRAY]', 'simplesaml', @@ -72,6 +75,8 @@ public function requireAdmin(): void 'module_cron.php', 'simplesaml', ); + + ModuleEventDispatcherFactory::testingRemakeInstance(); } @@ -112,7 +117,10 @@ public function testRunCorrectKey(): void $this->assertFalse($response->data['mail_required']); $this->assertArrayHasKey('time', $response->data); $this->assertCount(1, $response->data['summary']); - $this->assertEquals('Cron did run tag [daily] at ' . $response->data['time'], $response->data['summary'][0]); + $this->assertEquals( + 'Cron did run tag [daily] at ' . $response->data['time'], + $response->data['summary']['cron info'], + ); } diff --git a/tests/modules/multiauth/src/Auth/Source/MultiAuthTest.php b/tests/modules/multiauth/src/Auth/Source/MultiAuthTest.php index c42b2b62d1..9f29166e91 100644 --- a/tests/modules/multiauth/src/Auth/Source/MultiAuthTest.php +++ b/tests/modules/multiauth/src/Auth/Source/MultiAuthTest.php @@ -26,6 +26,8 @@ class MultiAuthTest extends ClearStateTestCase */ public function setUp(): void { + parent::setUp(); + $this->config = Configuration::loadFromArray( ['module.enable' => ['multiauth' => true]], '[ARRAY]', diff --git a/tests/modules/multiauth/src/Controller/DiscoControllerTest.php b/tests/modules/multiauth/src/Controller/DiscoControllerTest.php index 4ae08f790f..955f1a96a8 100644 --- a/tests/modules/multiauth/src/Controller/DiscoControllerTest.php +++ b/tests/modules/multiauth/src/Controller/DiscoControllerTest.php @@ -128,11 +128,14 @@ public function __construct() // stub } - public function authenticate(array &$state): void + + public function authenticate(array &$state): never { // stub + exit(); } + public static function getById(string $authId, ?string $type = null): ?Source { return new static(); @@ -182,11 +185,14 @@ public function __construct() // stub } - public function authenticate(array &$state): void + + public function authenticate(array &$state): never { // stub + exit(); } + public static function getById(string $authId, ?string $type = null): ?Source { return new static(); @@ -238,11 +244,14 @@ public function __construct() // stub } - public function authenticate(array &$state): void + + public function authenticate(array &$state): never { // stub + exit(); } + public static function getById(string $authId, ?string $type = null): ?Source { return new static(); @@ -294,11 +303,14 @@ public function __construct() // stub } - public function authenticate(array &$state): void + + public function authenticate(array &$state): never { // stub + exit(); } + public static function getById(string $authId, ?string $type = null): ?Source { return new static(); @@ -348,11 +360,14 @@ public function __construct() // stub } - public function authenticate(array &$state): void + + public function authenticate(array &$state): never { // stub + exit(); } + public static function getById(string $authId, ?string $type = null): ?Source { return new static(); diff --git a/tests/modules/saml/MessageTest.php b/tests/modules/saml/MessageTest.php new file mode 100644 index 0000000000..52dcb00d52 --- /dev/null +++ b/tests/modules/saml/MessageTest.php @@ -0,0 +1,269 @@ +acmeeEntityId = 'https://idp.acmee.com/example'; + + $this->acmeeCertificate = PEMCertificatesMock::getPlainCertificateContents(); + + $this->acmeeCertificateMismatch = PEMCertificatesMock::getPlainCertificateContents( + PEMCertificatesMock::OTHER_CERTIFICATE, + ); + + $this->acmeeCertificateWrong = PEMCertificatesMock::getPlainCertificateContents( + PEMCertificatesMock::CORRUPTED_CERTIFICATE, + ); + + // Metadata array, just like you'd see in saml20-idp-remote.php + $this->acmeeMetadata = [ + 'entityid' => $this->acmeeEntityId, + 'description' => [ + 'en' => 'Acmee IdP for testing', + ], + 'OrganizationName' => [ + 'en' => 'Acmee', + ], + 'name' => [ + 'en' => 'Acmee Example Identity Provider', + ], + 'OrganizationDisplayName' => [ + 'en' => 'Acmee', + ], + 'url' => [ + 'en' => 'https://www.acmee.com/', + ], + 'OrganizationURL' => [ + 'en' => 'https://www.acmee.com/', + ], + 'contacts' => [ + [ + 'contactType' => 'technical', + 'givenName' => 'Test Admin', + 'emailAddress' => [ + 'admin@acmee.com', + ], + ], + ], + 'metadata-set' => 'saml20-idp-remote', + 'SingleSignOnService' => [ + [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + 'Location' => 'https://idp.acmee.com/example/sso', + ], + [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'Location' => 'https://idp.acmee.com/example/sso', + ], + ], + 'SingleLogoutService' => [ + [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + 'Location' => 'https://idp.acmee.com/example/logout', + ], + ], + 'ArtifactResolutionService' => [], + 'NameIDFormats' => [ + 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + ], + 'keys' => [ + [ + 'encryption' => false, + 'signing' => true, + 'type' => 'X509Certificate', + 'X509Certificate' => $this->acmeeCertificate, + ], + ], + 'scope' => [ + 'acmee.com', + ], + 'UIInfo' => [ + 'DisplayName' => [ + 'en' => 'Acmee Identity Provider', + ], + 'Description' => [], + 'InformationURL' => [], + 'PrivacyStatementURL' => [], + ], + ]; + + // Build minimal SP metadata: + $this->spEntityId = 'https://sp.acmee.com/demo'; + $this->spMetadata = [ + 'entityID' => $this->spEntityId, + 'AssertionConsumerService' => [ + [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'Location' => 'https://sp.acmee.com/demo/acs', + ], + ], + ]; + + parent::setUp(); + } + + + public function testCheckSignThrowsWhenBadCertificate(): void + { + $this->expectException(\Exception::class); + + $localMetadata = $this->acmeeMetadata; + foreach ($localMetadata['keys'] as $index => $cert) { + $localMetadata['keys'][$index]['X509Certificate'] = $this->acmeeCertificateWrong; + } + $idpConfig = Configuration::loadFromArray( + $localMetadata, + $localMetadata['entityid'], + ); + + $spConfig = Configuration::loadFromArray( + $this->spMetadata, + $this->spMetadata['entityID'], + ); + + // Build the AuthnRequest using the Message helper: + $authnRequest = Message::buildAuthnRequest($spConfig, $idpConfig); + + // You may now use $authnRequest with checkSign: + Message::checkSign($idpConfig, $authnRequest); + } + + + public function testCheckSignThrowsWhenMissingCertificate(): void + { + $this->expectException(SSP_Error\Exception::class); + $this->expectExceptionMessage('Missing certificate in metadata for \'https://idp.acmee.com/example\''); + + $localMetadata = $this->acmeeMetadata; + unset($localMetadata['keys']); + $idpConfig = Configuration::loadFromArray( + $localMetadata, + $localMetadata['entityid'], + ); + + $spConfig = Configuration::loadFromArray( + $this->spMetadata, + $this->spMetadata['entityID'], + ); + + // Build the AuthnRequest using the Message helper: + $authnRequest = Message::buildAuthnRequest($spConfig, $idpConfig); + + // You may now use $authnRequest with checkSign: + Message::checkSign($idpConfig, $authnRequest); + } + + + public function testCheckSignThrowsWhenCertificateMismatch(): void + { + $this->expectException(SSP_Error\Error::class); + $expectedMessage = [ + 'errorCode' => ErrorCodes::NOTVALIDCERTSIGNATURE, + '%MESSAGE%' => (new ErrorCodes())->getMessage(ErrorCodes::NOTVALIDCERTSIGNATURE), + '%ELEMENT%' => 'SAML2\AuthnRequest', + '%ISSUER%' => 'https://sp.acmee.com/demo', + '%ENTITYID%' => 'https://idp.acmee.com/example', + ]; + + $this->expectExceptionMessage(json_encode($expectedMessage)); + + $localMetadata = $this->acmeeMetadata; + $localMetadata['signature.privatekey'] = PEMCertificatesMock::buildKeysPath( + PEMCertificatesMock::PRIVATE_KEY, + ); + $localMetadata['signature.privatekey_pass'] = PEMCertificatesMock::PASSPHRASE; + $localMetadata['sign.authnrequest'] = true; + foreach ($localMetadata['keys'] as $index => $cert) { + $localMetadata['keys'][$index]['X509Certificate'] = $this->acmeeCertificateMismatch; + } + $idpConfig = Configuration::loadFromArray( + $localMetadata, + $localMetadata['entityid'], + ); + + $spConfig = Configuration::loadFromArray( + $this->spMetadata, + $this->spMetadata['entityID'], + ); + + // Build the AuthnRequest using the Message helper: + $authnRequest = Message::buildAuthnRequest($spConfig, $idpConfig); + // Extract to signed xml and then parse again to validate + $signedDomElement = $authnRequest->toSignedXML(); + $parsed = new AuthnRequest($signedDomElement); + Message::checkSign($idpConfig, $parsed); + } + + + public function testCheckSignSucceeds(): void + { + $localMetadata = $this->acmeeMetadata; + $localMetadata['signature.privatekey'] = PEMCertificatesMock::buildKeysPath( + PEMCertificatesMock::PRIVATE_KEY, + ); + $localMetadata['signature.privatekey_pass'] = PEMCertificatesMock::PASSPHRASE; + $localMetadata['sign.authnrequest'] = true; + + $idpConfig = Configuration::loadFromArray( + $localMetadata, + $localMetadata['entityid'], + ); + + $spConfig = Configuration::loadFromArray( + $this->spMetadata, + $this->spMetadata['entityID'], + ); + + // Build the AuthnRequest using the Message helper: + $authnRequest = Message::buildAuthnRequest($spConfig, $idpConfig); + // Extract to signed xml and then parse again to validate + $signedDomElement = $authnRequest->toSignedXML(); + $parsed = new AuthnRequest($signedDomElement); + $isValid = Message::checkSign($idpConfig, $parsed); + $this->assertTrue($isValid); + } +} diff --git a/tests/modules/saml/src/Auth/Process/FilterScopesTest.php b/tests/modules/saml/src/Auth/Process/FilterScopesTest.php index 068dae1210..ebe3b9319b 100644 --- a/tests/modules/saml/src/Auth/Process/FilterScopesTest.php +++ b/tests/modules/saml/src/Auth/Process/FilterScopesTest.php @@ -84,6 +84,7 @@ public function testValidScopes(): void $this->assertEquals($request['Attributes'], $result['Attributes']); } + /** * Test implicit scope matching on IdP hostname */ @@ -114,6 +115,7 @@ public function testImplicitScopes(): void $this->assertEquals([], $result['Attributes']); } + /** * Test invalid scopes. */ @@ -161,6 +163,7 @@ public function testInvalidScopes(): void $this->assertEquals($request['Attributes'], $result['Attributes']); } + /** * Test that implicit matching is not done when explicit scopes present */ @@ -189,6 +192,7 @@ public function testNoImplicitMatchingWhenExplicitScopes(): void $this->assertEquals([], $result['Attributes']); } + /** * Test that the scope is considered to be the part after the first @ sign */ diff --git a/tests/modules/saml/src/Auth/Process/PairwiseIDTest.php b/tests/modules/saml/src/Auth/Process/PairwiseIDTest.php index a73ca709b0..41a61ee87d 100644 --- a/tests/modules/saml/src/Auth/Process/PairwiseIDTest.php +++ b/tests/modules/saml/src/Auth/Process/PairwiseIDTest.php @@ -9,8 +9,10 @@ use RuntimeException; use SAML2\Constants as C; use SAML2\Exception\ProtocolViolationException; -use SimpleSAML\{Configuration, Logger, Utils}; +use SimpleSAML\Configuration; +use SimpleSAML\Logger; use SimpleSAML\Module\saml\Auth\Process\PairwiseID; +use SimpleSAML\Utils; /** * Test for the saml:PairwiseID filter. diff --git a/tests/modules/saml/src/Auth/Process/SubjectIDTest.php b/tests/modules/saml/src/Auth/Process/SubjectIDTest.php index 501ce92d94..961514655d 100644 --- a/tests/modules/saml/src/Auth/Process/SubjectIDTest.php +++ b/tests/modules/saml/src/Auth/Process/SubjectIDTest.php @@ -9,8 +9,10 @@ use RuntimeException; use SAML2\Constants as C; use SAML2\Exception\ProtocolViolationException; -use SimpleSAML\{Configuration, Logger, Utils}; +use SimpleSAML\Configuration; +use SimpleSAML\Logger; use SimpleSAML\Module\saml\Auth\Process\SubjectID; +use SimpleSAML\Utils; /** * Test for the saml:SubjectID filter. @@ -247,6 +249,7 @@ public function testWeakIdentifierLogsWarning(): void self::processFilter($config, $request); } + /** * Test that weak identifiers log a warning: not an actual domain name */ diff --git a/tests/modules/saml/src/Auth/Source/SPTest.php b/tests/modules/saml/src/Auth/Source/SPTest.php index 9e074d669c..cf1fe67d72 100644 --- a/tests/modules/saml/src/Auth/Source/SPTest.php +++ b/tests/modules/saml/src/Auth/Source/SPTest.php @@ -7,13 +7,12 @@ use InvalidArgumentException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; -use SAML2\{AuthnRequest, LogoutRequest}; +use SAML2\AuthnRequest; use SAML2\Constants; -use SAML2\DOMDocumentFactory; use SAML2\Exception\Protocol\NoAvailableIDPException; use SAML2\Exception\Protocol\NoSupportedIDPException; +use SAML2\LogoutRequest; use SAML2\Utils; -use SAML2\XML\Chunk; use SAML2\XML\saml\NameID; use SimpleSAML\Assert\AssertionFailedException; use SimpleSAML\Configuration; @@ -23,6 +22,8 @@ use SimpleSAML\Test\Utils\ExitTestException; use SimpleSAML\Test\Utils\SpTester; use SimpleSAML\TestUtils\ClearStateTestCase; +use SimpleSAML\XML\Chunk; +use SimpleSAML\XML\DOMDocumentFactory; /** * Set of test cases for \SimpleSAML\Module\saml\Auth\Source\SP. @@ -30,20 +31,16 @@ #[CoversClass(SP::class)] class SPTest extends ClearStateTestCase { - /** @var string */ - private const SECURITY = 'vendor/simplesamlphp/xml-security/resources'; + private const string SECURITY = 'vendor/simplesamlphp/xml-security/resources'; - /** @var string */ - public const CERT_KEY = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.key'; + public const string CERT_KEY = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.key'; - /** @var string */ - public const CERT_PUBLIC = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.crt'; + public const string CERT_PUBLIC = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.crt'; - /** @var string */ - public const CERT_OTHER_KEY = '../' . self::SECURITY . '/certificates/other.simplesamlphp.org.key'; + public const string CERT_OTHER_KEY = '../' . self::SECURITY . '/certificates/other.simplesamlphp.org.key'; + + public const string CERT_OTHER_PUBLIC = '../' . self::SECURITY . '/certificates/other.simplesamlphp.org.crt'; - /** @var string */ - public const CERT_OTHER_PUBLIC = '../' . self::SECURITY . '/certificates/other.simplesamlphp.org.crt'; /** @var \SimpleSAML\Configuration|null $idpMetadata */ private ?Configuration $idpMetadata = null; @@ -128,16 +125,13 @@ private function createAuthnRequest(array $state = []): AuthnRequest $config = ['entityID' => 'urn:x-simplesamlphp:example-sp']; $as = new SpTester($info, $config); - /** @var \SAML2\AuthnRequest $ar */ - $ar = null; try { $as->startSSO2Test($this->getIdpMetadata(), $state); - $this->assertTrue(false, 'Expected ExitTestException'); + $this->fail('Expected ExitTestException'); } catch (ExitTestException $e) { $r = $e->getTestResult(); - $ar = $r['ar']; + return $r['ar']; } - return $ar; } @@ -155,16 +149,13 @@ private function createLogoutRequest(array $state = []): LogoutRequest $config = ['entityID' => 'urn:x-simplesamlphp:example-sp']; $as = new SpTester($info, $config); - /** @var \SAML2\LogoutRequest $lr */ - $lr = null; try { $as->startSLO2($state); - $this->assertTrue(false, 'Expected ExitTestException'); + $this->fail('Expected ExitTestException'); } catch (ExitTestException $e) { $r = $e->getTestResult(); - $lr = $r['lr']; + return $r['lr']; } - return $lr; } @@ -173,7 +164,7 @@ private function createLogoutRequest(array $state = []): LogoutRequest */ public function testAuthnRequest(): void { - $ar = $this->createAuthnRequest(); + $ar = $this->createAuthnRequest(['NameIDPolicy' => ['AllowCreate' => false]]); $xml = $ar->toSignedXML(); @@ -354,7 +345,7 @@ public function testIdpListWithExplicitIdpMatch(): void $this->fail('Expected ExitTestException'); } catch (ExitTestException $e) { $r = $e->getTestResult(); - /** @var AuthnRequest $ar */ + /** @var \SAML2\AuthnRequest $ar */ $ar = $r['ar']; $xml = $ar->toSignedXML(); @@ -393,7 +384,7 @@ public function testIdpListWithSingleMatch(): void $this->fail('Expected ExitTestException'); } catch (ExitTestException $e) { $r = $e->getTestResult(); - /** @var AuthnRequest $ar */ + /** @var \SAML2\AuthnRequest $ar */ $ar = $r['ar']; $xml = $ar->toSignedXML(); @@ -442,6 +433,7 @@ public function testIdpListWithMultipleMatch(): void $as->authenticate($state); } + /** * Basic test for the hosted metadata generation in a default config */ @@ -469,6 +461,7 @@ public function testMetadataHostedBasicConfig(): void } } + /** * Test that adding IDPList to the state (of an AuthRequest) * will result in that IDP being added to the scope @@ -485,6 +478,7 @@ public function testSPIdpListScoping(): void ); } + /** * Test that adding IDPList to the idp metadata * will result in that IDP being added to the scope @@ -500,6 +494,7 @@ public function testIdpMetadataScoping(): void ); } + /** * Test that adding IDPList to the config * will result in that IDP being added to the scope @@ -513,10 +508,9 @@ public function testRemoteMetadataScoping(): void ]; $as = new SpTester($info, $config); - /** @var \SAML2\AuthnRequest $ar */ try { $as->startSSO2Test($this->getIdpMetadata(), []); - $this->assertTrue(false, 'Expected ExitTestException'); + $this->fail('Expected ExitTestException'); } catch (ExitTestException $e) { ['ar' => $ar] = $e->getTestResult(); $this->assertContains( @@ -556,10 +550,9 @@ public function testSPIdpListScopingOrder( $as = new SpTester($info, $config); - /** @var \SAML2\AuthnRequest $ar */ try { $as->startSSO2Test($this->getIdpMetadata(), $state); - $this->assertTrue(false, 'Expected ExitTestException'); + $this->fail('Expected ExitTestException'); } catch (ExitTestException $e) { ['ar' => $ar] = $e->getTestResult(); @@ -570,6 +563,7 @@ public function testSPIdpListScopingOrder( } } + public static function getScopingOrders(): array { return [ @@ -606,6 +600,7 @@ public static function getScopingOrders(): array ]; } + /** * Test for the hosted metadata generation with a custom entityID */ @@ -620,6 +615,7 @@ public function testMetadataHostedSetEntityId(): void $this->assertEquals('urn:example:mysp:001', $md['entityid']); } + /** * Contacts in SP hosted config appear in metadata */ @@ -679,6 +675,7 @@ public function testMetadataHostedContacts(): void $this->assertArrayNotHasKey('attributes', $contact); } + /** * A globally set tech contact also appears in SP hosted metadata */ @@ -724,6 +721,7 @@ public function testMetadataHostedContactsIncludesGlobalTechContact(): void $this->assertArrayNotHasKey('surName', $contact); } + /** * The special value na@example.org global tech contact is not included in SP metadata */ @@ -753,8 +751,9 @@ public function testMetadataHostedContactsSkipsNAGlobalTechContact(): void $this->assertEquals('j.doe@example.edu', $md['contacts'][0]['emailAddress']); } + /** - * Contacts in SP hosted of unknown type throws Exceptiona + * Contacts in SP hosted of unknown type throws Exception */ public function testMetadataHostedContactsUnknownTypeThrowsException(): void { @@ -779,6 +778,7 @@ public function testMetadataHostedContactsUnknownTypeThrowsException(): void $md = $as->getHostedMetadata(); } + /** * SP acs.Bindings option overrides default bindigs */ @@ -808,6 +808,7 @@ public function testMetadataHostedAcsBindingsOption(): void ); } + /** * SP acs.Bindings option with unsupported value should be skipped */ @@ -837,6 +838,7 @@ public function testMetadataHostedAcsBindingsUnknownValueIsSkipped(): void ); } + /** * SP SLO Bindings option overrides default bindigs */ @@ -858,6 +860,7 @@ public function testMetadataHostedSloBindingsOption(): void ); } + /** * SP empty SLO Bindings option omits SLO in metadata */ @@ -875,6 +878,7 @@ public function testMetadataHostedSloBindingsEmptyNotInMetadata(): void $this->assertCount(0, $md['SingleLogoutService']); } + /** * SP SLO Bindings option with unknown value is accepted as-is */ @@ -900,6 +904,7 @@ public function testMetadataHostedSloBindingsUnknownValueIsAccepted(): void $this->assertEquals('urn:this:doesnotexist', $md['SingleLogoutService'][1]['Binding']); } + /** * SP SLO Location option is used as URL for all SLO Bindings */ @@ -928,6 +933,7 @@ public function testMetadataHostedSloURLIsUsedForAllSLOBindings(): void $this->assertEquals('https://sp.example.org/logout', $md['SingleLogoutService'][1]['Location']); } + /** * SP AssertionConsumerService option overrides default bindigs */ @@ -1023,6 +1029,7 @@ public function testMetadataHostedSigning(): void $this->assertTrue($md['validate.authnrequest']); } + /** * SP config option RegistrationInfo is reflected in metadata */ @@ -1052,6 +1059,7 @@ public function testMetadataHostedContainsRegistrationInfo(): void $this->assertEquals('http://sp.example.org/politica', $reginfo['policies']['es']); } + /** * SP config option NameIDPolicy is reflected in metadata */ @@ -1074,6 +1082,7 @@ public function testMetadataHostedNameIDPolicy(): void $this->assertEquals('urn:mace:shibboleth:1.0:nameIdentifier', $md['NameIDFormat']); } + /** * SP config option NameIDPolicy specified without Format is reflected in metadata */ @@ -1093,6 +1102,7 @@ public function testMetadataHostedNameIDPolicyNullFormat(): void $this->assertEquals('urn:oasis:names:tc:SAML:2.0:nameid-format:transient', $md['NameIDFormat']); } + /** * SP config option Organization* are reflected in metadata */ @@ -1124,6 +1134,7 @@ public function testMetadataHostedOrganizationData(): void $this->assertEquals('https://example.com/nl', $md['OrganizationURL']['nl']); } + /** * SP config option Organization* without explicit DisplayName are reflected in metadata */ @@ -1149,6 +1160,7 @@ public function testMetadataHostedOrganizationDataDefaultForDisplayNameIsName(): $this->assertEquals('https://example.com/nl', $md['OrganizationURL']['nl']); } + /** * SP config option Organization* without URL is rejected with an Exception */ @@ -1173,6 +1185,7 @@ public function testMetadataHostedOrganizationURLMissingRaisesException(): void $md = $as->getHostedMetadata(); } + /** * SP config option for UIInfo is reflected in metadata */ @@ -1202,6 +1215,7 @@ public function testMetadataHostedUIInfo(): void $this->assertEquals('Descripción en Español', $md['UIInfo']['Description']['es']); } + /** * SP config option for entity attribute extensions is reflected in metadata */ @@ -1222,6 +1236,7 @@ public function testMetadataHostedEntityExtensions(): void $this->assertEquals($ea, $md['EntityAttributes']); } + /** * SP config option for Name, Description, Attributes is in metadata */ @@ -1270,6 +1285,7 @@ public function testMetadataHostedNameDescriptionAttributes(): void ); } + /** * SP config option for Name, Description require attributes to be specified */ @@ -1294,6 +1310,7 @@ public function testMetadataHostedNameDescriptionAbsentWhenNoAttributes(): void $this->assertArrayNotHasKey('description', $md); } + /** * SP config for attributes also requires name in metadata */ @@ -1315,6 +1332,7 @@ public function testMetadataHostedAttributesRequiresName(): void $this->assertArrayNotHasKey('attributes', $md); } + /** * SP config for attributes with extra options */ @@ -1344,6 +1362,7 @@ public function testMetadataHostedAttributesExtraOptions(): void $this->assertEquals(true, $md['attributes.isDefault']); } + /** * SP config for holder-of-key profile via ProtocolBinding is reflected in metadata */ @@ -1371,6 +1390,7 @@ public function testMetadataHolderOfKeyViaProtocolBindingIsInMetadata(): void $this->assertEquals('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', $hok['hoksso:ProtocolBinding']); } + /** * SP config with certificate are reflected in metadata */ @@ -1397,6 +1417,7 @@ public function testMetadatCertificateIsInMetadata(): void $this->assertEquals('', $md['keys'][0]['prefix']); } + /** * SP config with certificate in rollocer scenario are reflected in metadata */ @@ -1430,6 +1451,7 @@ public function testMetadatCertificateInRolloverIsInMetadata(): void $this->assertEquals('', $md['keys'][1]['prefix']); } + /** * We only support SAML 2.0 as a protocol with this auth source */ @@ -1446,6 +1468,7 @@ public function testSupportedProtocolsReturnsSAML20Only(): void $this->assertEquals('urn:oasis:names:tc:SAML:2.0:protocol', $protocols[0]); } + /** * We only support SAML 2.0 as a protocol with this auth source */ @@ -1478,9 +1501,10 @@ public function testSAML11BindingsDoesNotInfluenceProtocolsSupported(): void $this->assertEquals('urn:oasis:names:tc:SAML:2.0:protocol', $protocols[0]); } - /** - * Test sending a LogoutRequest - */ + + /** + * Test sending a LogoutRequest + */ public function testLogoutRequest(): void { $nameId = new NameID(); @@ -1508,7 +1532,7 @@ public function testLogoutRequest(): void $lr = $this->createLogoutRequest($state); - /** @var \SAML2\XML\samlp\Extensions $extensions */ + /** @var \SimpleSAML\XML\Chunk[] $extensions */ $extensions = $lr->getExtensions(); $this->assertcount(1, $state['saml:logout:Extensions']); @@ -1525,7 +1549,8 @@ public function testLogoutRequest(): void $this->assertEquals('urn:some:namespace', $q[0]->firstChild->namespaceURI); } - /* + + /** * Test using the entityID from config/authsources.php.dist */ public function testSampleEntityIdException(): void diff --git a/tests/modules/saml/src/Controller/MetadataTest.php b/tests/modules/saml/src/Controller/MetadataTest.php index e94b73d984..73c6cfdc6c 100644 --- a/tests/modules/saml/src/Controller/MetadataTest.php +++ b/tests/modules/saml/src/Controller/MetadataTest.php @@ -24,13 +24,13 @@ #[CoversClass(Controller\Metadata::class)] class MetadataTest extends TestCase { - /** @var \SimpleSAML\Configuration */ protected Configuration $config; - /** @var \SimpleSAML\Utils\Auth */ protected Utils\Auth $authUtils; protected MetaDataStorageHandler $mdh; + + /** * Set up for each test. */ @@ -39,17 +39,17 @@ protected function setUp(): void parent::setUp(); $this->mdh = new class () extends MetaDataStorageHandler { - /** @var string */ - private const XMLSEC = '../vendor/simplesamlphp/xml-security/resources'; + private const string XMLSEC = '../vendor/simplesamlphp/xml-security/resources'; - /** @var string */ - public const CERT_KEY = self::XMLSEC . '/certificates/selfsigned.simplesamlphp.org.key'; + public const string CERT_KEY = self::XMLSEC . '/certificates/selfsigned.simplesamlphp.org.key'; - /** @var string */ - public const CERT_PUBLIC = self::XMLSEC . '/certificates/selfsigned.simplesamlphp.org.crt'; + public const string CERT_PUBLIC = self::XMLSEC . '/certificates/selfsigned.simplesamlphp.org.crt'; + + /** @var array */ private array $idps; + public function __construct() { $this->idps = [ @@ -72,6 +72,7 @@ public function __construct() ]; } + public function getMetaData(?string $entityId, string $set): array { if (isset($this->idps[$entityId]) && $set === 'saml20-idp-hosted') { @@ -81,6 +82,7 @@ public function getMetaData(?string $entityId, string $set): array throw new Error\MetadataNotFound($entityId ?? ''); } + public function getList(string $set = 'saml20-idp-remote', bool $showExpired = false): array { if ($set === 'saml20-idp-hosted') { @@ -89,6 +91,7 @@ public function getList(string $set = 'saml20-idp-remote', bool $showExpired = f return []; } + public function getMetaDataCurrentEntityID(string $set, string $type = 'entityid'): string { return 'urn:example:simplesaml:another:idp'; @@ -165,7 +168,7 @@ public function testMetadataAccess(bool $authenticated, bool $protected): void if ($protected && !$authenticated) { $this->assertInstanceOf(RunnableResponse::class, $result); - /** @psalm-var array $callable */ + /** @var callable $callable */ $callable = $result->getCallable(); $this->assertEquals("requireAdmin", $callable[1]); } else { @@ -179,6 +182,7 @@ public function testMetadataAccess(bool $authenticated, bool $protected): void } } + public static function provideMetadataAccess(): array { return [ @@ -190,6 +194,7 @@ public static function provideMetadataAccess(): array ]; } + /** * Test that saml20-idp setting disabled disables access */ @@ -217,6 +222,7 @@ public function testDisabledSAML20IDPReturnsNoAccess(): void $result = $c->metadata($request); } + /** * Test that requesting a non-existing entityID throws an exception */ @@ -235,6 +241,7 @@ public function testMetadataUnknownEntityThrowsError(): void $result = $c->metadata($request); } + /** * Basic smoke test of generated metadata */ @@ -260,6 +267,7 @@ public function testMetadataYieldsContent(): void $this->assertStringContainsString($expect, $content); } + /** * Test not specifying explict entityID falls back to a default */ @@ -285,6 +293,7 @@ public function testMetadataDefaultIdPYieldsContent(): void $this->assertStringContainsString($expect, $content); } + /** * Check if caching headers are set */ diff --git a/tests/modules/saml/src/Controller/ServiceProviderTest.php b/tests/modules/saml/src/Controller/ServiceProviderTest.php index da839c37f0..8cdfdcb26b 100644 --- a/tests/modules/saml/src/Controller/ServiceProviderTest.php +++ b/tests/modules/saml/src/Controller/ServiceProviderTest.php @@ -40,18 +40,19 @@ class ServiceProviderTest extends TestCase /** @var \SimpleSAML\Utils\Auth */ protected Utils\Auth $authUtils; - /** @var Controller\ServiceProvider */ + /** @var \SimpleSAML\Module\saml\Controller\ServiceProvider */ protected Controller\ServiceProvider $serviceProvider; - /** @var Utils\HTTP */ + /** @var \SimpleSAML\Utils\HTTP */ protected Utils\HTTP $httpUtils; - /** @var string */ - public const RELAY_STATE = 'https://example.org'; + + public const string RELAY_STATE = 'https://example.org'; + /** * Set up for each test. - * @throws Exception + * @throws \Exception */ protected function setUp(): void { @@ -144,6 +145,7 @@ public function testLoginMissingReturnTo(): void $c->login($request, 'phpunit'); } + /** * Test that accessing the login-endpoint with empty ReturnTo parameter leads to an exception * @@ -294,6 +296,7 @@ public static function loginNotAuthenticatedDataProvider(): array ]; } + /** * Test Login without Session/ Not Authenticated * @@ -336,17 +339,17 @@ public function testLoginHandleNotAuthenticated( if ($expectingException) { $this->expectException(\SimpleSAML\Error\Exception::class); $this->expectExceptionMessage('URL not allowed: https://evil.com'); - /** @psalm-suppress UndefinedMethod method defined in anonymous class */ + /** @phpstan-ignore method.notFound */ $this->serviceProvider->callHandleLogin($request, $as, $spSource, $this->httpUtils); } else { $as->expects($this->once())->method('requireAuth')->with($options); - /** @psalm-suppress UndefinedMethod method defined in anonymous class */ - $returnsTo = $this->serviceProvider - ->callHandleLogin($request, $as, $spSource, $this->httpUtils); + /** @phpstan-ignore method.notFound */ + $returnsTo = $this->serviceProvider->callHandleLogin($request, $as, $spSource, $this->httpUtils); $this->assertEquals($options['ReturnTo'], $returnsTo); } } + /** * @return array */ @@ -412,6 +415,7 @@ public static function loginAuthenticatedDataProvider(): array ]; } + /** * Test Login with Session/Authenticated * @@ -456,7 +460,7 @@ public function testLoginHandleAuthenticated( if ($expectingException) { $this->expectException(\SimpleSAML\Error\Exception::class); $this->expectExceptionMessage('URL not allowed: https://evil.com'); - /** @psalm-suppress UndefinedMethod method defined in anonymous class */ + /** @phpstan-ignore method.notFound */ $this->serviceProvider->callHandleLogin($request, $as, $spSource, $this->httpUtils); } else { if ($expectLoginCalled) { @@ -464,13 +468,13 @@ public function testLoginHandleAuthenticated( } else { $as->expects($this->never())->method('login'); } - /** @psalm-suppress UndefinedMethod method defined in anonymous class */ - $returnsTo = $this->serviceProvider - ->callHandleLogin($request, $as, $spSource, $this->httpUtils); + /** @phpstan-ignore method.notFound */ + $returnsTo = $this->serviceProvider->callHandleLogin($request, $as, $spSource, $this->httpUtils); $this->assertEquals($options['ReturnTo'], $returnsTo); } } + /** * Test that accessing the discoResponse-endpoint with unknown authsource in state results in an exception * @@ -649,7 +653,11 @@ public function testACSWithCorrectMessageUnknownEntity(): void $c = new Controller\ServiceProvider($this->config, $this->session); $this->expectException(Error\MetadataNotFound::class); - $this->expectExceptionMessage("METADATANOTFOUND('%ENTITYID%' => 'https://engine.test.surfconext.nl/authentication/idp/metadata')"); + $exceptionMessage = [ + "errorCode" => "METADATANOTFOUND", + "%ENTITYID%" => "https://engine.test.surfconext.nl/authentication/idp/metadata", + ]; + $this->expectExceptionMessage(json_encode($exceptionMessage)); $c->assertionConsumerService('phpunit'); } @@ -737,7 +745,11 @@ public function testSLOWithCorrectMessageUnknownEntity(): void $c = new Controller\ServiceProvider($this->config, $this->session); $this->expectException(Error\MetadataNotFound::class); - $this->expectExceptionMessage("METADATANOTFOUND('%ENTITYID%' => 'TheIssuer')"); + $exceptionMessage = [ + "errorCode" => "METADATANOTFOUND", + "%ENTITYID%" => "TheIssuer", + ]; + $this->expectExceptionMessage(json_encode($exceptionMessage)); $c->singleLogoutService('phpunit'); } @@ -776,7 +788,7 @@ public function testMetadataAccess(bool $authenticated, bool $protected): void if ($protected && !$authenticated) { $this->assertInstanceOf(RunnableResponse::class, $result); - /** @psalm-var array $callable */ + /** @var callable $callable */ $callable = $result->getCallable(); $this->assertEquals("requireAdmin", $callable[1]); } else { @@ -790,6 +802,7 @@ public function testMetadataAccess(bool $authenticated, bool $protected): void } } + public static function provideMetadataAccess(): array { return [ @@ -801,6 +814,7 @@ public static function provideMetadataAccess(): array ]; } + /** * Test that requesting a non-existing authsource yields an error */ @@ -818,6 +832,7 @@ public function testMetadataUnknownEntityThrowsError(): void $c->metadata($request, 'phpnonunit'); } + /** * Basic smoke test of generated metadata */ @@ -838,6 +853,7 @@ public function testMetadataYieldsContent(): void $this->assertStringContainsString($expect, $content); } + /** * Check if caching headers are set */ diff --git a/tests/modules/saml/src/IdP/SAML2Test.php b/tests/modules/saml/src/IdP/SAML2Test.php index 9e51018cec..adf4caf0bc 100644 --- a/tests/modules/saml/src/IdP/SAML2Test.php +++ b/tests/modules/saml/src/IdP/SAML2Test.php @@ -6,27 +6,26 @@ use InvalidArgumentException; use PHPUnit\Framework\Attributes\CoversClass; -use SAML2\XML\Chunk; use SimpleSAML\Configuration; use SimpleSAML\Error\Exception; use SimpleSAML\IdP; use SimpleSAML\Metadata\MetaDataStorageHandlerSerialize; use SimpleSAML\Module\saml\IdP\SAML2; use SimpleSAML\TestUtils\ClearStateTestCase; +use SimpleSAML\XML\Chunk; +use SimpleSAML\XML\DOMDocumentFactory; /** */ #[CoversClass(SAML2::class)] class SAML2Test extends ClearStateTestCase { - /** @var string */ - private const SECURITY = 'vendor/simplesamlphp/xml-security/resources'; + private const string SECURITY = 'vendor/simplesamlphp/xml-security/resources'; - /** @var string */ - public const CERT_KEY = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.key'; + public const string CERT_KEY = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.key'; + + public const string CERT_PUBLIC = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.crt'; - /** @var string */ - public const CERT_PUBLIC = '../' . self::SECURITY . '/certificates/selfsigned.simplesamlphp.org.crt'; /** * Default values for the state array expected to be generated at the start of logins @@ -60,6 +59,7 @@ public function testIdPInitiatedLoginMinimumParams(): void $this->assertEquals('https://some-sp-entity-id', $state['SPMetadata']['entityid']); $this->assertStringStartsWith( + // phpcs:ignore Generic.Files.LineLength.TooLong 'http://idp.examlple.com/module.php/saml/idp/singleSignOnService?spentityid=https%3A%2F%2Fsome-sp-entity-id&cookie', $state['\SimpleSAML\Auth\State.restartURL'], ); @@ -120,6 +120,7 @@ public function testIdPInitShibCompatyMinimumParams(): void $this->assertEquals('https://some-sp-entity-id', $state['SPMetadata']['entityid']); $this->assertStringStartsWith( + // phpcs:ignore Generic.Files.LineLength.TooLong 'http://idp.examlple.com/module.php/saml/idp/singleSignOnService?spentityid=https%3A%2F%2Fsome-sp-entity-id&cookie', $state['\SimpleSAML\Auth\State.restartURL'], ); @@ -232,6 +233,7 @@ function ($arg) use (&$state) { return $state; } + /** * Perform needed setup to be able to provide an array config * of IdP-hosted metadata and be able to query this back from @@ -325,6 +327,7 @@ public function testIdPGetHostedMetadataMinimal(): void $this->assertStringStartsWith('MIICxDCCAi2gAwI', $hostedMd['keys'][0]['X509Certificate']); } + public function testIdPGetHostedKeyRollover(): void { $md = ['new_certificate' => self::CERT_PUBLIC, 'new_privatekey' => self::CERT_KEY]; @@ -345,6 +348,7 @@ public function testIdPGetHostedKeyRollover(): void $this->assertStringStartsWith('MIICxDCCAi2gAwI', $hostedMd['keys'][1]['X509Certificate']); } + public function testIdPGetHostedHttpsCertificate(): void { $md = ['https.certificate' => self::CERT_PUBLIC]; @@ -365,6 +369,7 @@ public function testIdPGetHostedHttpsCertificate(): void $this->assertStringStartsWith('MIICxDCCAi2gAwI', $hostedMd['keys'][1]['X509Certificate']); } + public function testIdPGetHostedMetadataArtifact(): void { $md = ['saml20.sendartifact' => true]; @@ -383,6 +388,7 @@ public function testIdPGetHostedMetadataArtifact(): void ); } + public function testIdPGetHostedMetadataHolderOfKey(): void { $md = ['saml20.hok.assertion' => true]; @@ -408,6 +414,7 @@ public function testIdPGetHostedMetadataHolderOfKey(): void ); } + public function testIdPGetHostedMetadataECP(): void { $md = ['saml20.ecp' => true]; @@ -433,6 +440,7 @@ public function testIdPGetHostedMetadataECP(): void ); } + /** * NameIDFormat option can be specified as string or array */ @@ -463,6 +471,7 @@ public function testIdPGetHostedNameIdFormat(): void $this->assertEquals('urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', $hostedMd['NameIDFormat'][1]); } + public function testIdPGetHostedScopes(): void { $md = [ @@ -478,6 +487,7 @@ public function testIdPGetHostedScopes(): void $this->assertArrayNotHasKey('unknown-option', $hostedMd); } + /** * IdP config option Organization* are reflected in metadata */ @@ -504,6 +514,7 @@ public function testMetadataHostedOrganizationData(): void $this->assertEquals('https://example.com/nl', $md['OrganizationURL']['nl']); } + /** * IdP config option Organization* without explicit DisplayName are reflected in metadata */ @@ -524,6 +535,7 @@ public function testMetadataHostedOrganizationDataDefaultForDisplayNameIsName(): $this->assertEquals('https://example.com/nl', $md['OrganizationURL']['nl']); } + /** * IdP config option Organization* without URL is rejected with an Exception */ @@ -543,6 +555,7 @@ public function testMetadataHostedOrganizationURLMissingRaisesException(): void $md = $this->idpMetadataHandlerHelper($config); } + /** * IdP config option for entity attributes is reflected in metadata */ @@ -571,12 +584,13 @@ public function testMetadataHostedEntityAttributes(): void $this->assertTrue($md['hide.from.discovery']); } + /** * IdP config option for entity attribute extensions is reflected in metadata */ public function testMetadataHostedEntityExtensions(): void { - $dom = \SAML2\DOMDocumentFactory::create(); + $dom = DOMDocumentFactory::create(); $republishRequest = $dom->createElementNS('http://eduid.cz/schema/metadata/1.0', 'eduidmd:RepublishRequest'); $republishTarget = $dom->createElementNS( 'http://eduid.cz/schema/metadata/1.0', @@ -600,6 +614,7 @@ public function testMetadataHostedEntityExtensions(): void ); } + /** * IdP config option for UIInfo is reflected in metadata */ @@ -639,6 +654,7 @@ public function testMetadataHostedUIInfo(): void $this->assertEquals('geo:19.34343,12.342514', $md['DiscoHints']['GeolocationHint'][1]); } + /** * IdP config option RegistrationInfo is reflected in metadata */ @@ -663,6 +679,7 @@ public function testMetadataHostedContainsRegistrationInfo(): void $this->assertEquals('http://sp.example.org/politica', $reginfo['policies']['es']); } + /** * IdP config options wrt signing are reflected in metadata */ @@ -681,6 +698,7 @@ public function testMetadataHostedSigning(): void $this->assertArrayNotHasKey('redirect.validate', $md); } + /** * Contacts in IdP hosted config appear in metadata */ @@ -734,6 +752,7 @@ public function testMetadataHostedContacts(): void $this->assertArrayNotHasKey('attributes', $contact); } + /** * A globally set tech contact also appears in IdP hosted metadata */ @@ -773,6 +792,7 @@ public function testMetadataHostedContactsIncludesGlobalTechContact(): void $this->assertArrayNotHasKey('surName', $contact); } + /** * The special value na@example.org global tech contact is not included in IdP metadata */ @@ -798,6 +818,7 @@ public function testMetadataHostedContactsSkipsNAGlobalTechContact(): void $this->assertEquals('j.doe@example.edu', $md['contacts'][0]['emailAddress']); } + /** * Contacts in IdP hosted of unknown type throws Exceptiona */ diff --git a/tests/src/SimpleSAML/Auth/ProcessingChainTest.php b/tests/src/SimpleSAML/Auth/ProcessingChainTest.php index 2f6f83f5dc..3c9cd8446a 100644 --- a/tests/src/SimpleSAML/Auth/ProcessingChainTest.php +++ b/tests/src/SimpleSAML/Auth/ProcessingChainTest.php @@ -44,6 +44,7 @@ public function testInsertAuthProcs(): void $this->assertInstanceOf(AttributeLimit::class, $filterInChain[2]); } + public function testInsertAuthFromConfigs(): void { $config = []; diff --git a/tests/src/SimpleSAML/Auth/SimpleTest.php b/tests/src/SimpleSAML/Auth/SimpleTest.php index 3512733bbe..8a3b6e5099 100644 --- a/tests/src/SimpleSAML/Auth/SimpleTest.php +++ b/tests/src/SimpleSAML/Auth/SimpleTest.php @@ -4,9 +4,10 @@ namespace SimpleSAML\Test\Auth; -use ReflectionClass; use PHPUnit\Framework\Attributes\CoversClass; -use SimpleSAML\{Auth, Configuration}; +use ReflectionClass; +use SimpleSAML\Auth; +use SimpleSAML\Configuration; use SimpleSAML\TestUtils\ClearStateTestCase; /** diff --git a/tests/src/SimpleSAML/ConfigurationTest.php b/tests/src/SimpleSAML/ConfigurationTest.php index 0253117851..e43d0f98d3 100644 --- a/tests/src/SimpleSAML/ConfigurationTest.php +++ b/tests/src/SimpleSAML/ConfigurationTest.php @@ -1076,7 +1076,7 @@ public function testGetConfigNonexistentFilePreload(): void /** * @return void - * @throws Exception + * @throws \Exception */ public function testCanLoadDefinedConfigFromFile(): void { @@ -1090,7 +1090,7 @@ public function testCanLoadDefinedConfigFromFile(): void /** * @return void - * @throws Exception + * @throws \Exception */ public function testCanLoadReturnedConfigFromFile(): void { @@ -1101,9 +1101,10 @@ public function testCanLoadReturnedConfigFromFile(): void $this->assertArrayHasKey('returned', $config->toArray()); } + /** * @return void - * @throws Exception + * @throws \Exception */ public function testLoadFromFileConfigurationError(): void { diff --git a/tests/src/SimpleSAML/DatabaseTest.php b/tests/src/SimpleSAML/DatabaseTest.php index ac698789c4..4faa8bc224 100644 --- a/tests/src/SimpleSAML/DatabaseTest.php +++ b/tests/src/SimpleSAML/DatabaseTest.php @@ -23,7 +23,7 @@ * @package SimpleSAMLphp */ #[CoversClass(Database::class)] -class DatabaseTest extends TestCase +final class DatabaseTest extends TestCase { /** * @var \SimpleSAML\Configuration diff --git a/tests/src/SimpleSAML/Error/ErrorCodesTest.php b/tests/src/SimpleSAML/Error/ErrorCodesTest.php index 0743a79507..c7bb122a8b 100644 --- a/tests/src/SimpleSAML/Error/ErrorCodesTest.php +++ b/tests/src/SimpleSAML/Error/ErrorCodesTest.php @@ -18,6 +18,7 @@ protected function instance(): ErrorCodes return new ErrorCodes(); } + /** * @deprecated */ @@ -28,6 +29,7 @@ public function testCanStaticallyGetFallbackValuesForNonExistentErrorCode(): voi $this->assertStringContainsString($nonExistentCode, ErrorCodes::getErrorCodeDescription($nonExistentCode)); } + public function testCanGetFallbackValuesForNonExistentErrorCode(): void { $nonExistentCode = 'nonexistent'; @@ -35,6 +37,7 @@ public function testCanGetFallbackValuesForNonExistentErrorCode(): void $this->assertStringContainsString($nonExistentCode, $this->instance()->getDescription($nonExistentCode)); } + /** * @deprecated */ @@ -79,6 +82,7 @@ public function testCanStaticallyGetDefaultErrorCodes(): void ); } + public function testCanGetDefaultErrorCodes(): void { $this->assertSameSize( @@ -120,6 +124,7 @@ public function testCanGetDefaultErrorCodes(): void ); } + /** * @deprecated */ @@ -127,15 +132,20 @@ public function testCanStaticallyExtendWithCustomErrorCodes(): void { $customErrorCodes = new class extends ErrorCodes { - public const CUSTOMCODE = 'CUSTOMCODE'; + public const string CUSTOMCODE = 'CUSTOMCODE'; + + public static string $customTitle = 'customTitle'; + public static string $customDescription = 'customDescription'; + public static function getCustomErrorCodeTitles(): array { return [self::CUSTOMCODE => self::$customTitle]; } + public static function getCustomErrorCodeDescriptions(): array { return [self::CUSTOMCODE => self::$customDescription]; @@ -181,6 +191,7 @@ public static function getCustomErrorCodeDescriptions(): array ); } + /** * @deprecated */ @@ -188,15 +199,20 @@ public function testCanExtendWithCustomErrorCodes(): void { $customErrorCodes = new class extends ErrorCodes { - public const CUSTOMCODE = 'CUSTOMCODE'; + public const string CUSTOMCODE = 'CUSTOMCODE'; + + public static string $customTitle = 'customTitle'; + public static string $customDescription = 'customDescription'; + public function getCustomTitles(): array { return [self::CUSTOMCODE => self::$customTitle]; } + public function getCustomDescriptions(): array { return [self::CUSTOMCODE => self::$customDescription]; diff --git a/tests/src/SimpleSAML/Error/ErrorTest.php b/tests/src/SimpleSAML/Error/ErrorTest.php index a73dd77e19..fc5cb0e0fb 100644 --- a/tests/src/SimpleSAML/Error/ErrorTest.php +++ b/tests/src/SimpleSAML/Error/ErrorTest.php @@ -17,11 +17,16 @@ class ErrorTest extends TestCase { private ErrorCodes $errorCodes; + private array|string $errorCodeSample; + private ?int $httpCodeSample; + private MockObject|Throwable|null $causeMock; + private MockObject|ErrorCodes|null $errorCodesMock; + protected function setUp(): void { $this->errorCodes = new ErrorCodes(); @@ -32,6 +37,7 @@ protected function setUp(): void $this->errorCodesMock = $this->createMock(ErrorCodes::class); } + protected function mocked(): Error { return new Error( @@ -42,6 +48,7 @@ protected function mocked(): Error ); } + public function testCanInstantiateWithErrorCodeString(): void { $error = new Error(ErrorCodes::WRONGUSERPASS); @@ -61,6 +68,7 @@ public function testCanInstantiateWithErrorCodeString(): void ); } + public function testCanInstantiateWithErrorCodeParamsArray(): void { $errorCodeParams = [ @@ -85,6 +93,7 @@ public function testCanInstantiateWithErrorCodeParamsArray(): void ); } + public function testCanUseInjectedMockedErrorCodes(): void { $testTitle = 'testTitle'; @@ -106,6 +115,7 @@ public function testCanUseInjectedMockedErrorCodes(): void $this->assertSame($testDescription, $error->getDictDescr()); } + public function testCanExtendWithCustomErrorCodes(): void { $customErrorCode = 'CUSTOMCODE'; @@ -116,11 +126,15 @@ public function getErrorCodes(): ErrorCodes { return new class extends ErrorCodes { - public const CUSTOMCODE = 'CUSTOMCODE'; + public const string CUSTOMCODE = 'CUSTOMCODE'; + + public function getCustomTitles(): array { return [self::CUSTOMCODE => 'customCodeTitle']; } + + public function getCustomDescriptions(): array { return [self::CUSTOMCODE => 'customCodeDescription']; diff --git a/tests/src/SimpleSAML/Locale/LanguageTest.php b/tests/src/SimpleSAML/Locale/LanguageTest.php index 5a28695597..08794661a3 100644 --- a/tests/src/SimpleSAML/Locale/LanguageTest.php +++ b/tests/src/SimpleSAML/Locale/LanguageTest.php @@ -174,6 +174,7 @@ public function testSetLanguage(): void $this->assertEquals('en', $l->getLanguage()); } + public function testGetPreferredLanguages(): void { // test defaults diff --git a/tests/src/SimpleSAML/Locale/LocalizationTest.php b/tests/src/SimpleSAML/Locale/LocalizationTest.php index c27d48646a..c8c31cfa2a 100644 --- a/tests/src/SimpleSAML/Locale/LocalizationTest.php +++ b/tests/src/SimpleSAML/Locale/LocalizationTest.php @@ -49,6 +49,7 @@ public function testAddDomain(): void $this->assertEquals($newDomainLocaleDir, $registeredDomains[$newDomain]); } + /** * Test SimpleSAML\Locale\Localization::addModuleDomains(). */ @@ -66,6 +67,7 @@ public function testAddModuleDomain(): void $this->assertEquals($newDomainLocaleDir, $registeredDomains['attributes']); } + /** * Test SimpleSAML\Locale\Localization::addModuleDomains() with a theme. */ diff --git a/tests/src/SimpleSAML/Locale/TranslateTest.php b/tests/src/SimpleSAML/Locale/TranslateTest.php index a98594c315..b638a73cf8 100644 --- a/tests/src/SimpleSAML/Locale/TranslateTest.php +++ b/tests/src/SimpleSAML/Locale/TranslateTest.php @@ -26,6 +26,7 @@ public function testNoop(): void $this->assertEquals($testString, $t->noop($testString)); } + /** * Test SimpleSAML\Locale\Translate::translateFromArray(). */ @@ -38,6 +39,7 @@ public function testTranslateFromArray(): void $this->assertEquals('interlingua', $result); } + public function testTranslateFromArrayFallback(): void { $result = Translate::translateFromArray( @@ -47,6 +49,7 @@ public function testTranslateFromArrayFallback(): void $this->assertEquals('english', $result); } + public function testTranslateFromArrayFail(): void { $result = Translate::translateFromArray( diff --git a/tests/src/SimpleSAML/Metadata/MetaDataStorageHandlerTest.php b/tests/src/SimpleSAML/Metadata/MetaDataStorageHandlerTest.php index 2fe5f92578..26aaa9880e 100644 --- a/tests/src/SimpleSAML/Metadata/MetaDataStorageHandlerTest.php +++ b/tests/src/SimpleSAML/Metadata/MetaDataStorageHandlerTest.php @@ -26,6 +26,7 @@ protected function getHandler(?array $config = null): MetaDataStorageHandler return MetaDataStorageHandler::getMetadataHandler(); } + public static function entityIDsList(): array { return [ @@ -41,6 +42,8 @@ public static function entityIDsList(): array 'empty list' => [[]], ]; } + + /** * Test that loading specific entities work, and that metadata source precedence is followed */ @@ -69,6 +72,7 @@ public function testLoadEntities(array $entityIDs): void } } + /** * Test that retrieving a full metadataSet from a source works and precedence works */ @@ -93,6 +97,7 @@ public function testLoadMetadataSet(): void $this->assertEquals('hostname SP from source1', $entities['http://localhost/simplesaml']['name']['en']); } + /** * Query from a metadata set for which we have no entities should be empty */ @@ -103,6 +108,7 @@ public function testLoadMetadataSetEmpty(): void $this->assertCount(0, $entities); } + /** * Test the current metadata entity selection */ @@ -113,6 +119,7 @@ public function testGetMetadataCurrent(): void $this->assertEquals('http://localhost/simplesaml', $entity['entityid']); } + /** * Test the helper that returns the metadata as a Configuration object */ @@ -124,6 +131,7 @@ public function testGetMetadataConfig(): void $this->assertEquals('entityA', $entity->getValue('entityid')); } + /** * Test the helper that searches metadata by sha1 hash */ @@ -136,6 +144,7 @@ public function testGetMetadataConfigForSha1(): void $this->assertEquals('entityB', $entity->getValue('entityid')); } + /** * Test the helper that searches metadata by sha1 hash */ @@ -147,6 +156,7 @@ public function testGetMetadataConfigForSha1NotFoundReturnsNull(): void $this->assertNull($entity); } + /** * Test the current metadata entity selection, empty set */ @@ -157,16 +167,22 @@ public function testGetMetadataCurrentEmptySet(): void $this->getHandler()->getMetaDataCurrent('saml20-idp-remote'); } + /** * Test that trying to fetch a non-existent entity throws Exception */ public function testGetMetaDataNonExistentEntity(): void { $this->expectException(MetadataNotFound::class); - $this->expectExceptionMessage("METADATANOTFOUND('%ENTITYID%' => 'doesnotexist')"); + $exceptionMessage = [ + "errorCode" => "METADATANOTFOUND", + "%ENTITYID%" => "doesnotexist", + ]; + $this->expectExceptionMessage(json_encode($exceptionMessage)); $this->getHandler()->getMetaData('doesnotexist', 'saml20-sp-remote'); } + /* * Test using the entityID from metadata-templates/saml20-idp-hosted.php */ @@ -177,6 +193,7 @@ public function testSampleEntityIdException(): void $this->getHandler()->getMetaDataCurrent('saml20-idp-hosted'); } + public function testCanHaveMultipleHostedIdps(): void { $config = [ @@ -191,6 +208,7 @@ public function testCanHaveMultipleHostedIdps(): void $this->assertCount(2, $idps); } + public function testCanGetDefaultHostedIdpInCaseOfMultipleHostedIdps(): void { $config = [ @@ -205,6 +223,7 @@ public function testCanGetDefaultHostedIdpInCaseOfMultipleHostedIdps(): void $this->assertSame('urn:x-simplesamlphp:example-idp-1', $defaultIdp['entityid']); } + public function testCanGetParticularIdpInCaseOfMultipleHostedIdps(): void { $config = [ @@ -219,6 +238,7 @@ public function testCanGetParticularIdpInCaseOfMultipleHostedIdps(): void $this->assertSame('urn:x-simplesamlphp:example-idp-2', $particularIdp['entityid']); } + public function testCanOverrideHostedIdpOptionsInCaseOfMultipleHostedIdps(): void { $config = [ diff --git a/tests/src/SimpleSAML/Metadata/SAMLBuilderTest.php b/tests/src/SimpleSAML/Metadata/SAMLBuilderTest.php index a1ad20e4fa..bd7732ff5f 100644 --- a/tests/src/SimpleSAML/Metadata/SAMLBuilderTest.php +++ b/tests/src/SimpleSAML/Metadata/SAMLBuilderTest.php @@ -9,6 +9,8 @@ use SimpleSAML\Configuration; use SimpleSAML\Metadata\SAMLBuilder; use SimpleSAML\Module\saml\Auth\Source\SP; +use SimpleSAML\XML\Chunk; +use SimpleSAML\XML\DOMDocumentFactory; /** * Class SAMLBuilderTest @@ -16,7 +18,8 @@ #[CoversClass(SAMLBuilder::class)] class SAMLBuilderTest extends TestCase { - private const SECURITY = 'vendor/simplesamlphp/xml-security/resources'; + private const string SECURITY = 'vendor/simplesamlphp/xml-security/resources'; + /** */ @@ -25,6 +28,7 @@ protected function setUp(): void Configuration::loadFromArray([], '', 'simplesaml'); } + /** */ protected function tearDown(): void @@ -32,6 +36,7 @@ protected function tearDown(): void Configuration::clearInternalState(); } + /** * Test the requested attributes are valued correctly. */ @@ -260,6 +265,7 @@ public function testProtocolSupportEnumeration(): void ); } + /** * Test custom metadata extension (saml:Extensions). */ @@ -268,7 +274,7 @@ public function testCustomMetadataExtension(): void $entityId = 'https://entity.example.com/id'; $set = 'saml20-idp-remote'; - $dom = \SAML2\DOMDocumentFactory::create(); + $dom = DOMDocumentFactory::create(); $republishRequest = $dom->createElementNS( 'http://eduid.cz/schema/metadata/1.0', 'eduidmd:RepublishRequest', @@ -280,7 +286,7 @@ public function testCustomMetadataExtension(): void $republishTargetContent, ); $republishRequest->appendChild($republishTarget); - $ext = [new \SAML2\XML\Chunk($republishRequest)]; + $ext = [new Chunk($republishRequest)]; $metadata = [ 'entityid' => $entityId, @@ -300,6 +306,7 @@ public function testCustomMetadataExtension(): void $this->assertEquals($republishTargetContent, $rt1->textContent); } + /** * Test adding contacts to metadata */ @@ -338,11 +345,9 @@ public function testContacts(): void $samlBuilder->addMetadata($set, $metadata); $spDesc = $samlBuilder->getEntityDescriptor(); - /** @psalm-var \DOMNodeList $acs */ $contacts = $spDesc->getElementsByTagName("ContactPerson"); $this->assertEquals(2, $contacts->length); - /** @psalm-var \DOMElement $first */ $first = $contacts->item(0); $this->assertTrue($first->hasAttribute("contactType")); $this->assertEquals("other", $first->getAttribute("contactType")); @@ -390,6 +395,7 @@ public function testContacts(): void $this->assertEquals("Doe", $sn->item(0)->nodeValue); } + /* * Test certificate data. */ diff --git a/tests/src/SimpleSAML/Metadata/SAMLParserTest.php b/tests/src/SimpleSAML/Metadata/SAMLParserTest.php index 575c99790b..58d1c38730 100644 --- a/tests/src/SimpleSAML/Metadata/SAMLParserTest.php +++ b/tests/src/SimpleSAML/Metadata/SAMLParserTest.php @@ -7,9 +7,9 @@ use DOMDocument; use PHPUnit\Framework\Attributes\CoversClass; use SAML2\Constants; -use SAML2\DOMDocumentFactory; use SimpleSAML\Metadata\SAMLParser; use SimpleSAML\Test\SigningTestCase; +use SimpleSAML\XML\DOMDocumentFactory; use SimpleSAML\XML\Signer; /** @@ -222,6 +222,9 @@ public function testAttributeConsumingServiceParsing(): void + + urn:mace:dir:entitlement:common-lib-terms + @@ -243,6 +246,9 @@ public function testAttributeConsumingServiceParsing(): void "urn:mace:dir:attribute-def:eduPersonPrincipalName", "urn:mace:dir:attribute-def:mail", "urn:mace:dir:attribute-def:displayName", + "urn:oid:1.3.6.1.4.1.5923.1.1.1.7" => [ + "urn:mace:dir:entitlement:common-lib-terms", + ], ]; $expected_r = ["urn:mace:dir:attribute-def:eduPersonPrincipalName"]; @@ -368,6 +374,7 @@ public function testRoleDescriptorExtensions(): void $this->assertEquals($expected['name'], $metadata['name']); } + /** * Test entity category hidden from discovery is parsed */ @@ -400,6 +407,7 @@ public function testHiddenFromDiscovery(): void $this->assertTrue($metadata['hide.from.discovery']); } + /** * Test entity category hidden from discovery is not returned when not present */ @@ -431,6 +439,7 @@ public function testHiddenFromDiscoveryNotHidden(): void $this->assertArrayNotHasKey('hide.from.discovery', $metadata); } + /** * Test entity category hidden from discovery is not returned when no mace dir entity categories present */ diff --git a/tests/src/SimpleSAML/ModuleTest.php b/tests/src/SimpleSAML/ModuleTest.php index 02dcaacf03..7942cb36f1 100644 --- a/tests/src/SimpleSAML/ModuleTest.php +++ b/tests/src/SimpleSAML/ModuleTest.php @@ -8,11 +8,13 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use SimpleSAML\Configuration; +use SimpleSAML\Event\Dispatcher\ModuleEventDispatcherFactory; +use SimpleSAML\Locale\Translate; use SimpleSAML\Module; +use SimpleSAML\Module\admin\Event\ConfigPageEvent; +use SimpleSAML\XHTML\Template; use Symfony\Component\Filesystem\Path; -/** - */ #[CoversClass(Module::class)] class ModuleTest extends TestCase { @@ -115,18 +117,45 @@ public function testResolveClass(): void )); } + /** * Test for SimpleSAML\Module::getModuleHooks(). It covers happy path. */ public function testGetModuleHooks(): void { - $hooks = Module::getModuleHooks('cron'); - $this->assertArrayHasKey('configpage', $hooks); - $this->assertEquals('cron_hook_configpage', $hooks['configpage']['func']); - $expectedFile = Path::canonicalize(dirname(__DIR__, 3) . '/modules/cron/hooks/hook_configpage.php'); - $this->assertEquals($expectedFile, $hooks['configpage']['file']); + $c = Configuration::loadFromArray( + [ + 'assets' => [ 'salt' => '1234567890'], + 'module.enable' => ['saml' => true, 'admin' => true, 'cron' => true,], + ], + '', + 'simplesaml', + ); + Configuration::setPreLoadedConfig($c); + $t = new Template($c, 'admin:config.twig'); + $t->data = [ + 'links' => [ + [ + 'href' => Module::getModuleURL('admin/diagnostics'), + 'text' => Translate::noop('Diagnostics on hostname, port and protocol'), + ], + [ + 'href' => Module::getModuleURL('admin/phpinfo'), + 'text' => Translate::noop('Information on your PHP installation'), + ], + ], + ]; + + $eventDispatcher = ModuleEventDispatcherFactory::getInstance(); + $event = $eventDispatcher->dispatch(new ConfigPageEvent($t)); + $t = $event->getTemplate(); + $this->assertEquals( + Translate::noop('Cron module information page'), + $t->data['links'][2]['text'], + ); } + /** * Test for SimpleSAML\Module::getModuleHooks(). It covers invalid hook names */ diff --git a/tests/src/SimpleSAML/SessionHandlerPHPTest.php b/tests/src/SimpleSAML/SessionHandlerPHPTest.php index 75ad24b730..b5aa40d8d2 100644 --- a/tests/src/SimpleSAML/SessionHandlerPHPTest.php +++ b/tests/src/SimpleSAML/SessionHandlerPHPTest.php @@ -9,7 +9,8 @@ use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\Attributes\RunInSeparateProcess; -use SimpleSAML\{Configuration, SessionHandlerPHP}; +use SimpleSAML\Configuration; +use SimpleSAML\SessionHandlerPHP; use SimpleSAML\TestUtils\ClearStateTestCase; /** @@ -35,7 +36,10 @@ class SessionHandlerPHPTest extends ClearStateTestCase */ protected function setUp(): void { + parent::setUp(); + $this->original = $_SERVER; + $_SERVER['HTTP_HOST'] = 'example.com'; $_SERVER['SERVER_NAME'] = 'example.com'; $_SERVER['HTTPS'] = 'on'; @@ -168,7 +172,7 @@ public function testRestorePrevious(): void session_start(); Configuration::loadFromArray($this->sessionConfig, '[ARRAY]', 'simplesaml'); - /** @var SessionHandlerPHP $sh */ + /** @var \SimpleSAML\SessionHandlerPHP $sh */ $sh = SessionHandlerPHP::getSessionHandler(); $sh->setCookie('SimpleSAMLSessionID', 'Restore'); $sh->restorePrevious(); diff --git a/tests/src/SimpleSAML/SessionTest.php b/tests/src/SimpleSAML/SessionTest.php index 6444be04cb..c6d943fba0 100644 --- a/tests/src/SimpleSAML/SessionTest.php +++ b/tests/src/SimpleSAML/SessionTest.php @@ -6,8 +6,9 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; +use SimpleSAML\Configuration; +use SimpleSAML\Session; use SimpleSAML\TestUtils\ClearStateTestCase; -use SimpleSAML\{Configuration, Session}; /** */ @@ -17,15 +18,19 @@ class SessionTest extends ClearStateTestCase /** @var \SimpleSAML\Session */ protected Session $session; + /** */ public function setUp(): void { + parent::setUp(); + Configuration::loadFromArray([], '[ARRAY]', 'simplesaml'); $this->session = Session::getSessionFromRequest(); } + /** */ public function testSetRememberMeExpireDefaults(): void @@ -39,6 +44,7 @@ public function testSetRememberMeExpireDefaults(): void $this->assertEquals(time() + 14 * 86400, $this->session->getRememberMeExpire()); } + /** */ public function testSetRememberMeExpireExplicit(): void @@ -49,6 +55,7 @@ public function testSetRememberMeExpireExplicit(): void $this->assertEquals(time() + 1000, $this->session->getRememberMeExpire()); } + public static function expirationValues(): array { return [ @@ -58,6 +65,7 @@ public static function expirationValues(): array ]; } + #[DataProvider('expirationValues')] public function testSetDataExpiration(int|string $expire): void { @@ -74,6 +82,7 @@ public function testSetDataExpiration(int|string $expire): void } } + public static function getAllowedExpired(): array { return [ @@ -104,6 +113,7 @@ public static function getAllowedExpired(): array ]; } + /** * Tests that getData returns expected data when session is expired. * @throws \Exception diff --git a/tests/src/SimpleSAML/Store/RedisStoreTest.php b/tests/src/SimpleSAML/Store/RedisStoreTest.php index e7c06a6865..f26f634467 100644 --- a/tests/src/SimpleSAML/Store/RedisStoreTest.php +++ b/tests/src/SimpleSAML/Store/RedisStoreTest.php @@ -44,29 +44,35 @@ public function __construct( ) { } + public function __deconstruct() { } + public function disconnect(): void { } + public function get(string $str): ?string { return $this->unitTest->getMocked($str); } + public function set(string $str, mixed $value): void { $this->unitTest->setMocked($str, $value); } + public function setEx(string $str, int $expire, mixed $value): void { $this->unitTest->setExMocked($str, $expire, $value); } + public function del(string $str): void { $this->unitTest->delMocked($str); @@ -130,6 +136,7 @@ public function testRedisInstance(): void $this->assertInstanceOf(Store\RedisStore::class, $this->store); } + /** */ public function testRedisInstanceWithInsecureTLS(): void @@ -144,6 +151,7 @@ public function testRedisInstanceWithInsecureTLS(): void $this->assertInstanceOf(Store\RedisStore::class, $this->store); } + /** */ public function testRedisInstanceWithSecureTLS(): void @@ -160,6 +168,7 @@ public function testRedisInstanceWithSecureTLS(): void $this->assertInstanceOf(Store\RedisStore::class, $this->store); } + /** */ public function testRedisInstanceWithPassword(): void @@ -173,6 +182,7 @@ public function testRedisInstanceWithPassword(): void $this->assertInstanceOf(Store\RedisStore::class, $this->store); } + /** */ public function testRedisInstanceWithPasswordAndUsername(): void @@ -187,6 +197,7 @@ public function testRedisInstanceWithPasswordAndUsername(): void $this->assertInstanceOf(Store\RedisStore::class, $this->store); } + /** */ public function testRedisSentinelInstance(): void @@ -200,6 +211,7 @@ public function testRedisSentinelInstance(): void $this->assertInstanceOf(Store\RedisStore::class, $this->store); } + /** */ public function testInsertData(): void diff --git a/tests/src/SimpleSAML/Utils/Config/MetadataTest.php b/tests/src/SimpleSAML/Utils/Config/MetadataTest.php index 48967ff7ed..f9084a81ae 100644 --- a/tests/src/SimpleSAML/Utils/Config/MetadataTest.php +++ b/tests/src/SimpleSAML/Utils/Config/MetadataTest.php @@ -246,6 +246,7 @@ public function testParseNameIdPolicy(): void ], Metadata::parseNameIdPolicy($nameIdPolicy)); } + /** * Test \SimpleSAML\Utils\Config\Metadata::parseNameIdPolicy(). * Test with settings that produce the fallback defaults. @@ -256,7 +257,7 @@ public function testParseNameIdPolicyDefaults(): void $nameIdPolicy = null; $this->assertEquals([ 'Format' => Constants::NAMEID_TRANSIENT, - 'AllowCreate' => true, + 'AllowCreate' => false, ], Metadata::parseNameIdPolicy($nameIdPolicy)); $nameIdPolicy = [ @@ -276,6 +277,7 @@ public function testParseNameIdPolicyDefaults(): void ], Metadata::parseNameIdPolicy($nameIdPolicy)); } + /** * Test \SimpleSAML\Utils\Config\Metadata::parseNameIdPolicy(). * Test with setting to empty array (meaning to not send any NameIdPolicy). diff --git a/tests/src/SimpleSAML/Utils/ConfigTest.php b/tests/src/SimpleSAML/Utils/ConfigTest.php index 51330c6a22..fee85e3cc0 100644 --- a/tests/src/SimpleSAML/Utils/ConfigTest.php +++ b/tests/src/SimpleSAML/Utils/ConfigTest.php @@ -55,6 +55,7 @@ public function testEnvVariableConfigDir(): void $this->assertEquals($configDir, __DIR__); } + /** * Test valid dir specified by env redirect var overrides default config dir */ diff --git a/tests/src/SimpleSAML/Utils/CryptoTest.php b/tests/src/SimpleSAML/Utils/CryptoTest.php index 4070dec357..ea277a8c62 100644 --- a/tests/src/SimpleSAML/Utils/CryptoTest.php +++ b/tests/src/SimpleSAML/Utils/CryptoTest.php @@ -5,7 +5,8 @@ namespace SimpleSAML\Test\Utils; use InvalidArgumentException; -use org\bovigo\vfs\{vfsStream, vfsStreamDirectory}; +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use SimpleSAML\Configuration; @@ -18,9 +19,10 @@ #[CoversClass(Utils\Crypto::class)] class CryptoTest extends TestCase { - private const ROOTDIRNAME = 'testdir'; + private const string ROOTDIRNAME = 'testdir'; + + private const string DEFAULTCERTDIR = 'certdir'; - private const DEFAULTCERTDIR = 'certdir'; /** @var \org\bovigo\vfs\vfsStreamDirectory */ protected VfsStreamDirectory $root; @@ -75,6 +77,7 @@ class CryptoTest extends TestCase -----END CERTIFICATE----- PHP; + /** */ public function setUp(): void @@ -249,6 +252,7 @@ public function testBadPwInvalid(): void $this->assertFalse($res); } + /** * Check that hash cannot be used to authenticate ith. * @deprecated diff --git a/tests/src/SimpleSAML/Utils/EMailTest.php b/tests/src/SimpleSAML/Utils/EMailTest.php index 0a07de6e82..2fd3e88a4e 100644 --- a/tests/src/SimpleSAML/Utils/EMailTest.php +++ b/tests/src/SimpleSAML/Utils/EMailTest.php @@ -133,6 +133,7 @@ public function testInvalidSMTPConfiguration(): void $email->setTransportMethod('smtp'); } + /** * Test setting configuration. * diff --git a/tests/src/SimpleSAML/Utils/HTTPTest.php b/tests/src/SimpleSAML/Utils/HTTPTest.php index ff5e2332ba..4afb357be7 100644 --- a/tests/src/SimpleSAML/Utils/HTTPTest.php +++ b/tests/src/SimpleSAML/Utils/HTTPTest.php @@ -10,14 +10,28 @@ use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\Attributes\RunInSeparateProcess; -use SimpleSAML\{Configuration, Error, Utils}; +use SimpleSAML\Configuration; +use SimpleSAML\Error; use SimpleSAML\TestUtils\ClearStateTestCase; +use SimpleSAML\Utils; /** */ #[CoversClass(Utils\HTTP::class)] class HTTPTest extends ClearStateTestCase { + /** + * Set up the test. + */ + protected function setUp(): void + { + parent::setUp(); + if (!defined('SIMPLESAMLPHP_TEST_NOEXIT')) { + define('SIMPLESAMLPHP_TEST_NOEXIT', true); + } + } + + /** * Set up the environment ($_SERVER) populating the typical variables from a given URL. * @@ -76,6 +90,7 @@ public function testAddURLParameters(): void $this->assertEquals($url . '&bar=foo', $httpUtils->addURLParameters($url, $params)); } + private function makeNativePath($s) { if (DIRECTORY_SEPARATOR == '\\') { @@ -84,6 +99,7 @@ private function makeNativePath($s) return $s; } + /** * Test SimpleSAML\Utils\HTTP::guessBasePath(). */ @@ -582,9 +598,10 @@ public function testDetectSameSiteNoneBehavior(?string $userAgent, bool $support $this->assertEquals($supportsNone, $httpUtils->canSetSameSiteNone(), $userAgent ?? 'No user agent set'); } + public static function detectSameSiteProvider(): array { - // @codingStandardsIgnoreStart + // phpcs:disable Generic.Files.LineLength return [ [null, true], ['some-new-browser', true], @@ -618,6 +635,177 @@ public static function detectSameSiteProvider(): array // old embedded browser ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/605.1.15 (KHTML, like Gecko)', false], ]; - // @codingStandardsIgnoreEnd + // phpcs:enable Generic.Files.LineLength + } + + + /** + * submitPOSTData() should throw Error\Exception for an invalid destination URL. + */ + public function testSubmitPOSTDataThrowsOnInvalidURL(): void + { + $httpUtils = new Utils\HTTP(); + + // Minimal configuration to satisfy internals; won’t be used since we fail early. + Configuration::loadFromArray([ + 'baseurlpath' => 'https://example.com/simplesaml/', + ], '[ARRAY]', 'simplesaml'); + + $this->expectException(Error\Exception::class); + $this->expectExceptionMessage('Invalid destination URL: not-a-url'); + + $httpUtils->submitPOSTData('not-a-url', ['a' => 'b']); + } + + + /** + * When enable.http_post = true, destination is http:// and current request is HTTPS, we must redirect. + * We assert a Location header is sent pointing to a postredirect URL. + */ + #[Depends('testXdebugMode')] + #[RunInSeparateProcess] + public function testSubmitPOSTDataRedirectsFromHttpsToHttp(): void + { + $httpUtils = new Utils\HTTP(); + + // Configure base URL and allow http post. + Configuration::loadFromArray([ + 'baseurlpath' => 'https://idp.example.org/simplesaml/', + 'enable.http_post' => true, + 'secretsalt' => 'abc', + ], '[ARRAY]', 'simplesaml'); + + // Simulate the current request being HTTPS + $this->setupEnvFromURL('https://idp.example.org/simplesaml/module.php/core/someaction?x=1'); + + // Destination is explicitly http:// + $destination = 'http://sp.example.com/acs'; + $post = ['SAMLResponse' => 'abc', 'RelayState' => 'xyz']; + + try { + $httpUtils->submitPOSTData($destination, $post); + } catch (\Throwable $e) { + } + + $headers = function_exists('xdebug_get_headers') ? xdebug_get_headers() : []; + + // Find the Location header + $locationHeader = null; + foreach ($headers as $h) { + if (stripos($h, 'Location: ') === 0) { + $locationHeader = substr($h, 10); + break; + } + } + + $this->assertNotNull($locationHeader, 'Expected a Location header to be sent'); + $this->assertStringStartsWith('http://', $locationHeader, 'Location should be http://'); + $this->assertStringContainsString('/core/postredirect', $locationHeader); + $this->assertTrue( + (str_contains($locationHeader, 'RedirInfo=')), + 'Location should contain RedirInfo parameter', + ); + } + + + /** + * submitPOSTData() should pass slow_post_delay_ms to the template: + * - default 30000 when config key missing + * - default 30000 when config value < 0 + * - exact value when config value is 10000 + */ + #[DataProvider('slowPostDelayProvider')] + #[RunInSeparateProcess] + public function testSubmitPOSTDataSlowPostDelay(?int $configured, int $expected): void + { + $httpUtils = new Utils\HTTP(); + + // Base config + $config = [ + 'baseurlpath' => 'https://idp.example.org/simplesaml/', + 'enable.http_post' => false, + ]; + if ($configured !== null) { + $config['slow_post_delay_ms'] = $configured; + } + Configuration::loadFromArray($config, '[ARRAY]', 'simplesaml'); + + // Use https destination to bypass the http-redirect branch entirely + $destination = 'https://sp.example.com/acs'; + $post = ['k' => 'v']; + + // Capture output + ob_start(); + try { + $httpUtils->submitPOSTData($destination, $post); + } catch (\Throwable $e) { + } + $html = ob_get_clean(); + + // The template writes the delay into data-slow-post-delay attribute + $needle = 'data-slow-post-delay="' . $expected . '"'; + $this->assertStringContainsString($needle, $html); + } + + + public static function slowPostDelayProvider(): array + { + return [ + // [configured, expected] + 'missing config => default' => [null, 30000], + 'negative config => default' => [-5, 30000], + 'positive config 10000 => 10000' => [10000, 10000], + ]; + } + + + /** + * Ensure getSelfURL() returns the externally visible URL when SimpleSAMLphp + * is reached via a rewritten path (e.g. /cas/login -> /simplesaml/module.php/...), + * and the internal script name (module.php) appears only in the query string. + * + * This simulates an Apache mod_rewrite rule like: + * RewriteRule ^/cas/login(.*) /${SSP_APACHE_ALIAS}module.php/casserver/login.php$1 [PT] + * + * In this scenario the public URL is /cas/login?... while the actual script is + * public/module.php. + */ + public function testGetSelfURLWithRewrittenCasLogin(): void + { + $originalServer = $_SERVER; + + $httpUtils = new Utils\HTTP(); + + $cfg = Configuration::loadFromArray([ + 'baseurlpath' => 'https://tr-monitor-okta2.qa.athena-institute.net/simplesaml/', + ], '[ARRAY]', 'simplesaml'); + $baseDir = $cfg->getBaseDir(); + + $_SERVER = [ + 'HTTPS' => 'on', + 'HTTP_HOST' => 'tr-monitor-okta2.qa.athena-institute.net', + 'SERVER_NAME' => 'tr-monitor-okta2.qa.athena-institute.net', + 'SERVER_PORT' => 443, + 'SCRIPT_URI' => 'https://tr-monitor-okta2.qa.athena-institute.net/cas/login', + 'SCRIPT_NAME' => '/module.php', + 'SCRIPT_FILENAME' => $baseDir . 'public' . DIRECTORY_SEPARATOR . 'module.php', + 'PATH_TRANSLATED' => $baseDir . 'public' . DIRECTORY_SEPARATOR . 'casserver/login.php', + 'PHP_SELF' => '/module.php/casserver/login.php', + 'QUERY_STRING' => 'service=' + . 'https%3A%2F%2Fcas-test-bridge.bridge.qa.cirrusidentity.com%2Fmodule.php%2Fcas%2Flinkback.php' + . '%3FstateId%3D_somestate', + 'REQUEST_URI' => '/cas/login?service=' + . 'https%3A%2F%2Fcas-test-bridge.bridge.qa.cirrusidentity.com%2Fmodule.php%2Fcas%2Flinkback.php' + . '%3FstateId%3D_somestate', + ]; + + $expected = 'https://tr-monitor-okta2.qa.athena-institute.net' + . '/cas/login' + . '?service=https%3A%2F%2Fcas-test-bridge.bridge.qa.cirrusidentity.com%2Fmodule.php%2Fcas%2Flinkback.php' + . '%3FstateId%3D_somestate'; + + $this->assertSame($expected, $httpUtils->getSelfURL()); + + $_SERVER = $originalServer; } } diff --git a/tests/src/SimpleSAML/Utils/SystemTest.php b/tests/src/SimpleSAML/Utils/SystemTest.php index b1eb14728f..d728f394f5 100644 --- a/tests/src/SimpleSAML/Utils/SystemTest.php +++ b/tests/src/SimpleSAML/Utils/SystemTest.php @@ -4,7 +4,8 @@ namespace SimpleSAML\Test\Utils; -use org\bovigo\vfs\{vfsStream, vfsStreamDirectory}; +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use ReflectionClass; @@ -18,9 +19,10 @@ #[CoversClass(Utils\System::class)] class SystemTest extends TestCase { - private const ROOTDIRNAME = 'testdir'; + private const string ROOTDIRNAME = 'testdir'; + + private const string DEFAULTTEMPDIR = 'tempdir'; - private const DEFAULTTEMPDIR = 'tempdir'; /** @var \org\bovigo\vfs\vfsStreamDirectory */ protected VfsStreamDirectory $root; diff --git a/tests/src/SimpleSAML/Utils/TimeTest.php b/tests/src/SimpleSAML/Utils/TimeTest.php index 5831648bc5..ba46bc8e74 100644 --- a/tests/src/SimpleSAML/Utils/TimeTest.php +++ b/tests/src/SimpleSAML/Utils/TimeTest.php @@ -7,7 +7,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use ReflectionClass; -use SimpleSAML\Assert\AssertionFailedException; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\Utils; @@ -170,22 +169,5 @@ public function testParseDuration(): void $timeUtils->parseDuration('PT1M'), "Failure testing for 1 minute over current time.", ); - - // test invalid durations - try { - // invalid string - $timeUtils->parseDuration('abcdefg'); - $this->fail("Did not fail with invalid ISO 8601 duration."); - } catch (AssertionFailedException $e) { - $this->assertStringMatchesFormat('\'%s\' is not a valid xs:duration', $e->getMessage()); - } - - try { - // missing T delimiter - $timeUtils->parseDuration('P1S'); - $this->fail("Did not fail with duration missing T delimiter."); - } catch (AssertionFailedException $e) { - $this->assertStringMatchesFormat('\'%s\' is not a valid xs:duration', $e->getMessage()); - } } } diff --git a/tests/src/SimpleSAML/Utils/XMLTest.php b/tests/src/SimpleSAML/Utils/XMLTest.php index ac35908545..bd2397f1f6 100644 --- a/tests/src/SimpleSAML/Utils/XMLTest.php +++ b/tests/src/SimpleSAML/Utils/XMLTest.php @@ -11,8 +11,8 @@ use InvalidArgumentException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use SAML2\DOMDocumentFactory; use SimpleSAML\Utils; +use SimpleSAML\XML\DOMDocumentFactory; /** * Tests for SimpleSAML\Utils\XML. @@ -20,7 +20,8 @@ #[CoversClass(Utils\XML::class)] class XMLTest extends TestCase { - private const FRAMEWORK = 'vendor/simplesamlphp/saml2/tests/resources/xml'; + private const string FRAMEWORK = 'vendor/simplesamlphp/saml2/tests/resources/xml'; + /** */ @@ -285,6 +286,7 @@ public function testIsValidMetadata(): void $this->assertTrue($res === true); } + /** */ public function testCheckSAMLMessageInvalidType(): void diff --git a/tests/src/SimpleSAML/XHTML/TemplateTest.php b/tests/src/SimpleSAML/XHTML/TemplateTest.php index 8cf0015914..2bfebec83c 100644 --- a/tests/src/SimpleSAML/XHTML/TemplateTest.php +++ b/tests/src/SimpleSAML/XHTML/TemplateTest.php @@ -15,7 +15,8 @@ #[CoversClass(Template::class)] class TemplateTest extends TestCase { - private const TEMPLATE = 'sandbox.twig'; + private const string TEMPLATE = 'sandbox.twig'; + public function testSetup(): void { @@ -24,6 +25,7 @@ public function testSetup(): void $this->assertEquals(self::TEMPLATE, $t->getTemplateName()); } + public function testNormalizeName(): void { $c = Configuration::loadFromArray(['assets' => [ 'salt' => '1234567890']], '', 'simplesaml'); @@ -31,6 +33,7 @@ public function testNormalizeName(): void $this->assertEquals(self::TEMPLATE, $t->getTemplateName()); } + public function testTemplateModuleNamespace(): void { $c = Configuration::loadFromArray(['assets' => [ 'salt' => '1234567890']], '', 'simplesaml'); @@ -38,6 +41,7 @@ public function testTemplateModuleNamespace(): void $this->assertEquals('core:welcome.twig', $t->getTemplateName()); } + public static function debugModeProvider(): array { return [ @@ -46,6 +50,7 @@ public static function debugModeProvider(): array ]; } + #[DataProvider('debugModeProvider')] public function testTemplateDebugMode(bool $debugMode): void { @@ -61,6 +66,7 @@ public function testTemplateDebugMode(bool $debugMode): void } } + public function testGetEntityDisplayNameBasic(): void { $c = Configuration::loadFromArray(['assets' => [ 'salt' => '1234567890']], '', 'simplesaml'); @@ -79,6 +85,7 @@ public function testGetEntityDisplayNameBasic(): void $this->assertEquals('Something', $name); } + public function testGetEntityDisplayNamePriorities(): void { $c = Configuration::loadFromArray(['assets' => [ 'salt' => '1234567890']], '', 'simplesaml'); @@ -116,6 +123,7 @@ public function testGetEntityDisplayNamePriorities(): void $this->assertEquals('UIname NL', $name); } + public function testGetEntityPropertyTranslation(): void { $c = Configuration::loadFromArray(['assets' => [ 'salt' => '1234567890']], '', 'simplesaml'); @@ -147,6 +155,7 @@ public function testGetEntityPropertyTranslation(): void $this->assertNull($name); } + public function testAssetModuleTagDoesNotMatchCoreTag(): void { $c = Configuration::loadFromArray(['assets' => [ 'salt' => '1234567890']], '', 'simplesaml'); @@ -165,6 +174,7 @@ public function testAssetModuleTagDoesNotMatchCoreTag(): void ); } + public function testAssetWillReturnPathOnTagIsFalse(): void { $c = Configuration::loadFromArray(['assets' => [ 'salt' => '1234567890']], '', 'simplesaml'); @@ -177,6 +187,7 @@ public function testAssetWillReturnPathOnTagIsFalse(): void ); } + public function testAssetDebugTagProduction(): void { echo "testAssetDebugTagProduction! \n"; diff --git a/tests/src/SimpleSAML/XHTML/TemplateTranslationTest.php b/tests/src/SimpleSAML/XHTML/TemplateTranslationTest.php index 45add235b2..0b24d4778c 100644 --- a/tests/src/SimpleSAML/XHTML/TemplateTranslationTest.php +++ b/tests/src/SimpleSAML/XHTML/TemplateTranslationTest.php @@ -34,16 +34,12 @@ public function testCoreCardinalityErrorTemplate(): void 'test 2' => [1, 2], ]; - $getContent = function (): string { - /** @var \SimpleSAML\XHTML\Template $this */ - return $this->getContents(); - }; - $html = $getContent->call($t); - + $html = $t->getContents(); $this->assertStringContainsString('got 0 values, want 1', $html); $this->assertStringContainsString('got 1 values, want 2', $html); } + public function testCoreLoginUserPassTemplate(): void { $c = Configuration::loadFromArray(['assets' => [ 'salt' => '1234567890']], '', 'simplesaml'); @@ -58,15 +54,11 @@ public function testCoreLoginUserPassTemplate(): void $t->data['AuthState'] = '_abc123'; $t->data['formURL'] = Module::getModuleURL('core/loginuserpass'); - $getContent = function (): string { - /** @var \SimpleSAML\XHTML\Template $this */ - return $this->getContents(); - }; - $html = $getContent->call($t); - + $html = $t->getContents(); $this->assertStringContainsString('value="h.c oersted"', $html); } + public function testCoreLogoutIframeTemplate(): void { $c = Configuration::loadFromArray(['assets' => [ 'salt' => '1234567890']], '', 'simplesaml'); @@ -92,16 +84,12 @@ public function testCoreLogoutIframeTemplate(): void ], ]; - $getContent = function (): string { - /** @var \SimpleSAML\XHTML\Template $this */ - return $this->getContents(); - }; - $html = $getContent->call($t); - + $html = $t->getContents(); $this->assertStringContainsString('You are now successfully logged out from ze testing service.', $html); $this->assertStringContainsString('ze missing service', $html); } + public function testAuthStatusTemplate(): void { $c = Configuration::loadFromArray(['assets' => [ 'salt' => '1234567890']], '', 'simplesaml'); @@ -113,18 +101,14 @@ public function testAuthStatusTemplate(): void $t->data['trackid'] = ''; $t->data['authData'] = false; - $getContent = function (): string { - /** @var \SimpleSAML\XHTML\Template $this */ - return $this->getContents(); - }; - $html = $getContent->call($t); - + $html = $t->getContents(); $this->assertStringContainsString( 'Your session is valid for ' . $t->data['remaining'] . ' seconds from now.', $html, ); } + public function testValidateTwigFiles(): void { $root = dirname(__DIR__, 4); diff --git a/tests/src/SimpleSAML/XML/ParserTest.php b/tests/src/SimpleSAML/XML/ParserTest.php index a64dde88be..e37dace2b5 100644 --- a/tests/src/SimpleSAML/XML/ParserTest.php +++ b/tests/src/SimpleSAML/XML/ParserTest.php @@ -20,13 +20,14 @@ #[CoversClass(Parser::class)] class ParserTest extends TestCase { - private const XMLDOC = <<< XML + private const string XMLDOC = <<< XML Hello, World! XML; + /** @var \SimpleSAML\XML\Parser */ private Parser $xml; @@ -35,7 +36,7 @@ class ParserTest extends TestCase */ protected function setUp(): void { - $this->xml = new Parser(static::XMLDOC); + $this->xml = new Parser(self::XMLDOC); } diff --git a/tests/src/SimpleSAML/XML/SignerTest.php b/tests/src/SimpleSAML/XML/SignerTest.php index a70108ffae..3988c77e3d 100644 --- a/tests/src/SimpleSAML/XML/SignerTest.php +++ b/tests/src/SimpleSAML/XML/SignerTest.php @@ -9,9 +9,9 @@ use Exception; use PHPUnit\Framework\Attributes\CoversClass; use ReflectionClass; -use SAML2\DOMDocumentFactory; use SimpleSAML\Configuration; use SimpleSAML\Test\SigningTestCase; +use SimpleSAML\XML\DOMDocumentFactory; use SimpleSAML\XML\Signer; /** @@ -49,7 +49,8 @@ class SignerTest extends SigningTestCase -----END CERTIFICATE----- NOWDOC; - private const OTHER_CERTIFICATE = 'other_certificate.pem'; + + private const string OTHER_CERTIFICATE = 'other_certificate.pem'; /** @@ -81,7 +82,7 @@ public function testSignBasic(): void { $node = DOMDocumentFactory::fromString('value'); - /** @psalm-var DOMElement $element */ + /** @psalm-var \DOMElement $element */ $element = $node->getElementsByTagName("node")->item(0); $doc = new DOMDocument(); @@ -120,7 +121,7 @@ public function testSignWithCertificate(): void { $node = DOMDocumentFactory::fromString('value'); - /** @psalm-var DOMElement $element */ + /** @psalm-var \DOMElement $element */ $element = $node->getElementsByTagName("node")->item(0); $doc = new DOMDocument(); @@ -148,7 +149,7 @@ public function testSignWithMultiCertificate(): void $node = DOMDocumentFactory::fromString('value'); - /** @psalm-var DOMElement $element */ + /** @psalm-var \DOMElement $element */ $element = $node->getElementsByTagName("node")->item(0); $doc = new DOMDocument(); @@ -177,7 +178,7 @@ public function testSignMissingPrivateKey(): void { $node = DOMDocumentFactory::fromString('value'); - /** @psalm-var DOMElement $element */ + /** @psalm-var \DOMElement $element */ $element = $node->getElementsByTagName("node")->item(0); $doc = new DOMDocument(); diff --git a/tests/src/SimpleSAML/XML/ValidatorTest.php b/tests/src/SimpleSAML/XML/ValidatorTest.php index 87220d75e4..8bcce88883 100644 --- a/tests/src/SimpleSAML/XML/ValidatorTest.php +++ b/tests/src/SimpleSAML/XML/ValidatorTest.php @@ -7,8 +7,8 @@ use DOMElement; use Exception; use PHPUnit\Framework\Attributes\CoversClass; -use SAML2\DOMDocumentFactory; use SimpleSAML\Test\SigningTestCase; +use SimpleSAML\XML\DOMDocumentFactory; use SimpleSAML\XML\Signer; use SimpleSAML\XML\Validator; @@ -35,7 +35,7 @@ public function testGetX509Certificate(): void { $doc = DOMDocumentFactory::fromString('value'); - /** @psalm-var DOMElement $node */ + /** @psalm-var \DOMElement $node */ $node = $doc->getElementsByTagName('node')->item(0); $signature_parent = $doc->appendChild(new DOMElement('signature_parent')); @@ -62,7 +62,7 @@ public function testIsNodeValidatedSuccess(): void { $doc = DOMDocumentFactory::fromString('value'); - /** @psalm-var DOMElement $node */ + /** @psalm-var \DOMElement $node */ $node = $doc->getElementsByTagName('node')->item(0); $signature_parent = $doc->appendChild(new DOMElement('signature_parent')); @@ -89,10 +89,10 @@ public function testIsNodeValidatedFailure(): void { $doc = DOMDocumentFactory::fromString('value1value2'); - /** @psalm-var DOMElement $node1 */ + /** @psalm-var \DOMElement $node1 */ $node1 = $doc->getElementsByTagName('node1')->item(0); - /** @psalm-var DOMElement $node2 */ + /** @psalm-var \DOMElement $node2 */ $node2 = $doc->getElementsByTagName('node2')->item(0); $signature_parent = $doc->appendChild(new DOMElement('signature_parent')); diff --git a/tests/www/IndexTest.php b/tests/www/IndexTest.php index 5be54e8431..83a50b3f41 100644 --- a/tests/www/IndexTest.php +++ b/tests/www/IndexTest.php @@ -107,6 +107,7 @@ public function testRedirection(): void ); } + /** * Test the frontpage.redirect config option */ @@ -125,6 +126,7 @@ public function testRedirectionFrontpageRedirectOption(): void ); } + /** * The tear down method that is executed after all tests in this class. */