diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99335ffd..3425c972 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: test: strategy: matrix: - php-version: [7.4, 8.1, 8.2, 8.3, 8.4] + php-version: [7.4, 8.1, 8.2, 8.3, 8.4, 8.5] runs-on: ubuntu-22.04 container: image: fsiopenpl/docker-php-apache:alpine-${{ matrix.php-version }} diff --git a/.gitignore b/.gitignore index 9695bea9..07ef76fd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /features/fixtures/project/var/mysql/* !/features/fixtures/project/var/.gitkeep !/features/fixtures/project/var/data/.gitkeep +/features/fixtures/project/config/reference.php /features/fixtures/project/config/parameters.yaml *.sqlite .phpcs-cache diff --git a/Behat/Context/AdminUserManagementContext.php b/Behat/Context/AdminUserManagementContext.php index 8523ed6c..ec79af98 100644 --- a/Behat/Context/AdminUserManagementContext.php +++ b/Behat/Context/AdminUserManagementContext.php @@ -15,6 +15,7 @@ use Behat\Gherkin\Node\TableNode; use Behat\Mink\Driver\BrowserKitDriver; use FriendsOfBehat\PageObjectExtension\Page\UnexpectedPageException; +use FriendsOfBehat\SymfonyExtension\Driver\SymfonyDriver; use FSi\Bundle\AdminSecurityBundle\Behat\Element\BatchAction; use FSi\Bundle\AdminSecurityBundle\Behat\Element\Datagrid; use FSi\Bundle\AdminSecurityBundle\Behat\Page\UserList; @@ -125,7 +126,7 @@ private function performBatchAction(string $action, int $cellIndex): void ] ]; - /** @var BrowserKitDriver $driver */ + /** @var SymfonyDriver $driver */ $driver = $this->getSession()->getDriver(); $batchActionUrl = $batchActionNode->getAttribute('value'); diff --git a/Behat/Context/DataContext.php b/Behat/Context/DataContext.php index 8c5328e1..8fac58ed 100644 --- a/Behat/Context/DataContext.php +++ b/Behat/Context/DataContext.php @@ -70,6 +70,7 @@ public function deleteDatabaseIfExists(): void */ public function thereIsUserWithRoleAndPassword(string $email, string $role, string $password): void { + Assertion::notEmpty($email); $user = new User(); $user->setUsername($email); $user->setEmail($email); @@ -95,6 +96,7 @@ public function thereAreFollowingUsers(TableNode $table): void $manager = $this->getEntityManager(); foreach ($table->getHash() as $userInfo) { + Assertion::notEmpty($userInfo['Email']); $user = new User(); $user->setUsername($userInfo['Email']); $user->setEmail($userInfo['Email']); diff --git a/Behat/Element/Form.php b/Behat/Element/Form.php index 0663927b..15818132 100644 --- a/Behat/Element/Form.php +++ b/Behat/Element/Form.php @@ -17,7 +17,7 @@ final class Form extends Element { - public function getField(string $locator): ?NodeElement + public function getField(string $locator): NodeElement { $field = $this->getElement('form')->findField($locator); if (null === $field) { diff --git a/EventListener/EnforcePasswordChangeListener.php b/EventListener/EnforcePasswordChangeListener.php index 199566e8..27c83976 100644 --- a/EventListener/EnforcePasswordChangeListener.php +++ b/EventListener/EnforcePasswordChangeListener.php @@ -66,7 +66,7 @@ public static function getSubscribedEvents(): array public function onKernelRequest(RequestEvent $event): void { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { + if (true !== $event->isMainRequest()) { return; } diff --git a/FSiAdminSecurityBundle.php b/FSiAdminSecurityBundle.php index 9e544802..8bf1208a 100644 --- a/FSiAdminSecurityBundle.php +++ b/FSiAdminSecurityBundle.php @@ -62,7 +62,7 @@ public function boot(): void public function getContainerExtension(): FSIAdminSecurityExtension { - if (null === $this->extension) { + if (false === $this->extension instanceof FSIAdminSecurityExtension) { $this->extension = new FSIAdminSecurityExtension(); } diff --git a/Security/User/User.php b/Security/User/User.php index 98d89b31..d53961e2 100644 --- a/Security/User/User.php +++ b/Security/User/User.php @@ -25,6 +25,9 @@ abstract class User implements UserInterface { protected ?int $id; + /** + * @var non-empty-string|null + */ protected ?string $username; protected ?string $email; protected bool $enabled; @@ -81,7 +84,7 @@ public function __serialize(): array /** * @param array{ * password: string|null, - * username: string|null, + * username: non-empty-string|null, * enabled: bool, * id: int|null, * } $serialized @@ -120,7 +123,7 @@ public function getUsername(): ?string public function getUserIdentifier(): string { - return $this->username ?? ''; + return $this->username ?? 'anonymous'; } public function getEmail(): ?string @@ -212,6 +215,9 @@ public function removeRole(string $role): void $this->roles = array_values($this->roles); } + /** + * @param non-empty-string $username + */ public function setUsername(string $username): void { $this->username = $username; diff --git a/composer.json b/composer.json index f603e6c3..0a543255 100644 --- a/composer.json +++ b/composer.json @@ -15,20 +15,20 @@ "beberlei/assert": "^3.3", "doctrine/doctrine-bundle": "^2.7", "doctrine/orm": "^2.7|^3.0", - "doctrine/persistence": "^2.5|^3.0", + "doctrine/persistence": "^2.5.7|^3.0|^4.0", "fsi/admin-bundle" : "^4.0", "fsi/data" : "^1.0.2", "psr/clock": "^1.0", - "symfony/console": "^4.4|^5.4|^6.0", - "symfony/dependency-injection": "^4.4|^5.4|^6.0", - "symfony/doctrine-bridge": "^4.4|^5.4|^6.0", - "symfony/form": "^4.4|^5.4|^6.0", - "symfony/framework-bundle": "^4.4|^5.4|^6.0", - "symfony/mailer": "^4.4|^5.4|^6.0", - "symfony/property-access": "^4.4|^5.4|^6.0", - "symfony/security-bundle": "^4.4|^5.4|^6.0", - "symfony/twig-bundle": "^4.4|^5.4|^6.0", - "symfony/validator": "^4.4|^5.4|^6.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^4.4|^5.4|^6.0|^7.0", + "symfony/doctrine-bridge": "^4.4|^5.4|^6.0|^7.0", + "symfony/form": "^4.4|^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^4.4|^5.4|^6.0|^7.0", + "symfony/mailer": "^4.4|^5.4|^6.0|^7.0", + "symfony/property-access": "^4.4|^5.4|^6.0|^7.0", + "symfony/security-bundle": "^4.4|^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^4.4|^5.4|^6.0|^7.0", + "symfony/validator": "^4.4|^5.4|^6.0|^7.0", "twig/twig": "^3.6" }, "require-dev": { @@ -36,39 +36,39 @@ "behat/behat": "^3.11", "behat/mink": "^1.10", "behat/mink-selenium2-driver": "^1.6", + "behat/mink-browserkit-driver": "^2.0", "caciobanu/behat-deprecation-extension": "^2.1", "egulias/email-validator": "^3.2|^4.0", "friendsofphp/proxy-manager-lts": "^1.0", - "friends-of-behat/mink-browserkit-driver": "^1.6.1", "friends-of-behat/mink-extension": "^2.7.4", "friends-of-behat/page-object-extension": "^0.3.2", "friends-of-behat/symfony-extension": "^2.4.1", - "fsi/resource-repository-bundle": "^2.2|^3.0.2@dev", + "fsi/resource-repository-bundle": "^2.2|^3.0.2|^4.0", "fsi/translatable": "^1.0", "ocramius/proxy-manager": "^2.5", - "phpspec/phpspec": "^7.0|^8.0@dev", - "phpstan/phpstan": "^1.10", - "phpstan/phpstan-beberlei-assert": "^1.0", + "phpspec/phpspec": "^7.0|^8.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-beberlei-assert": "^2.0", "squizlabs/php_codesniffer": "^3.7", - "symfony/dom-crawler": "^4.4|^5.4|^6.0", - "symfony/error-handler": "^4.4|^5.4|^6.0", - "symfony/event-dispatcher": "^4.4|^5.4|^6.0", - "symfony/http-kernel": "^4.4|^5.4|^6.0", - "symfony/monolog-bridge": "^4.4|^5.4|^6.0", + "symfony/dom-crawler": "^4.4|^5.4|^6.0|^7.0", + "symfony/error-handler": "^4.4|^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^4.4|^5.4|^6.0|^7.0", + "symfony/http-kernel": "^4.4|^5.4|^6.0|^7.0", + "symfony/monolog-bridge": "^4.4|^5.4|^6.0|^7.0", "symfony/monolog-bundle": "^3.8", - "symfony/routing": "^4.4|^5.4|^6.0", - "symfony/twig-bridge": "^4.4|^5.4|^6.0", - "symfony/var-dumper": "^4.4|^5.4|^6.0", + "symfony/routing": "^4.4|^5.4|^6.0|^7.0", + "symfony/twig-bridge": "^4.4|^5.4|^6.0|^7.0", + "symfony/var-dumper": "^4.4|^5.4|^6.0|^7.0", + "symfony/var-exporter": "^4.4|^5.4|^6.0|^7.0", "rize/uri-template": "^0.3.5|^0.4.0" }, "conflict": { "doctrine/doctrine-cache-bundle": "<1.4.0", "fsi/datagrid": "*", "fsi/datasource": "*", - "twig/twig": "<2.0", - "symfony/property-info": ">=7.0", + "symfony/property-info": ">=8.0", "symfony/expression-language": "<4.4", - "symfony/security-core": ">=7.0" + "symfony/security-core": ">=8.0" }, "config": { "bin-dir": "vendor/bin" @@ -94,7 +94,8 @@ }, "extra": { "branch-alias": { - "dev-master": "4.0-dev", + "dev-master": "4.1-dev", + "4.0": "4.0-dev", "3.2": "3.2-dev", "3.1": "3.1-dev", "3.0": "3.0-dev", diff --git a/features/fixtures/project/src/DependencyInjection/FSiFixturesExtension.php b/features/fixtures/project/src/DependencyInjection/FSiFixturesExtension.php index 30f081d2..cd66dd4c 100644 --- a/features/fixtures/project/src/DependencyInjection/FSiFixturesExtension.php +++ b/features/fixtures/project/src/DependencyInjection/FSiFixturesExtension.php @@ -11,6 +11,7 @@ namespace FSi\FixturesBundle\DependencyInjection; +use Composer\InstalledVersions; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -29,6 +30,10 @@ public function prepend(ContainerBuilder $container): void return; } + if (InstalledVersions::getVersion('symfony/security-bundle') >= '7.0.0') { + return; + } + $container->prependExtensionConfig('security', [ 'enable_authenticator_manager' => true ]); diff --git a/features/fixtures/project/src/Time/Clock.php b/features/fixtures/project/src/Time/Clock.php index eb47367c..bdee6038 100644 --- a/features/fixtures/project/src/Time/Clock.php +++ b/features/fixtures/project/src/Time/Clock.php @@ -17,7 +17,6 @@ final class Clock implements ClockInterface { private static ?DateTimeImmutable $now = null; - private static ?DateTimeImmutable $traveledAt = null; public function now(): DateTimeImmutable { @@ -25,24 +24,16 @@ public function now(): DateTimeImmutable return new DateTimeImmutable(); } - if (null === self::$traveledAt) { - return clone self::$now; - } - - // TODO this works reliably only for short periods like under 60 seconds - $interval = self::$traveledAt->diff(new DateTimeImmutable()); - return self::$now->add($interval); + return clone self::$now; } public function freeze(DateTimeImmutable $time): void { self::$now = $time; - self::$traveledAt = null; } public function return(): void { self::$now = null; - self::$traveledAt = null; } } diff --git a/phpstan.neon b/phpstan.neon index c64c9c57..30ae5ae2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,12 +7,11 @@ parameters: paths: - . excludePaths: - - DependencyInjection/Configuration - - '%rootDir%/../../../docker' - - '%rootDir%/../../../features/fixtures/project/var' - - '%rootDir%/../../../spec' - - '%rootDir%/../../../var' - - '%rootDir%/../../../vendor' + - DependencyInjection/Configuration.php + - features/fixtures/project/config + - features/fixtures/project/var + - spec + - vendor ignoreErrors: - '#Property FSi\\Bundle\\AdminSecurityBundle\\EventListener\\EncodePasswordListener::\$passwordHasherFactory has unknown class Symfony\\Component\\PasswordHasher\\Hasher\\PasswordHasherFactoryInterface as its type\.#' - '#Parameter \$passwordHasherFactory of method FSi\\Bundle\\AdminSecurityBundle\\EventListener\\EncodePasswordListener::__construct\(\) has invalid type Symfony\\Component\\PasswordHasher\\Hasher\\PasswordHasherFactoryInterface\.#' @@ -23,5 +22,11 @@ parameters: path: %currentWorkingDirectory%/FSiAdminSecurityBundle.php - message: '#Call to an undefined method Symfony\\Component\\HttpFoundation\\RequestStack\:\:get(Main|Master)Request\(\)\.#' path: %currentWorkingDirectory%/EventListener/LogoutUserListener.php + - message: '#Call to function method_exists\(\) with Symfony\\Component\\HttpFoundation\\RequestStack and ''get(Main|Master)Request'' will always evaluate to true\.#' + path: %currentWorkingDirectory%/EventListener/LogoutUserListener.php + - message: '#Call to function method_exists\(\) with Symfony\\Component\\Security\\Core\\User\\UserInterface and ''getUser(name|Identifier)'' will always evaluate to true\.#' + path: %currentWorkingDirectory%/Security/User/UserIdentifierHelper.php + - message: '#Call to function method_exists\(\) with Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface and ''getUser(name|Identifier)'' will always evaluate to true\.#' + path: %currentWorkingDirectory%/Security/User/UserIdentifierHelper.php - message: '#.*Symfony\\Component\\Routing\\RouteCollectionBuilder.*#' path: %currentWorkingDirectory%/features/fixtures/project/src/Kernel.php diff --git a/spec/FSi/Bundle/AdminSecurityBundle/EventListener/EnforcePasswordChangeListenerSpec.php b/spec/FSi/Bundle/AdminSecurityBundle/EventListener/EnforcePasswordChangeListenerSpec.php index 2b43fab6..78085ea8 100644 --- a/spec/FSi/Bundle/AdminSecurityBundle/EventListener/EnforcePasswordChangeListenerSpec.php +++ b/spec/FSi/Bundle/AdminSecurityBundle/EventListener/EnforcePasswordChangeListenerSpec.php @@ -20,7 +20,6 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; @@ -44,7 +43,7 @@ public function let( RouterInterface $router ): void { $event->getRequest()->willReturn($request); - $event->getRequestType()->willReturn(HttpKernelInterface::MASTER_REQUEST); + $event->isMainRequest()->willReturn(true); $firewallMap->getFirewallConfig($request)->willReturn($firewallConfig); $firewallConfig->getName()->willReturn(self::CONFIGURED_FIREWALL); $authorizationChecker->isGranted('IS_AUTHENTICATED_FULLY')->willReturn(true); @@ -74,7 +73,7 @@ public function it_does_nothing_if_not_master_request( RequestEvent $event, TokenStorageInterface $tokenStorage ): void { - $event->getRequestType()->willReturn(HttpKernelInterface::SUB_REQUEST); + $event->isMainRequest()->willReturn(false); $tokenStorage->getToken()->shouldNotBeCalled(); $event->setResponse(Argument::any())->shouldNotBeCalled();