diff --git a/.Build/composer.json b/.Build/composer.json index 33f4844..1f627a8 100644 --- a/.Build/composer.json +++ b/.Build/composer.json @@ -9,19 +9,19 @@ ], "require": { "derhansen/fe_change_pwd": "@dev", - "typo3/cms-core": "^12.4", + "typo3/cms-core": "^13.2 || dev-main", "ext-curl": "*", "ext-pdo": "*", "php": ">=8.1" }, "require-dev": { - "typo3/cms-backend": "^12.4", - "typo3/cms-frontend": "^12.4", - "typo3/cms-recordlist": "^12.4", - "typo3/cms-extbase": "^12.4", - "typo3/cms-fluid": "^12.4", + "typo3/cms-backend": "^13.2 || dev-main", + "typo3/cms-frontend": "^13.2 || dev-main", + "typo3/cms-recordlist": "^13.2 || dev-main", + "typo3/cms-extbase": "^13.2 || dev-main", + "typo3/cms-fluid": "^13.2 || dev-main", "typo3/cms-composer-installers": "^5.0", - "typo3/testing-framework": "^8.0.0 || dev-main", + "typo3/testing-framework": "^8.2.0 || dev-main", "friendsofphp/php-cs-fixer": "^3.12.0", "saschaegerer/phpstan-typo3": "^1.1.2", "phpstan/extension-installer": "^1.1" diff --git a/.Build/php-cs-fixer/.php-cs-fixer.php b/.Build/php-cs-fixer/.php-cs-fixer.php index 679104b..4d6d1bb 100644 --- a/.Build/php-cs-fixer/.php-cs-fixer.php +++ b/.Build/php-cs-fixer/.php-cs-fixer.php @@ -14,39 +14,45 @@ ->setRiskyAllowed(true) ->setRules([ '@DoctrineAnnotation' => true, - '@PER' => true, + // @todo: Switch to @PER-CS2.0 once php-cs-fixer's todo list is done: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7247 + '@PER-CS1.0' => true, + 'array_indentation' => true, 'array_syntax' => ['syntax' => 'short'], - 'blank_line_after_opening_tag' => true, - 'braces' => ['allow_single_line_closure' => true], 'cast_spaces' => ['space' => 'none'], - 'compact_nullable_typehint' => true, + // @todo: Can be dropped once we enable @PER-CS2.0 'concat_space' => ['spacing' => 'one'], 'declare_equal_normalize' => ['space' => 'none'], + 'declare_parentheses' => true, 'dir_constant' => true, - 'function_typehint_space' => true, - 'lowercase_cast' => true, - 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], + // @todo: Can be dropped once we enable @PER-CS2.0 + 'function_declaration' => [ + 'closure_fn_spacing' => 'none', + ], + 'function_to_constant' => ['functions' => ['get_called_class', 'get_class', 'get_class_this', 'php_sapi_name', 'phpversion', 'pi']], + 'type_declaration_spaces' => true, + 'global_namespace_import' => ['import_classes' => false, 'import_constants' => false, 'import_functions' => false], + 'list_syntax' => ['syntax' => 'short'], + // @todo: Can be dropped once we enable @PER-CS2.0 + 'method_argument_space' => true, + 'modernize_strpos' => true, 'modernize_types_casting' => true, 'native_function_casing' => true, - 'new_with_braces' => true, 'no_alias_functions' => true, 'no_blank_lines_after_phpdoc' => true, 'no_empty_phpdoc' => true, 'no_empty_statement' => true, 'no_extra_blank_lines' => true, - 'no_leading_import_slash' => true, 'no_leading_namespace_whitespace' => true, 'no_null_property_initialization' => true, 'no_short_bool_cast' => true, 'no_singleline_whitespace_before_semicolons' => true, 'no_superfluous_elseif' => true, - 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_comma_in_singleline' => true, 'no_unneeded_control_parentheses' => true, 'no_unused_imports' => true, 'no_useless_else' => true, 'no_useless_nullsafe_operator' => true, - 'no_whitespace_in_blank_line' => true, - 'ordered_imports' => true, + 'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], 'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']], 'php_unit_mock_short_will_return' => true, 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], @@ -59,8 +65,10 @@ 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 'return_type_declaration' => ['space_before' => 'none'], 'single_quote' => true, + 'single_space_around_construct' => true, 'single_line_comment_style' => ['comment_types' => ['hash']], - 'single_trait_insert_per_statement' => true, + // @todo: Can be dropped once we enable @PER-CS2.0 + 'single_line_empty_body' => true, 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 'whitespace_after_comma_in_array' => ['ensure_single_space' => true], 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], diff --git a/.Build/phpstan/phpstan.neon b/.Build/phpstan/phpstan.neon index cbea7b6..759a33e 100644 --- a/.Build/phpstan/phpstan.neon +++ b/.Build/phpstan/phpstan.neon @@ -7,5 +7,9 @@ parameters: - ../../Classes/ - ../../Configuration/ - checkMissingIterableValueType: false - reportUnmatchedIgnoredErrors: true \ No newline at end of file + ignoreErrors: + - identifier: missingType.iterableValue + reportUnmatchedIgnoredErrors: true + typo3: + requestGetAttributeMapping: + frontend.page.information: \TYPO3\CMS\Frontend\Page\PageInformation diff --git a/.github/workflows/CodeQuality.yml b/.github/workflows/CodeQuality.yml index 61ed605..0177402 100644 --- a/.github/workflows/CodeQuality.yml +++ b/.github/workflows/CodeQuality.yml @@ -10,8 +10,9 @@ jobs: strategy: matrix: env: - - { php: 8.1} - { php: 8.2} + - { php: 8.3} + - { php: 8.4} env: ${{ matrix.env }} @@ -44,4 +45,4 @@ jobs: - name: Run phpstan checks run: | - .Build/vendor/bin/phpstan --configuration=.Build/phpstan/phpstan.neon \ No newline at end of file + .Build/vendor/bin/phpstan --configuration=.Build/phpstan/phpstan.neon diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 8b0c611..6f477c1 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -8,8 +8,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - typo3: ['^12.4'] - php: ['8.1', '8.2'] + typo3: ['^13.2', 'dev-main'] + php: ['8.2', '8.3', '8.4'] steps: - uses: actions/checkout@v3 @@ -37,4 +37,4 @@ jobs: git checkout composer.json - name: Unit Tests - run: .Build/vendor/bin/phpunit --colors -c .Build/phpunit/UnitTests.xml + run: .Build/vendor/bin/phpunit -c .Build/phpunit/UnitTests.xml diff --git a/Classes/Controller/PasswordController.php b/Classes/Controller/PasswordController.php index fa55d67..0dd2731 100644 --- a/Classes/Controller/PasswordController.php +++ b/Classes/Controller/PasswordController.php @@ -14,10 +14,10 @@ use Derhansen\FeChangePwd\Domain\Model\Dto\ChangePassword; use Derhansen\FeChangePwd\Event\AfterPasswordUpdatedEvent; use Derhansen\FeChangePwd\Exception\InvalidEmailAddressException; -use Derhansen\FeChangePwd\Exception\MissingFeatureToggleException; use Derhansen\FeChangePwd\Service\FrontendUserService; +use Derhansen\FeChangePwd\Service\SettingsService; use Psr\Http\Message\ResponseInterface; -use TYPO3\CMS\Core\Configuration\Features; +use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Extbase\Annotation as Extbase; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; @@ -28,29 +28,20 @@ class PasswordController extends ActionController { public function __construct( protected readonly FrontendUserService $frontendUserService, - protected readonly Features $features, + protected readonly SettingsService $settingsService, ) {} - public function initializeAction(): void - { - if (!$this->features->isFeatureEnabled('security.usePasswordPolicyForFrontendUsers')) { - throw new MissingFeatureToggleException( - 'Extension fe_change_pwd relies on TYPO3 password policies being enabled. Please activate security.usePasswordPolicyForFrontendUsers feature toggle.', - 1683482651 - ); - } - } - /** * Edit action */ public function editAction(): ResponseInterface { $changePassword = new ChangePassword(); - $changePassword->setChangeHmac($this->frontendUserService->getChangeHmac()); + $changePassword->setChangeHmac($this->frontendUserService->getChangeHmac($this->request)); $this->view->assignMultiple([ - 'changePasswordReason' => $this->frontendUserService->getMustChangePasswordReason(), + 'changePasswordReason' => $this->frontendUserService->getMustChangePasswordReason($this->request), 'changePassword' => $changePassword, + 'siteSettings' => $this->settingsService->getSiteSettings($this->request), ]); return $this->htmlResponse(); @@ -63,7 +54,7 @@ public function initializeUpdateAction(): void { $changePasswordArray = $this->request->getArgument('changePassword'); $changeHmac = $changePasswordArray['changeHmac'] ? (string)$changePasswordArray['changeHmac'] : ''; - if (!$this->frontendUserService->validateChangeHmac($changeHmac)) { + if (!$this->frontendUserService->validateChangeHmac($this->request, $changeHmac)) { throw new InvalidHashException( 'Possible CSRF detected. Ensure a valid "changeHmac" is provided.', 1572672118 @@ -78,7 +69,7 @@ public function initializeUpdateAction(): void */ public function updateAction(ChangePassword $changePassword): ResponseInterface { - $this->frontendUserService->updatePassword($changePassword->getPassword1(), $this->settings); + $this->frontendUserService->updatePassword($this->request, $changePassword->getPassword1(), $this->settings); $this->eventDispatcher->dispatch(new AfterPasswordUpdatedEvent($changePassword, $this)); @@ -123,4 +114,9 @@ protected function getErrorFlashMessage(): bool { return false; } + + protected function getFrontendUser(): AbstractUserAuthentication + { + return $this->request->getAttribute('frontend.user'); + } } diff --git a/Classes/Exception/InvalidEmailAddressException.php b/Classes/Exception/InvalidEmailAddressException.php index 0ac0764..1ca28c0 100644 --- a/Classes/Exception/InvalidEmailAddressException.php +++ b/Classes/Exception/InvalidEmailAddressException.php @@ -4,6 +4,4 @@ namespace Derhansen\FeChangePwd\Exception; -use Exception; - -class InvalidEmailAddressException extends Exception {} +class InvalidEmailAddressException extends \Exception {} diff --git a/Classes/Exception/InvalidUserException.php b/Classes/Exception/InvalidUserException.php index 461b6e8..7a7c1a9 100644 --- a/Classes/Exception/InvalidUserException.php +++ b/Classes/Exception/InvalidUserException.php @@ -4,6 +4,4 @@ namespace Derhansen\FeChangePwd\Exception; -use Exception; - -class InvalidUserException extends Exception {} +class InvalidUserException extends \Exception {} diff --git a/Classes/Exception/MissingFeatureToggleException.php b/Classes/Exception/MissingFeatureToggleException.php index 7bb97cf..6c176d4 100644 --- a/Classes/Exception/MissingFeatureToggleException.php +++ b/Classes/Exception/MissingFeatureToggleException.php @@ -4,6 +4,4 @@ namespace Derhansen\FeChangePwd\Exception; -use Exception; - -class MissingFeatureToggleException extends Exception {} +class MissingFeatureToggleException extends \Exception {} diff --git a/Classes/Exception/MissingPasswordHashServiceException.php b/Classes/Exception/MissingPasswordHashServiceException.php index 4359952..9909211 100644 --- a/Classes/Exception/MissingPasswordHashServiceException.php +++ b/Classes/Exception/MissingPasswordHashServiceException.php @@ -4,6 +4,4 @@ namespace Derhansen\FeChangePwd\Exception; -use Exception; - -class MissingPasswordHashServiceException extends Exception {} +class MissingPasswordHashServiceException extends \Exception {} diff --git a/Classes/Exception/NoChangePasswordPidException.php b/Classes/Exception/NoChangePasswordPidException.php index 46666fe..426fce9 100644 --- a/Classes/Exception/NoChangePasswordPidException.php +++ b/Classes/Exception/NoChangePasswordPidException.php @@ -4,6 +4,4 @@ namespace Derhansen\FeChangePwd\Exception; -use Exception; - -class NoChangePasswordPidException extends Exception {} +class NoChangePasswordPidException extends \Exception {} diff --git a/Classes/Middleware/ForcePasswordChangeRedirect.php b/Classes/Middleware/ForcePasswordChangeRedirect.php index 8366e0b..73bbfed 100644 --- a/Classes/Middleware/ForcePasswordChangeRedirect.php +++ b/Classes/Middleware/ForcePasswordChangeRedirect.php @@ -19,9 +19,8 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use TYPO3\CMS\Core\Http\RedirectResponse; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Utility\MathUtility; -use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; +use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Site\Entity\SiteLanguage; /** * This middleware redirects the current frontend user to a configured page if the user must change the password @@ -39,49 +38,50 @@ public function __construct( */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $typoScriptFrontendController = $request->getAttribute('frontend.controller'); + $pageInformation = $request->getAttribute('frontend.page.information'); $frontendUser = $request->getAttribute('frontend.user'); - $pageUid = $typoScriptFrontendController->id; + $pageUid = $pageInformation->getId(); // Early return, if no frontend user if (!isset($frontendUser->user['uid'])) { return $handler->handle($request); } - $settings = $this->settingsService->getSettings($request); + $siteSettings = $this->settingsService->getSiteSettings($request); // Early return if page is excluded from redirect or user is not forced to change the password - if (!$this->frontendUserService->mustChangePassword($frontendUser->user) || - $this->pageAccessService->isExcludePage($pageUid, $settings) + if (!$this->frontendUserService->mustChangePassword($request, $frontendUser->user) || + $this->pageAccessService->isExcludePage($pageUid, $siteSettings) ) { return $handler->handle($request); } - switch ($this->pageAccessService->getRedirectMode($settings)) { + switch ($this->pageAccessService->getRedirectMode($siteSettings)) { case 'allAccessProtectedPages': - $mustRedirect = $this->pageAccessService->isAccessProtectedPageInRootline($typoScriptFrontendController->rootLine); + $mustRedirect = $this->pageAccessService->isAccessProtectedPageInRootline( + $pageInformation->getLocalRootLine() + ); break; case 'includePageUids': - $mustRedirect = $this->pageAccessService->isIncludePage($pageUid, $settings); + $mustRedirect = $this->pageAccessService->isIncludePage($pageUid, $siteSettings); break; default: $mustRedirect = false; } if ($mustRedirect) { - $typoScriptFrontendController->calculateLinkVars($request->getQueryParams()); - $parameter = $this->pageAccessService->getRedirectPid($settings); - if (MathUtility::canBeInterpretedAsInteger($pageUid) && - $typoScriptFrontendController->getPageArguments()->getPageType() - ) { - $parameter .= ',' . $typoScriptFrontendController->getPageArguments()->getPageType(); - } - $url = GeneralUtility::makeInstance(ContentObjectRenderer::class, $typoScriptFrontendController)->typoLink_URL([ - 'parameter' => $parameter, - 'addQueryString' => true, - 'addQueryString.' => ['exclude' => 'id'], - 'forceAbsoluteUrl' => true, - ]); + $redirectPid = $this->pageAccessService->getRedirectPid($siteSettings); + + /** @var SiteLanguage $language */ + $language = $request->getAttribute('language'); + + /** @var Site $site */ + $site = $request->getAttribute('site'); + $router = $site->getRouter(); + + $parameter = ['_language' => $language]; + // @todo: Provide PSR-14 event to modify parameter + $url = (string)$router->generateUri($redirectPid, $parameter); return new RedirectResponse($url, 307); } diff --git a/Classes/Service/FrontendUserService.php b/Classes/Service/FrontendUserService.php index db6d716..12352ff 100644 --- a/Classes/Service/FrontendUserService.php +++ b/Classes/Service/FrontendUserService.php @@ -14,15 +14,17 @@ use Derhansen\FeChangePwd\Exception\InvalidEmailAddressException; use Derhansen\FeChangePwd\Exception\InvalidUserException; use Derhansen\FeChangePwd\Exception\MissingPasswordHashServiceException; +use Psr\Http\Message\ServerRequestInterface; use Symfony\Component\Mime\Address; use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Crypto\HashService; use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory; +use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Mail\FluidEmail; use TYPO3\CMS\Core\Mail\MailerInterface; use TYPO3\CMS\Core\Session\SessionManager; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Mvc\RequestInterface; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; /** @@ -34,19 +36,20 @@ class FrontendUserService public function __construct( protected readonly SettingsService $settingsService, - protected readonly Context $context + protected readonly Context $context, + protected readonly HashService $hashService ) {} /** * Returns if the frontend user must change the password */ - public function mustChangePassword(array $feUserRecord): bool + public function mustChangePassword(ServerRequestInterface $request, array $feUserRecord): bool { $reason = ''; $result = false; - $mustChangePassword = $feUserRecord['must_change_password'] ?? 0; + $mustChangePassword = (bool)($feUserRecord['must_change_password'] ?? 0); $passwordExpiryTimestamp = $feUserRecord['password_expiry_date'] ?? 0; - if ((bool)$mustChangePassword) { + if ($mustChangePassword) { $reason = 'forcedChange'; $result = true; } elseif (((int)$passwordExpiryTimestamp > 0 && (int)$passwordExpiryTimestamp < time())) { @@ -56,8 +59,9 @@ public function mustChangePassword(array $feUserRecord): bool if ($result) { // Store reason for password change in user session - $this->getFrontendUser()->setKey('ses', self::SESSION_KEY, $reason); - $this->getFrontendUser()->storeSessionData(); + $frontendUser = $this->getFrontendUser($request); + $frontendUser->setKey('ses', self::SESSION_KEY, $reason); + $frontendUser->storeSessionData(); } return $result; } @@ -65,15 +69,15 @@ public function mustChangePassword(array $feUserRecord): bool /** * Returns the reason for the password change stored in the session */ - public function getMustChangePasswordReason(): string + public function getMustChangePasswordReason(ServerRequestInterface $request): string { - return (string)$this->getFrontendUser()->getKey('ses', self::SESSION_KEY); + return (string)$this->getFrontendUser($request)->getKey('ses', self::SESSION_KEY); } /** * Updates the password of the current user if a current user session exist */ - public function updatePassword(string $newPassword, array $settings): void + public function updatePassword(ServerRequestInterface $request, string $newPassword, array $settings): void { if (!$this->isUserLoggedIn()) { return; @@ -81,8 +85,8 @@ public function updatePassword(string $newPassword, array $settings): void $password = $this->getPasswordHash($newPassword); - $userTable = $this->getFrontendUser()->user_table; - $userUid = $this->getFrontendUser()->user['uid']; + $userTable = $this->getFrontendUser($request)->user_table; + $userUid = $this->getFrontendUser($request)->user['uid']; $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($userTable); $queryBuilder->getRestrictions()->removeAll(); $queryBuilder->update($userTable) @@ -95,58 +99,61 @@ public function updatePassword(string $newPassword, array $settings): void ->where( $queryBuilder->expr()->eq( 'uid', - $queryBuilder->createNamedParameter($userUid, \PDO::PARAM_INT) + $queryBuilder->createNamedParameter($userUid, Connection::PARAM_INT) ) ) ->executeStatement(); // Unset reason for password change in user session - $this->getFrontendUser()->setKey('ses', self::SESSION_KEY, null); - $this->getFrontendUser()->storeSessionData(); + $frontendUser = $this->getFrontendUser($request); + $frontendUser->setKey('ses', self::SESSION_KEY, null); + $frontendUser->storeSessionData(); // Destroy all sessions of the user except the current one $sessionManager = GeneralUtility::makeInstance(SessionManager::class); $sessionBackend = $sessionManager->getSessionBackend('FE'); $sessionManager->invalidateAllSessionsByUserId( $sessionBackend, - (int)$this->getFrontendUser()->user['uid'], - $this->getFrontendUser() + (int)$frontendUser->user['uid'], + $frontendUser ); } /** * Returns the changeHmac for the current logged in user */ - public function getChangeHmac(): string + public function getChangeHmac(ServerRequestInterface $request): string { if (!$this->isUserLoggedIn()) { return ''; } - $userUid = $this->getFrontendUser()->user['uid']; + $frontendUser = $this->getFrontendUser($request); + $userUid = $frontendUser->user['uid']; if (!is_int($userUid) || (int)$userUid <= 0) { throw new InvalidUserException('The fe_user uid is not a positive number.', 1574102778917); } - $tstamp = $this->getFrontendUser()->user['tstamp']; - return GeneralUtility::hmac('fe_user_' . $userUid . '_' . $tstamp, 'fe_change_pwd'); + $tstamp = $frontendUser->user['tstamp']; + return $this->hashService->hmac('fe_user_' . $userUid . '_' . $tstamp, 'fe_change_pwd'); } /** * Validates the given changeHmac */ - public function validateChangeHmac(string $changeHmac): bool + public function validateChangeHmac(ServerRequestInterface $request, string $changeHmac): bool { - return $changeHmac !== '' && hash_equals($this->getChangeHmac(), $changeHmac); + return $changeHmac !== '' && hash_equals($this->getChangeHmac($request), $changeHmac); } /** * Generates the change password code, saves it to the current frontend user record and sends an email * containing the change password code to the user */ - public function sendChangePasswordCodeEmail(array $settings, RequestInterface $request): void + public function sendChangePasswordCodeEmail(array $settings, ServerRequestInterface $request): void { - $recipientEmail = $this->getFrontendUser()->user['email'] ?? ''; + $frontendUser = $this->getFrontendUser($request); + $recipientEmail = $frontendUser->user['email'] ?? ''; if (!GeneralUtility::validEmail($recipientEmail)) { throw new InvalidEmailAddressException('Email address of frontend user is not valid'); } @@ -155,22 +162,22 @@ public function sendChangePasswordCodeEmail(array $settings, RequestInterface $r $validUntil = (new \DateTime()) ->modify('+' . ($settings['requireChangePasswordCode']['validityInMinutes'] ?? 5) . ' minutes'); - $userTable = $this->getFrontendUser()->user_table; - $userUid = $this->getFrontendUser()->user['uid']; + $userTable = $frontendUser->user_table; + $userUid = $frontendUser->user['uid']; $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($userTable); $queryBuilder->getRestrictions()->removeAll(); $queryBuilder->update($userTable) - ->set('change_password_code_hash', GeneralUtility::hmac($changePasswordCode, self::class)) + ->set('change_password_code_hash', $this->hashService->hmac($changePasswordCode, self::class)) ->set('change_password_code_expiry_date', $validUntil->getTimestamp()) ->where( $queryBuilder->expr()->eq( 'uid', - $queryBuilder->createNamedParameter($userUid, \PDO::PARAM_INT) + $queryBuilder->createNamedParameter($userUid, Connection::PARAM_INT) ) ) ->executeStatement(); - $userData = $this->getFrontendUser()->user; + $userData = $frontendUser->user; unset($userData['password']); $email = GeneralUtility::makeInstance(FluidEmail::class); @@ -211,19 +218,13 @@ protected function getPasswordHash(string $password): string return $password; } - /** - * Returns is there is a current user login - */ public function isUserLoggedIn(): bool { return $this->context->getAspect('frontend.user')->isLoggedIn(); } - /** - * Returns the frontendUserAuthentication - */ - protected function getFrontendUser(): FrontendUserAuthentication + protected function getFrontendUser(ServerRequestInterface $request): FrontendUserAuthentication { - return $GLOBALS['TSFE']->fe_user; + return $request->getAttribute('frontend.user'); } } diff --git a/Classes/Service/PageAccessService.php b/Classes/Service/PageAccessService.php index 603694f..a0c014f 100644 --- a/Classes/Service/PageAccessService.php +++ b/Classes/Service/PageAccessService.php @@ -26,11 +26,13 @@ class PageAccessService /** * Returns the redirect mode */ - public function getRedirectMode(array $settings): string + public function getRedirectMode(array $siteSettings): string { - if (($settings['redirect']['allAccessProtectedPages'] ?? false)) { + if (($siteSettings['redirect']['allAccessProtectedPages'] ?? false)) { $redirectMode = 'allAccessProtectedPages'; - } elseif (isset($settings['redirect']['includePageUids']) && $settings['redirect']['includePageUids'] !== '') { + } elseif (isset($siteSettings['redirect']['includePageUids']) && + $siteSettings['redirect']['includePageUids'] !== '' + ) { $redirectMode = 'includePageUids'; } else { $redirectMode = ''; @@ -41,26 +43,28 @@ public function getRedirectMode(array $settings): string /** * Returns the configured redirect PID */ - public function getRedirectPid(array $settings): int + public function getRedirectPid(array $siteSettings): int { - if (!isset($settings['changePasswordPid']) || (int)$settings['changePasswordPid'] === 0) { + if (!isset($siteSettings['changePasswordPid']) || (int)$siteSettings['changePasswordPid'] === 0) { throw new NoChangePasswordPidException( - 'settings.changePasswordPid is not set or zero', - 1580040840163 + 'Site setting fe_change_pwd.changePasswordPid is not defined or zero', + 1580040840 ); } - return (int)$settings['changePasswordPid']; + return (int)$siteSettings['changePasswordPid']; } /** * Returns, if the given page uid is configured as included for redirects */ - public function isIncludePage(int $pageUid, array $settings): bool + public function isIncludePage(int $pageUid, array $siteSettings): bool { - if (isset($settings['redirect']['includePageUids']) && $settings['redirect']['includePageUids'] !== '') { + if (isset($siteSettings['redirect']['includePageUids']) && + $siteSettings['redirect']['includePageUids'] !== '' + ) { $includePids = $this->extendPidListByChildren( - $settings['redirect']['includePageUids'], - (int)$settings['redirect']['includePageUidsRecursionLevel'] + $siteSettings['redirect']['includePageUids'], + (int)$siteSettings['redirect']['includePageUidsRecursionLevel'] ); $includePids = GeneralUtility::intExplode(',', $includePids, true); } else { @@ -72,19 +76,21 @@ public function isIncludePage(int $pageUid, array $settings): bool /** * Returns, if the given page uid is configured as excluded from redirects */ - public function isExcludePage(int $pageUid, array $settings): bool + public function isExcludePage(int $pageUid, array $siteSettings): bool { - if (isset($settings['redirect']['excludePageUids']) && $settings['redirect']['excludePageUids'] !== '') { + if (isset($siteSettings['redirect']['excludePageUids']) && + $siteSettings['redirect']['excludePageUids'] !== '' + ) { $excludePids = $this->extendPidListByChildren( - $settings['redirect']['excludePageUids'], - (int)$settings['redirect']['excludePageUidsRecursionLevel'] + $siteSettings['redirect']['excludePageUids'], + (int)$siteSettings['redirect']['excludePageUidsRecursionLevel'] ); $excludePids = GeneralUtility::intExplode(',', $excludePids, true); } else { $excludePids = []; } // Always add the changePasswordPid as exclude PID - $excludePids[] = (int)$settings['changePasswordPid']; + $excludePids[] = (int)$siteSettings['changePasswordPid']; return in_array($pageUid, $excludePids, true); } diff --git a/Classes/Service/SettingsService.php b/Classes/Service/SettingsService.php index cc36c4c..77bf680 100644 --- a/Classes/Service/SettingsService.php +++ b/Classes/Service/SettingsService.php @@ -12,46 +12,39 @@ namespace Derhansen\FeChangePwd\Service; use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Core\Context\Context; -use TYPO3\CMS\Core\Context\TypoScriptAspect; +use TYPO3\CMS\Core\Site\Entity\Site; use TYPO3\CMS\Core\Utility\GeneralUtility; /** - * Service with helper function for settings handling + * Service with helper function for handling typoscript settings */ class SettingsService { protected array $settings = []; /** - * Returns the settings + * Returns TypoScript settings */ - public function getSettings(ServerRequestInterface $request): array + public function getTypoScriptSettings(ServerRequestInterface $request): array { if (empty($this->settings)) { - try { - $fullTypoScript = $request->getAttribute('frontend.typoscript')->getSetupArray(); - } catch (\Exception $e) { - // An exception is thrown, when TypoScript setup array is not available. This is usually the case, - // when the current page request is cached. Therefore, the TSFE TypoScript parsing is forced here. - - // Set a TypoScriptAspect which forces template parsing - GeneralUtility::makeInstance(Context::class) - ->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, true)); - - // Call TSFE getFromCache, which re-processes TypoScript respecting $forcedTemplateParsing property - // from TypoScriptAspect - $tsfe = $request->getAttribute('frontend.controller'); - $requestWithFullTypoScript = $tsfe->getFromCache($request); - - $fullTypoScript = $requestWithFullTypoScript->getAttribute('frontend.typoscript')->getSetupArray(); - } + $fullTypoScript = $request->getAttribute('frontend.typoscript')->getSetupArray(); $settings = $fullTypoScript['plugin.']['tx_fechangepwd.']['settings.'] ?? []; $this->settings = GeneralUtility::removeDotsFromTS($settings); } return $this->settings; } + /** + * Returns site settings for the extension + */ + public function getSiteSettings(ServerRequestInterface $request): array + { + /** @var Site $site */ + $site = $request->getAttribute('site'); + return $site->getSettings()->get('fe_change_pwd'); + } + /** * Returns the password expiry timestamp depending on the configured setting switch. If password expiry is not * enabled, 0 is returned. If no password validity in days is configured, 90 days is taken as fallback diff --git a/Classes/Validation/Validator/ChangePasswordValidator.php b/Classes/Validation/Validator/ChangePasswordValidator.php index 1184013..999d205 100644 --- a/Classes/Validation/Validator/ChangePasswordValidator.php +++ b/Classes/Validation/Validator/ChangePasswordValidator.php @@ -17,8 +17,8 @@ use Derhansen\FeChangePwd\Service\OldPasswordService; use Derhansen\FeChangePwd\Service\SettingsService; use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; +use TYPO3\CMS\Core\Crypto\HashService; use TYPO3\CMS\Core\PasswordPolicy\Event\EnrichPasswordValidationContextDataEvent; use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction; use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator; @@ -35,6 +35,7 @@ class ChangePasswordValidator extends AbstractValidator protected LocalizationService $localizationService; protected OldPasswordService $oldPasswordService; protected EventDispatcherInterface $eventDispatcher; + protected HashService $hashService; public function __construct(array $options = []) { @@ -42,6 +43,7 @@ public function __construct(array $options = []) $this->localizationService = GeneralUtility::makeInstance(LocalizationService::class); $this->oldPasswordService = GeneralUtility::makeInstance(OldPasswordService::class); $this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $this->hashService = GeneralUtility::makeInstance(HashService::class); } /** @@ -51,7 +53,7 @@ public function __construct(array $options = []) */ protected function isValid($value): void { - $settings = $this->settingsService->getSettings($this->getRequest()); + $settings = $this->settingsService->getTypoScriptSettings($this->getRequest()); // Early return if old password is required, but either empty or not valid if (isset($settings['requireCurrentPassword']['enabled']) && @@ -157,7 +159,7 @@ protected function evaluateRequireCurrentPassword(ChangePassword $changePassword protected function evaluateChangePasswordCode(ChangePassword $changePassword): bool { $currentHash = $this->getFrontendUser()->user['change_password_code_hash'] ?? ''; - $calculatedHash = GeneralUtility::hmac($changePassword->getChangePasswordCode(), FrontendUserService::class); + $calculatedHash = $this->hashService->hmac($changePassword->getChangePasswordCode(), FrontendUserService::class); $expirationTime = (int)($this->getFrontendUser()->user['change_password_code_expiry_date'] ?? 0); if (empty($changePassword->getChangePasswordCode())) { diff --git a/Configuration/Sets/FeChangePwd/config.yaml b/Configuration/Sets/FeChangePwd/config.yaml new file mode 100644 index 0000000..885d6c7 --- /dev/null +++ b/Configuration/Sets/FeChangePwd/config.yaml @@ -0,0 +1,2 @@ +name: derhansen/fe_change_pwd +label: Change password for frontend users diff --git a/Configuration/Sets/FeChangePwd/settings.definitions.yaml b/Configuration/Sets/FeChangePwd/settings.definitions.yaml new file mode 100644 index 0000000..eb9a496 --- /dev/null +++ b/Configuration/Sets/FeChangePwd/settings.definitions.yaml @@ -0,0 +1,31 @@ +settings: + fe_change_pwd.changePasswordPid: + default: 0 + label: 'Change password Page' + type: int + description: 'The pid to redirect to if a password change is required (usually the page with the plugin of the extension)' + fe_change_pwd.redirect.allAccessProtectedPages: + default: true + label: 'Redirect for all access protected pages' + type: bool + description: 'If true, a redirect to the configured changePasswordPid will be forced for all access protected pages, if the user password has expired. Overrides includePageUids setting!' + fe_change_pwd.redirect.includePageUids: + default: '' + label: 'Include Page UIDs' + type: string + description: 'If set, a redirect to the configured changePasswordPid will be forced for the configured PIDs' + fe_change_pwd.redirect.includePageUidsRecursionLevel: + default: 0 + label: 'Include Page UIDs recursion level' + type: int + description: 'If > 0, all page uids configured in includePageUids will be resolved by the given recursion level' + fe_change_pwd.redirect.excludePageUids: + default: '' + label: 'Exclude Page UIDs' + type: string + description: 'List of PIDs to exclude from redirect checks' + fe_change_pwd.redirect.excludePageUidsRecursionLevel: + default: 0 + label: 'Exclude Page UIDs recursion level' + type: int + description: 'If > 0, all page uids configured in excludePageUids will be resolved by the given recursion level' diff --git a/Configuration/Sets/FeChangePwd/setup.typoscript b/Configuration/Sets/FeChangePwd/setup.typoscript new file mode 100644 index 0000000..ed77dc2 --- /dev/null +++ b/Configuration/Sets/FeChangePwd/setup.typoscript @@ -0,0 +1 @@ +@import 'EXT:fe_change_pwd/Configuration/TypoScript/setup.typoscript' diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript index 04838aa..f7a59b8 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.typoscript @@ -17,9 +17,6 @@ plugin.tx_fechangepwd { } } settings { - # The pid to redirect to if a password change is required (usually the page with the plugin of the extension) - changePasswordPid = - # If enabled, it is required to enter the current password in order to set a new one requireCurrentPassword { enabled = 1 @@ -39,24 +36,6 @@ plugin.tx_fechangepwd { validityInDays = 90 } - redirect { - # If set, a redirect to the configured changePasswordPid will be forced for all access protected pages - # Overrides includePageUids setting! - allAccessProtectedPages = 1 - - # If set, a redirect to the configured changePasswordPid will be forced for the configured PIDs - includePageUids = - - # If > 0, all page uids configured in includePageUids will be resolved by the given recursion level - includePageUidsRecursionLevel = 0 - - # List of PIDs to exclude from redirect checks - excludePageUids = - - # If > 0, all page uids configured in excludePageUids will be resolved by the given recursion level - excludePageUidsRecursionLevel = 0 - } - # What to do after a successful password change (allowed values are "redirect" or "view") afterPasswordChangeAction = redirect } diff --git a/README.md b/README.md index 407cb86..e9c31ad 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,18 @@ a weak password. 1) Install the extension from the TYPO3 Extension Repository or using composer and add the Static Typoscript "Change password for frontend users" to your TypoScript template. -2) Create a new page and make sure, that the page is only visible to logged in frontend users. +2) Add the site set "Change password for frontend users" to your site -3) Add the Plugin "Change Frontend User Password" to the page created in step 2 +3) Create a new page and make sure, that the page is only visible to logged in frontend users. -4) Change TypoScript settings to your needs. Please note, that if you want to use the password change enforcement, -you **must** set `settings.changePasswordPid` to the page uid of the page created in step 2 +4) Add the Plugin "Change Frontend User Password" to the page created in step 2 -5) Optionally change the path to the extension templates in TypoScript and modify the templates to your needs. +5) Change Site settings to your needs. Please note, that if you want to use the password change enforcement, + you **must** set `fe_change_pwd.changePasswordPid` to the page uid of the page created in step 2 + +6) Change TypoScript settings to your needs. + +7) Optionally change the path to the extension templates in TypoScript and modify the templates to your needs. ## New fe_user fields @@ -59,15 +63,30 @@ The password expiry date defines the date, after a user must change the password **Tip:** If you quickly want all frontend users to change their passwords, you can use a simple SQL statement to set the field in the database like shown in this example `UPDATE fe_users set must_change_password=1;` +## Site configuration settings + +* `fe_change_pwd.changePasswordPid` *(integer)* The pid to redirect to if a password change is required. This is usually the + page with the Plugin of the extension + +* `fe_change_pwd.redirect.allAccessProtectedPages` *(bool)* If set to `1`, a + redirect to the configured `fe_change_pwd.changePasswordPid` will be forced + for all access protected pages. Note, that if this option is set, the + `includePageUids` is ignored! +* `fe_change_pwd.redirect.includePageUids` *(string)* A redirect to the configured + changePasswordPid will be forced for the configured PIDs separated by a comma +* `fe_change_pwd.redirect.includePageUidsRecursionLevel` *(integer)* The recursion + level for all pages configured in `fe_change_pwd.redirect.includePageUids`. Use + this option, if you e.g. want to force a redirect for a page and all subpages +* `fe_change_pwd.redirect.excludePageUids` (string) No redirect will be forced + for the configured PIDs separated by a comma +* `fe_change_pwd.redirect.excludePageUidsRecursionLevel` *(integer)* The + recursion level for all pages configured in `fe_change_pwd.redirect.excludePageUids`. + Use this option, if you e.g. want to exclude a page and all subpages for the redirect + ## TypoScript configuration settings The following TypoScript settings are available. -**plugin.tx_fechangepwd.settings** - -* `changePasswordPid` *(integer)* The pid to redirect to if a password change is required. This is usually the -page with the Plugin of the extension - **plugin.tx_fechangepwd.settings.requireCurrentPassword** * `enabled` *(bool)* If set to `1`, the user must enter the current password in order to set a new password. Default setting is `1`. @@ -76,25 +95,14 @@ page with the Plugin of the extension * `enabled` *(bool)* If set to `1`, the user must enter a change password code, which will be sent to the users email address, in order to set a new password. Default setting is `0`. * `validityInMinutes` *(integer)* The time in minutes the change password code is valid, when it has been requested by the user. -* `senderEmail` *(string)* Sender email address for email send to user -* `senderName` *(string)* Sender name for email sent to user +* `senderEmail` *(string)* Sender email address for email send to user +* `senderName` *(string)* Sender name for email sent to user **plugin.tx_fechangepwd.settings.passwordExpiration** * `enabled` *(bool)* Is set to `1`, new passwords will expire after the configured amount of days * `validityInDays` *(integer)* The amount of days, a new password is valid before it needs to be changed -**plugin.tx_fechangepwd.settings.redirect** - -* `allAccessProtectedPages` *(bool)* If set to `1`, a redirect to the configured `changePasswordPid` will be forced -for all access protected pages. Note, that if this option is set, the `includePageUids` is ignored! -* `includePageUids` *(string)* A redirect to the configured changePasswordPid will be forced for the configured PIDs separated by a comma -* `includePageUidsRecursionLevel` *(integer)* The recursion level for all pages configured in `includePageUids`. Use this option, -if you e.g. want to force a redirect for a page and all subpages -* `excludePageUids` (string) No redirect will be forced for the configured PIDs separated by a comma -* `excludePageUidsRecursionLevel` *(integer)* The recursion level for all pages configured in `excludePageUids`. Use this option, -if you e.g. want to exclude a page and all subpages for the redirect - **plugin.tx_fechangepwd.settings.afterPasswordChangeAction** * `redirect` *(string)* Redirects the user to the "update" action and adds a flash message, that the password has been updated. @@ -148,22 +156,43 @@ use this event to add the data to the `ContextData` DTO. | Version | TYPO3 | PHP | Support/Development | |---------|------------|-----------|--------------------------------------| -| 4.x | 12.4 | 8.1 - 8.3 | Features, Bugfixes, Security Updates | -| 3.x | 11.5 | 7.4 - 8.3 | Features, Bugfixes, Security Updates | -| 2.x | 9.5 - 10.4 | 7.2 - 7.4 | Security Updates | +| 5.x | 13.4 | 8.2 - 8.4 | Features, Bugfixes, Security Updates | +| 4.x | 12.4 | 8.1 - 8.4 | Features, Bugfixes, Security Updates | +| 3.x | 11.5 | 7.4 - 8.3 | Security Updates | +| 2.x | 9.5 - 10.4 | 7.2 - 7.4 | Support dropped | | 1.x | 8.7 - 9.5 | 7.0 - 7.3 | Support dropped | ## Breaking changes +### Version 5.0.0 + +This version contains major breaking changes, which must be migrated manually. +The following TypoScript settings must be migrated to site settings: + +* `plugin.tx_fechangepwd.settings.changePasswordPid` => `fe_change_pwd.changePasswordPid` +* `plugin.tx_fechangepwd.settings.redirect.*` => `fe_change_pwd.redirect.*` + +This change is required, since full TypoScript is not available for cached +pages in a PSR-15 MiddleWare. + +This breaking change limits the plugin to be used once per Site, if the +"Must change password" or "Password expiry date" features are used, which +both need to redirect to a single page UID, which now is configured in +site settings. + ### Version 4.0.0 This version contains major breaking changes, since now the TYPO3 password policy is used for password validation. -* All password validators have been removed in favor to TYPO3 password policies. Make sure to check, if the TYPO3 default password policy suits your needs -* The pwned password check has been removed. If this check is required, please use TYPO3 extension [add_pwd_policy](https://github.com/derhansen/add_pwd_policy) in the password policy for frontend users -* The extension now requires the current user password by default. This check can be disabled in settings using `requireCurrentPassword` -* The extension requires TYPO3 `security.usePasswordPolicyForFrontendUsers` feature toggle to be active +* All password validators have been removed in favor to TYPO3 password policies. + Make sure to check, if the TYPO3 default password policy suits your needs +* The pwned password check has been removed. If this check is required, please + use TYPO3 extension [add_pwd_policy](https://github.com/derhansen/add_pwd_policy) in the password policy for frontend users +* The extension now requires the current user password by default. This check + can be disabled in settings using `requireCurrentPassword` +* The extension requires TYPO3 `security.usePasswordPolicyForFrontendUsers` + feature toggle to be active * Dropped TYPO3 11.5 compatibility. ### Version 3.0.0 diff --git a/Resources/Private/Templates/Password/Edit.html b/Resources/Private/Templates/Password/Edit.html index 97d6019..de4ba7b 100644 --- a/Resources/Private/Templates/Password/Edit.html +++ b/Resources/Private/Templates/Password/Edit.html @@ -75,4 +75,4 @@

- \ No newline at end of file + diff --git a/Tests/Unit/Service/FrontendUserServiceTest.php b/Tests/Unit/Service/FrontendUserServiceTest.php index 38b7e0a..1c1d7c1 100644 --- a/Tests/Unit/Service/FrontendUserServiceTest.php +++ b/Tests/Unit/Service/FrontendUserServiceTest.php @@ -2,6 +2,8 @@ declare(strict_types=1); +namespace Derhansen\FeChangePwd\Tests\Unit\Service; + /* * This file is part of the Extension "fe_change_pwd" for TYPO3 CMS. * @@ -11,8 +13,13 @@ use Derhansen\FeChangePwd\Service\FrontendUserService; use Derhansen\FeChangePwd\Service\SettingsService; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Crypto\HashService; +use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Session\UserSessionManager; +use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; @@ -49,10 +56,8 @@ public static function mustChangePasswordReturnsExpectedResultDataProvider(): ar ]; } - /** - * @test - * @dataProvider mustChangePasswordReturnsExpectedResultDataProvider - */ + #[DataProvider('mustChangePasswordReturnsExpectedResultDataProvider')] + #[Test] public function mustChangePasswordReturnsExpectedResult(array $feUserRecord, bool $expected): void { $userSessionManager = $this->getMockBuilder(UserSessionManager::class) @@ -64,10 +69,11 @@ public function mustChangePasswordReturnsExpectedResult(array $feUserRecord, boo $mockSettingsService = $this->createMock(SettingsService::class); $mockContext = $this->createMock(Context::class); + $hashService = new HashService(); - $service = new FrontendUserService($mockSettingsService, $mockContext); - $GLOBALS['TSFE'] = new \stdClass(); - $GLOBALS['TSFE']->fe_user = $feUser; - self::assertEquals($expected, $service->mustChangePassword($feUserRecord)); + $service = new FrontendUserService($mockSettingsService, $mockContext, $hashService); + $serverRequest = (new ServerRequest())->withAttribute('extbase', new ExtbaseRequestParameters()); + $serverRequest = $serverRequest->withAttribute('frontend.user', $feUser); + self::assertEquals($expected, $service->mustChangePassword($serverRequest, $feUserRecord)); } } diff --git a/Tests/Unit/Service/SettingsServiceTest.php b/Tests/Unit/Service/SettingsServiceTest.php index 531bbdc..72fb223 100644 --- a/Tests/Unit/Service/SettingsServiceTest.php +++ b/Tests/Unit/Service/SettingsServiceTest.php @@ -12,6 +12,8 @@ */ use Derhansen\FeChangePwd\Service\SettingsService; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; class SettingsServiceTest extends UnitTestCase @@ -55,10 +57,8 @@ public static function getPasswordExpiryTimestampReturnsExpectedResultDataProvid ]; } - /** - * @test - * @dataProvider getPasswordExpiryTimestampReturnsExpectedResultDataProvider - */ + #[DataProvider('getPasswordExpiryTimestampReturnsExpectedResultDataProvider')] + #[Test] public function getPasswordExpiryTimestampReturnsExpectedResult( array $settings, \DateTime $currentDate, diff --git a/Tests/Unit/Validation/Validator/ChangePasswordValidatorTest.php b/Tests/Unit/Validation/Validator/ChangePasswordValidatorTest.php index 825f660..92531c5 100644 --- a/Tests/Unit/Validation/Validator/ChangePasswordValidatorTest.php +++ b/Tests/Unit/Validation/Validator/ChangePasswordValidatorTest.php @@ -13,6 +13,7 @@ use Derhansen\FeChangePwd\Service\LocalizationService; use Derhansen\FeChangePwd\Service\SettingsService; use Derhansen\FeChangePwd\Validation\Validator\ChangePasswordValidator; +use PHPUnit\Framework\Attributes\Test; use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; @@ -23,9 +24,6 @@ class ChangePasswordValidatorTest extends UnitTestCase { protected ChangePasswordValidator $validator; - /** - * Initialize validator - */ public function initialize(): void { $this->validator = $this->getAccessibleMock( @@ -35,13 +33,10 @@ public function initialize(): void '', false ); - - $GLOBALS['TYPO3_REQUEST'] = new ServerRequest(); + $this->validator->setRequest(new ServerRequest()); } - /** - * @test - */ + #[Test] public function noCurrentPasswordGivenTest(): void { $this->initialize(); @@ -58,7 +53,7 @@ public function noCurrentPasswordGivenTest(): void $mockSettingsService = $this->getMockBuilder(SettingsService::class) ->disableOriginalConstructor() ->getMock(); - $mockSettingsService->expects(self::once())->method('getSettings')->willReturn($settings); + $mockSettingsService->expects(self::once())->method('getTypoScriptSettings')->willReturn($settings); $this->validator->_set('settingsService', $mockSettingsService); $mockLocalizationService = $this->getMockBuilder(LocalizationService::class) @@ -70,9 +65,7 @@ public function noCurrentPasswordGivenTest(): void self::assertEquals(1570880411, $this->validator->validate($changePassword)->getErrors()[0]->getCode()); } - /** - * @test - */ + #[Test] public function currentPasswordValidationSkipped(): void { $this->initialize(); @@ -90,7 +83,7 @@ public function currentPasswordValidationSkipped(): void $mockSettingsService = $this->getMockBuilder(SettingsService::class) ->disableOriginalConstructor() ->getMock(); - $mockSettingsService->expects(self::once())->method('getSettings')->willReturn($settings); + $mockSettingsService->expects(self::once())->method('getTypoScriptSettings')->willReturn($settings); $this->validator->_set('settingsService', $mockSettingsService); $mockLocalizationService = $this->getMockBuilder(LocalizationService::class) diff --git a/composer.json b/composer.json index 84c0f32..707fe3c 100644 --- a/composer.json +++ b/composer.json @@ -19,13 +19,10 @@ "GPL-2.0-or-later" ], "require": { - "typo3/cms-core": "^12.4", + "typo3/cms-core": "^13.2", "ext-curl": "*", "ext-pdo": "*", - "php": ">=8.1" - }, - "replace": { - "typo3-ter/fe-change-pwd": "self.version" + "php": ">=8.2" }, "autoload": { "psr-4": { diff --git a/ext_emconf.php b/ext_emconf.php index e5e984f..23206ed 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -8,10 +8,10 @@ 'author' => 'Torben Hansen', 'author_email' => 'torben@derhansen.com', 'state' => 'stable', - 'version' => '4.2.1', + 'version' => '5.0.0', 'constraints' => [ 'depends' => [ - 'typo3' => '12.4.0-12.4.99' + 'typo3' => '13.2.0-13.4.99' ], 'conflicts' => [], 'suggests' => [], diff --git a/ext_tables.sql b/ext_tables.sql index 3188dc7..1855d85 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -2,8 +2,6 @@ # Table structure for table 'fe_users' # CREATE TABLE fe_users ( - must_change_password smallint(5) unsigned DEFAULT '0' NOT NULL, - password_expiry_date int(11) DEFAULT '0' NOT NULL, change_password_code_hash varchar(255) DEFAULT '' NOT NULL, change_password_code_expiry_date int(11) DEFAULT '0' NOT NULL, -); \ No newline at end of file +);