From 43297480222f5360172ba5afef1221c6dbc79546 Mon Sep 17 00:00:00 2001 From: Iryna Pshennik Date: Thu, 3 Nov 2022 18:42:46 +0300 Subject: [PATCH] Release 1.1.1 --- .gitignore | 5 + .../FrontendUserAuthentication.php | 239 +++++ Classes/Controller/FrontendUserController.php | 500 +++++++++ Classes/Domain/Model/FrontendUser.php | 708 +++++++++++++ Classes/Domain/Model/FrontendUserGroup.php | 152 +++ .../FrontendUserGroupRepository.php | 23 + .../Repository/FrontendUserRepository.php | 48 + .../Validator/FrontendUserValidator.php | 56 + Classes/Domain/Validator/PhoneValidator.php | 39 + Classes/Enumeration/FrontendUser/Property.php | 40 + .../Event/AbstractFrontendUserActionEvent.php | 91 ++ .../Event/FrontendUserCreateAfterEvent.php | 21 + .../Event/FrontendUserDeleteAfterEvent.php | 21 + .../Event/FrontendUserFormViewModifyEvent.php | 60 ++ .../Event/FrontendUserUpdateAfterEvent.php | 21 + .../FrontendUserCreateAfterEventListener.php | 76 ++ .../UploadedFileReferenceConverter.php | 145 +++ Classes/Service/FrontendUserFormService.php | 53 + .../Validation/Validator/DigitValidator.php | 42 + .../Format/LowercaseHyphenatedViewHelper.php | 58 + Configuration/Extbase/Persistence/Classes.php | 53 + Configuration/FlexForms/FrontendUserForm.xml | 182 ++++ Configuration/Services.yaml | 23 + Configuration/TCA/Overrides/tt_content.php | 64 ++ .../Mod/Wizards/NewContentElement.tsconfig | 19 + Configuration/TypoScript/constants.typoscript | 51 + Configuration/TypoScript/setup.typoscript | 41 + Documentation/Configuration/Index.rst | 25 + .../Configuration/Settings/General.rst | 153 +++ Documentation/Configuration/Settings/View.rst | 74 ++ .../TypoScriptReference/Index.rst | 29 + Documentation/Developer/Domain.rst | 19 + Documentation/Developer/Events.rst | 37 + .../Developer/FrontendUser/Index.rst | 15 + .../Developer/FrontendUser/Model.rst | 384 +++++++ .../Developer/FrontendUser/Repository.rst | 42 + .../Developer/FrontendUser/Validators.rst | 48 + .../Developer/FrontendUserGroup/Index.rst | 13 + .../Developer/FrontendUserGroup/Model.rst | 53 + Documentation/Developer/Index.rst | 15 + Documentation/Developer/ViewHelpers.rst | 43 + Documentation/Editor/Index.rst | 56 + Documentation/Images/ContentElementWizard.png | Bin 0 -> 41678 bytes Documentation/Images/GeneralTab.png | Bin 0 -> 56099 bytes .../Images/GeneralTabHiddenFields.png | Bin 0 -> 28760 bytes Documentation/Images/ViewTab.png | Bin 0 -> 72335 bytes Documentation/Includes.rst.txt | 34 + Documentation/Index.rst | 55 + Documentation/Installation/Index.rst | 33 + Documentation/Introduction/Index.rst | 35 + Documentation/KnownIssues/Index.rst | 28 + Documentation/Settings.cfg | 62 ++ Documentation/Sitemap.rst | 9 + Documentation/genindex.rst | 7 + README.rst | 9 + Resources/Private/Language/de.locallang.xlf | 188 ++++ .../Private/Language/de.locallang_be.xlf | 140 +++ Resources/Private/Language/locallang.xlf | 145 +++ Resources/Private/Language/locallang_be.xlf | 115 ++ Resources/Private/Layouts/Form/Default.html | 16 + .../Partials/Form/Fieldset/PersonalData.html | 60 ++ .../Partials/Form/ValidationResults.html | 26 + .../Private/Templates/FrontendUser/Edit.html | 101 ++ .../Private/Templates/FrontendUser/New.html | 60 ++ Resources/Public/Icons/Extension.svg | 1 + Resources/Public/JavaScript/Form.js | 52 + .../FrontendUserAuthenticationTest.php | 309 ++++++ .../Controller/FrontendUserControllerTest.php | 990 ++++++++++++++++++ .../Domain/Model/FrontendUserGroupTest.php | 146 +++ Tests/Unit/Domain/Model/FrontendUserTest.php | 489 +++++++++ .../Repository/FrontendUserRepositoryTest.php | 84 ++ .../Validator/FrontendUserValidatorTest.php | 117 +++ .../Domain/Validator/PhoneValidatorTest.php | 93 ++ .../AbstractFrontendUserActionEventTest.php | 126 +++ .../FrontendUserCreateAfterEventTest.php | 82 ++ .../FrontendUserFormViewModifyEventTest.php | 66 ++ ...ontendUserCreateAfterEventListenerTest.php | 118 +++ .../UploadedFileReferenceConverterTest.php | 190 ++++ .../Service/FrontendUserFormServiceTest.php | 74 ++ .../Validator/DigitValidatorTest.php | 93 ++ .../LowercaseHyphenatedViewHelperTest.php | 76 ++ composer.json | 57 + ext_emconf.php | 35 + ext_localconf.php | 46 + 84 files changed, 8204 insertions(+) create mode 100644 .gitignore create mode 100644 Classes/Authentication/FrontendUserAuthentication.php create mode 100644 Classes/Controller/FrontendUserController.php create mode 100644 Classes/Domain/Model/FrontendUser.php create mode 100644 Classes/Domain/Model/FrontendUserGroup.php create mode 100644 Classes/Domain/Repository/FrontendUserGroupRepository.php create mode 100644 Classes/Domain/Repository/FrontendUserRepository.php create mode 100644 Classes/Domain/Validator/FrontendUserValidator.php create mode 100644 Classes/Domain/Validator/PhoneValidator.php create mode 100644 Classes/Enumeration/FrontendUser/Property.php create mode 100644 Classes/Event/AbstractFrontendUserActionEvent.php create mode 100644 Classes/Event/FrontendUserCreateAfterEvent.php create mode 100644 Classes/Event/FrontendUserDeleteAfterEvent.php create mode 100644 Classes/Event/FrontendUserFormViewModifyEvent.php create mode 100644 Classes/Event/FrontendUserUpdateAfterEvent.php create mode 100644 Classes/EventListener/FrontendUserCreateAfterEventListener.php create mode 100644 Classes/Property/TypeConverter/UploadedFileReferenceConverter.php create mode 100644 Classes/Service/FrontendUserFormService.php create mode 100644 Classes/Validation/Validator/DigitValidator.php create mode 100644 Classes/ViewHelpers/Format/LowercaseHyphenatedViewHelper.php create mode 100644 Configuration/Extbase/Persistence/Classes.php create mode 100644 Configuration/FlexForms/FrontendUserForm.xml create mode 100644 Configuration/Services.yaml create mode 100644 Configuration/TCA/Overrides/tt_content.php create mode 100644 Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig create mode 100644 Configuration/TypoScript/constants.typoscript create mode 100644 Configuration/TypoScript/setup.typoscript create mode 100644 Documentation/Configuration/Index.rst create mode 100644 Documentation/Configuration/Settings/General.rst create mode 100644 Documentation/Configuration/Settings/View.rst create mode 100644 Documentation/Configuration/TypoScriptReference/Index.rst create mode 100644 Documentation/Developer/Domain.rst create mode 100644 Documentation/Developer/Events.rst create mode 100644 Documentation/Developer/FrontendUser/Index.rst create mode 100644 Documentation/Developer/FrontendUser/Model.rst create mode 100644 Documentation/Developer/FrontendUser/Repository.rst create mode 100644 Documentation/Developer/FrontendUser/Validators.rst create mode 100644 Documentation/Developer/FrontendUserGroup/Index.rst create mode 100644 Documentation/Developer/FrontendUserGroup/Model.rst create mode 100644 Documentation/Developer/Index.rst create mode 100644 Documentation/Developer/ViewHelpers.rst create mode 100644 Documentation/Editor/Index.rst create mode 100644 Documentation/Images/ContentElementWizard.png create mode 100644 Documentation/Images/GeneralTab.png create mode 100644 Documentation/Images/GeneralTabHiddenFields.png create mode 100644 Documentation/Images/ViewTab.png create mode 100644 Documentation/Includes.rst.txt create mode 100644 Documentation/Index.rst create mode 100644 Documentation/Installation/Index.rst create mode 100644 Documentation/Introduction/Index.rst create mode 100644 Documentation/KnownIssues/Index.rst create mode 100644 Documentation/Settings.cfg create mode 100644 Documentation/Sitemap.rst create mode 100644 Documentation/genindex.rst create mode 100644 README.rst create mode 100644 Resources/Private/Language/de.locallang.xlf create mode 100644 Resources/Private/Language/de.locallang_be.xlf create mode 100644 Resources/Private/Language/locallang.xlf create mode 100644 Resources/Private/Language/locallang_be.xlf create mode 100644 Resources/Private/Layouts/Form/Default.html create mode 100644 Resources/Private/Partials/Form/Fieldset/PersonalData.html create mode 100644 Resources/Private/Partials/Form/ValidationResults.html create mode 100644 Resources/Private/Templates/FrontendUser/Edit.html create mode 100644 Resources/Private/Templates/FrontendUser/New.html create mode 100644 Resources/Public/Icons/Extension.svg create mode 100644 Resources/Public/JavaScript/Form.js create mode 100644 Tests/Unit/Authentication/FrontendUserAuthenticationTest.php create mode 100644 Tests/Unit/Controller/FrontendUserControllerTest.php create mode 100644 Tests/Unit/Domain/Model/FrontendUserGroupTest.php create mode 100644 Tests/Unit/Domain/Model/FrontendUserTest.php create mode 100644 Tests/Unit/Domain/Repository/FrontendUserRepositoryTest.php create mode 100644 Tests/Unit/Domain/Validator/FrontendUserValidatorTest.php create mode 100644 Tests/Unit/Domain/Validator/PhoneValidatorTest.php create mode 100644 Tests/Unit/Event/AbstractFrontendUserActionEventTest.php create mode 100644 Tests/Unit/Event/FrontendUserCreateAfterEventTest.php create mode 100644 Tests/Unit/Event/FrontendUserFormViewModifyEventTest.php create mode 100644 Tests/Unit/EventListener/FrontendUserCreateAfterEventListenerTest.php create mode 100644 Tests/Unit/Property/TypeConverter/UploadedFileReferenceConverterTest.php create mode 100644 Tests/Unit/Service/FrontendUserFormServiceTest.php create mode 100644 Tests/Unit/Validation/Validator/DigitValidatorTest.php create mode 100644 Tests/Unit/ViewHelpers/Format/LowercaseHyphenatedViewHelperTest.php create mode 100644 composer.json create mode 100644 ext_emconf.php create mode 100644 ext_localconf.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8399524 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +composer.lock +.Build/ +var/ +Documentation-GENERATED-temp/ diff --git a/Classes/Authentication/FrontendUserAuthentication.php b/Classes/Authentication/FrontendUserAuthentication.php new file mode 100644 index 0000000..07d3df0 --- /dev/null +++ b/Classes/Authentication/FrontendUserAuthentication.php @@ -0,0 +1,239 @@ + + */ + +namespace Ydt\FrontendUser\Authentication; + +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Authentication\LoginType; +use TYPO3\CMS\Core\Http\ServerRequestFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication as CoreFrontendUserAuthentication; +use TYPO3\CMS\Core\Exception; + +/** + * Class FrontendUserAuthentication + * Authentication of frontend user after creation on website + */ +class FrontendUserAuthentication extends CoreFrontendUserAuthentication +{ + /** + * Permanent login is forced to be enabled + */ + const PERMANENT_LOGIN_ENABLED = 2; + + /** + * Form field with login-name + * + * @var string + */ + public $formfield_uname = 'username'; + + /** + * Form field with password + * + * @var string + */ + public $formfield_uident = 'password'; + + /** + * Form object name + * + * @var string + */ + protected $objectName = 'newFrontendUser'; + + /** + * @inheritdoc + */ + public function start(ServerRequestInterface $request = null): void + { + $request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); + $this->logger->debug('## Beginning of created frontend user auth logging.'); + + $parsedBody = $request->getParsedBody(); + $pid = (int)$parsedBody['tx_frontenduser_form']['pid'] ?? 0; + if ($pid) { + $this->checkPid_value = $pid; + } + + $formData = $parsedBody['tx_frontenduser_form'][$this->objectName] ?? []; + $loginData = $this->getLoginData($formData); + + $this->user = null; + if (!isset($this->userSessionManager)) { + $this->initializeUserSessionManager(); + } + $this->userSession = $this->userSessionManager->createFromRequestOrAnonymous($request, $this->name); + + $this->authenticate($loginData); + + if (!$this->dontSetCookie || $this->isRefreshTimeBasedCookie()) { + $this->setSessionCookie(); + } + + $this->fetchGroupData($request); + } + + /** + * Get login data + * + * @param array $formData + * @return array + * @throws Exception + */ + protected function getLoginData(array $formData): array + { + $username = $formData[$this->formfield_uname] ?? ''; + $password = $formData[$this->formfield_uident] ?? ''; + + if (empty($username) || empty($password)) { + throw new Exception( + sprintf('%s and %s are required.', $this->formfield_uname, $this->formfield_uident) + ); + } + + $loginData = [ + 'status' => LoginType::LOGIN, + 'uname' => $username, + 'uident' => $password, + ]; + + $loginData = $this->processLoginData($loginData); + + $isPermanent = (int)$GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] === self::PERMANENT_LOGIN_ENABLED; + $loginData['permanent'] = $isPermanent; + $this->is_permanent = $isPermanent; + + return $loginData; + } + + /** + * @inheritdoc + */ + protected function setSessionCookie(): void + { + parent::setSessionCookie(); + + if ($this->setCookie) { + $cookie = clone $this->setCookie; + + setcookie( + $cookie->getName(), + $cookie->getValue(), + [ + 'expires' => $cookie->getExpiresTime(), + 'path' => $cookie->getPath(), + 'domain' => $cookie->getDomain(), + 'secure' => $cookie->isSecure(), + 'httponly' => $cookie->isHttpOnly(), + 'samesite' => $cookie->getSameSite(), + ] + ); + } + } + + /** + * Authenticate frontend user with provided credentials + * + * @param array $loginData + * @return void + */ + protected function authenticate(array $loginData): void + { + $this->logger->debug(sprintf('Login type: %s', $this->loginType)); + $this->logger->debug('Login data', $this->removeSensitiveLoginDataForLoggingInfo($loginData)); + + $this->logoff(); + + $authInfo = $this->getAuthInfoArray(); + $tempUsers = []; + $authenticated = false; + + foreach ($this->getAuthServices('getUser' . $this->loginType, $loginData, $authInfo) as $authService) { + $user = $authService->getUser(); + if (is_array($user)) { + $tempUsers[] = $user; + $this->logger->debug('User found', [ + $this->userid_column => $user[$this->userid_column], + $this->username_column => $user[$this->username_column], + ]); + break; + } + } + + if (!empty($tempUsers)) { + $this->logger->debug(sprintf('%s user records found by services', count($tempUsers))); + + foreach ($tempUsers as $tempUser) { + $this->logger->debug('Auth user', $this->removeSensitiveLoginDataForLoggingInfo($tempUser, true)); + + foreach ($this->getAuthServices('authUser' . $this->loginType, $loginData, $authInfo) as $authService) { + $result = (int)$authService->authUser($tempUser); + if ($result <= 0) { + $authenticated = false; + break; + } + + if ($result >= 200) { + $authenticated = true; + break; + } + + if ($result < 100) { + $authenticated = true; + } + } + + if ($authenticated) { + break; + } + } + + if ($authenticated) { + $this->userSession = $this->createUserSession($tempUser); + $this->user = array_merge($tempUser, $this->user ?? []); + + $this->loginSessionStarted = true; + if (is_array($this->user)) { + $this->logger->debug('User session finally read', [ + $this->userid_column => $this->user[$this->userid_column], + $this->username_column => $this->user[$this->username_column], + ]); + } + + $this->logger->info(sprintf('User %s logged in from %s', + $tempUser[$this->username_column], + GeneralUtility::getIndpEnv('REMOTE_ADDR') + )); + } else { + $this->logger->debug('Login failed', [ + $this->userid_column => $tempUser[$this->userid_column], + $this->username_column => $tempUser[$this->username_column], + ]); + $this->handleLoginFailure(); + } + } else { + $this->logger->debug('No user found by services'); + $this->logger->debug('Login failed', [ + 'loginData' => $this->removeSensitiveLoginDataForLoggingInfo($loginData), + ]); + $this->handleLoginFailure(); + } + } + + /** + * @inheritdoc + */ + public function checkAuthentication(ServerRequestInterface $request = null): void + { + + } +} diff --git a/Classes/Controller/FrontendUserController.php b/Classes/Controller/FrontendUserController.php new file mode 100644 index 0000000..bd64c51 --- /dev/null +++ b/Classes/Controller/FrontendUserController.php @@ -0,0 +1,500 @@ + + */ + +namespace Ydt\FrontendUser\Controller; + +use TYPO3\CMS\Extbase\Domain\Model\FileReference; +use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder; +use TYPO3\CMS\Core\Messaging\FlashMessage; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Annotation as Extbase; +use TYPO3\CMS\Extbase\Property\PropertyMapper; +use TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration; +use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +use Psr\Http\Message\ResponseInterface; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException; +use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory; +use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; +use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; +use Ydt\FrontendUser\Domain\Model\FrontendUser; +use Ydt\FrontendUser\Domain\Model\FrontendUserGroup; +use Ydt\FrontendUser\Domain\Repository\FrontendUserRepository; +use Ydt\FrontendUser\Domain\Repository\FrontendUserGroupRepository; +use Ydt\FrontendUser\Event\FrontendUserCreateAfterEvent; +use Ydt\FrontendUser\Event\FrontendUserUpdateAfterEvent; +use Ydt\FrontendUser\Event\FrontendUserDeleteAfterEvent; +use Ydt\FrontendUser\Event\FrontendUserFormViewModifyEvent; +use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException; +use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException; +use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException; +use TYPO3\CMS\Extbase\Http\ForwardResponse; +use TYPO3\CMS\Extbase\Property\Exception; +use Ydt\FrontendUser\Property\TypeConverter\UploadedFileReferenceConverter; + +/** + * Class FrontendUserController + * Frontend user controller + */ +class FrontendUserController extends ActionController +{ + /** + * Frontend User Repository + * + * @var FrontendUserRepository + */ + protected $frontendUserRepository; + + /** + * Frontend User Group Repository + * + * @var FrontendUserGroupRepository + */ + protected $frontendUserGroupRepository; + + /** + * Password Hash Factory + * + * @var PasswordHashFactory + */ + protected $passwordHashFactory; + + /** + * Persistence Manager + * + * @var PersistenceManager + */ + protected $persistenceManager; + + /** + * Context + * + * @var Context + */ + protected $context; + + /** + * Property Mapper + * + * @var PropertyMapper + */ + protected $propertyMapper; + + /** + * FrontendUserController constructor + * + * @param FrontendUserRepository $frontendUserRepository + * @param FrontendUserGroupRepository $frontendUserGroupRepository + * @param PasswordHashFactory $passwordHashFactory + * @param PersistenceManager $persistenceManager + * @param Context $context + * @param PropertyMapper $propertyMapper + */ + public function __construct( + FrontendUserRepository $frontendUserRepository, + FrontendUserGroupRepository $frontendUserGroupRepository, + passwordHashFactory $passwordHashFactory, + PersistenceManager $persistenceManager, + Context $context, + PropertyMapper $propertyMapper + ) { + $this->frontendUserRepository = $frontendUserRepository; + $this->frontendUserGroupRepository = $frontendUserGroupRepository; + $this->passwordHashFactory = $passwordHashFactory; + $this->persistenceManager = $persistenceManager; + $this->context = $context; + $this->propertyMapper = $propertyMapper; + } + + /** + * Create new frontend user + * + * @param FrontendUser|null $newFrontendUser + * @return ResponseInterface + */ + public function newAction(?FrontendUser $newFrontendUser = null): ResponseInterface + { + $frontendUserId = $this->getFrontendUserIdFromAspect(); + if ($frontendUserId) { + $frontendUser = $this->frontendUserRepository->findByUid($frontendUserId); + + return $this->redirect('edit', null, null, ['frontendUser' => $frontendUser]); + } + + $formFields = []; + if (!empty($this->settings['newFrontendUserFormFields'])) { + $formFields = explode(',', $this->settings['newFrontendUserFormFields']); + } + + $this->view->assignMultiple([ + 'newFrontendUser' => $newFrontendUser, + 'storagePid' => $this->getStoragePid(), + 'formFields' => $formFields, + ]); + + $this->eventDispatcher->dispatch(new FrontendUserFormViewModifyEvent($this->view)); + + return $this->htmlResponse(); + } + + /** + * Create frontend user + * + * @param FrontendUser $newFrontendUser + * @return ResponseInterface + * @throws IllegalObjectTypeException + * @throws InvalidPasswordHashException + * @Extbase\Validate (param="newFrontendUser", validator="Ydt\FrontendUser\Domain\Validator\FrontendUserValidator") + */ + public function createAction(FrontendUser $newFrontendUser): ResponseInterface + { + $arguments = $this->request->getArguments(); + $pid = (int)$arguments['pid'] ?? 0; + + $username = $newFrontendUser->getUsername(); + $frontendUser = $this->frontendUserRepository->findByUsername($username, $pid); + if ($frontendUser instanceof FrontendUser) { + $this->addFlashMessage( + $this->getTranslation('errorMessage.username.notAvailable'), + '', + FlashMessage::ERROR + ); + + return (new ForwardResponse('new')) + ->withArguments(['forwarded' => true]); + } + + $newFrontendUser->setPid($pid); + + $password = $newFrontendUser->getPassword(); + $hashedPassword = $this->generateHashedPassword($password); + $newFrontendUser->setPassword($hashedPassword); + + $defaultFrontendUserGroupId = $this->settings['defaultFrontendUserGroupId'] ?? ''; + if (!empty($defaultFrontendUserGroupId)) { + $defaultFrontendUserGroup = $this->frontendUserGroupRepository->findByUid($defaultFrontendUserGroupId); + if ($defaultFrontendUserGroup instanceof FrontendUserGroup) { + $newFrontendUser->addUserGroup($defaultFrontendUserGroup); + } + } + + $imageFileData = $arguments['image'] ?? []; + if (!empty($imageFileData) && (!isset($imageFileData['error']) || $imageFileData['error'] === UPLOAD_ERR_OK)) { + $imageFileReference = $this->mapArrayToFileReference($imageFileData); + if ($imageFileReference) { + $newFrontendUser->addImage($imageFileReference); + } else { + $this->addFlashMessage( + $this->getTranslation('errorMessage.image.canNotBeSaved'), + '', + FlashMessage::ERROR + ); + } + } + + $this->frontendUserRepository->add($newFrontendUser); + $this->persistenceManager->persistAll(); + + $this->eventDispatcher->dispatch(new FrontendUserCreateAfterEvent($newFrontendUser, $this->request)); + + $loginPageUrl = $this->getLoginPageUrl(); + if ( + isset($this->settings['enableRedirectToLoginPage']) + && $this->settings['enableRedirectToLoginPage'] + && !empty($loginPageUrl) + ) { + return $this->redirectToUri($loginPageUrl); + } + + return $this->redirect('edit', null, null, ['frontendUser' => $newFrontendUser]); + } + + /** + * Edit frontend user + * + * @param FrontendUser $frontendUser + * @return ResponseInterface + * @Extbase\IgnoreValidation("frontendUser") + */ + public function editAction(FrontendUser $frontendUser): ResponseInterface + { + $formFields = []; + if (!empty($this->settings['editFrontendUserFormFields'])) { + $formFields = explode(',', $this->settings['editFrontendUserFormFields']); + } + + $loginPageUrl = $this->getLoginPageUrl(); + + $this->view->assignMultiple([ + 'frontendUser' => $frontendUser, + 'formFields' => $formFields, + 'loginPageUrl' => $loginPageUrl, + ]); + + $this->eventDispatcher->dispatch(new FrontendUserFormViewModifyEvent($this->view)); + + return $this->htmlResponse(); + } + + /** + * Update frontend user + * + * @param FrontendUser $frontendUser + * @return ResponseInterface + * @throws AspectNotFoundException + * @throws IllegalObjectTypeException + * @throws InvalidPasswordHashException + * @throws UnknownObjectException + */ + public function updateAction(FrontendUser $frontendUser): ResponseInterface + { + $frontendUserId = $this->getFrontendUserIdFromAspect(); + if ($frontendUserId === (int)$frontendUser->getUid()) { + $frontendUserUsername = (string)$this->context->getPropertyFromAspect('frontend.user', 'username'); + if (!$frontendUserUsername || $frontendUserUsername !== $frontendUser->getUsername()) { + $this->addFlashMessage( + $this->getTranslation('errorMessage.username.canNotBeChanged'), + '', + FlashMessage::ERROR + ); + + return (new ForwardResponse('edit')) + ->withArguments([ + 'forwarded' => true, + 'frontendUser' => $frontendUser, + ]); + } + + $arguments = $this->request->getArguments(); + + if (isset($arguments['changePassword']) && $arguments['changePassword']) { + $password = $frontendUser->getPassword(); + $passwordConfirmation = $frontendUser->getPasswordConfirmation(); + if ($password && $passwordConfirmation && $password === $passwordConfirmation) { + $hashedPassword = $this->generateHashedPassword($password); + $frontendUser->setPassword($hashedPassword); + } else { + $this->addFlashMessage( + $this->getTranslation('errorMessage.password.notMatched'), + '', + FlashMessage::ERROR + ); + + return (new ForwardResponse('edit')) + ->withArguments([ + 'forwarded' => true, + 'frontendUser' => $frontendUser, + ]); + } + } + + $imageFileData = $arguments['image'] ?? []; + if (!empty($imageFileData) && (!isset($imageFileData['error']) || $imageFileData['error'] === UPLOAD_ERR_OK)) { + $imageFileReference = $this->mapArrayToFileReference($imageFileData); + if ($imageFileReference) { + $frontendUser->addImage($imageFileReference); + } else { + $this->addFlashMessage( + $this->getTranslation('errorMessage.image.canNotBeSaved'), + '', + FlashMessage::ERROR + ); + } + } + + $this->frontendUserRepository->update($frontendUser); + $this->persistenceManager->persistAll(); + + $this->addFlashMessage($this->getTranslation('successMessage.frontendUserSaved')); + + $this->eventDispatcher->dispatch(new FrontendUserUpdateAfterEvent($frontendUser, $this->request)); + } + + return $this->redirect('edit', null, null, ['frontendUser' => $frontendUser]); + } + + /** + * Delete frontend user + * + * @param FrontendUser $frontendUser + * @return ResponseInterface + * @throws IllegalObjectTypeException + * @Extbase\IgnoreValidation("frontendUser") + */ + public function deleteAction(FrontendUser $frontendUser): ResponseInterface + { + $frontendUserId = $this->getFrontendUserIdFromAspect(); + if ( + isset($this->settings['enableFrontendUserDeletion']) + && $this->settings['enableFrontendUserDeletion'] + && $frontendUserId === (int)$frontendUser->getUid() + ) { + $this->frontendUserRepository->remove($frontendUser); + + $this->eventDispatcher->dispatch(new FrontendUserDeleteAfterEvent($frontendUser, $this->request)); + } + + return $this->redirect('new'); + } + + /** + * Delete frontend user image + * + * @param FrontendUser $frontendUser + * @param FileReference $frontendUserImage + * @return ResponseInterface + * @throws IllegalObjectTypeException + * @throws UnknownObjectException + * @Extbase\IgnoreValidation("frontendUser") + */ + public function deleteImageAction(FrontendUser $frontendUser, FileReference $frontendUserImage): ResponseInterface + { + $frontendUserId = $this->getFrontendUserIdFromAspect(); + if ($frontendUserId === (int)$frontendUser->getUid()) { + $frontendUser->removeImage($frontendUserImage); + $this->frontendUserRepository->update($frontendUser); + } + + return $this->redirect('edit', null, null, ['frontendUser' => $frontendUser]); + } + + /** + * @inheritdoc + */ + protected function getErrorFlashMessage(): string + { + $action = substr($this->actionMethodName, 0, strpos($this->actionMethodName, 'Action')); + + return sprintf('An error occurred while trying to %s a user.', $action); + } + + /** + * Get frontend user id property from aspect + * + * @return int + */ + protected function getFrontendUserIdFromAspect(): int + { + try { + $frontendUserId = (int)$this->context->getPropertyFromAspect('frontend.user', 'id'); + } catch (AspectNotFoundException $exception) { + $frontendUserId = 0; + } + + return $frontendUserId; + } + + /** + * Generate hashed password + * + * @param string $password + * @return string + * @throws InvalidPasswordHashException + */ + protected function generateHashedPassword(string $password): string + { + $hashInstance = $this->passwordHashFactory->getDefaultHashInstance('FE'); + + return $hashInstance->getHashedPassword($password); + } + + /** + * Map an array to FileReference + * + * @param array $imageFileData + * @return FileReference|null + */ + protected function mapArrayToFileReference(array $imageFileData): ?FileReference + { + $frontendUserImageFolderId = ''; + if (!empty($this->settings['frontendUserImageFolder'])) { + $frontendUserImageFolderId = substr( + $this->settings['frontendUserImageFolder'], + strpos($this->settings['frontendUserImageFolder'], ':') + 1 + ); + } + + try { + $configuration = new PropertyMappingConfiguration(); + $configuration->setTypeConverterOptions( + UploadedFileReferenceConverter::class, + [UploadedFileReferenceConverter::CONFIGURATION_TARGET_FOLDER_IDENTIFIER => $frontendUserImageFolderId] + ); + + $fileReference = $this->propertyMapper->convert($imageFileData, FileReference::class, $configuration); + } catch (Exception $exception) { + $fileReference = null; + } + + return $fileReference; + } + + /** + * Get frontend user storage page ID + * + * @return int + */ + protected function getStoragePid(): int + { + $storagePid = 0; + if ( + isset($GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid']) + && $GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid'] + && isset($this->settings['frontendUserStoragePid']) + ) { + $storagePid = (int)$this->settings['frontendUserStoragePid']; + } + + return $storagePid; + } + + /** + * Get login page url + * + * @return string + */ + protected function getLoginPageUrl(): string + { + $loginPageUrl = ''; + if (!empty($this->settings['redirectLoginPageId'])) { + $uriBuilder = $this->getUriBuilder(); + $uriBuilder->setTargetPageUid((int)$this->settings['redirectLoginPageId']); + $loginPageUrl = $uriBuilder->buildFrontendUri(); + } + + return $loginPageUrl; + } + + /** + * Get translation + * + * @param string $key + * @return string + */ + protected function getTranslation(string $key): string + { + return (string)LocalizationUtility::translate($key, 'frontend_user'); + } + + /** + * Get uri builder + * + * @return UriBuilder + */ + private function getUriBuilder(): UriBuilder + { + if ($this->uriBuilder === null) { + $this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + } + + return $this->uriBuilder; + } +} diff --git a/Classes/Domain/Model/FrontendUser.php b/Classes/Domain/Model/FrontendUser.php new file mode 100644 index 0000000..b2f92a3 --- /dev/null +++ b/Classes/Domain/Model/FrontendUser.php @@ -0,0 +1,708 @@ + + */ + +namespace Ydt\FrontendUser\Domain\Model; + +use TYPO3\CMS\Extbase\Annotation as Extbase; +use TYPO3\CMS\Extbase\Domain\Model\FileReference; +use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; +use TYPO3\CMS\Extbase\Persistence\ObjectStorage; +use DateTime; + +/** + * Class FrontendUser + * Frontend user domain model + */ +class FrontendUser extends AbstractEntity +{ + /** + * Username + * + * @var string + * @Extbase\Validate("NotEmpty") + * @Extbase\Validate("StringLength", options={"maximum": 255}) + * @Extbase\Validate("Text") + */ + protected $username; + + /** + * Password + * + * @var string + */ + protected $password; + + /** + * Password confirmation + * + * @var string + * @Extbase\ORM\Transient + */ + protected $passwordConfirmation; + + /** + * Frontend user groups + * + * @var ObjectStorage + */ + protected $userGroups; + + /** + * Company + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 80}) + * @Extbase\Validate("Text") + */ + protected $company; + + /** + * Job title + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 40}) + * @Extbase\Validate("Text") + */ + protected $jobTitle; + + /** + * Name + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 160}) + * @Extbase\Validate("Text") + */ + protected $name; + + /** + * First name + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 50}) + * @Extbase\Validate("Text") + */ + protected $firstName; + + /** + * Middle name + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 50}) + * @Extbase\Validate("Text") + */ + protected $middleName; + + /** + * Last name + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 50}) + * @Extbase\Validate("Text") + */ + protected $lastName; + + /** + * Street address + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 255}) + * @Extbase\Validate("Text") + */ + protected $streetAddress; + + /** + * Zip code + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 10}) + * @Extbase\Validate("AlphanumericValidator") + */ + protected $zipCode; + + /** + * City + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 50}) + * @Extbase\Validate("Text") + */ + protected $city; + + /** + * Country + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 40}) + * @Extbase\Validate("Text") + */ + protected $country; + + /** + * Phone + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 30}) + * @Extbase\Validate("Ydt\FrontendUser\Domain\Validator\PhoneValidator") + */ + protected $phone; + + /** + * Fax + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 30}) + * @Extbase\Validate("Ydt\FrontendUser\Validation\Validator\DigitValidator") + */ + protected $fax; + + /** + * Email + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 255}) + * @Extbase\Validate("EmailAddress") + */ + protected $email; + + /** + * Homepage url + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 80}) + * @Extbase\Validate("Text") + * @Extbase\Validate("Url") + */ + protected $url; + + /** + * Images + * + * @var ObjectStorage + */ + protected $images; + + /** + * Last login + * + * @var DateTime|null + */ + protected $lastLogin; + + /** + * FrontendUser constructor + * + * @param string $username + * @param string $password + */ + public function __construct( + string $username, + string $password = '' + ) { + $this->username = $username; + $this->password = $password; + } + + /** + * Get username + * + * @return string + */ + public function getUsername(): string + { + return (string)$this->username; + } + + /** + * Set username + * + * @param string $username + * @return void + */ + public function setUsername(string $username): void + { + $this->username = $username; + } + + /** + * Get password + * + * @return string + */ + public function getPassword(): string + { + return (string)$this->password; + } + + /** + * Set password + * + * @param string $password + * @return void + */ + public function setPassword(string $password): void + { + $this->password = $password; + } + + /** + * Get password confirmation + * + * @return string + */ + public function getPasswordConfirmation(): string + { + return (string)$this->passwordConfirmation; + } + + /** + * Set password confirmation + * + * @param string $passwordConfirmation + * @return void + */ + public function setPasswordConfirmation(string $passwordConfirmation): void + { + $this->passwordConfirmation = $passwordConfirmation; + } + + /** + * Get frontend user groups + * + * @return ObjectStorage + */ + public function getUserGroups(): ObjectStorage + { + if ($this->userGroups === null) { + $this->userGroups = new ObjectStorage(); + } + + return $this->userGroups; + } + + /** + * Set frontend user groups + * + * @param ObjectStorage $userGroups + * @return void + */ + public function setUserGroups(ObjectStorage $userGroups): void + { + $this->userGroups = $userGroups; + } + + /** + * Add frontend user group + * + * @param FrontendUserGroup $userGroup + * @return void + */ + public function addUserGroup(FrontendUserGroup $userGroup): void + { + $userGroups = $this->getUserGroups(); + if (!$userGroups->contains($userGroup)) { + $userGroups->attach($userGroup); + } + } + + /** + * Remove frontend user group + * + * @param FrontendUserGroup $userGroups + * @return void + */ + public function removeUserGroup(FrontendUserGroup $userGroup): void + { + $userGroups = $this->getUserGroups(); + if ($userGroups->contains($userGroup)) { + $userGroups->detach($userGroup); + } + } + + /** + * Get company + * + * @return string + */ + public function getCompany(): string + { + return (string)$this->company; + } + + /** + * Set company + * + * @param string $company + * @return void + */ + public function setCompany(string $company = ''): void + { + $this->company = $company; + } + + /** + * Get job title + * + * @return string + */ + public function getJobTitle(): string + { + return (string)$this->jobTitle; + } + + /** + * Set job title + * + * @param string $jobTitle + * @return void + */ + public function setJobTitle(string $jobTitle = ''): void + { + $this->jobTitle = $jobTitle; + } + + /** + * Get name + * + * @return string + */ + public function getName(): string + { + return (string)$this->name; + } + + /** + * Set name + * + * @param string $name + * @return void + */ + public function setName(string $name = ''): void + { + $this->name = $name; + } + + /** + * Get first name + * + * @return string + */ + public function getFirstName(): string + { + return (string)$this->firstName; + } + + /** + * Set first name + * + * @param string $firstName + * @return void + */ + public function setFirstName(string $firstName = ''): void + { + $this->firstName = $firstName; + } + + /** + * Get middle name + * + * @return string + */ + public function getMiddleName(): string + { + return (string)$this->middleName; + } + + /** + * Set middle name + * + * @param string $middleName + * @return void + */ + public function setMiddleName(string $middleName = ''): void + { + $this->middleName = $middleName; + } + + /** + * Get last name + * + * @return string + */ + public function getLastName(): string + { + return (string)$this->lastName; + } + + /** + * Set last name + * + * @param string $lastName + * @return void + */ + public function setLastName(string $lastName = ''): void + { + $this->lastName = $lastName; + } + + /** + * Get street address + * + * @return string + */ + public function getStreetAddress(): string + { + return (string)$this->streetAddress; + } + + /** + * Set street address + * + * @param string $streetAddress + * @return void + */ + public function setStreetAddress(string $streetAddress = ''): void + { + $this->streetAddress = $streetAddress; + } + + /** + * Get zip code + * + * @return string + */ + public function getZipCode(): string + { + return (string)$this->zipCode; + } + + /** + * Set zip code + * + * @param string $zipCode + * @return void + */ + public function setZipCode(string $zipCode = ''): void + { + $this->zipCode = $zipCode; + } + + /** + * Get city + * + * @return string + */ + public function getCity(): string + { + return (string)$this->city; + } + + /** + * Set city + * + * @param string $city + * @return void + */ + public function setCity(string $city = ''): void + { + $this->city = $city; + } + + /** + * Get country + * + * @return string + */ + public function getCountry(): string + { + return (string)$this->country; + } + + /** + * Set country + * + * @param string $country + * @return void + */ + public function setCountry(string $country = ''): void + { + $this->country = $country; + } + + /** + * Get phone + * + * @return string + */ + public function getPhone(): string + { + return (string)$this->phone; + } + + /** + * Set phone + * + * @param string $phone + * @return void + */ + public function setPhone(string $phone = ''): void + { + $this->phone = $phone; + } + + /** + * Get fax + * + * @return string + */ + public function getFax(): string + { + return (string)$this->fax; + } + + /** + * Set fax + * + * @param string $fax + * @return void + */ + public function setFax(string $fax = ''): void + { + $this->fax = $fax; + } + + /** + * Get email + * + * @return string + */ + public function getEmail(): string + { + return (string)$this->email; + } + + /** + * Set email + * + * @param string $email + * @return void + */ + public function setEmail(string $email): void + { + $this->email = $email; + } + + /** + * Get homepage url + * + * @return string + */ + public function getUrl(): string + { + return (string)$this->url; + } + + /** + * Set homepage url + * + * @param string $url + * @return void + */ + public function setUrl(string $url = ''): void + { + $this->url = $url; + } + + /** + * Get images + * + * @return ObjectStorage + */ + public function getImages(): ObjectStorage + { + if ($this->images === null) { + $this->images = new ObjectStorage(); + } + + return $this->images; + } + + /** + * Set images + * + * @param ObjectStorage $images + */ + public function setImages(ObjectStorage $images): void + { + $this->images = $images; + } + + /** + * Add image + * + * @param FileReference $image + * @return void + */ + public function addImage(FileReference $image): void + { + $images = $this->getImages(); + if (!$images->contains($image)) { + $images->attach($image); + } + } + + /** + * Remove image + * + * @param FileReference $image + * @return void + */ + public function removeImage(FileReference $image): void + { + $images = $this->getImages(); + if ($images->contains($image)) { + $images->detach($image); + } + } + + /** + * Get first image + * + * @return FileReference|null + */ + public function getImage(): ?FileReference + { + $images = $this->getImages(); + $images->rewind(); + + return $images->count() > 0 ? $images->current() : null; + } + + /** + * Get last login + * + * @return DateTime|null + */ + public function getLastLogin(): ?DateTime + { + return $this->lastLogin; + } + + /** + * Set last login + * + * @param DateTime|null $lastLogin + * @return void + */ + public function setLastLogin(?DateTime $lastLogin): void + { + $this->lastLogin = $lastLogin; + } +} diff --git a/Classes/Domain/Model/FrontendUserGroup.php b/Classes/Domain/Model/FrontendUserGroup.php new file mode 100644 index 0000000..461f980 --- /dev/null +++ b/Classes/Domain/Model/FrontendUserGroup.php @@ -0,0 +1,152 @@ + + */ + +namespace Ydt\FrontendUser\Domain\Model; + +use TYPO3\CMS\Extbase\Annotation as Extbase; +use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; +use TYPO3\CMS\Extbase\Persistence\ObjectStorage; + +/** + * Class FrontendUserGroup + * Frontend user group domain model + */ +class FrontendUserGroup extends AbstractEntity +{ + /** + * Title + * + * @var string + * @Extbase\Validate("NotEmpty") + * @Extbase\Validate("StringLength", options={"maximum": 50}) + */ + protected $title; + + /** + * Description + * + * @var string|null + */ + protected $description; + + /** + * Subgroups + * + * @var ObjectStorage + */ + protected $subgroups; + + /** + * FrontendUserGroup constructor + * + * @param string $title + */ + public function __construct( + string $title + ) { + $this->title = $title; + } + + /** + * Get title + * + * @return string + */ + public function getTitle(): string + { + return (string)$this->title; + } + + /** + * Set title + * + * @param string $title + * @return void + */ + public function setTitle(string $title): void + { + $this->title = $title; + } + + /** + * Get description + * + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * Set description + * + * @param string|null $description + * @return void + */ + public function setDescription(?string $description): void + { + $this->description = $description; + } + + /** + * Get subgroups + * + * @return ObjectStorage + */ + public function getSubgroups(): ObjectStorage + { + if ($this->subgroups == null) { + $this->subgroups = new ObjectStorage(); + } + + return $this->subgroups; + } + + /** + * Set subgroups + * + * @param ObjectStorage $subgroups + * @return void + */ + public function setSubgroups(ObjectStorage $subgroups): void + { + $this->subgroups = $subgroups; + } + + /** + * Add subgroup + * + * @param FrontendUserGroup $subgroup + * @return void + */ + public function addSubgroup(FrontendUserGroup $subgroup): void + { + $subgroups = $this->getSubgroups(); + if (!$subgroups->contains($subgroup)) { + $subgroups->attach($subgroup); + } + } + + /** + * Remove subgroup + * + * @param FrontendUserGroup $subgroup + * @return void + */ + public function removeSubgroup(FrontendUserGroup $subgroup): void + { + $subgroups = $this->getSubgroups(); + if ($subgroups->contains($subgroup)) { + $subgroups->detach($subgroup); + } + } +} \ No newline at end of file diff --git a/Classes/Domain/Repository/FrontendUserGroupRepository.php b/Classes/Domain/Repository/FrontendUserGroupRepository.php new file mode 100644 index 0000000..5ae6942 --- /dev/null +++ b/Classes/Domain/Repository/FrontendUserGroupRepository.php @@ -0,0 +1,23 @@ + + */ + +namespace Ydt\FrontendUser\Domain\Repository; + +use TYPO3\CMS\Extbase\Persistence\Repository; + +/** + * Class FrontendUserGroupRepository + * Frontend user group repository + */ +class FrontendUserGroupRepository extends Repository +{ + +} diff --git a/Classes/Domain/Repository/FrontendUserRepository.php b/Classes/Domain/Repository/FrontendUserRepository.php new file mode 100644 index 0000000..5ba4196 --- /dev/null +++ b/Classes/Domain/Repository/FrontendUserRepository.php @@ -0,0 +1,48 @@ + + */ + +namespace Ydt\FrontendUser\Domain\Repository; + +use TYPO3\CMS\Extbase\Persistence\Repository; +use Ydt\FrontendUser\Domain\Model\FrontendUser; + +/** + * Class FrontendUserRepository + * Frontend user repository + */ +class FrontendUserRepository extends Repository +{ + /** + * Find frontend user by username + * + * @param string $username + * @param int $pid + * @return FrontendUser|null + */ + public function findByUsername(string $username, int $pid = 0): ?FrontendUser + { + $query = $this->createQuery(); + + if ($pid) { + $settings = $query->getQuerySettings(); + $storagePageIds = $settings->getStoragePageIds(); + $storagePageIds[] = $pid; + $settings->setStoragePageIds(array_unique($storagePageIds)); + } + + $constraint = $query->equals('username', $username); + $query->matching($constraint); + + $result = $query->execute(); + + return $result->getFirst(); + } +} diff --git a/Classes/Domain/Validator/FrontendUserValidator.php b/Classes/Domain/Validator/FrontendUserValidator.php new file mode 100644 index 0000000..7ba0b90 --- /dev/null +++ b/Classes/Domain/Validator/FrontendUserValidator.php @@ -0,0 +1,56 @@ + + */ + +namespace Ydt\FrontendUser\Domain\Validator; + +use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator; +use Ydt\FrontendUser\Domain\Model\FrontendUser; + +/** + * Class FrontendUserValidator + * Frontend user domain model validator + */ +class FrontendUserValidator extends AbstractValidator +{ + /** + * @inheritdoc + */ + protected function isValid($frontendUser): void + { + if (!$frontendUser instanceof FrontendUser) { + $this->addError( + $this->translateErrorMessage( + 'validator.object.wrongClass', + 'frontend_user' + ), + (int)uniqid() + ); + + return; + } + + if ( + !$frontendUser->getPassword() + || !$frontendUser->getPasswordConfirmation() + || $frontendUser->getPassword() !== $frontendUser->getPasswordConfirmation() + ) { + $this->addError( + $this->translateErrorMessage( + 'validator.property.password.notMatched', + 'frontend_user' + ), + (int)uniqid() + ); + + return; + } + } +} diff --git a/Classes/Domain/Validator/PhoneValidator.php b/Classes/Domain/Validator/PhoneValidator.php new file mode 100644 index 0000000..66371c5 --- /dev/null +++ b/Classes/Domain/Validator/PhoneValidator.php @@ -0,0 +1,39 @@ + + */ + +namespace Ydt\FrontendUser\Domain\Validator; + +use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator; + +/** + * Class PhoneValidator + * Phone frontend user domain model property validator + */ +class PhoneValidator extends AbstractValidator +{ + /** + * @inheritdoc + */ + protected function isValid($value): void + { + if (!is_string($value) || !(preg_match('/^[\d+](?:[\d]*\([\d]*\)){0,1}[\d\s\-]*\d$/u', $value))) { + $this->addError( + $this->translateErrorMessage( + 'validator.property.phone.notValid', + 'frontend_user' + ), + (int)uniqid() + ); + + return; + } + } +} diff --git a/Classes/Enumeration/FrontendUser/Property.php b/Classes/Enumeration/FrontendUser/Property.php new file mode 100644 index 0000000..597ffb1 --- /dev/null +++ b/Classes/Enumeration/FrontendUser/Property.php @@ -0,0 +1,40 @@ + + */ + +namespace Ydt\FrontendUser\Enumeration\FrontendUser; + +use TYPO3\CMS\Core\Type\Enumeration; + +/** + * Class Property + * List of frontend user properties + */ +final class Property extends Enumeration +{ + /** + * List of frontend user properties + */ + const COMPANY = 'company'; + const JOB_TITLE = 'jobTitle'; + const NAME = 'name'; + const FIRST_NAME = 'firstName'; + const MIDDLE_NAME = 'middleName'; + const LAST_NAME = 'lastName'; + const STREET_ADDRESS = 'streetAddress'; + const ZIP_CODE = 'zipCode'; + const CITY = 'city'; + const COUNTRY = 'country'; + const PHONE = 'phone'; + const FAX = 'fax'; + const EMAIL = 'email'; + const URL = 'url'; + const IMAGE = 'image'; +} diff --git a/Classes/Event/AbstractFrontendUserActionEvent.php b/Classes/Event/AbstractFrontendUserActionEvent.php new file mode 100644 index 0000000..2f2ae44 --- /dev/null +++ b/Classes/Event/AbstractFrontendUserActionEvent.php @@ -0,0 +1,91 @@ + + */ + +namespace Ydt\FrontendUser\Event; + +use Psr\Http\Message\ServerRequestInterface; +use Ydt\FrontendUser\Domain\Model\FrontendUser; + +/** + * Class AbstractFrontendUserActionEvent + * Abstract frontend user action event + */ +abstract class AbstractFrontendUserActionEvent +{ + /** + * Frontend user + * + * @var FrontendUser + */ + protected $frontendUser; + + /** + * Request + * + * @var ServerRequestInterface|null + */ + protected $request; + + /** + * AbstractFrontendUserActionEvent constructor + * + * @param FrontendUser $frontendUser + * @param ServerRequestInterface|null $request + */ + public function __construct( + FrontendUser $frontendUser, + ServerRequestInterface $request = null + ) { + $this->frontendUser = $frontendUser; + $this->request = $request; + } + + /** + * Get frontend user + * + * @return FrontendUser + */ + public function getFrontendUser(): FrontendUser + { + return $this->frontendUser; + } + + /** + * Set frontend user + * + * @param FrontendUser $frontendUser + */ + public function setFrontendUser(FrontendUser $frontendUser): void + { + $this->frontendUser = $frontendUser; + } + + /** + * Get request + * + * @return ServerRequestInterface|null + */ + public function getRequest(): ?ServerRequestInterface + { + return $this->request; + } + + /** + * Set request + * + * @param ServerRequestInterface|null $request + * @return void + */ + public function setRequest(?ServerRequestInterface $request): void + { + $this->request = $request; + } +} diff --git a/Classes/Event/FrontendUserCreateAfterEvent.php b/Classes/Event/FrontendUserCreateAfterEvent.php new file mode 100644 index 0000000..34eed77 --- /dev/null +++ b/Classes/Event/FrontendUserCreateAfterEvent.php @@ -0,0 +1,21 @@ + + */ + +namespace Ydt\FrontendUser\Event; + +/** + * Class FrontendUserCreateAfterEvent + * Frontend user create after event + */ +final class FrontendUserCreateAfterEvent extends AbstractFrontendUserActionEvent +{ + +} diff --git a/Classes/Event/FrontendUserDeleteAfterEvent.php b/Classes/Event/FrontendUserDeleteAfterEvent.php new file mode 100644 index 0000000..df9bde4 --- /dev/null +++ b/Classes/Event/FrontendUserDeleteAfterEvent.php @@ -0,0 +1,21 @@ + + */ + +namespace Ydt\FrontendUser\Event; + +/** + * Class FrontendUserDeleteAfterEvent + * Frontend user delete after event + */ +final class FrontendUserDeleteAfterEvent extends AbstractFrontendUserActionEvent +{ + +} diff --git a/Classes/Event/FrontendUserFormViewModifyEvent.php b/Classes/Event/FrontendUserFormViewModifyEvent.php new file mode 100644 index 0000000..1e1e7a5 --- /dev/null +++ b/Classes/Event/FrontendUserFormViewModifyEvent.php @@ -0,0 +1,60 @@ + + */ + +namespace Ydt\FrontendUser\Event; + +use TYPO3\CMS\Extbase\Mvc\View\ViewInterface; + +/** + * Class FrontendUserFormViewModifyEvent + * Frontend user form view modify event + */ +final class FrontendUserFormViewModifyEvent +{ + /** + * View + * + * @var ViewInterface + */ + protected $view; + + /** + * FrontendUserFormViewModifyEvent constructor + * + * @param ViewInterface $view + */ + public function __construct( + ViewInterface $view + ) { + $this->view = $view; + } + + /** + * Get view + * + * @return ViewInterface + */ + public function getView(): ViewInterface + { + return $this->view; + } + + /** + * Set view + * + * @param ViewInterface $view + * @return void + */ + public function setView(ViewInterface $view): void + { + $this->view = $view; + } +} diff --git a/Classes/Event/FrontendUserUpdateAfterEvent.php b/Classes/Event/FrontendUserUpdateAfterEvent.php new file mode 100644 index 0000000..8d81972 --- /dev/null +++ b/Classes/Event/FrontendUserUpdateAfterEvent.php @@ -0,0 +1,21 @@ + + */ + +namespace Ydt\FrontendUser\Event; + +/** + * Class FrontendUserUpdateAfterEvent + * Frontend user update after event + */ +final class FrontendUserUpdateAfterEvent extends AbstractFrontendUserActionEvent +{ + +} diff --git a/Classes/EventListener/FrontendUserCreateAfterEventListener.php b/Classes/EventListener/FrontendUserCreateAfterEventListener.php new file mode 100644 index 0000000..2a172d4 --- /dev/null +++ b/Classes/EventListener/FrontendUserCreateAfterEventListener.php @@ -0,0 +1,76 @@ + + */ + +namespace Ydt\FrontendUser\EventListener; + +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManager; +use Ydt\FrontendUser\Authentication\FrontendUserAuthentication; +use Ydt\FrontendUser\Event\FrontendUserCreateAfterEvent; + +/** + * Class FrontendUserCreateAfterEventListener + * Frontend user create after event listener + */ +class FrontendUserCreateAfterEventListener implements LoggerAwareInterface +{ + use LoggerAwareTrait; + + /** + * Frontend User Authentication + * + * @var FrontendUserAuthentication + */ + protected $frontendUserAuthentication; + + /** + * Configuration Manager + * + * @var ConfigurationManager + */ + protected $configurationManager; + + /** + * FrontendUserCreateAfterEventListener constructor + * + * @param FrontendUserAuthentication $frontendUserAuthentication + * @param ConfigurationManager $configurationManager + */ + public function __construct( + FrontendUserAuthentication $frontendUserAuthentication, + ConfigurationManager $configurationManager + ) { + $this->frontendUserAuthentication = $frontendUserAuthentication; + $this->configurationManager = $configurationManager; + } + + /** + * Log in new frontend user + * + * @param FrontendUserCreateAfterEvent $event + * @return void + */ + public function __invoke(FrontendUserCreateAfterEvent $event): void + { + try { + $settings = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS); + $request = $event->getRequest(); + + if (isset($settings['enableFrontendUserAutoLogin']) && $settings['enableFrontendUserAutoLogin'] && $request) { + $this->frontendUserAuthentication->start($request); + } + } catch (Exception $exception) { + $this->logger->error('Login failed. ' . $exception->getMessage()); + } + } +} diff --git a/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php b/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php new file mode 100644 index 0000000..ee4fc7d --- /dev/null +++ b/Classes/Property/TypeConverter/UploadedFileReferenceConverter.php @@ -0,0 +1,145 @@ + + */ + +namespace Ydt\FrontendUser\Property\TypeConverter; + +use TYPO3\CMS\Core\Resource\ResourceFactory; +use TYPO3\CMS\Core\Resource\StorageRepository; +use TYPO3\CMS\Extbase\Domain\Model\FileReference; +use TYPO3\CMS\Extbase\Error\Error; +use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface; +use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter; +use Exception; + +/** + * Class UploadedFileReferenceConverter + * Convert uploaded file data array to Extbase file reference + */ +class UploadedFileReferenceConverter extends AbstractTypeConverter +{ + /** + * Converter configuration + */ + const CONFIGURATION_TARGET_FOLDER_IDENTIFIER = 'targetFolderIdentifier'; + + /** + * Storage Repository + * + * @var StorageRepository + */ + protected $storageRepository; + + /** + * Resource Factory + * + * @var ResourceFactory + */ + protected $resourceFactory; + + /** + * Source types + * + * @var array + */ + protected $sourceTypes = ['array']; + + /** + * Target type + * + * @var string + */ + protected $targetType = FileReference::class; + + /** + * Priority + * + * @var int + */ + protected $priority = 15; + + /** + * UploadedFileReferenceConverter constructor + * + * @param StorageRepository $storageRepository + * @param ResourceFactory $resourceFactory + */ + public function __construct( + StorageRepository $storageRepository, + ResourceFactory $resourceFactory + ) { + $this->storageRepository = $storageRepository; + $this->resourceFactory = $resourceFactory; + } + + /** + * @inheritdoc + */ + public function convertFrom( + $source, + string $targetType, + array $convertedChildProperties = [], + PropertyMappingConfigurationInterface $configuration = null + ) { + $result = null; + $defaultStorage = $this->storageRepository->getDefaultStorage(); + if ($defaultStorage) { + try { + if ( + $configuration + && $configuration->getConfigurationValue(self::class, self::CONFIGURATION_TARGET_FOLDER_IDENTIFIER) + ) { + $targetFolderIdentifier = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_TARGET_FOLDER_IDENTIFIER); + $targetFolder = $defaultStorage->getFolder($targetFolderIdentifier); + } else { + $targetFolder = $defaultStorage->getDefaultFolder(); + } + + $file = $defaultStorage->addFile($source['tmp_name'], $targetFolder, $source['name']); + + $fileResource = $this->resourceFactory->createFileReferenceObject(['uid_local' => (int)$file->getUid()]); + + $result = new FileReference(); + $result->setOriginalResource($fileResource); + } catch (Exception $exception) { + $result = new Error($exception->getMessage(), (int)uniqid()); + } + } + + return $result; + } + + /** + * @inheritdoc + */ + public function canConvertFrom($source, string $targetType): bool + { + return is_array($source) + && !empty($source['tmp_name']) + && !empty($source['name']) + && !empty($source['size']) + && !empty($source['type']) + && $this->isImageFileExtensionValid($source['name']); + } + + /** + * Check if image file extension valid + * + * @param string $imageFileName + * @return bool + */ + protected function isImageFileExtensionValid(string $imageFileName): bool + { + $imageFileNameInfo = pathinfo($imageFileName); + $imageFileExtensions = explode(',', $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']); + + return isset($imageFileNameInfo['extension']) && in_array($imageFileNameInfo['extension'], $imageFileExtensions); + } +} diff --git a/Classes/Service/FrontendUserFormService.php b/Classes/Service/FrontendUserFormService.php new file mode 100644 index 0000000..369299f --- /dev/null +++ b/Classes/Service/FrontendUserFormService.php @@ -0,0 +1,53 @@ + + */ + +namespace Ydt\FrontendUser\Service; + +use TYPO3\CMS\Core\Localization\LanguageService; +use Ydt\FrontendUser\Enumeration\FrontendUser\Property as FrontendUserProperty; + +/** + * Class FrontendUserFormService + * Frontend user form service + */ +class FrontendUserFormService +{ + /** + * itemsProcFunc for getting new/edit frontend user form fields + * + * @param array $configuration + * @return void + */ + public function getFieldItems(array &$configuration): void + { + $languageService = $this->getLanguageService(); + + $items = []; + foreach (FrontendUserProperty::getConstants() as $property) { + $items[] = [ + $languageService->sL('LLL:EXT:frontend_user/Resources/Private/Language/locallang.xlf:' . $property), + $property, + ]; + } + + $configuration['items'] = $items; + } + + /** + * Get language service + * + * @return LanguageService + */ + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Validation/Validator/DigitValidator.php b/Classes/Validation/Validator/DigitValidator.php new file mode 100644 index 0000000..f0ac77d --- /dev/null +++ b/Classes/Validation/Validator/DigitValidator.php @@ -0,0 +1,42 @@ + + */ + +namespace Ydt\FrontendUser\Validation\Validator; + +use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator; + +/** + * Class DigitValidator + * Validator for digital strings + */ +class DigitValidator extends AbstractValidator +{ + /** + * Check if all characters in the given string value are numerical + * + * @param mixed $value + * @return void + */ + protected function isValid($value): void + { + if (!is_string($value) || !ctype_digit($value)) { + $this->addError( + $this->translateErrorMessage( + 'validator.digit.notValid', + 'frontend_user' + ), + (int)uniqid() + ); + + return; + } + } +} diff --git a/Classes/ViewHelpers/Format/LowercaseHyphenatedViewHelper.php b/Classes/ViewHelpers/Format/LowercaseHyphenatedViewHelper.php new file mode 100644 index 0000000..5ce3fe0 --- /dev/null +++ b/Classes/ViewHelpers/Format/LowercaseHyphenatedViewHelper.php @@ -0,0 +1,58 @@ + + */ + +namespace Ydt\FrontendUser\ViewHelpers\Format; + +use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; +use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; +use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic; +use Closure; + +/** + * Class LowercaseHyphenatedViewHelper + * Split string by capital letters, make parts lowercase and join them with a hyphen + */ +class LowercaseHyphenatedViewHelper extends AbstractViewHelper +{ + use CompileWithRenderStatic; + + /** + * @inheritdoc + */ + public function initializeArguments(): void + { + $this->registerArgument('value', 'string', 'String value in camelCase', true, []); + } + + /** + * @inheritdoc + */ + public static function renderStatic( + array $arguments, + Closure $renderChildrenClosure, + RenderingContextInterface $renderingContext + ): string { + $value = $arguments['value']; + + if (empty($value)) { + $value = $renderChildrenClosure(); + } + + $parts = preg_split('/(?=[A-Z])/', $value); + + $formattedParts = []; + foreach ($parts as $part) { + $formattedParts[] = strtolower(trim($part)); + } + + return implode('-', $formattedParts); + } +} diff --git a/Configuration/Extbase/Persistence/Classes.php b/Configuration/Extbase/Persistence/Classes.php new file mode 100644 index 0000000..db2ae0f --- /dev/null +++ b/Configuration/Extbase/Persistence/Classes.php @@ -0,0 +1,53 @@ + + */ + +use Ydt\FrontendUser\Domain\Model\FrontendUser; +use Ydt\FrontendUser\Domain\Model\FrontendUserGroup; + +return [ + FrontendUser::class => [ + 'tableName' => 'fe_users', + 'properties' => [ + 'userGroups' => [ + 'fieldName' => 'usergroup', + ], + 'jobTitle' => [ + 'fieldName' => 'title', + ], + 'streetAddress' => [ + 'fieldName' => 'address', + ], + 'zipCode' => [ + 'fieldName' => 'zip', + ], + 'phone' => [ + 'fieldName' => 'telephone', + ], + 'url' => [ + 'fieldName' => 'www', + ], + 'images' => [ + 'fieldName' => 'image', + ], + 'lastLogin' => [ + 'fieldName' => 'lastlogin', + ], + ], + ], + FrontendUserGroup::class => [ + 'tableName' => 'fe_groups', + 'properties' => [ + 'subgroups' => [ + 'fieldName' => 'subgroup', + ], + ], + ], +]; diff --git a/Configuration/FlexForms/FrontendUserForm.xml b/Configuration/FlexForms/FrontendUserForm.xml new file mode 100644 index 0000000..9ea7c51 --- /dev/null +++ b/Configuration/FlexForms/FrontendUserForm.xml @@ -0,0 +1,182 @@ + + + + + + + + + LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.sheets.general + + + array + + + + + select + selectSingle + + + fe_groups + + + + + + + group + pages + 1 + 1 + 0 + + + + + + + group + folder + 1 + 1 + 0 + + + + reload + + + check + 1 + + + + LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enabled + + 1 + + + + + + + FIELD:settings.enableFrontendUserAutoLogin:REQ:false + + check + + + + LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enabled + + 1 + + + + + + + + FIELD:settings.enableFrontendUserAutoLogin:REQ:false + + group + pages + 1 + 1 + 0 + + + + + + + check + + + + LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enabled + + 1 + + + + + + + + + + + + LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.sheets.view + + + array + + + + + select + Ydt\FrontendUser\Service\FrontendUserFormService->getFieldItems + selectMultipleSideBySide + + + + + + + + select + Ydt\FrontendUser\Service\FrontendUserFormService->getFieldItems + selectMultipleSideBySide + + + + + + + + LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.streetAddressLineNum.description + + + input + int + 2 + + 1 + 4 + + + + + + + + \ No newline at end of file diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..fd8d289 --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,23 @@ +#### +# Frontend User Extension +# +# @copyright Copyright (c) 2022 Your Dev Team Global (https://ydt-global.com/) +# @author YDT Global Team +#### +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Ydt\FrontendUser\: + resource: '../Classes/*' + + Ydt\FrontendUser\Authentication\FrontendUserAuthentication: + public: true + + Ydt\FrontendUser\EventListener\FrontendUserCreateAfterEventListener: + tags: + - name: event.listener + identifier: 'newFrontendUserAutoLogin' + event: Ydt\FrontendUser\Event\FrontendUserCreateAfterEvent \ No newline at end of file diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php new file mode 100644 index 0000000..934a01e --- /dev/null +++ b/Configuration/TCA/Overrides/tt_content.php @@ -0,0 +1,64 @@ + + */ + +use TYPO3\CMS\Extbase\Utility\ExtensionUtility; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; + +defined('TYPO3') or die(); + +(function () { + ExtensionUtility::registerPlugin( + 'FrontendUser', + 'Form', + 'LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.title', + 'content-user', + 'forms' + ); + + ExtensionManagementUtility::addPiFlexFormValue( + '*', + 'FILE:EXT:frontend_user/Configuration/FlexForms/FrontendUserForm.xml', + 'frontenduser_form' + ); + + ExtensionManagementUtility::addTcaSelectItem( + 'tt_content', + 'CType', + [ + 'LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.title', + 'frontenduser_form', + 'content-user', + ] + ); + + $GLOBALS['TCA']['tt_content']['types']['frontenduser_form']['showitem'] = ' + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general, + --palette--;;general, + --palette--;;headers, + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.plugin, + pi_flexform, + --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance, + --palette--;;frames, + --palette--;;appearanceLinks, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, + --palette--;;language, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access, + --palette--;;hidden, + --palette--;;access, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, + categories, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:notes, + rowDescription, + --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:extended, + '; + + $GLOBALS['TCA']['fe_users']['columns']['image']['config']['maxitems']= 1; +})(); \ No newline at end of file diff --git a/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig b/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig new file mode 100644 index 0000000..436fa11 --- /dev/null +++ b/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig @@ -0,0 +1,19 @@ +/** +* Frontend User Extension +* +* @copyright Copyright (c) 2022 Your Dev Team Global (https://ydt-global.com/) +* @author YDT Global Team +*/ +mod.wizards.newContentElement.wizardItems.forms { + elements { + frontenduser_form { + iconIdentifier = content-user + title = LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.title + description = LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.description + tt_content_defValues { + CType = frontenduser_form + } + } + } + show := addToList(frontenduser_form) +} diff --git a/Configuration/TypoScript/constants.typoscript b/Configuration/TypoScript/constants.typoscript new file mode 100644 index 0000000..560db63 --- /dev/null +++ b/Configuration/TypoScript/constants.typoscript @@ -0,0 +1,51 @@ +/** +* Frontend User Extension +* +* @copyright Copyright (c) 2022 Your Dev Team Global (https://ydt-global.com/) +* @author YDT Global Team +*/ +plugin.tx_frontenduser_form { + view { + # cat=plugin.tx_frontenduser_form/file; type=string; label=Path to template root (FE) + templateRootPath = + # cat=plugin.tx_frontenduser_form/file; type=string; label=Path to template partials (FE) + partialRootPath = + # cat=plugin.tx_frontenduser_form/file; type=string; label=Path to template layouts (FE) + layoutRootPath = + } + + settings { + # cat=plugin.tx_frontenduser_form/settings; type=int+; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.defaultFrontendUserGroupId + defaultFrontendUserGroupId = + + # cat=plugin.tx_frontenduser_form/settings; type=int+; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.frontendUserStoragePid.label + frontendUserStoragePid = + + # cat=plugin.tx_frontenduser_form/settings; type=string; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.frontendUserImageFolder + frontendUserImageFolder = + + # cat=plugin.tx_frontenduser_form/settings; type=boolean; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.enableFrontendUserAutoLogin + enableFrontendUserAutoLogin = + + # cat=plugin.tx_frontenduser_form/settings; type=boolean; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.enableRedirectToLoginPage + enableRedirectToLoginPage = + + # cat=plugin.tx_frontenduser_form/settings; type=int+; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.redirectLoginPageId + redirectLoginPageId = + + # cat=plugin.tx_frontenduser_form/settings; type=boolean; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.enableFrontendUserDeletion + enableFrontendUserDeletion = + + # cat=plugin.tx_frontenduser_form/settings; type=string; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.newFrontendUserFormFields + newFrontendUserFormFields = + + # cat=plugin.tx_frontenduser_form/settings; type=string; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.editFrontendUserFormFields + editFrontendUserFormFields = + + # cat=plugin.tx_frontenduser_form/settings; type=int; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.streetAddressLineNum.label + streetAddressLineNum = + + # cat=plugin.tx_frontenduser_form/settings; type=string; label=LLL:EXT:frontend_user/Resources/Private/Language/locallang_be.xlf:plugin.flexForm.settings.frontendUserImageSize + frontendUserImageSize = 150 + } +} diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.typoscript new file mode 100644 index 0000000..c4e5f0c --- /dev/null +++ b/Configuration/TypoScript/setup.typoscript @@ -0,0 +1,41 @@ +/** +* Frontend User Extension +* +* @copyright Copyright (c) 2022 Your Dev Team Global (https://ydt-global.com/) +* @author YDT Global Team +*/ +plugin.tx_frontenduser_form { + view { + templateRootPaths { + 10 = EXT:frontend_user/Resources/Private/Templates/ + 20 = {$plugin.tx_frontenduser_form.view.templateRootPath} + } + + partialRootPaths { + 10 = EXT:frontend_user/Resources/Private/Partials/ + 20 = {$plugin.tx_frontenduser_form.view.partialRootPath} + } + + layoutRootPaths { + 10 = EXT:frontend_user/Resources/Private/Layouts/ + 20 = {$plugin.tx_frontenduser_form.view.layoutRootPath} + } + } + + settings { + # General + defaultFrontendUserGroupId = {$plugin.tx_frontenduser_form.settings.defaultFrontendUserGroupId} + frontendUserStoragePid = {$plugin.tx_frontenduser_form.settings.frontendUserStoragePid} + frontendUserImageFolder = {$plugin.tx_frontenduser_form.settings.frontendUserImageFolder} + enableFrontendUserAutoLogin = {$plugin.tx_frontenduser_form.settings.enableFrontendUserAutoLogin} + enableRedirectToLoginPage = {$plugin.tx_frontenduser_form.settings.enableRedirectToLoginPage} + redirectLoginPageId = {$plugin.tx_frontenduser_form.settings.redirectLoginPageId} + enableFrontendUserDeletion = {$plugin.tx_frontenduser_form.settings.enableFrontendUserDeletion} + + # View + newFrontendUserFormFields = {$plugin.tx_frontenduser_form.settings.newFrontendUserFormFields} + editFrontendUserFormFields = {$plugin.tx_frontenduser_form.settings.editFrontendUserFormFields} + streetAddressLineNum = {$plugin.tx_frontenduser_form.settings.streetAddressLineNum} + frontendUserImageSize = {$plugin.tx_frontenduser_form.settings.frontendUserImageSize} + } +} \ No newline at end of file diff --git a/Documentation/Configuration/Index.rst b/Documentation/Configuration/Index.rst new file mode 100644 index 0000000..cf215ed --- /dev/null +++ b/Documentation/Configuration/Index.rst @@ -0,0 +1,25 @@ +.. include:: /Includes.rst.txt + +.. _configuration: + +============= +Configuration +============= + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + Settings/General + Settings/View + TypoScriptReference/Index + +Where to find the plugin? +========================= + +The :php:`frontenduser_form` plugin is available through the Content Element Wizard as *Website User Form*: + +.. figure:: ../Images/ContentElementWizard.png + :alt: The content element wizard + + *Website User Form* plugin in the Content Element Wizard \ No newline at end of file diff --git a/Documentation/Configuration/Settings/General.rst b/Documentation/Configuration/Settings/General.rst new file mode 100644 index 0000000..531e6c4 --- /dev/null +++ b/Documentation/Configuration/Settings/General.rst @@ -0,0 +1,153 @@ +.. include:: /Includes.rst.txt + +.. _configuration-settings-general: + +================ +General Settings +================ + +.. figure:: ../../Images/GeneralTab.png + :class: with-shadow + :alt: General Tab in the TYPO3 backend + + *General Tab* in the TYPO3 backend + +If `Login After Successful User Creation <#enablefrontenduserautologin>`__ is disabled, additional options are displayed. + +.. figure:: ../../Images/GeneralTabHiddenFields.png + :class: with-shadow + :alt: General Tab in the TYPO3 backend + + + +.. _defaultfrontendusergroupid: + +Default User Group +================== +.. container:: table-row + + Property + defaultFrontendUserGroupId + + Data type + int + + Description + During creation, a website user will be assigned to the selected user group. + + + +.. _frontenduserstoragepid: + +User Storage Page +================= +.. container:: table-row + + Property + frontendUserStoragePid + + Data type + int + + Description + The page (folder) where website users are stored. This value must be in the array of Login Form plugin + `User Storage Page `__ values. + Otherwise, website user authentication will fail. + + .. important:: + + If `checkFeUserPid `__ + is set to "1", this setting will be ignored. + + .. seealso:: + + `Choosing a User Storage Page for Website Users `__. + + + +.. _frontenduserimagefolder: + +User Image Folder +================= +.. container:: table-row + + Property + frontendUserImageFolder + + Data type + string + + Description + The folder where website user image files are stored. + + + +.. _enablefrontenduserautologin: + +Login After Successful User Creation +==================================== +.. container:: table-row + + Property + enableFrontendUserAutoLogin + + Data type + bool + + Default + true + + Description + If enabled, a new website user will be automatically logged in. + + + +.. _enableredirecttologinpage: + +Redirect to Login Page +====================== +.. container:: table-row + + Property + enableRedirectToLoginPage + + Data type + bool + + Description + If enabled, a new website user will be redirected to the page specified in `Login Page <#redirectloginpageid>`__. + + + +.. _redirectloginpageid: + +Login Page +========== +.. container:: table-row + + Property + redirectLoginPageId + + Data type + int + + Description + The page supposedly contains the *Login Form* plugin. + + + +.. _enablefrontenduserdeletion: + +Enable Deletion of a User +========================= +.. container:: table-row + + Property + enableFrontendUserDeletion + + Data type + bool + + Description + If enabled, on the TYPO3 frontend :php:`Delete User` link will be displayed. + Also, this setting is checked in the :php:`deleteAction(…$args)` method of the controller. diff --git a/Documentation/Configuration/Settings/View.rst b/Documentation/Configuration/Settings/View.rst new file mode 100644 index 0000000..ade70be --- /dev/null +++ b/Documentation/Configuration/Settings/View.rst @@ -0,0 +1,74 @@ +.. include:: /Includes.rst.txt + +.. _configuration-settings-view: + +============= +View Settings +============= + +.. figure:: ../../Images/ViewTab.png + :class: with-shadow + :alt: View Tab in the TYPO3 backend + + *View Tab* in the TYPO3 Backend + +.. _newfrontenduserformfields: + +Create User Form Fields +======================= +.. container:: table-row + + Property + newFrontendUserFormFields + + Data type + string + + Description + List of fields that will be displayed in the form for website user creation. + + .. note:: + + :php:`Username`, :php:`Password`, and :php:`Password Confirmation` are required fields and are displayed by default. + + + +.. _editfrontenduserformfields: + +Edit User Form Fields +===================== +.. container:: table-row + + Property + editFrontendUserFormFields + + Data type + string + + Description + List of fields that will be displayed in the form for website user update. + + .. note:: + + :php:`Username` field is disabled as it cannot be updated by a website user. + :php:`Password` and :php:`Password Confirmation` are displayed and updated only if :php:`Change Password` is checked. + + + +.. _streetaddresslinenum: + +Number of Lines in Street Address +================================= +.. container:: table-row + + Property + streetAddressLineNum + + Data type + int+ + + Default + 2 + + Description + The number of lines for the street address :php:`textarea` field. Available values are ranged between 1-4. diff --git a/Documentation/Configuration/TypoScriptReference/Index.rst b/Documentation/Configuration/TypoScriptReference/Index.rst new file mode 100644 index 0000000..cfce6e0 --- /dev/null +++ b/Documentation/Configuration/TypoScriptReference/Index.rst @@ -0,0 +1,29 @@ +.. include:: /Includes.rst.txt + +.. _configuration-typoscript: + +==================== +TypoScript Reference +==================== + +.. _configuration-typoscript-constants: + +Constants +========= + +.. _frontenduserimagesize: + +User Image Size +--------------- + +.. confval:: frontendUserImageSize + + :type: int + :Default: 150 + + Width of website user image size in the TYPO3 frontend. In case of overriding a template, this setting can be used + as image height or just ignored for styling form with CSS. + + Example:: + + plugin.tx_frontenduser_form.settings.frontendUserImageSize = 200 diff --git a/Documentation/Developer/Domain.rst b/Documentation/Developer/Domain.rst new file mode 100644 index 0000000..af70159 --- /dev/null +++ b/Documentation/Developer/Domain.rst @@ -0,0 +1,19 @@ +.. include:: /Includes.rst.txt + +.. _developer-domain: + +========================================== +Domain Models, Repositories and Validators +========================================== + +As :php:`Frontend User` and :php:`Frontend User Group` domain models have been marked as deprecated, +the extension provides its implementation of these domain models and repositories for them. Names of domain models' properties +have been changed a bit to make them more precise (for example, :php:`$userGroups` instead of :php:`$userGroup` +for the property of :php:`TYPO3\CMS\Extbase\Persistence\ObjectStorage` type) as well as appropriate labels in the TYPO3 backend. + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + FrontendUser/Index + FrontendUserGroup/Index \ No newline at end of file diff --git a/Documentation/Developer/Events.rst b/Documentation/Developer/Events.rst new file mode 100644 index 0000000..55cb6b3 --- /dev/null +++ b/Documentation/Developer/Events.rst @@ -0,0 +1,37 @@ +.. include:: /Includes.rst.txt + +.. _developer-events: + +====== +Events +====== + +The extension is designed to be easily extended and provides the following events: + +.. _frontenduserformviewmodifyevent: + +FrontendUserFormViewModifyEvent +=============================== + +Event dispatched after assigning variables to a view and used for its modification. + +.. _frontendusercreateafterevent: + +FrontendUserCreateAfterEvent +============================ + +Event dispatched after website user creation. + +.. _frontenduserupdateafterevent: + +FrontendUserUpdateAfterEvent +============================ + +Event dispatched after website user update. + +.. _frontenduserdeleteafterevent: + +FrontendUserDeleteAfterEvent +============================ + +Event dispatched after website user deletion. diff --git a/Documentation/Developer/FrontendUser/Index.rst b/Documentation/Developer/FrontendUser/Index.rst new file mode 100644 index 0000000..26681f6 --- /dev/null +++ b/Documentation/Developer/FrontendUser/Index.rst @@ -0,0 +1,15 @@ +.. include:: /Includes.rst.txt + +.. _frontenduser: + +============= +Frontend User +============= + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + Model + Repository + Validators \ No newline at end of file diff --git a/Documentation/Developer/FrontendUser/Model.rst b/Documentation/Developer/FrontendUser/Model.rst new file mode 100644 index 0000000..d4de903 --- /dev/null +++ b/Documentation/Developer/FrontendUser/Model.rst @@ -0,0 +1,384 @@ +.. include:: /Includes.rst.txt + +.. _frontenduser-domainmodel: + +============ +Domain Model +============ + +:php:`FrontendUser` domain model contains the following properties that are validated with annotation. +The validation rule for a property is specified based on the appropriate :php:`fe_users` table field description. +Some properties have been renamed to clarify what data are stored in them. +For more information about mapping between a database table and its model +see `Use arbitrary database tables with an Extbase model `__. + + + +.. _username: + +username +======== + +.. code-block:: php + + /** + * Username + * + * @var string + * @Extbase\Validate("NotEmpty") + * @Extbase\Validate("StringLength", options={"maximum": 255}) + * @Extbase\Validate("Text") + */ + protected $username; + + + +.. _password: + +password +======== + +.. code-block:: php + + /** + * Password + * + * @var string + */ + protected $password; + + + +.. _passwordconfirmation: + +passwordConfirmation +==================== + +.. code-block:: php + + /** + * Password confirmation + * + * @var string + * @Extbase\ORM\Transient + */ + protected $passwordConfirmation; + + + +.. _usergroups: + +userGroups +========== + +:php:`usergroup` field in :php:`fe_users` table that stores a comma-separated list of user groups. + +.. code-block:: php + + /** + * Frontend user groups + * + * @var ObjectStorage + */ + protected $userGroups; + + + +.. _company: + +company +======= + +.. code-block:: php + + /** + * Company + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 80}) + * @Extbase\Validate("Text") + */ + protected $company; + + + +.. _jobtitle: + +jobTitle +======== + +:php:`title` field in :php:`fe_users` table. + +.. code-block:: php + + /** + * Job title + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 40}) + * @Extbase\Validate("Text") + */ + protected $jobTitle; + + + +.. _name: + +name +==== + +.. code-block:: php + + /** + * Name + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 160}) + * @Extbase\Validate("Text") + */ + protected $name; + + + +.. _firstname: + +firstName +========= + +.. code-block:: php + + /** + * First name + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 50}) + * @Extbase\Validate("Text") + */ + protected $firstName; + + + +.. _middlename: + +middleName +========== + +.. code-block:: php + + /** + * Middle name + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 50}) + * @Extbase\Validate("Text") + */ + protected $middleName; + + + +.. _lastname: + +lastName +======== + +.. code-block:: php + + /** + * Last name + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 50}) + * @Extbase\Validate("Text") + */ + protected $lastName; + + + +.. _streetaddress: + +streetAddress +============= + +:php:`address` field in :php:`fe_users` table. + +.. code-block:: php + + /** + * Street address + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 255}) + * @Extbase\Validate("Text") + */ + protected $streetAddress; + + + +.. _zipcode: + +zipCode +======= + +:php:`zip` field in :php:`fe_users` table. + +.. code-block:: php + + /** + * Zip code + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 10}) + * @Extbase\Validate("AlphanumericValidator") + */ + protected $zipCode; + + + +.. _city: + +city +==== + +.. code-block:: php + + /** + * City + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 50}) + * @Extbase\Validate("Text") + */ + protected $city; + + + +.. _country: + +country +======= + +.. code-block:: php + + /** + * Country + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 40}) + * @Extbase\Validate("Text") + */ + protected $country; + + + +.. _phone: + +phone +===== + +:php:`telephone` field in :php:`fe_users` table. See :ref:`phone-validator`. + +.. code-block:: php + + /** + * Phone + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 30}) + * @Extbase\Validate("Ydt\FrontendUser\Domain\Validator\PhoneValidator") + */ + protected $phone; + + + +.. _fax: + +fax +=== + +See :ref:`digit-validator`. + +.. code-block:: php + + /** + * Fax + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 30}) + * @Extbase\Validate("Ydt\FrontendUser\Validation\Validator\DigitValidator") + */ + protected $fax; + + + +.. _email: + +email +===== + +.. code-block:: php + + /** + * Email + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 255}) + * @Extbase\Validate("EmailAddress") + */ + protected $email; + + + +.. _url: + +url +=== + +:php:`url` field in :php:`fe_users` table. + +.. code-block:: php + + /** + * Homepage url + * + * @var string + * @Extbase\Validate("StringLength", options={"maximum": 80}) + * @Extbase\Validate("Text") + * @Extbase\Validate("Url") + */ + protected $url; + + + +.. _images: + +images +====== + +See :ref:`faq2` + +.. code-block:: php + + /** + * Images + * + * @var ObjectStorage + */ + protected $images; + + + +.. _lastlogin: + +lastLogin +========= + +:php:`lastlogin` field in :php:`fe_users` table that stores the timestamp of last login date and time. + +.. code-block:: php + + /** + * Last login + * + * @var DateTime|null + */ + protected $lastLogin; \ No newline at end of file diff --git a/Documentation/Developer/FrontendUser/Repository.rst b/Documentation/Developer/FrontendUser/Repository.rst new file mode 100644 index 0000000..9811b8e --- /dev/null +++ b/Documentation/Developer/FrontendUser/Repository.rst @@ -0,0 +1,42 @@ +.. include:: /Includes.rst.txt + +.. _frontenduser-repository: + +========== +Repository +========== + +:php:`FrontendUserRepository` provides the following functionality: + +findByUsername +============== + +The method is used to find frontend user by username and page Uid. + +.. code-block:: php + + /** + * Find frontend user by username + * + * @param string $username + * @param int $pid + * @return FrontendUser|null + */ + public function findByUsername(string $username, int $pid = 0): ?FrontendUser + { + $query = $this->createQuery(); + + if ($pid) { + $settings = $query->getQuerySettings(); + $storagePageIds = $settings->getStoragePageIds(); + $storagePageIds[] = $pid; + $settings->setStoragePageIds(array_unique($storagePageIds)); + } + + $constraint = $query->equals('username', $username); + $query->matching($constraint); + + $result = $query->execute(); + + return $result->getFirst(); + } \ No newline at end of file diff --git a/Documentation/Developer/FrontendUser/Validators.rst b/Documentation/Developer/FrontendUser/Validators.rst new file mode 100644 index 0000000..dc89c79 --- /dev/null +++ b/Documentation/Developer/FrontendUser/Validators.rst @@ -0,0 +1,48 @@ +.. include:: /Includes.rst.txt + +.. _frontenduser-validators: + +========== +Validators +========== + +FrontendUser Validator +====================== + +The extension contains a validator for :php:`FrontendUser` domain model that checks :php:`password` and :php:`passwordConfirmation` +properties as well as type of :php:`$frontendUser` argument. + +Validation error codes +---------------------- +* Error message: + * :php:`The given object is not an instance of FrontendUser class.` + * :php:`Please make sure your passwords match.` + + + +.. _digit-validator: + +Digit Validator (:yaml:`Digit`) +=============================== +The digital validator checks the string contains only numeric characters [0-9]. + +Validation error codes +---------------------- +* Error message: :php:`The given subject is not a valid digit string.` + + + +.. _phone-validator: + +Phone Validator (:yaml:`Phone`) +=============================== +Phone frontend user domain model property validator. It checks if the provided string matches the following requirements: + +* Digit or :php:`+` sign in the beginning +* Digit in the end +* One combination of digits in parentheses is allowed +* Space and dash as digit separator are allowed + +Validation error codes +---------------------- +* Error message: :php:`The given subject is not a valid phone.` diff --git a/Documentation/Developer/FrontendUserGroup/Index.rst b/Documentation/Developer/FrontendUserGroup/Index.rst new file mode 100644 index 0000000..31e5527 --- /dev/null +++ b/Documentation/Developer/FrontendUserGroup/Index.rst @@ -0,0 +1,13 @@ +.. include:: /Includes.rst.txt + +.. _frontendusergroup: + +=================== +Frontend User Group +=================== + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + Model \ No newline at end of file diff --git a/Documentation/Developer/FrontendUserGroup/Model.rst b/Documentation/Developer/FrontendUserGroup/Model.rst new file mode 100644 index 0000000..fd6f0c7 --- /dev/null +++ b/Documentation/Developer/FrontendUserGroup/Model.rst @@ -0,0 +1,53 @@ +.. include:: /Includes.rst.txt + +.. _frontendusergroup-domainmodel: + +============ +Domain Model +============ + +:php:`FrontendUserGroup` domain model contains the following properties that are validated with annotation. +The validation rule for a property is specified based on the appropriate :php:`fe_groups` table field description. +Some properties have been renamed to clarify what data are stored in them. +For more information about mapping between a database table and its model +see `Use arbitrary database tables with an Extbase model `__. + +title +===== + +.. code-block:: php + + /** + * Title + * + * @var string + * @Extbase\Validate("NotEmpty") + * @Extbase\Validate("StringLength", options={"maximum": 50}) + */ + protected $title; + +description +=========== + +.. code-block:: php + + /** + * Description + * + * @var string|null + */ + protected $description; + +subgroups +========= + +`subgroup` field in `fe_groups` table that stores a comma-separated list of frontend user subgroups. + +.. code-block:: php + + /** + * Subgroups + * + * @var ObjectStorage + */ + protected $subgroups; \ No newline at end of file diff --git a/Documentation/Developer/Index.rst b/Documentation/Developer/Index.rst new file mode 100644 index 0000000..12dab3a --- /dev/null +++ b/Documentation/Developer/Index.rst @@ -0,0 +1,15 @@ +.. include:: /Includes.rst.txt + +.. _developer: + +============== +For Developers +============== + +.. toctree:: + :maxdepth: 5 + :titlesonly: + + Domain + Events + ViewHelpers diff --git a/Documentation/Developer/ViewHelpers.rst b/Documentation/Developer/ViewHelpers.rst new file mode 100644 index 0000000..10127cd --- /dev/null +++ b/Documentation/Developer/ViewHelpers.rst @@ -0,0 +1,43 @@ +.. include:: /Includes.rst.txt +.. highlight:: html + +.. _developer-viewhelpers: + +=========== +ViewHelpers +=========== + +.. _lowercasehyphenated: + +lowercaseHyphenated +=================== + +ViewHelper splits text strings by capital letters, making parts lowercase, and joining them with a hyphen. + +It is used for the generating form field ids from website user properties. + +Default +------- + +Code: :: + + + + +Output: :: + + first-name + + +Inline Notation +--------------- + +Code: :: + + + + + +Output: :: + + \ No newline at end of file diff --git a/Documentation/Editor/Index.rst b/Documentation/Editor/Index.rst new file mode 100644 index 0000000..67ce9c0 --- /dev/null +++ b/Documentation/Editor/Index.rst @@ -0,0 +1,56 @@ +.. include:: /Includes.rst.txt + +.. _editor: + +=========== +For Editors +=========== + +.. _faq1: + +What happens after a website user is created? +============================================= + +After a website user is created the *Website User Form* plugin can behave in the following ways: + +* Log in a website user with the provided credentials. + +In this case, new website users are authenticated with the provided credentials and can view, update or delete website user data. +For this scenario :ref:`enablefrontenduserautologin` has to be enabled. + +* Redirect a website user to the login page. + +In this case, new website users are redirected to the page supposedly contains the login form. +For this scenario :ref:`enablefrontenduserautologin` has to be **disabled**, :ref:`enableredirecttologinpage` has to be **enabled** +and :ref:`redirectloginpageid` should be selected. + +* Stay on the current page and display the message :php:`Please log in with the provided credentials to view the user account.` + +This scenario is possible when: + +* :ref:`enablefrontenduserautologin` and :ref:`enableredirecttologinpage` are disabled. If :ref:`redirectloginpageid` is specified the link to the page will be added to the message. +* :ref:`enableredirecttologinpage` is enabled but :ref:`redirectloginpageid` is not specified. +* :ref:`enablefrontenduserautologin` is enabled but a website user is not authenticated for any reason. + +.. note:: + + Keep in mind that when the option is hidden its value does not reset and still is stored in a database + which can influence the behavior of the plugin in the third scenario. + +.. _faq2: + +How to work with images? +======================== + +By default, TYPO3 is allowed to assign five images to a website user. This has been changed and now it is allowed +to assign one image file. For supported file extensions see +`imagefile_ext `__. +The file size is limited by server configuration. + +In the TYPO3 frontend image file size can be set through TypoScript in the setup field of your TypoScript template. +See :ref:`frontenduserimagesize`. + +.. important:: + + It is important to know that website user image deletion means only removing references between a website user and an image file. + The actual deletion of old image files should be performed based on project needs and requirements. diff --git a/Documentation/Images/ContentElementWizard.png b/Documentation/Images/ContentElementWizard.png new file mode 100644 index 0000000000000000000000000000000000000000..8831adb4ff894f4c9fe80a76ce4eb7fbb252a893 GIT binary patch literal 41678 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 DqB}o} literal 0 HcmV?d00001 diff --git a/Documentation/Images/GeneralTab.png b/Documentation/Images/GeneralTab.png new file mode 100644 index 0000000000000000000000000000000000000000..b01e508c30eacf5265420f21cddf6da66fe25225 GIT binary patch literal 56099 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ΜXF%LKh<(A6~-=?Iftu(a{ww=8Ln>kSxp(or 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 x5VGWHB={Gc(yDiD$qYd8Yz8^Q@ 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 + license. + +:Rendered: + |today| + +---- + +This extension provides a template-based plugin for managing website users on the TYPO3 frontend. + +---- + +**Table of Contents:** + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + Introduction/Index + Installation/Index + Configuration/Index + Editor/Index + Developer/Index + KnownIssues/Index + +.. Meta Menu + +.. toctree:: + :hidden: + + Sitemap + genindex diff --git a/Documentation/Installation/Index.rst b/Documentation/Installation/Index.rst new file mode 100644 index 0000000..a6bcee4 --- /dev/null +++ b/Documentation/Installation/Index.rst @@ -0,0 +1,33 @@ +.. include:: /Includes.rst.txt + +.. _installation: + +============ +Installation +============ + +.. important:: + + For the correct work of the extension, the jQuery library has to be included. + +Composer Installation +===================== + +For composer-based installation the following command should be run: + +.. code-block:: php + + composer require ydt/frontend-user + +Then setup the extension with the command: + +.. code-block:: php + + ./vendor/bin/typo3 extension:setup + +For further details see `Managing Extensions `__. + +Legacy Installation +=================== + +For legacy installation see `Legacy Management `__. diff --git a/Documentation/Introduction/Index.rst b/Documentation/Introduction/Index.rst new file mode 100644 index 0000000..eebc55c --- /dev/null +++ b/Documentation/Introduction/Index.rst @@ -0,0 +1,35 @@ +.. include:: /Includes.rst.txt + +.. _introduction: + +============ +Introduction +============ + +What does it do? +================ + +The extension is designed to provide the functionality of managing website users on the TYPO3 frontend. The goal was +to develop a lightweight solution to create, update or delete website users with the ability to extend it for any project needs. + +The extension includes a template-based plugin that provides a website user form. This form contains a configurable set +of fields that match website user properties. + +Another feature of the extension is the ability to log in new website users after successful creation. + +The extension supports English and German languages. + +Screenshots +=========== + +.. figure:: ../Images/GeneralTab.png + :class: with-shadow + :alt: General Tab in the TYPO3 backend + + *General Tab* in the TYPO3 backend + +.. figure:: ../Images/ViewTab.png + :class: with-shadow + :alt: View Tab in the TYPO3 backend + + *View Tab* in the TYPO3 backend \ No newline at end of file diff --git a/Documentation/KnownIssues/Index.rst b/Documentation/KnownIssues/Index.rst new file mode 100644 index 0000000..8c7d704 --- /dev/null +++ b/Documentation/KnownIssues/Index.rst @@ -0,0 +1,28 @@ +.. include:: /Includes.rst.txt + +.. _issues: + +============== +Known Issues +============== + +* Displaying of validation errors + +Build-in and custom validators are responsible for the validation of website user data on creation. That is why +the validation result is rendered by an appropriate template with an error flash message +:php:`An error occurred while trying to create a user`. Nevertheless, the additional validation of the username is required +to ensure the uniqueness of this field. This validation occurs in the controller and an error message is displayed +as a flash message, not a validation error. + +When website user data is updated, it is not required to update a password as well so the validation of password +and password confirmation values is moved to the controller. If validation fails, an error flash message is displayed. + +When an error occurs during the upload of the website user image file, an error flash message also will be displayed. + +* The issue with multiple plugins on a page + +During extension testing it was found out that in case of *Website User Form* plugin and *Login Form* plugin +(or any other plugin that submits a form) are located on the same page an error can occur. An error occurs in the case +when validation of website user data fails and :php:`errorAction()` is called, and then any other form on the page +is submitted. The *Website User Form* plugin tries to perform an action from the URL but the form data is empty and +the required arguments are not set. \ No newline at end of file diff --git a/Documentation/Settings.cfg b/Documentation/Settings.cfg new file mode 100644 index 0000000..20b7756 --- /dev/null +++ b/Documentation/Settings.cfg @@ -0,0 +1,62 @@ +# More information about this file: +# https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/GeneralConventions/FileStructure.html#settings-cfg + +[general] + +project = Frontend User Extension +version = master +release = master +copyright = since 2022 by Your Dev Team Global + +[html_theme_options] + +# "Edit on GitHub" button +github_repository = YourDevTeamGlobal/frontend_user +github_branch = master + +# Footer links +project_home = https://github.com/YourDevTeamGlobal/frontend_user +project_contact = mailto:typo3@ydt-global.com +project_repository = https://github.com/YourDevTeamGlobal/frontend_user +project_issues = +project_discussions = + +use_opensearch = + +[intersphinx_mapping] + +# Official TYPO3 manuals +h2document = https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/ +# t3cheatsheets = https://docs.typo3.org/m/typo3/docs-cheatsheets/main/en-us/ +# t3contribute = https://docs.typo3.org/m/typo3/guide-contributionworkflow/main/en-us/ +t3coreapi = https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ +# t3docteam = https://docs.typo3.org/m/typo3/team-t3docteam/main/en-us/ +# t3editors = https://docs.typo3.org/m/typo3/tutorial-editors/main/en-us/ +# t3extbasebook = https://docs.typo3.org/m/typo3/book-extbasefluid/main/en-us/ +# t3extexample = https://docs.typo3.org/m/typo3/guide-example-extension-manual/main/en-us/ +# t3home = https://docs.typo3.org/ +t3install = https://docs.typo3.org/m/typo3/guide-installation/main/en-us/ +# t3l10n = https://docs.typo3.org/m/typo3/guide-frontendlocalization/main/en-us/ +t3sitepackage = https://docs.typo3.org/m/typo3/tutorial-sitepackage/main/en-us/ +# t3start = https://docs.typo3.org/m/typo3/tutorial-getting-started/main/en-us/ +# t3tca = https://docs.typo3.org/m/typo3/reference-tca/main/en-us/ +# t3templating = https://docs.typo3.org/m/typo3/tutorial-templating/main/en-us/ +# t3translate = https://docs.typo3.org/m/typo3/guide-frontendlocalization/main/en-us/ +# t3tsconfig = https://docs.typo3.org/m/typo3/reference-tsconfig/main/en-us/ +# t3tsref = https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/ +# t3ts45 = https://docs.typo3.org/m/typo3/tutorial-typoscript-in-45-minutes/main/en-us/ +t3viewhelper = https://docs.typo3.org/other/typo3/view-helper-reference/main/en-us/ +# t3upgrade = https://docs.typo3.org/m/typo3/guide-installation/main/en-us/ + +# TYPO3 system extensions +# ext_adminpanel = https://docs.typo3.org/c/typo3/cms-adminpanel/main/en-us/ +# ext_core = https://docs.typo3.org/c/typo3/cms-core/main/en-us/ +# ext_dashboard = https://docs.typo3.org/c/typo3/cms-dashboard/main/en-us/ +# ext_felogin = https://docs.typo3.org/c/typo3/cms-felogin/main/en-us/ +# ext_form = https://docs.typo3.org/c/typo3/cms-form/main/en-us/ +# ext_fsc = https://docs.typo3.org/c/typo3/cms-fluid-styled-content/main/en-us/ +# ext_indexed_search = https://docs.typo3.org/c/typo3/cms-indexed-search/main/en-us/ +# ext_rte_ckeditor = https://docs.typo3.org/c/typo3/cms-rte-ckeditor/main/en-us/ +# ext_scheduler = https://docs.typo3.org/c/typo3/cms-scheduler/main/en-us/ +# ext_seo = https://docs.typo3.org/c/typo3/cms-seo/main/en-us/ +# ext_workspaces = https://docs.typo3.org/c/typo3/cms-workspaces/main/en-us/ diff --git a/Documentation/Sitemap.rst b/Documentation/Sitemap.rst new file mode 100644 index 0000000..09d3c6f --- /dev/null +++ b/Documentation/Sitemap.rst @@ -0,0 +1,9 @@ +:template: sitemap.html + +.. include:: /Includes.rst.txt + +======= +Sitemap +======= + +.. The sitemap.html template will insert here the page tree automatically. diff --git a/Documentation/genindex.rst b/Documentation/genindex.rst new file mode 100644 index 0000000..806ec56 --- /dev/null +++ b/Documentation/genindex.rst @@ -0,0 +1,7 @@ +.. include:: /Includes.rst.txt + +===== +Index +===== + +.. Sphinx will insert here the general index automatically. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..27a0052 --- /dev/null +++ b/README.rst @@ -0,0 +1,9 @@ +======================= +Frontend User Extension +======================= + +This extension provides a template-based plugin for managing website users on the TYPO3 frontend. + +:Repository: https://github.com/YourDevTeamGlobal/frontend_user +:Read online: +:TER: diff --git a/Resources/Private/Language/de.locallang.xlf b/Resources/Private/Language/de.locallang.xlf new file mode 100644 index 0000000..72974c5 --- /dev/null +++ b/Resources/Private/Language/de.locallang.xlf @@ -0,0 +1,188 @@ + + + + +
+ + + Username + Benutzername + + + Password + Passwort + + + Confirm Password + Passwort Bestätigen + + + Company + Firma + + + Job Title + Berufsbezeichnung + + + Name + Name + + + First Name + Vorname + + + Middle Name + Mittlerer Name + + + Last Name + Nachname + + + Street Address + Adresse + + + Zip Code + PLZ + + + City + Stadt + + + Country + Land + + + Phone + Telefon + + + Email + E-Mail + + + Fax + Fax + + + Homepage Url + Homepage-URL + + + Image + Bild + + + The given subject is not a valid digit string. + Der angegebene Betreff ist keine gültige Ziffernfolge. + + + The given object is not an instance of FrontendUser class. + Das angegebene Objekt ist keine Instanz der Klasse "FrontendUser". + + + Please make sure your passwords match. + Bitte stellen Sie sicher, dass Ihre Kennwörter übereinstimmen. + + + The given subject is not a valid phone. + Der angegebene Betreff ist kein gültiges Telefon. + + + Username is not available. Please try another one. + Benutzername ist nicht verfügbar. Bitte versuchen Sie es mit einem anderen. + + + Username can not be changed. + Der Benutzername kann nicht geändert werden. + + + Please make sure your passwords match. + Bitte stellen Sie sicher, dass Ihre Kennwörter übereinstimmen. + + + Something went wrong while saving the image file. + Beim Speichern der Bilddatei ist ein Fehler aufgetreten. + + + You saved the user account. + Sie haben das Benutzerkonto gespeichert. + + + Create New User + Neuen Benutzer erstellen + + + + + + + Show Password + Passwort anzeigen + + + Create User + Benutzer erstellen + + + Edit User + Benutzer bearbeiten + + + Change Password + Passwort ändern + + + Change Password + Passwort ändern + + + Password + Passwort + + + Confirm Password + Passwort Bestätigen + + + Save + Speichern + + + Delete User + Benutzer löschen + + + the link.]]> + dem Link.]]> + + + log in with the provided credentials to view the user account.]]> + melden Sie sich an mit den bereitgestellten Anmeldedaten, um das Benutzerkonto anzuzeigen.]]> + + + Please log in with the provided credentials to view the user account. + Bitte melden Sie sich mit den bereitgestellten Zugangsdaten an, um das Benutzerkonto anzuzeigen. + + + Personal Data + Persönliche Daten + + + Delete Image + Lösche Bild + + + + diff --git a/Resources/Private/Language/de.locallang_be.xlf b/Resources/Private/Language/de.locallang_be.xlf new file mode 100644 index 0000000..1f47e24 --- /dev/null +++ b/Resources/Private/Language/de.locallang_be.xlf @@ -0,0 +1,140 @@ + + + + +
+ + + Company + Firma + + + Job Title + Berufsbezeichnung + + + Name + Name + + + First name + Vorname + + + Middle Name + Mittlerer Name + + + Last name + Nachname + + + Street Address + Adresse + + + Zip Code + PLZ + + + City + Stadt + + + Country + Land + + + Phone + Telefon + + + Fax + Fax + + + Email + E-Mail + + + Homepage Url + Homepage-URL + + + Image + Bild + + + Website User Form + Website-Benutzerformular + + + Create/update website user form. + Website-Benutzerformular erstellen/aktualisieren. + + + General + Allgemein + + + View + Aussicht + + + Default User Group + Standardbenutzergruppe + + + User Storage Page + Benutzerspeicherseite + + + User Image Folder + Benutzerbildordner + + + Log in After Successful User Creation + Melden Sie sich nach erfolgreicher Benutzererstellung an + + + Redirect to Login Page + Weiterleitung zur Anmeldeseite + + + Login Page + Loginseite + + + Enable Deletion of a User + Löschen eines Benutzers aktivieren + + + Create User Form Fields + Benutzerformularfelder erstellen + + + Edit User Form Fields + Benutzerformularfelder bearbeiten + + + Number of Lines in Street Address + Anzahl der Zeilen in einer Adresse + + + Range: 1-4 + Bereich: 1-4. + + + User Image Size + Benutzerbildgröße + + + + diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf new file mode 100644 index 0000000..8b81117 --- /dev/null +++ b/Resources/Private/Language/locallang.xlf @@ -0,0 +1,145 @@ + + + + +
+ + + Username + + + Password + + + Confirm Password + + + Company + + + Job Title + + + Name + + + First Name + + + Middle Name + + + Last Name + + + Street Address + + + Zip Code + + + City + + + Country + + + Phone + + + Email + + + Fax + + + Homepage Url + + + Image + + + The given subject is not a valid digit string. + + + The given object is not an instance of FrontendUser class. + + + Please make sure your passwords match. + + + The given subject is not a valid phone. + + + Username is not available. Please try another one. + + + Username can not be changed. + + + Please make sure your passwords match. + + + Something went wrong while saving the image file. + + + You saved the user account. + + + Create New User + + + + + + Show Password + + + Create User + + + Edit User + + + Change Password + + + Change Password + + + Password + + + Confirm Password + + + Save + + + Delete User + + + the link.]]> + + + log in with the provided credentials to view the user account.]]> + + + Please log in with the provided credentials to view the user account. + + + Personal Data + + + Delete Image + + + + diff --git a/Resources/Private/Language/locallang_be.xlf b/Resources/Private/Language/locallang_be.xlf new file mode 100644 index 0000000..344c3be --- /dev/null +++ b/Resources/Private/Language/locallang_be.xlf @@ -0,0 +1,115 @@ + + + + +
+ + + Company + + + Job Title + + + Company + + + Job Title + + + Name + + + First Name + + + Middle Name + + + Last Name + + + Street Address + + + Zip Code + + + City + + + Country + + + Phone + + + Fax + + + Email + + + Homepage Url + + + Image + + + Website User Form + + + Create/update website user form. + + + General + + + View + + + Default User Group + + + User Storage Page + + + User Image Folder + + + Log in After Successful User Creation + + + Redirect to Login Page + + + Login Page + + + Enable Deletion of a User + + + Create User Form Fields + + + Edit User Form Fields + + + Number of Lines in Street Address + + + Range: 1-4. + + + User Image Size + + + + \ No newline at end of file diff --git a/Resources/Private/Layouts/Form/Default.html b/Resources/Private/Layouts/Form/Default.html new file mode 100644 index 0000000..cfb1678 --- /dev/null +++ b/Resources/Private/Layouts/Form/Default.html @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/Resources/Private/Partials/Form/Fieldset/PersonalData.html b/Resources/Private/Partials/Form/Fieldset/PersonalData.html new file mode 100644 index 0000000..061a9b2 --- /dev/null +++ b/Resources/Private/Partials/Form/Fieldset/PersonalData.html @@ -0,0 +1,60 @@ + + + + + +
+ + + + +
+ + + + + +
+
+ +
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +
+
+
+
+
+ + \ No newline at end of file diff --git a/Resources/Private/Partials/Form/ValidationResults.html b/Resources/Private/Partials/Form/ValidationResults.html new file mode 100644 index 0000000..749aeb3 --- /dev/null +++ b/Resources/Private/Partials/Form/ValidationResults.html @@ -0,0 +1,26 @@ + + + + + + +
    + +
  • {propertyPath} +
      + +
    • {error}
    • +
      +
    +
  • +
    +
+
+
\ No newline at end of file diff --git a/Resources/Private/Templates/FrontendUser/Edit.html b/Resources/Private/Templates/FrontendUser/Edit.html new file mode 100644 index 0000000..5ed0863 --- /dev/null +++ b/Resources/Private/Templates/FrontendUser/Edit.html @@ -0,0 +1,101 @@ + + + + + + + + + + + + +
+ + + + + +
+ + + +
+ + +
+
+ + +
+
+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + + + + + + + +
+
+
+ +
+ + + + + + + + + + +
+
+
+
+ + \ No newline at end of file diff --git a/Resources/Private/Templates/FrontendUser/New.html b/Resources/Private/Templates/FrontendUser/New.html new file mode 100644 index 0000000..10c379d --- /dev/null +++ b/Resources/Private/Templates/FrontendUser/New.html @@ -0,0 +1,60 @@ + + + + + + + + + + +
+ + + + + +
+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+ + \ No newline at end of file diff --git a/Resources/Public/Icons/Extension.svg b/Resources/Public/Icons/Extension.svg new file mode 100644 index 0000000..96235e6 --- /dev/null +++ b/Resources/Public/Icons/Extension.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/Public/JavaScript/Form.js b/Resources/Public/JavaScript/Form.js new file mode 100644 index 0000000..e05db59 --- /dev/null +++ b/Resources/Public/JavaScript/Form.js @@ -0,0 +1,52 @@ +/** + * Frontend User Extension + * + * @copyright Copyright (c) 2022 Your Dev Team Global (https://ydt-global.com/) + * @author YDT Global Team + */ +$(document).ready(function () { + const showPassword = document.getElementById('show-password'); + const changePassword = document.getElementById('change-password'); + const changePasswordFieldset = document.getElementById('change-password-fieldset'); + const password = document.getElementById('password'); + const passwordConfirmation = document.getElementById('password-confirmation'); + const deleteUserLinkWrapper = document.getElementById('delete-user-link-wrapper'); + + if (showPassword) { + showPassword.checked = false; + } + + if (changePasswordFieldset && !changePassword.checked) { + changePasswordFieldset.hidden = true; + password.disabled = true; + passwordConfirmation.disabled = true; + } + + if (deleteUserLinkWrapper) { + deleteUserLinkWrapper.hidden = true; + } + + $('#change-password').on('click', function (event) { + changePasswordFieldset.hidden = !this.checked; + password.disabled = !this.checked; + passwordConfirmation.disabled = !this.checked; + this.value = Number(this.checked); + }); + + $('#show-password').on('click', function (event) { + const textInputType = 'text'; + const passwordInputType = 'password'; + + if (this.checked) { + password.type = textInputType; + passwordConfirmation.type = textInputType; + } else { + password.type = passwordInputType; + passwordConfirmation.type = passwordInputType; + } + }); + + $('#delete-user-link').on('click', function (event) { + deleteUserLinkWrapper.hidden = false; + }); +}); diff --git a/Tests/Unit/Authentication/FrontendUserAuthenticationTest.php b/Tests/Unit/Authentication/FrontendUserAuthenticationTest.php new file mode 100644 index 0000000..f388c12 --- /dev/null +++ b/Tests/Unit/Authentication/FrontendUserAuthenticationTest.php @@ -0,0 +1,309 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Authentication; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Log\LoggerInterface; +use TYPO3\CMS\Core\Authentication\AuthenticationService; +use TYPO3\CMS\Core\Authentication\GroupResolver; +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Core\Session\UserSession; +use TYPO3\CMS\Core\Session\UserSessionManager; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Authentication\ModifyResolvedFrontendGroupsEvent; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Authentication\FrontendUserAuthentication; +use ReflectionClass; +use ReflectionException; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Class FrontendUserAuthenticationTest + * Testcase for class \Ydt\FrontendUser\Authentication\FrontendUserAuthentication + */ +class FrontendUserAuthenticationTest extends UnitTestCase +{ + /** + * Authentication Service Mock + * + * @var MockObject|AuthenticationService + */ + private $authServiceMock; + + /** + * Frontend User Authentication + * + * @var FrontendUserAuthentication + */ + private $subject; + + /** + * @inheritDoc + */ + protected $resetSingletonInstances = true; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->authServiceMock = $this->createMock(AuthenticationService::class); + + $GLOBALS['T3_SERVICES']['auth'][get_class($this->authServiceMock)] = [ + 'className' => AuthenticationService::class, + 'serviceKey' => get_class($this->authServiceMock), + 'serviceType' => 'auth', + 'subtype' => 'getUserFE,authUserFE,processLoginDataFE', + 'available' => true, + 'priority' => 50, + 'quality' => 50, + 'os' => '', + 'exec' => '', + 'serviceSubTypes' => [ + 'getUserFE' => 'getUserFE', + 'authUserFE' => 'authUserFE', + 'processLoginDataFE' => 'processLoginDataFE', + ], + ]; + + $expressionBuilderMock = $this->createMock(ExpressionBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $builder = $queryBuilderMock->method('expr'); + $builder->willReturn($expressionBuilderMock); + + $connectionMock = $this->createMock(Connection::class); + $builder = $connectionMock->method('update'); + $builder->willReturn(1); + + $connectionPoolMock = $this->createMock(ConnectionPool::class); + + $builder = $connectionPoolMock->method('getQueryBuilderForTable'); + $builder->willReturn($queryBuilderMock); + $builder = $connectionPoolMock->method('getConnectionForTable'); + $builder->willReturn($connectionMock); + + GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolMock); + GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolMock); + GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolMock); + + $userSessionMock = $this->createMock(UserSession::class); + + $builder = $userSessionMock->method('isNew'); + $builder->willReturn(true); + $builder = $userSessionMock->method('getIdentifier'); + $builder->willReturn('123456789'); + + $userSessionManagerMock = $this->createMock(UserSessionManager::class); + $builder = $userSessionManagerMock->method('createAnonymousSession'); + $builder->willReturn($userSessionMock); + + GeneralUtility::addInstance(UserSessionManager::class, $userSessionManagerMock); + + $groupResolverMock = $this->createMock(GroupResolver::class); + $builder = $groupResolverMock->method('resolveGroupsForUser'); + $builder->willReturn([]); + + GeneralUtility::addInstance(GroupResolver::class, $groupResolverMock); + + $this->subject = new FrontendUserAuthentication(); + + $loggerMock = $this->createMock(LoggerInterface::class); + $this->subject->setLogger($loggerMock); + + $event = new ModifyResolvedFrontendGroupsEvent($this->subject, [], null); + $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class); + $builder = $eventDispatcherMock->method('dispatch'); + $builder->willReturn($event); + + GeneralUtility::addInstance(EventDispatcherInterface::class, $eventDispatcherMock); + } + + /** + * Test start + * + * @return void + */ + public function testStart(): void + { + $userData = [ + 'uid' => 1, + 'username' => 'test', + 'password' => md5('test123'), + ]; + + $builder = $this->authServiceMock->method('init'); + $builder->willReturn(true); + $builder = $this->authServiceMock->method('processLoginData'); + $builder->willReturn(200); + $builder = $this->authServiceMock->method('getUser'); + $builder->willReturn($userData); + $builder = $this->authServiceMock->method('authUser'); + $builder->with($userData); + $builder->willReturn(200); + + GeneralUtility::addInstance(AuthenticationService::class, $this->authServiceMock); + GeneralUtility::addInstance(AuthenticationService::class, $this->authServiceMock); + GeneralUtility::addInstance(AuthenticationService::class, $this->authServiceMock); + + $requestBody = [ + 'tx_frontenduser_form' => [ + 'pid' => 1, + 'newFrontendUser' => [ + 'username' => 'test', + 'password' => 'test123', + ], + ], + ]; + + $requestMock = $this->createMock(ServerRequestInterface::class); + $builder = $requestMock->method('getParsedBody'); + $builder->willReturn($requestBody); + + $this->subject->start($requestMock); + } + + /** + * Test setSessionCookie + * + * @return void + * @throws ReflectionException + */ + public function testSetSessionCookie(): void + { + $this->subject->initializeUserSessionManager(); + + $reflection = new ReflectionClass(FrontendUserAuthentication::class); + $method = $reflection->getMethod('setSessionCookie'); + $method->setAccessible(true); + + $method->invoke($this->subject); + } + + /** + * Test getLoginData with exception + * + * @return void + * @throws ReflectionException + */ + public function testGetLoginDataWithException(): void + { + $this->expectExceptionObject(new Exception('username and password are required.')); + + $reflection = new ReflectionClass(FrontendUserAuthentication::class); + $method = $reflection->getMethod('getLoginData'); + $method->setAccessible(true); + + $method->invoke($this->subject, []); + } + + /** + * Test authenticate + * + * @dataProvider authenticateDataProvider + * + * @param bool $initResult + * @param array $userData + * @param int $authUserResult + * @param int $instanceCount + * @return void + * @throws ReflectionException + */ + public function testAuthenticate(bool $initResult, array $userData, int $authUserResult, int $instanceCount): void + { + $this->addAuthServiceInstance($initResult, $userData, $authUserResult, $instanceCount); + + $this->subject->initializeUserSessionManager(); + + $loginData = [ + 'status' => 'login', + 'uname' => 'test', + 'uident' => md5('test123'), + 'uident_text' => 'test123', + 'is_permanents' => false, + ]; + + $reflection = new ReflectionClass(FrontendUserAuthentication::class); + $method = $reflection->getMethod('authenticate'); + $method->setAccessible(true); + + $method->invoke($this->subject, $loginData); + } + + /** + * Add auth service instance + * + * @param bool $initResult + * @param array $userData + * @param int $authUserResult + * @param int $instanceCount + * @return void + */ + protected function addAuthServiceInstance(bool $initResult, array $userData, int $authUserResult, int $instanceCount): void + { + $authServiceMock = $this->createMock(AuthenticationService::class); + + $builder = $authServiceMock->method('init'); + $builder->willReturn($initResult); + $builder = $authServiceMock->method('getUser'); + $builder->willReturn($userData); + + if ($authUserResult === 50) { + $builder = $authServiceMock->method('authUser'); + $builder->with($userData); + $builder->willReturnOnConsecutiveCalls($authUserResult, 200); + } else { + $builder = $authServiceMock->method('authUser'); + $builder->with($userData); + $builder->willReturn($authUserResult); + } + + for ($i = 0; $i < $instanceCount; $i++) { + GeneralUtility::addInstance(AuthenticationService::class, $authServiceMock); + } + } + + /** + * Data provider for authenticate + * + * @return array + */ + public function authenticateDataProvider(): array + { + $userData = [ + 'uid' => 1, + 'username' => 'test', + 'password' => md5('test123'), + ]; + + return [ + [false, $userData, 200, 1], + [true, $userData, 0, 2], + [true, $userData, 50, 3], + ]; + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + GeneralUtility::purgeInstances(); + parent::tearDown(); + } +} diff --git a/Tests/Unit/Controller/FrontendUserControllerTest.php b/Tests/Unit/Controller/FrontendUserControllerTest.php new file mode 100644 index 0000000..17d0676 --- /dev/null +++ b/Tests/Unit/Controller/FrontendUserControllerTest.php @@ -0,0 +1,990 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Controller; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; +use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException; +use TYPO3\CMS\Core\Http\ResponseFactory; +use TYPO3\CMS\Core\Http\StreamFactory; +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Messaging\FlashMessageQueue; +use TYPO3\CMS\Core\Messaging\FlashMessageService; +use TYPO3\CMS\Extbase\Domain\Model\FileReference; +use TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService; +use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException; +use TYPO3\CMS\Core\Localization\LocalizationFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder; +use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; +use TYPO3\CMS\Extbase\Property\PropertyMapper; +use TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration; +use TYPO3\CMS\Extbase\Reflection\ReflectionService; +use Psr\EventDispatcher\EventDispatcherInterface; +use TYPO3\CMS\Extbase\Mvc\Request; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory; +use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; +use TYPO3\CMS\Extbase\Mvc\View\ViewInterface; +use TYPO3\CMS\Extbase\Mvc\View\ViewResolverInterface; +use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; +use TYPO3\CMS\Extbase\Service\ExtensionService; +use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator; +use TYPO3\CMS\Extbase\Validation\ValidatorResolver; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Controller\FrontendUserController; +use Ydt\FrontendUser\Domain\Model\FrontendUser; +use Ydt\FrontendUser\Domain\Model\FrontendUserGroup; +use Ydt\FrontendUser\Domain\Repository\FrontendUserGroupRepository; +use Ydt\FrontendUser\Domain\Repository\FrontendUserRepository; +use PHPUnit\Framework\MockObject\MockObject; +use ReflectionClass; +use ReflectionException; +use TYPO3\CMS\Extbase\Property\Exception; +use TYPO3\CMS\Extbase\Reflection\ClassSchema; +use TYPO3\CMS\Extbase\Reflection\ClassSchema\Method as ClassSchemaMethod; +use TYPO3\CMS\Extbase\Reflection\ClassSchema\MethodParameter as ClassSchemaMethodParameter; + +/** + * Class FrontendUserControllerTest + * Testcase for class \Ydt\FrontendUser\Controller\FrontendUserController + */ +class FrontendUserControllerTest extends UnitTestCase +{ + /** + * Frontend User Controller + * + * @var FrontendUserController + */ + private $subject; + + /** + * Frontend User Repository Mock + * + * @var MockObject|FrontendUserRepository + */ + private $frontendUserRepositoryMock; + + /** + * Frontend User Group Repository Mock + * + * @var MockObject|FrontendUserGroupRepository + */ + private $frontendUserGroupRepositoryMock; + + /** + * Password Hash Factory Mock + * + * @var MockObject|PasswordHashFactory + */ + private $passwordHashFactoryMock; + + /** + * Context Mock + * + * @var MockObject|Context + */ + private $contextMock; + + /** + * Property Mapper Mock + * + * @var MockObject|PropertyMapper + */ + private $propertyMapperMock; + + /** + * @inheritDoc + */ + protected $resetSingletonInstances = true; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid'] = 1; + + $languageServiceMock = $this->createMock(LanguageService::class); + $GLOBALS['LANG'] = $languageServiceMock; + + $localizationFactoryMock = $this->createMock(LocalizationFactory::class); + $builder = $localizationFactoryMock->method('getParsedData'); + $builder->willReturn([]); + + GeneralUtility::setSingletonInstance(LocalizationFactory::class, $localizationFactoryMock); + + $uriBuilderMock = $this->createMock(UriBuilder::class); + + $builder = $uriBuilderMock->method('setTargetPageUid'); + $builder->willReturnSelf(); + $builder = $uriBuilderMock->method('buildFrontendUri'); + $builder->willReturn('https://test'); + + GeneralUtility::addInstance(UriBuilder::class, $uriBuilderMock); + + $frameworkConfigurationManagerMock = $this->createMock(ConfigurationManagerInterface::class); + $builder = $frameworkConfigurationManagerMock->method('getConfiguration'); + $builder->with(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, 'frontend_user'); + $builder->willReturn([]); + + GeneralUtility::setSingletonInstance(ConfigurationManagerInterface::class, $frameworkConfigurationManagerMock); + + $this->frontendUserRepositoryMock = $this->createMock(FrontendUserRepository::class); + $this->frontendUserGroupRepositoryMock = $this->createMock(FrontendUserGroupRepository::class); + $this->passwordHashFactoryMock = $this->createMock(PasswordHashFactory::class); + $this->persistenceManagerMock = $this->createMock(PersistenceManager::class); + $this->contextMock = $this->createMock(Context::class); + $this->propertyMapperMock = $this->createMock(PropertyMapper::class); + + $this->subject = new FrontendUserController( + $this->frontendUserRepositoryMock, + $this->frontendUserGroupRepositoryMock, + $this->passwordHashFactoryMock, + $this->persistenceManagerMock, + $this->contextMock, + $this->propertyMapperMock + ); + + $conjunctionValidatorMock = $this->createMock(ConjunctionValidator::class); + $validatorResolverMock = $this->createMock(ValidatorResolver::class); + $builder = $validatorResolverMock->method('getBaseValidatorConjunction'); + $builder->willReturn($conjunctionValidatorMock); + + $this->subject->injectValidatorResolver($validatorResolverMock); + + $settingsConfigurationManagerMock = $this->createMock(ConfigurationManagerInterface::class); + $builder = $settingsConfigurationManagerMock->method('getConfiguration'); + $builder->willReturn([ + 'frontendUserStoragePid' => 1, + 'newFrontendUserFormFields' => 'email', + 'editFrontendUserFormFields' => 'email,image', + 'enableFrontendUserDeletion' => 1, + 'redirectLoginPageId' => 10, + 'frontendUserImageFolder' => '1:test', + ]); + + $this->subject->injectConfigurationManager($settingsConfigurationManagerMock); + + $objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $this->subject->injectObjectManager($objectManagerMock); + + $mvcPropertyMappingConfigurationServiceMock = $this->createMock(MvcPropertyMappingConfigurationService::class); + $builder = $mvcPropertyMappingConfigurationServiceMock->method('initializePropertyMappingConfigurationFromRequest'); + $builder->willReturnSelf(); + + $this->subject->injectMvcPropertyMappingConfigurationService($mvcPropertyMappingConfigurationServiceMock); + + $viewMock = $this->createMock(ViewInterface::class); + + $builder = $viewMock->method('initializeView'); + $builder->willReturnSelf(); + $builder = $viewMock->method('assign'); + $builder->willReturnSelf(); + $builder = $viewMock->method('assignMultiple'); + $builder->willReturnSelf(); + $builder = $viewMock->method('render'); + $builder->willReturn(''); + + $viewResolverMock = $this->createMock(ViewResolverInterface::class); + $builder = $viewResolverMock->method('resolve'); + $builder->willReturn($viewMock); + + $this->subject->injectViewResolver($viewResolverMock); + + $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class); + $this->subject->injectEventDispatcher($eventDispatcherMock); + + $responseMock = $this->createMock(ResponseInterface::class); + + $builder = $responseMock->method('withHeader'); + $builder->willReturnSelf(); + $builder = $responseMock->method('withBody'); + $builder->willReturnSelf(); + + $responseFactoryMock = $this->createMock(ResponseFactory::class); + $builder = $responseFactoryMock->method('createResponse'); + $builder->willReturn($responseMock); + + $this->subject->injectResponseFactory($responseFactoryMock); + + $streamMock = $this->createMock(StreamInterface::class); + $streamFactoryMock = $this->createMock(StreamFactory::class); + $builder = $streamFactoryMock->method('createStream'); + $builder->willReturn($streamMock); + + $this->subject->injectStreamFactory($streamFactoryMock); + + $internalExtensionServiceMock = $this->createMock(ExtensionService::class); + $builder = $internalExtensionServiceMock->method('getPluginNamespace'); + $builder->willReturn('tx_frontenduser_form'); + + $this->subject->injectInternalExtensionService($internalExtensionServiceMock); + + $flashMessageQueueMock = $this->createMock(FlashMessageQueue::class); + $builder = $flashMessageQueueMock->method('enqueue'); + $builder->willReturnSelf(); + + $internalFlashMessageServiceMock = $this->createMock(FlashMessageService::class); + $builder = $internalFlashMessageServiceMock->method('getMessageQueueByIdentifier'); + $builder->willReturn($flashMessageQueueMock); + + $this->subject->injectInternalFlashMessageService($internalFlashMessageServiceMock); + } + + /** + * Test newAction + * + * @return void + */ + public function testNewAction(): void + { + $this->configureRequest(); + + $builder = $this->contextMock->method('getPropertyFromAspect'); + $builder->willReturn(null); + + $requestMock = $this->createMock(Request::class); + $builder = $requestMock->method('getControllerActionName'); + $builder->willReturn('new'); + + $this->subject->processRequest($requestMock); + } + + /** + * Test newAction with redirect + * + * @return void + * @see StopActionException + */ + public function testNewActionWithRedirect(): void + { + $this->configureRequest(); + + $builder = $this->contextMock->method('getPropertyFromAspect'); + $builder->willReturn(1); + + $frontendUserMock = $this->createMock(FrontendUser::class); + $builder = $this->frontendUserRepositoryMock->method('findByUid'); + $builder->willReturn($frontendUserMock); + + $requestMock = $this->createMock(Request::class); + $builder = $requestMock->method('getControllerActionName'); + $builder->willReturn('new'); + + try { + $this->subject->processRequest($requestMock); + } catch (StopActionException $exception) { + } + } + + /** + * Test createAction + * + * @dataProvider createActionMethodDataProvider + * + * @param FileReference|null $fileReference + * @param bool $enableRedirectToLoginPage + * @return void + * @see StopActionException + */ + public function testCreateAction(?FileReference $fileReference, bool $enableRedirectToLoginPage): void + { + $configurationManagerMock = $this->createMock(ConfigurationManagerInterface::class); + $builder = $configurationManagerMock->method('getConfiguration'); + $builder->willReturn([ + 'defaultFrontendUserGroupId' => 1, + 'frontendUserImageFolder' => '1:test', + 'redirectLoginPageId' => 10, + 'enableRedirectToLoginPage' => $enableRedirectToLoginPage, + ]); + $this->subject->injectConfigurationManager($configurationManagerMock); + + $this->configureRequestWithFrontendUserParam(); + + $frontendUserMock = $this->createMock(FrontendUser::class); + $builder = $frontendUserMock->method('getUsername'); + $builder->willReturn('test'); + + $hashInstanceMock = $this->createMock(PasswordHashInterface::class); + $builder = $hashInstanceMock->method('getHashedPassword'); + $builder->willReturn(md5('test123')); + + $builder = $this->passwordHashFactoryMock->method('getDefaultHashInstance'); + $builder->with('FE'); + $builder->willReturn($hashInstanceMock); + + $builder = $frontendUserMock->method('getPassword'); + $builder->willReturn('test123'); + + $frontendUserGroupMock = $this->createMock(FrontendUserGroup::class); + $builder = $this->frontendUserGroupRepositoryMock->method('findByUid'); + $builder->willReturn($frontendUserGroupMock); + + $builder = $frontendUserMock->method('addUserGroup'); + $builder->with($frontendUserGroupMock); + $builder->willReturnSelf(); + + $propertyMappingConfigurationMock = $this->createMock(PropertyMappingConfiguration::class); + $builder = $propertyMappingConfigurationMock->method('setTypeConverterOptions'); + $builder->willReturnSelf(); + + $builder = $this->propertyMapperMock->method('convert'); + $builder->willReturn($fileReference); + + $fileReferenceMock = $this->createMock(FileReference::class); + $builder = $frontendUserMock->method('addImage'); + $builder->with($fileReferenceMock); + $builder->willReturnSelf(); + + $requestMock = $this->createMock(Request::class); + + $builder = $requestMock->method('getControllerActionName'); + $builder->willReturn('create'); + $builder = $requestMock->method('getControllerExtensionName'); + $builder->willReturn('frontend_user'); + $builder = $requestMock->method('getPluginName'); + $builder->willReturn('frontenduser_form'); + $builder = $requestMock->method('hasArgument'); + $builder->with('frontendUser'); + $builder->willReturn(true); + $builder = $requestMock->method('getArgument'); + $builder->with('frontendUser'); + $builder->willReturn($frontendUserMock); + $builder = $requestMock->method('getArguments'); + $builder->willReturn([ + 'pid' => 1, + 'image' => [ + 'error' => 0, + ], + ]); + + try { + $this->subject->processRequest($requestMock); + } catch (StopActionException $exception) { + } + } + + /** + * Data provider for testCreateAction + * + * @return array + */ + public function createActionMethodDataProvider(): array + { + $fileReferenceMock = $this->createMock(FileReference::class); + + return [ + [$fileReferenceMock, false], + [null, true], + ]; + } + + /** + * Test createAction with forward + * + * @return void + * @see StopActionException + */ + public function testCreateActionWithForward(): void + { + $this->configureRequestWithFrontendUserParam(); + + $frontendUserMock = $this->createMock(FrontendUser::class); + $builder = $frontendUserMock->method('getUsername'); + $builder->willReturn('test'); + + $builder = $this->frontendUserRepositoryMock->method('findByUsername'); + $builder->willReturn($frontendUserMock); + + $requestMock = $this->createMock(Request::class); + + $builder = $requestMock->method('getControllerActionName'); + $builder->willReturn('create'); + $builder = $requestMock->method('hasArgument'); + $builder->with('frontendUser'); + $builder->willReturn(true); + $builder = $requestMock->method('getArgument'); + $builder->with('frontendUser'); + $builder->willReturn($frontendUserMock); + $builder = $requestMock->method('getArguments'); + $builder->willReturn([ + 'pid' => 1, + ]); + + try { + $this->subject->processRequest($requestMock); + } catch (StopActionException $exception) { + } + } + + /** + * Test editAction + * + * @return void + */ + public function testEditAction(): void + { + $this->configureRequestWithFrontendUserParam(); + + $requestMock = $this->createMock(Request::class); + + $builder = $requestMock->method('getControllerActionName'); + $builder->willReturn('edit'); + + $builder = $requestMock->method('hasArgument'); + $builder->with('frontendUser'); + $builder->willReturn(true); + + $frontendUserMock = $this->createMock(FrontendUser::class); + $builder = $requestMock->method('getArgument'); + $builder->with('frontendUser'); + $builder->willReturn($frontendUserMock); + + $this->subject->processRequest($requestMock); + } + + /** + * Test updateAction + * + * @dataProvider updateActionMethodDataProvider + * + * @param FileReference|null $fileReference + * @return void + * @see StopActionException + */ + public function testUpdateAction(?FileReference $fileReference): void + { + $this->configureRequestWithFrontendUserParam(); + + $builder = $this->contextMock->method('getPropertyFromAspect'); + $builder->willReturnOnConsecutiveCalls(1, 'test'); + + $frontendUserMock = $this->createMock(FrontendUser::class); + + $builder = $frontendUserMock->method('getUid'); + $builder->willReturn(1); + $builder = $frontendUserMock->method('getUsername'); + $builder->willReturn('test'); + $builder = $frontendUserMock->method('getPassword'); + $builder->willReturn('test123'); + $builder = $frontendUserMock->method('getPasswordConfirmation'); + $builder->willReturn('test123'); + + $hashInstanceMock = $this->createMock(PasswordHashInterface::class); + $builder = $hashInstanceMock->method('getHashedPassword'); + $builder->willReturn(md5('test123')); + + $builder = $this->passwordHashFactoryMock->method('getDefaultHashInstance'); + $builder->with('FE'); + $builder->willReturn($hashInstanceMock); + + $propertyMappingConfigurationMock = $this->createMock(PropertyMappingConfiguration::class); + $builder = $propertyMappingConfigurationMock->method('setTypeConverterOptions'); + $builder->willReturnSelf(); + + $builder = $this->propertyMapperMock->method('convert'); + $builder->willReturn($fileReference); + + $fileReferenceMock = $this->createMock(FileReference::class); + $builder = $frontendUserMock->method('addImage'); + $builder->with($fileReferenceMock); + $builder->willReturnSelf(); + + $requestMock = $this->createMock(Request::class); + + $builder = $requestMock->method('getControllerActionName'); + $builder->willReturn('update'); + $builder = $requestMock->method('hasArgument'); + $builder->with('frontendUser'); + $builder->willReturn(true); + $builder = $requestMock->method('getArgument'); + $builder->with('frontendUser'); + $builder->willReturn($frontendUserMock); + $builder = $requestMock->method('getArguments'); + $builder->willReturn([ + 'pid' => 1, + 'changePassword' => 1, + 'image' => [ + 'error' => 0, + ], + ]); + + try { + $this->subject->processRequest($requestMock); + } catch (StopActionException $exception) { + } + } + + /** + * Data provider for testUpdateAction + * + * @return array + */ + public function updateActionMethodDataProvider(): array + { + $fileReferenceMock = $this->createMock(FileReference::class); + + return [ + [$fileReferenceMock], + [null], + ]; + } + + /** + * Test updateAction with forward + * + * @dataProvider updateActionMethodWithForwardDataProvider + * + * @param string $username + * @param string $password + * @return void + * @see StopActionException + */ + public function testUpdateActionWithForward(string $username, string $password): void + { + $this->configureRequestWithFrontendUserParam(); + + $builder = $this->contextMock->method('getPropertyFromAspect'); + $builder->willReturnOnConsecutiveCalls(1, $username); + + $frontendUserMock = $this->createMock(FrontendUser::class); + + $builder = $frontendUserMock->method('getUid'); + $builder->willReturn(1); + $builder = $frontendUserMock->method('getUsername'); + $builder->willReturn('test'); + $builder = $frontendUserMock->method('getPassword'); + $builder->willReturn($password); + $builder = $frontendUserMock->method('getPasswordConfirmation'); + $builder->willReturn('test123'); + + $requestMock = $this->createMock(Request::class); + + $builder = $requestMock->method('getControllerActionName'); + $builder->willReturn('update'); + $builder = $requestMock->method('hasArgument'); + $builder->with('frontendUser'); + $builder->willReturn(true); + $builder = $requestMock->method('getArgument'); + $builder->with('frontendUser'); + $builder->willReturn($frontendUserMock); + $builder = $requestMock->method('getArguments'); + $builder->willReturn([ + 'pid' => 1, + 'changePassword' => 1, + ]); + + try { + $this->subject->processRequest($requestMock); + } catch (StopActionException $exception) { + } + } + + /** + * Data provider for testUpdateAction + * + * @return array + */ + public function updateActionMethodWithForwardDataProvider(): array + { + return [ + ['test2', ''], + ['test', 'test1234'], + ]; + } + + /** + * Test deleteAction + * + * @return void + * @see StopActionException + */ + public function testDeleteAction(): void + { + $this->configureRequestWithFrontendUserParam(); + + $builder = $this->contextMock->method('getPropertyFromAspect'); + $builder->willReturn(1); + + $frontendUserMock = $this->createMock(FrontendUser::class); + $builder = $frontendUserMock->method('getUid'); + $builder->willReturn(1); + + $requestMock = $this->createMock(Request::class); + + $builder = $requestMock->method('getControllerActionName'); + $builder->willReturn('delete'); + $builder = $requestMock->method('hasArgument'); + $builder->with('frontendUser'); + $builder->willReturn(true); + $builder = $requestMock->method('getArgument'); + $builder->with('frontendUser'); + $builder->willReturn($frontendUserMock); + + try { + $this->subject->processRequest($requestMock); + } catch (StopActionException $exception) { + } + } + + /** + * Test deleteImageAction + * + * @return void + * @see StopActionException + */ + public function testDeleteImageAction(): void + { + $this->configureRequestWithParams(); + + $builder = $this->contextMock->method('getPropertyFromAspect'); + $builder->willReturn(1); + + $frontendUserMock = $this->createMock(FrontendUser::class); + + $builder = $frontendUserMock->method('getUid'); + $builder->willReturn(1); + $builder = $frontendUserMock->method('removeImage'); + $builder->willReturnSelf(); + + $requestMock = $this->createMock(Request::class); + + $builder = $requestMock->method('getControllerActionName'); + $builder->willReturn('deleteImage'); + + $builder = $requestMock->method('hasArgument'); + $builder->withConsecutive( + ['frontendUser'], + ['frontendUserImage'] + ); + $builder->willReturnOnConsecutiveCalls(true, true); + + $fileReferenceMock = $this->createMock(FileReference::class); + $builder = $requestMock->method('getArgument'); + $builder->withConsecutive( + ['frontendUser'], + ['frontendUserImage'] + ); + $builder->willReturnOnConsecutiveCalls($frontendUserMock, $fileReferenceMock); + + try { + $this->subject->processRequest($requestMock); + } catch (StopActionException $exception) { + } + } + + /** + * Test getErrorFlashMessage + * + * @return void + * @throws ReflectionException + */ + public function testGetErrorFlashMessage(): void + { + $reflection = new ReflectionClass(FrontendUserController::class); + $method = $reflection->getMethod('getErrorFlashMessage'); + $method->setAccessible(true); + + $result = $method->invoke($this->subject); + + $this->assertIsString($result); + $this->assertSame('An error occurred while trying to index a user.', $result); + } + + /** + * Test getFrontendUserIdFromAspect + * + * @return void + * @throws ReflectionException + */ + public function testGetFrontendUserIdFromAspect(): void + { + $reflection = new ReflectionClass(FrontendUserController::class); + $method = $reflection->getMethod('getFrontendUserIdFromAspect'); + $method->setAccessible(true); + + $builder = $this->contextMock->method('getPropertyFromAspect'); + $builder->willReturn(1); + + $result = $method->invoke($this->subject); + + $this->assertIsInt($result); + $this->assertSame(1, $result); + } + + /** + * Test getFrontendUserIdFromAspect with exception + * + * @return void + * @throws ReflectionException + */ + public function testGetFrontendUserIdFromAspectWithException(): void + { + $reflection = new ReflectionClass(FrontendUserController::class); + $method = $reflection->getMethod('getFrontendUserIdFromAspect'); + $method->setAccessible(true); + + $builder = $this->contextMock->method('getPropertyFromAspect'); + $builder->willThrowException(new AspectNotFoundException('No aspect named "frontend_user" found.', 1527777641)); + + $result = $method->invoke($this->subject); + + $this->assertIsInt($result); + $this->assertSame(0, $result); + } + + /** + * Test generateHashedPassword + * + * @return void + * @throws ReflectionException + */ + public function testGenerateHashedPassword(): void + { + $reflection = new ReflectionClass(FrontendUserController::class); + $method = $reflection->getMethod('generateHashedPassword'); + $method->setAccessible(true); + + $hashInstanceMock = $this->createMock(PasswordHashInterface::class); + $builder = $hashInstanceMock->method('getHashedPassword'); + $builder->willReturn(md5('test123')); + + $builder = $this->passwordHashFactoryMock->method('getDefaultHashInstance'); + $builder->with('FE'); + $builder->willReturn($hashInstanceMock); + + $result = $method->invoke($this->subject, 'test123'); + + $this->assertIsString($result); + } + + /** + * Test mapArrayToFileReference + * + * @return void + * @throws ReflectionException + */ + public function testMapArrayToFileReference(): void + { + $reflection = new ReflectionClass(FrontendUserController::class); + $method = $reflection->getMethod('mapArrayToFileReference'); + $method->setAccessible(true); + + $propertyMappingConfigurationMock = $this->createMock(PropertyMappingConfiguration::class); + $builder = $propertyMappingConfigurationMock->method('setTypeConverterOptions'); + $builder->willReturnSelf(); + + $fileReferenceMock = $this->createMock(FileReference::class); + $builder = $this->propertyMapperMock->method('convert'); + $builder->willReturn($fileReferenceMock); + + $result = $method->invoke($this->subject, []); + + $this->assertInstanceOf(FileReference::class, $result); + } + + /** + * Test mapArrayToFileReference with exception + * + * @return void + * @throws ReflectionException + */ + public function testMapArrayToFileReferenceWithException(): void + { + $reflection = new ReflectionClass(FrontendUserController::class); + $method = $reflection->getMethod('mapArrayToFileReference'); + $method->setAccessible(true); + + $propertyMappingConfigurationMock = $this->createMock(PropertyMappingConfiguration::class); + $builder = $propertyMappingConfigurationMock->method('setTypeConverterOptions'); + $builder->willReturnSelf(); + + $builder = $this->propertyMapperMock->method('convert'); + $builder->willThrowException(new Exception('Exception while property mapping at property path "name".', 1297759968)); + + $result = $method->invoke($this->subject, []); + + $this->assertNull($result); + } + + /** + * Test getStoragePid + * + * @return void + * @throws ReflectionException + */ + public function testGetStoragePid(): void + { + $reflection = new ReflectionClass(FrontendUserController::class); + $method = $reflection->getMethod('getStoragePid'); + $method->setAccessible(true); + + $result = $method->invoke($this->subject); + + $this->assertIsInt($result); + $this->assertSame(1, $result); + } + + /** + * Test for getLoginPageUrl + * + * @return void + * @throws ReflectionException + */ + public function testGetLoginPageUrl(): void + { + $reflection = new ReflectionClass(FrontendUserController::class); + $method = $reflection->getMethod('getLoginPageUrl'); + $method->setAccessible(true); + + $result = $method->invoke($this->subject); + + $this->assertIsString($result); + $this->assertSame('https://test', $result); + } + + /** + * Test getTranslation + * + * @return void + * @throws ReflectionException + */ + public function testGetTranslation(): void + { + $reflection = new ReflectionClass(FrontendUserController::class); + $method = $reflection->getMethod('getTranslation'); + $method->setAccessible(true); + + $result = $method->invoke($this->subject, 'key'); + + $this->assertIsString($result); + } + + /** + * Test getUriBuilder + * + * @return void + * @throws ReflectionException + */ + public function testGetUriBuilder(): void + { + $reflection = new ReflectionClass(FrontendUserController::class); + $method = $reflection->getMethod('getUriBuilder'); + $method->setAccessible(true); + + $result = $method->invoke($this->subject); + + $this->assertInstanceOf(UriBuilder::class, $result); + } + + /** + * Configure request without params + * + * @return void + */ + public function configureRequest(): void + { + $classSchemaMethodMock = $this->createMock(ClassSchemaMethod::class); + $builder = $classSchemaMethodMock->method('getParameters'); + $builder->willReturn([]); + + $classSchemaMock = $this->createMock(ClassSchema::class); + $builder = $classSchemaMock->method('getMethod'); + $builder->willReturn($classSchemaMethodMock); + + $reflectionServiceMock = $this->createMock(ReflectionService::class); + $builder = $reflectionServiceMock->method('getClassSchema'); + $builder->willReturn($classSchemaMock); + + $this->subject->injectReflectionService($reflectionServiceMock); + } + + /** + * Configure request with $frontendUser param + * + * @return void + */ + public function configureRequestWithFrontendUserParam(): void + { + $frontendUserParamMock = $this->createMock(ClassSchemaMethodParameter::class); + $builder = $frontendUserParamMock->method('getType'); + $builder->willReturn(FrontendUser::class); + + $classSchemaMethodMock = $this->createMock(ClassSchemaMethod::class); + $builder = $classSchemaMethodMock->method('getParameters'); + $builder->willReturn([ + 'frontendUser' => $frontendUserParamMock, + ]); + + $classSchemaMock = $this->createMock(ClassSchema::class); + $builder = $classSchemaMock->method('getMethod'); + $builder->willReturn($classSchemaMethodMock); + + $reflectionServiceMock = $this->createMock(ReflectionService::class); + $builder = $reflectionServiceMock->method('getClassSchema'); + $builder->willReturn($classSchemaMock); + + $this->subject->injectReflectionService($reflectionServiceMock); + } + + /** + * Configure request with params + * + * @return void + */ + public function configureRequestWithParams(): void + { + $frontendUserParamMock = $this->createMock(ClassSchemaMethodParameter::class); + $builder = $frontendUserParamMock->method('getType'); + $builder->willReturn(FrontendUser::class); + + $fileReferenceParamMock = $this->createMock(ClassSchemaMethodParameter::class); + $builder = $fileReferenceParamMock->method('getType'); + $builder->willReturn(FileReference::class); + + $classSchemaMethodMock = $this->createMock(ClassSchemaMethod::class); + $builder = $classSchemaMethodMock->method('getParameters'); + $builder->willReturn([ + 'frontendUser' => $frontendUserParamMock, + 'frontendUserImage' => $fileReferenceParamMock, + ]); + + $classSchemaMock = $this->createMock(ClassSchema::class); + $builder = $classSchemaMock->method('getMethod'); + $builder->willReturn($classSchemaMethodMock); + + $reflectionServiceMock = $this->createMock(ReflectionService::class); + $builder = $reflectionServiceMock->method('getClassSchema'); + $builder->willReturn($classSchemaMock); + + $this->subject->injectReflectionService($reflectionServiceMock); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $reflectionClass = new ReflectionClass(LocalizationUtility::class); + $property = $reflectionClass->getProperty('configurationManager'); + $property->setAccessible(true); + $property->setValue(null); + + GeneralUtility::purgeInstances(); + parent::tearDown(); + } +} diff --git a/Tests/Unit/Domain/Model/FrontendUserGroupTest.php b/Tests/Unit/Domain/Model/FrontendUserGroupTest.php new file mode 100644 index 0000000..bd2feca --- /dev/null +++ b/Tests/Unit/Domain/Model/FrontendUserGroupTest.php @@ -0,0 +1,146 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Domain\Model; + +use TYPO3\CMS\Extbase\Persistence\ObjectStorage; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Domain\Model\FrontendUserGroup; + +/** + * Class FrontendUserGroupTest + * Testcase for class \Ydt\FrontendUser\Domain\Model\FrontendUserGroup + */ +class FrontendUserGroupTest extends UnitTestCase +{ + /** + * Frontend User Group + * + * @var FrontendUserGroup + */ + private $subject; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->subject = new FrontendUserGroup(''); + } + + /** + * Test getTitle + * + * @return void + */ + public function testGetTitle(): void + { + $title = 'test'; + $this->subject->setTitle($title); + + $result = $this->subject->getTitle(); + + $this->assertIsString($result); + $this->assertSame($title, $result); + } + + /** + * Test getDescription + * + * @return void + */ + public function testGetDescription(): void + { + $description = 'test'; + $this->subject->setDescription($description); + + $result = $this->subject->getDescription(); + + $this->assertIsString($result); + $this->assertSame($description, $result); + } + + /** + * Test getDescription with NULL + * + * @return void + */ + public function testGetDescriptionReturnsNull(): void + { + $result = $this->subject->getDescription(); + + $this->assertNull($result); + } + + /** + * Test getSubgroups + * + * @return void + */ + public function testGetSubgroups(): void + { + $result = $this->subject->getSubgroups(); + + $this->assertInstanceOf(ObjectStorage::class, $result); + } + + /** + * Test setSubgroups + * + * @return void + */ + public function testSetSubgroups(): void + { + $objectStorageMock = $this->createMock(ObjectStorage::class); + + $this->subject->setSubgroups($objectStorageMock); + } + + /** + * Test addSubgroup + * + * @return void + */ + public function testAddSubgroup(): void + { + $subgroupMock = $this->createMock(FrontendUserGroup::class); + + $objectStorageMock = $this->createMock(ObjectStorage::class); + $builder = $objectStorageMock->method('contains'); + $builder->with($subgroupMock); + $builder->willReturn(false); + + $this->subject->setSubgroups($objectStorageMock); + + $this->subject->addSubgroup($subgroupMock); + } + + /** + * Test removeSubgroup + * + * @return void + */ + public function testRemoveSubgroup(): void + { + $subgroupMock = $this->createMock(FrontendUserGroup::class); + + $objectStorageMock = $this->createMock(ObjectStorage::class); + $builder = $objectStorageMock->method('contains'); + $builder->with($subgroupMock); + $builder->willReturn(true); + + $this->subject->setSubgroups($objectStorageMock); + + $this->subject->removeSubgroup($subgroupMock); + } +} diff --git a/Tests/Unit/Domain/Model/FrontendUserTest.php b/Tests/Unit/Domain/Model/FrontendUserTest.php new file mode 100644 index 0000000..4611af8 --- /dev/null +++ b/Tests/Unit/Domain/Model/FrontendUserTest.php @@ -0,0 +1,489 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Domain\Model; + +use TYPO3\CMS\Extbase\Domain\Model\FileReference; +use TYPO3\CMS\Extbase\Persistence\ObjectStorage; +use Ydt\FrontendUser\Domain\Model\FrontendUser; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Domain\Model\FrontendUserGroup; +use DateTime; + +/** + * Class FrontendUserTest + * Testcase for class \Ydt\FrontendUser\Domain\Model\FrontendUser + */ +class FrontendUserTest extends UnitTestCase +{ + /** + * Frontend User + * + * @var FrontendUser + */ + private $subject; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->subject = new FrontendUser(''); + } + + /** + * Test getUsername + * + * @return void + */ + public function testGetUsername(): void + { + $username = 'test'; + $this->subject->setUsername($username); + + $result = $this->subject->getUsername(); + + $this->assertIsString($result); + $this->assertEquals($username, $result); + } + + /** + * Test getPassword + * + * @return void + */ + public function testGetPassword(): void + { + $password = 'test123'; + $this->subject->setPassword($password); + + $result = $this->subject->getPassword(); + + $this->assertIsString($result); + } + + /** + * Test getPasswordConfirmation + * + * @return void + */ + public function testGetPasswordConfirmation(): void + { + $password = 'test123'; + $this->subject->setPasswordConfirmation($password); + + $result = $this->subject->getPasswordConfirmation(); + + $this->assertIsString($result); + } + + /** + * Test getUserGroups + * + * @return void + */ + public function testGetUserGroups(): void + { + $result = $this->subject->getUserGroups(); + + $this->assertInstanceOf(ObjectStorage::class, $result); + } + + /** + * Test setUserGroups + * + * @return void + */ + public function testSetUserGroups(): void + { + $objectStorageMock = $this->createMock(ObjectStorage::class); + + $this->subject->setUserGroups($objectStorageMock); + } + + /** + * Test addUserGroup + * + * @return void + */ + public function testAddUserGroup(): void + { + $userGroupMock = $this->createMock(FrontendUserGroup::class); + + $objectStorageMock = $this->createMock(ObjectStorage::class); + $builder = $objectStorageMock->method('contains'); + $builder->with($userGroupMock); + $builder->willReturn(false); + + $this->subject->setUserGroups($objectStorageMock); + + $this->subject->addUserGroup($userGroupMock); + } + + /** + * Test removeUserGroup + * + * @return void + */ + public function testRemoveUserGroup(): void + { + $userGroupMock = $this->createMock(FrontendUserGroup::class); + + $objectStorageMock = $this->createMock(ObjectStorage::class); + $builder = $objectStorageMock->method('contains'); + $builder->with($userGroupMock); + $builder->willReturn(true); + + $this->subject->setUserGroups($objectStorageMock); + + $this->subject->removeUserGroup($userGroupMock); + } + + /** + * Test getCompany + * + * @return void + */ + public function testGetCompany(): void + { + $company = 'test'; + $this->subject->setCompany($company); + + $result = $this->subject->getCompany(); + + $this->assertIsString($result); + $this->assertSame($result, $company); + } + + /** + * Test getJobTitle + * + * @return void + */ + public function testGetJobTitle(): void + { + $jobTitle = 'test'; + $this->subject->setJobTitle($jobTitle); + + $result = $this->subject->getJobTitle(); + + $this->assertIsString($result); + $this->assertSame($result, $jobTitle); + } + + /** + * Test getName + * + * @return void + */ + public function testGetName(): void + { + $name = 'test'; + $this->subject->setName($name); + + $result = $this->subject->getName(); + + $this->assertIsString($result); + $this->assertSame($result, $name); + } + + /** + * Test getFirstName + * + * @return void + */ + public function testGetFirstName(): void + { + $firstName = 'test'; + $this->subject->setFirstName($firstName); + + $result = $this->subject->getFirstName(); + + $this->assertIsString($result); + $this->assertSame($result, $firstName); + } + + /** + * Test getMiddleName + * + * @return void + */ + public function testGetMiddleName(): void + { + $middleName = 'test'; + $this->subject->setMiddleName($middleName); + + $result = $this->subject->getMiddleName(); + + $this->assertIsString($result); + $this->assertSame($result, $middleName); + } + + /** + * Test getLastName + * + * @return void + */ + public function testGetLastName(): void + { + $lastName = 'test'; + $this->subject->setLastName($lastName); + + $result = $this->subject->getLastName(); + + $this->assertIsString($result); + $this->assertSame($result, $lastName); + } + + /** + * Test getStreetAddress + * + * @return void + */ + public function testGetStreetAddress(): void + { + $streetAddress = 'test'; + $this->subject->setStreetAddress($streetAddress); + + $result = $this->subject->getStreetAddress(); + + $this->assertIsString($result); + $this->assertSame($result, $streetAddress); + } + + /** + * Test getZipCode + * + * @return void + */ + public function testGetZipCode(): void + { + $zipCode = '123'; + $this->subject->setZipCode($zipCode); + + $result = $this->subject->getZipCode(); + + $this->assertIsString($result); + $this->assertSame($result, $zipCode); + } + + /** + * Test getCity + * + * @return void + */ + public function testGetCity(): void + { + $city = 'test'; + $this->subject->setCity($city); + + $result = $this->subject->getCity(); + + $this->assertIsString($result); + $this->assertSame($result, $city); + } + + /** + * Test getCountry + * + * @return void + */ + public function testGetCountry(): void + { + $country = 'test'; + $this->subject->setCountry($country); + + $result = $this->subject->getCountry(); + + $this->assertIsString($result); + $this->assertSame($result, $country); + } + + /** + * Test getPhone + * + * @return void + */ + public function testGetPhone(): void + { + $phone = '123'; + $this->subject->setPhone($phone); + + $result = $this->subject->getPhone(); + + $this->assertIsString($result); + $this->assertSame($phone, $result); + } + + /** + * Test getFax + * + * @return void + */ + public function testGetFax(): void + { + $fax = '123'; + $this->subject->setFax($fax); + + $result = $this->subject->getFax(); + + $this->assertIsString($result); + $this->assertSame($result, $fax); + } + + /** + * Test getEmail + * + * @return void + */ + public function testGetEmail(): void + { + $email = 'test@test.com'; + $this->subject->setEmail($email); + + $result = $this->subject->getEmail(); + + $this->assertIsString($result); + $this->assertEquals($email, $result); + } + + /** + * Test getUrl + * + * @return void + */ + public function testGetUrl(): void + { + $url = 'https://test'; + $this->subject->setUrl($url); + + $result = $this->subject->getUrl(); + + $this->assertIsString($result); + $this->assertSame($result, $url); + } + + /** + * Test getImages + * + * @return void + */ + public function testGetImages(): void + { + $objectStorageMock = $this->createMock(ObjectStorage::class); + $this->subject->setImages($objectStorageMock); + + $result = $this->subject->getImages(); + + $this->assertInstanceOf(ObjectStorage::class, $result); + } + + /** + * Test setImages + * + * @return void + */ + public function testSetImages(): void + { + $objectStorageMock = $this->createMock(ObjectStorage::class); + + $this->subject->setImages($objectStorageMock); + } + + /** + * Test addImage + * + * @return void + */ + public function testAddImage(): void + { + $fileReferenceMock = $this->createMock(FileReference::class); + + $objectStorageMock = $this->createMock(ObjectStorage::class); + $builder = $objectStorageMock->method('contains'); + $builder->with($fileReferenceMock); + $builder->willReturn(false); + + $this->subject->setImages($objectStorageMock); + + $this->subject->addImage($fileReferenceMock); + } + + /** + * Test removeImage + * + * @return void + */ + public function testRemoveImage(): void + { + $fileReferenceMock = $this->createMock(FileReference::class); + + $objectStorageMock = $this->createMock(ObjectStorage::class); + $builder = $objectStorageMock->method('contains'); + $builder->with($fileReferenceMock); + $builder->willReturn(true); + + $this->subject->setImages($objectStorageMock); + + $this->subject->removeImage($fileReferenceMock); + } + + /** + * Test getImage + * + * @return void + */ + public function testGetImage(): void + { + $fileReferenceMock = $this->createMock(FileReference::class); + $objectStorageMock = $this->createMock(ObjectStorage::class); + + $builder = $objectStorageMock->method('count'); + $builder->willReturn(1); + $builder = $objectStorageMock->method('current'); + $builder->willReturn($fileReferenceMock); + + $this->subject->setImages($objectStorageMock); + + $result = $this->subject->getImage(); + + $this->assertInstanceOf(FileReference::class, $result); + } + + /** + * Test getImage if object storage is empty + * + * @return void + */ + public function testGetImageIfIsNull(): void + { + $result = $this->subject->getImage(); + + $this->assertNull($result); + } + + /** + * Test getLastLogin + * + * @return void + */ + public function testGetLastLogin(): void + { + $dateTimeMock = $this->createMock(DateTime::class); + $this->subject->setLastLogin($dateTimeMock); + + $result = $this->subject->getLastLogin(); + + $this->assertInstanceOf(DateTime::class, $result); + } +} diff --git a/Tests/Unit/Domain/Repository/FrontendUserRepositoryTest.php b/Tests/Unit/Domain/Repository/FrontendUserRepositoryTest.php new file mode 100644 index 0000000..ef35083 --- /dev/null +++ b/Tests/Unit/Domain/Repository/FrontendUserRepositoryTest.php @@ -0,0 +1,84 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Domain\Repository; + +use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface; +use TYPO3\CMS\Extbase\Persistence\QueryResultInterface; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Domain\Model\FrontendUser; +use Ydt\FrontendUser\Domain\Repository\FrontendUserRepository; +use TYPO3\CMS\Extbase\Persistence\QueryInterface; +use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; +use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface; + +/** + * Class FrontendUserRepositoryTest + * Testcase for class \Ydt\FrontendUser\Domain\Repository\FrontendUserRepository + */ +class FrontendUserRepositoryTest extends UnitTestCase +{ + /** + * Frontend User Repository + * + * @var FrontendUserRepository + */ + private $subject; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $querySettingsMock = $this->createMock(QuerySettingsInterface::class); + + $builder = $querySettingsMock->method('getStoragePageIds'); + $builder->willReturn([0]); + $builder = $querySettingsMock->method('setStoragePageIds'); + $builder->willReturnSelf(); + + $frontendUserMock = $this->createMock(FrontendUser::class); + $queryResultMock = $this->createMock(QueryResultInterface::class); + $builder = $queryResultMock->method('getFirst'); + $builder->willReturn($frontendUserMock); + + $queryMock = $this->createMock(QueryInterface::class); + + $builder = $queryMock->method('getQuerySettings'); + $builder->willReturn($querySettingsMock); + $builder = $queryMock->method('execute'); + $builder->willReturn($queryResultMock); + + $persistenceManagerMock = $this->createMock(PersistenceManagerInterface::class); + $builder = $persistenceManagerMock->method('createQueryForType'); + $builder->willReturn($queryMock); + + $objectManagerMock = $this->createMock(ObjectManagerInterface::class); + + $this->subject = new FrontendUserRepository($objectManagerMock); + + $this->subject->injectPersistenceManager($persistenceManagerMock); + } + + /** + * Test findByUsername + * + * @return void + */ + public function testFindByUsername(): void + { + $result = $this->subject->findByUsername('test', 1); + + $this->assertInstanceOf(FrontendUser::class, $result); + } +} diff --git a/Tests/Unit/Domain/Validator/FrontendUserValidatorTest.php b/Tests/Unit/Domain/Validator/FrontendUserValidatorTest.php new file mode 100644 index 0000000..74a0532 --- /dev/null +++ b/Tests/Unit/Domain/Validator/FrontendUserValidatorTest.php @@ -0,0 +1,117 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Domain\Validator; + +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Localization\LocalizationFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; +use TYPO3\CMS\Extbase\Error\Result; +use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Domain\Model\FrontendUser; +use Ydt\FrontendUser\Domain\Validator\FrontendUserValidator; +use ReflectionClass; + +/** + * Class FrontendUserValidatorTest + * Testcase for class \Ydt\FrontendUser\Domain\Validator\FrontendUserValidator + */ +class FrontendUserValidatorTest extends UnitTestCase +{ + /** + * Frontend User Validator + * + * @var FrontendUserValidator + */ + private $subject; + + /** + * @inheritDoc + */ + protected $resetSingletonInstances = true; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $languageServiceMock = $this->createMock(LanguageService::class); + $GLOBALS['LANG'] = $languageServiceMock; + + $localizationFactoryMock = $this->createMock(LocalizationFactory::class); + $builder = $localizationFactoryMock->method('getParsedData'); + $builder->willReturn([]); + + GeneralUtility::setSingletonInstance(LocalizationFactory::class, $localizationFactoryMock); + + $configurationManagerMock = $this->createMock(ConfigurationManagerInterface::class); + $builder = $configurationManagerMock->method('getConfiguration'); + $builder->with(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, 'frontend_user'); + $builder->willReturn([]); + + GeneralUtility::setSingletonInstance(ConfigurationManagerInterface::class, $configurationManagerMock); + + $this->subject = new FrontendUserValidator(); + } + + /** + * Test isValid + * + * @dataProvider frontendUserDataProvider + * + * @param FrontendUser|string $frontendUser + * @return void + */ + public function testValidate($frontendUser): void + { + $result = $this->subject->validate($frontendUser); + + $this->assertInstanceOf(Result::class, $result); + } + + /** + * Data provider for validate + * + * @return array + */ + public function frontendUserDataProvider(): array + { + $frontendUserMock = $this->createMock(FrontendUser::class); + + $builder = $frontendUserMock->method('getPassword'); + $builder->willReturn('test132'); + $builder = $frontendUserMock->method('getPasswordConfirmation'); + $builder->willReturn('test123'); + + return [ + ['Frontend User'], + [$frontendUserMock], + ]; + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $reflectionClass = new ReflectionClass(LocalizationUtility::class); + $property = $reflectionClass->getProperty('configurationManager'); + $property->setAccessible(true); + $property->setValue(null); + + GeneralUtility::purgeInstances(); + parent::tearDown(); + } +} diff --git a/Tests/Unit/Domain/Validator/PhoneValidatorTest.php b/Tests/Unit/Domain/Validator/PhoneValidatorTest.php new file mode 100644 index 0000000..ef6fea5 --- /dev/null +++ b/Tests/Unit/Domain/Validator/PhoneValidatorTest.php @@ -0,0 +1,93 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Domain\Validator; + +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Localization\LocalizationFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; +use TYPO3\CMS\Extbase\Error\Result; +use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Domain\Validator\PhoneValidator; +use ReflectionClass; + +/** + * Class PhoneValidatorTest + * Testcase for class \Ydt\FrontendUser\Domain\Validator\PhoneValidator + */ +class PhoneValidatorTest extends UnitTestCase +{ + /** + * Phone Validator + * + * @var PhoneValidator + */ + private $subject; + + /** + * @inheritDoc + */ + protected $resetSingletonInstances = true; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $languageServiceMock = $this->createMock(LanguageService::class); + $GLOBALS['LANG'] = $languageServiceMock; + + $localizationFactoryMock = $this->createMock(LocalizationFactory::class); + $builder = $localizationFactoryMock->method('getParsedData'); + $builder->willReturn([]); + + GeneralUtility::setSingletonInstance(LocalizationFactory::class, $localizationFactoryMock); + + $configurationManagerMock = $this->createMock(ConfigurationManagerInterface::class); + $builder = $configurationManagerMock->method('getConfiguration'); + $builder->with(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, 'frontend_user'); + $builder->willReturn([]); + + GeneralUtility::setSingletonInstance(ConfigurationManagerInterface::class, $configurationManagerMock); + + $this->subject = new PhoneValidator(); + } + + /** + * Test validate + * + * @return void + */ + public function testValidate(): void + { + $result = $this->subject->validate('+123(45)6789 '); + + $this->assertInstanceOf(Result::class, $result); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $reflectionClass = new ReflectionClass(LocalizationUtility::class); + $property = $reflectionClass->getProperty('configurationManager'); + $property->setAccessible(true); + $property->setValue(null); + + GeneralUtility::purgeInstances(); + parent::tearDown(); + } +} diff --git a/Tests/Unit/Event/AbstractFrontendUserActionEventTest.php b/Tests/Unit/Event/AbstractFrontendUserActionEventTest.php new file mode 100644 index 0000000..8a4a2a5 --- /dev/null +++ b/Tests/Unit/Event/AbstractFrontendUserActionEventTest.php @@ -0,0 +1,126 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Event; + +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Domain\Model\FrontendUser; +use Ydt\FrontendUser\Event\AbstractFrontendUserActionEvent; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Class AbstractFrontendUserActionEventTest + * Testcase for class \Ydt\FrontendUser\Event\AbstractFrontendUserActionEvent + */ +class AbstractFrontendUserActionEventTest extends UnitTestCase +{ + /** + * Event Mock + * + * @var MockObject|AbstractFrontendUserActionEvent + */ + private $eventMock; + + /** + * Frontend User Mock + * + * @var MockObject|FrontendUser + */ + private $frontendUserMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->frontendUserMock = $this->createMock(FrontendUser::class); + + $this->eventMock = $this->getMockForAbstractClass( + AbstractFrontendUserActionEvent::class, + [$this->frontendUserMock], + '', + true, + true, + true, + [ + 'getFrontendUser', + 'setFrontendUser', + 'getRequest', + 'setRequest', + ] + ); + } + + /** + * Test getFrontendUser + * + * @return void + */ + public function testGetFrontendUser(): void + { + $builder = $this->eventMock->method('getFrontendUser'); + $builder->willReturn($this->frontendUserMock); + + $result = $this->eventMock->getFrontendUser(); + + $this->assertInstanceOf(FrontendUser::class, $result); + } + + /** + * Test setFrontendUser + * + * @return void + */ + public function testSetFrontendUser(): void + { + $builder = $this->eventMock->method('getFrontendUser'); + $builder->with($this->frontendUserMock); + $builder->willReturnSelf(); + + $this->eventMock->setFrontendUser($this->frontendUserMock); + } + + /** + * Test getRequest + * + * @return void + */ + public function testGetRequest(): void + { + $requestMock = $this->createMock(ServerRequestInterface::class); + + $builder = $this->eventMock->method('getRequest'); + $builder->willReturn($requestMock); + + $result = $this->eventMock->getRequest(); + + $this->assertInstanceOf(ServerRequestInterface::class, $result); + } + + /** + * Test setRequest + * + * @return void + */ + public function testSetRequest(): void + { + $requestMock = $this->createMock(ServerRequestInterface::class); + + $builder = $this->eventMock->method('setRequest'); + $builder->with($requestMock); + $builder->willReturnSelf(); + + $this->eventMock->setRequest($requestMock); + } +} diff --git a/Tests/Unit/Event/FrontendUserCreateAfterEventTest.php b/Tests/Unit/Event/FrontendUserCreateAfterEventTest.php new file mode 100644 index 0000000..e060c4b --- /dev/null +++ b/Tests/Unit/Event/FrontendUserCreateAfterEventTest.php @@ -0,0 +1,82 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Event; + +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Domain\Model\FrontendUser; +use Ydt\FrontendUser\Event\FrontendUserCreateAfterEvent; + +/** + * Class FrontendUserCreateAfterEventTest + * Testcase for class \Ydt\FrontendUser\Event\FrontendUserCreateAfterEvent + */ +class FrontendUserCreateAfterEventTest extends UnitTestCase +{ + /** + * Frontend User Create After Event + * + * @var FrontendUserCreateAfterEvent + */ + private $subject; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $frontendUserMock = $this->createMock(FrontendUser::class); + + $this->subject = new FrontendUserCreateAfterEvent($frontendUserMock); + } + + /** + * Test getFrontendUser + * + * @return void + */ + public function testGetFrontendUser(): void + { + $result = $this->subject->getFrontendUser(); + + $this->assertInstanceOf(FrontendUser::class, $result); + } + + /** + * Test setFrontendUser + * + * @return void + */ + public function testSetFrontendUser(): void + { + $frontendUserMock = $this->createMock(FrontendUser::class); + + $this->subject->setFrontendUser($frontendUserMock); + } + + /** + * Test getRequest + * + * @return void + */ + public function testGetRequest(): void + { + $requestMock = $this->createMock(ServerRequestInterface::class); + $this->subject->setRequest($requestMock); + + $result = $this->subject->getRequest(); + + $this->assertInstanceOf(ServerRequestInterface::class, $result); + } +} diff --git a/Tests/Unit/Event/FrontendUserFormViewModifyEventTest.php b/Tests/Unit/Event/FrontendUserFormViewModifyEventTest.php new file mode 100644 index 0000000..af6d995 --- /dev/null +++ b/Tests/Unit/Event/FrontendUserFormViewModifyEventTest.php @@ -0,0 +1,66 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Event; + +use TYPO3\CMS\Extbase\Mvc\View\ViewInterface; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Event\FrontendUserFormViewModifyEvent; + +/** + * Class FrontendUserFormViewModifyEventTest + * Testcase for class \Ydt\FrontendUser\Event\FrontendUserFormViewModifyEvent + */ +class FrontendUserFormViewModifyEventTest extends UnitTestCase +{ + /** + * Frontend User Form View Modify Event + * + * @var FrontendUserFormViewModifyEvent + */ + private $subject; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $viewMock = $this->createMock(ViewInterface::class); + + $this->subject = new FrontendUserFormViewModifyEvent($viewMock); + } + + /** + * Test getView + * + * @return void + */ + public function testGetView(): void + { + $result = $this->subject->getView(); + + $this->assertInstanceOf(ViewInterface::class, $result); + } + + /** + * Test setView + * + * @return void + */ + public function testSetView(): void + { + $viewMock = $this->createMock(ViewInterface::class); + + $this->subject->setView($viewMock); + } +} diff --git a/Tests/Unit/EventListener/FrontendUserCreateAfterEventListenerTest.php b/Tests/Unit/EventListener/FrontendUserCreateAfterEventListenerTest.php new file mode 100644 index 0000000..b5edc0b --- /dev/null +++ b/Tests/Unit/EventListener/FrontendUserCreateAfterEventListenerTest.php @@ -0,0 +1,118 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\EventListener; + +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use TYPO3\CMS\Core\Exception; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManager; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\EventListener\FrontendUserCreateAfterEventListener; +use Ydt\FrontendUser\Authentication\FrontendUserAuthentication; +use Ydt\FrontendUser\Event\FrontendUserCreateAfterEvent; +use Psr\Http\Message\ServerRequestInterface; +use Ydt\FrontendUser\Domain\Model\FrontendUser; + +/** + * Class FrontendUserCreateAfterEventListenerTest + * Testcase for class \Ydt\FrontendUser\EventListener\FrontendUserCreateAfterEventListener + */ +class FrontendUserCreateAfterEventListenerTest extends UnitTestCase +{ + /** + * Frontend User Create After Event Listener + * + * @var FrontendUserCreateAfterEventListener + */ + private $subject; + + /** + * Configuration Manager Mock + * + * @var MockObject + */ + private $configurationManagerMock; + + /** + * Frontend User Authentication Mock + * + * @var MockObject + */ + private $frontendUserAuthenticationMock; + + /** + * Logger Mock + * + * @var MockObject|LoggerInterface + */ + private $loggerMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->configurationManagerMock = $this->createMock(ConfigurationManager::class); + $this->frontendUserAuthenticationMock = $this->createMock(FrontendUserAuthentication::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + + $this->subject = new FrontendUserCreateAfterEventListener( + $this->frontendUserAuthenticationMock, + $this->configurationManagerMock + ); + + $this->subject->setLogger($this->loggerMock); + } + + /** + * Test __invoke + * + * @return void + */ + public function testInvoke(): void + { + $builder = $this->configurationManagerMock->method('getConfiguration'); + $builder->with(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS); + $builder->willReturn(['enableFrontendUserAutoLogin' => true]); + + $frontendUserMock = $this->createMock(FrontendUser::class); + $requestMock = $this->createMock(ServerRequestInterface::class); + + $event = new FrontendUserCreateAfterEvent($frontendUserMock, $requestMock); + + $builder = $this->frontendUserAuthenticationMock->method('start'); + $builder->willReturnSelf(); + + $this->subject->__invoke($event); + } + + /** + * Test __invoke with exception + * + * @return void + */ + public function testInvokeWithException(): void + { + $builder = $this->configurationManagerMock->method('getConfiguration'); + $builder->willThrowException(new Exception('Invalid configuration type "Settings"', 1206031879)); + + $frontendUserMock = $this->createMock(FrontendUser::class); + $requestMock = $this->createMock(ServerRequestInterface::class); + + $event = GeneralUtility::makeInstance(FrontendUserCreateAfterEvent::class, $frontendUserMock, $requestMock); + + $this->subject->__invoke($event); + } +} diff --git a/Tests/Unit/Property/TypeConverter/UploadedFileReferenceConverterTest.php b/Tests/Unit/Property/TypeConverter/UploadedFileReferenceConverterTest.php new file mode 100644 index 0000000..c56dfb1 --- /dev/null +++ b/Tests/Unit/Property/TypeConverter/UploadedFileReferenceConverterTest.php @@ -0,0 +1,190 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Property\TypeConverter; + +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Config\Resource\FileResource; +use TYPO3\CMS\Core\Resource\File; +use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference; +use TYPO3\CMS\Extbase\Domain\Model\FileReference; +use TYPO3\CMS\Core\Resource\Folder; +use TYPO3\CMS\Core\Resource\ResourceFactory; +use TYPO3\CMS\Core\Resource\ResourceStorage; +use TYPO3\CMS\Core\Resource\StorageRepository; +use TYPO3\CMS\Extbase\Error\Error; +use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Property\TypeConverter\UploadedFileReferenceConverter; +use ReflectionClass; +use ReflectionException; +use InvalidArgumentException; + +/** + * Class UploadedFileReferenceConverterTest + * Testcase for class \Ydt\FrontendUser\Property\TypeConverter\UploadedFileReferenceConverter + */ +class UploadedFileReferenceConverterTest extends UnitTestCase +{ + /** + * Uploaded File Reference Converter + * + * @var UploadedFileReferenceConverter + */ + private $subject; + + /** + * Storage Repository Mock + * + * @var MockObject|StorageRepository + */ + private $storageRepositoryMock; + + /** + * Resource Factory Mock + * + * @var MockObject|ResourceFactory + */ + private $resourceFactoryMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] = 'jpeg,jpg'; + + $this->storageRepositoryMock = $this->createMock(StorageRepository::class); + $this->resourceFactoryMock = $this->createMock(ResourceFactory::class); + + $this->subject = new UploadedFileReferenceConverter( + $this->storageRepositoryMock, + $this->resourceFactoryMock + ); + } + + /** + * Test convertFrom + * + * @return void + */ + public function testConvertFrom(): void + { + $source = [ + 'tmp_name' => 'test', + 'name' => 'test.jpeg', + 'size' => 100, + 'type' => 'image/jpeg', + ]; + + $defaultStorageMock = $this->createMock(ResourceStorage::class); + + $folderMock = $this->createMock(Folder::class); + $builder = $defaultStorageMock->method('getFolder'); + $builder->willReturn($folderMock); + + $fileMock = $this->createMock(File::class); + $builder = $fileMock->method('getUid'); + $builder->willReturn(1); + + $builder = $defaultStorageMock->method('addFile'); + $builder->willReturn($fileMock); + + $builder = $this->storageRepositoryMock->method('getDefaultStorage'); + $builder->willReturn($defaultStorageMock); + + $fileReferenceMock = $this->createMock(CoreFileReference::class); + $builder = $fileReferenceMock->method('getOriginalFile'); + $builder->willReturn($fileMock); + + $builder = $this->resourceFactoryMock->method('createFileReferenceObject'); + $builder->willReturn($fileReferenceMock); + + $configurationMock = $this->createMock(PropertyMappingConfigurationInterface::class); + $builder = $configurationMock->method('getConfigurationValue'); + $builder->willReturn('test'); + + $result = $this->subject->convertFrom($source, FileResource::class, [], $configurationMock); + + $this->assertInstanceOf(FileReference::class, $result); + } + + /** + * Test convertFrom with exception + * + * @return void + */ + public function testConvertFromWithException(): void + { + $source = [ + 'tmp_name' => 'test', + 'name' => 'test.jpeg', + 'size' => 100, + 'type' => 'image/jpeg', + ]; + + $defaultStorageMock = $this->createMock(ResourceStorage::class); + + $defaultFolderMock = $this->createMock(Folder::class); + $builder = $defaultStorageMock->method('getDefaultFolder'); + $builder->willReturn($defaultFolderMock); + + $builder = $this->storageRepositoryMock->method('getDefaultStorage'); + $builder->willReturn($defaultStorageMock); + + $builder = $defaultStorageMock->method('addFile'); + $builder->willThrowException(new InvalidArgumentException('File "test.jpeg" does not exist.', 1319552745)); + + $result = $this->subject->convertFrom($source, FileResource::class); + + $this->assertInstanceOf(Error::class, $result); + } + + /** + * Test canConvertFrom + * + * @return void + */ + public function testCanConvertFrom(): void + { + $source = [ + 'tmp_name' => 'test', + 'name' => 'test.jpeg', + 'size' => 100, + 'type' => 'image/jpeg', + ]; + + $result = $this->subject->canConvertFrom($source, FileResource::class); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + /** + * Test isImageFileExtensionValid + * + * @return void + * @throws ReflectionException + */ + public function testIsImageFileExtensionValid(): void + { + $reflection = new ReflectionClass(UploadedFileReferenceConverter::class); + $method = $reflection->getMethod('isImageFileExtensionValid'); + $method->setAccessible(true); + + $result = $method->invoke($this->subject, 'test.jpg'); + + $this->assertIsBool($result); + $this->assertTrue($result); + } +} diff --git a/Tests/Unit/Service/FrontendUserFormServiceTest.php b/Tests/Unit/Service/FrontendUserFormServiceTest.php new file mode 100644 index 0000000..8a36575 --- /dev/null +++ b/Tests/Unit/Service/FrontendUserFormServiceTest.php @@ -0,0 +1,74 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Service; + +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Service\FrontendUserFormService; +use ReflectionClass; +use ReflectionException; + +/** + * Class FrontendUserFormServiceTest + * Testcase for class \Ydt\FrontendUser\Service\FrontendUserFormService + */ +class FrontendUserFormServiceTest extends UnitTestCase +{ + /** + * Frontend User Form Service + * + * @var FrontendUserFormService + */ + private $subject; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $languageServiceMock = $this->createMock(LanguageService::class); + $GLOBALS['LANG'] = $languageServiceMock; + + $this->subject = new FrontendUserFormService(); + } + + /** + * Test getFieldItems + * + * @return void + */ + public function testGetFieldItems(): void + { + $configuration = ['items' => []]; + + $this->subject->getFieldItems($configuration); + } + + /** + * Test getLanguageService + * + * @return void + * @throws ReflectionException + */ + public function testGetLanguageService(): void + { + $reflection = new ReflectionClass(FrontendUserFormService::class); + $method = $reflection->getMethod('getLanguageService'); + $method->setAccessible(true); + + $result = $method->invoke($this->subject); + + $this->assertInstanceOf(LanguageService::class, $result); + } +} diff --git a/Tests/Unit/Validation/Validator/DigitValidatorTest.php b/Tests/Unit/Validation/Validator/DigitValidatorTest.php new file mode 100644 index 0000000..41d0b6b --- /dev/null +++ b/Tests/Unit/Validation/Validator/DigitValidatorTest.php @@ -0,0 +1,93 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\Validation\Validator; + +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Localization\LocalizationFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; +use TYPO3\CMS\Extbase\Error\Result; +use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\Validation\Validator\DigitValidator; +use ReflectionClass; + +/** + * Class DigitValidatorTest + * Testcase for class \Ydt\FrontendUser\Validation\Validator\DigitValidator + */ +class DigitValidatorTest extends UnitTestCase +{ + /** + * Digit Validator + * + * @var DigitValidator + */ + private $subject; + + /** + * @inheritDoc + */ + protected $resetSingletonInstances = true; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $languageServiceMock = $this->createMock(LanguageService::class); + $GLOBALS['LANG'] = $languageServiceMock; + + $localizationFactoryMock = $this->createMock(LocalizationFactory::class); + $builder = $localizationFactoryMock->method('getParsedData'); + $builder->willReturn([]); + + GeneralUtility::setSingletonInstance(LocalizationFactory::class, $localizationFactoryMock); + + $configurationManagerMock = $this->createMock(ConfigurationManagerInterface::class); + $builder = $configurationManagerMock->method('getConfiguration'); + $builder->with(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, 'frontend_user'); + $builder->willReturn([]); + + GeneralUtility::setSingletonInstance(ConfigurationManagerInterface::class, $configurationManagerMock); + + $this->subject = new DigitValidator(); + } + + /** + * Test validate + * + * @return void + */ + public function testValidate(): void + { + $result = $this->subject->validate('12345a'); + + $this->assertInstanceOf(Result::class, $result); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $reflectionClass = new ReflectionClass(LocalizationUtility::class); + $property = $reflectionClass->getProperty('configurationManager'); + $property->setAccessible(true); + $property->setValue(null); + + GeneralUtility::purgeInstances(); + parent::tearDown(); + } +} diff --git a/Tests/Unit/ViewHelpers/Format/LowercaseHyphenatedViewHelperTest.php b/Tests/Unit/ViewHelpers/Format/LowercaseHyphenatedViewHelperTest.php new file mode 100644 index 0000000..1aa5424 --- /dev/null +++ b/Tests/Unit/ViewHelpers/Format/LowercaseHyphenatedViewHelperTest.php @@ -0,0 +1,76 @@ + + */ + +namespace Ydt\FrontendUser\Tests\Unit\ViewHelpers\Format; + +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use Ydt\FrontendUser\ViewHelpers\Format\LowercaseHyphenatedViewHelper; +use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; + +/** + * Class LowercaseHyphenatedViewHelperTest + * Testcase for class \Ydt\FrontendUser\ViewHelpers\Format\LowercaseHyphenatedViewHelper + */ +class LowercaseHyphenatedViewHelperTest extends UnitTestCase +{ + /** + * Lowercase Hyphenated View Helper + * + * @var LowercaseHyphenatedViewHelper + */ + private $subject; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->subject = new LowercaseHyphenatedViewHelper(); + + $this->subject->initializeArguments(); + } + + /** + * Test renderStatic + * + * @dataProvider argumentsDataProvider + * + * @param array $arguments + * @return void + */ + public function testRenderStatic(array $arguments): void + { + $renderChildrenClosureMock = function () { + return 'firstName'; + }; + $contextMock = $this->createMock(RenderingContextInterface::class); + + $result = $this->subject->renderStatic($arguments, $renderChildrenClosureMock, $contextMock); + + $this->assertIsString($result); + $this->assertSame($result, 'first-name'); + } + + /** + * Data provider for testRenderStatic + * + * @return array + */ + public function argumentsDataProvider(): array + { + return [ + [['value' => 'firstName']], + [['value' => '']], + ]; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7fc733c --- /dev/null +++ b/composer.json @@ -0,0 +1,57 @@ +{ + "name": "ydt/frontend-user", + "type": "typo3-cms-extension", + "description": "Frontend User", + "license": ["GPL-2.0-or-later"], + "authors": [ + { + "name": "YDT Global Team", + "email": "typo3@ydt-global.com", + "role": "Developer", + "homepage": "https://ydt-global.com/" + } + ], + "autoload": { + "psr-4": { + "Ydt\\FrontendUser\\": "Classes/" + } + }, + "autoload-dev": { + "psr-4": { + "Ydt\\FrontendUser\\Tests\\": "Tests/" + } + }, + "require": { + "typo3/cms-core": "^11.5", + "typo3/cms-extbase": "^11.5", + "typo3/cms-frontend": "^11.5", + "typo3/cms-fluid": "^11.5" + }, + "require-dev": { + "typo3/testing-framework": "^6.16", + "phpunit/phpunit": "^9.5" + }, + "extra": { + "typo3/cms": { + "branch-alias": { + "dev-master": "11.5" + }, + "extension-key": "frontend_user", + "cms-package-dir": "{$vendor-dir}/typo3/cms", + "web-dir": ".Build/Web" + } + }, + "config": { + "vendor-dir": ".Build/vendor", + "bin-dir": ".Build/bin", + "allow-plugins": { + "typo3/class-alias-loader": true, + "typo3/cms-composer-installers": true + } + }, + "scripts": { + "post-autoload-dump": [ + "TYPO3\\TestingFramework\\Composer\\ExtensionTestEnvironment::prepare" + ] + } +} diff --git a/ext_emconf.php b/ext_emconf.php new file mode 100644 index 0000000..23e8e97 --- /dev/null +++ b/ext_emconf.php @@ -0,0 +1,35 @@ + + */ + +$EM_CONF[$_EXTKEY] = [ + 'title' => 'Frontend User', + 'description' => 'The template-based plugin for managing website users on the TYPO3 frontend', + 'version' => '1.1.1', + 'category' => 'fe', + 'constraints' => [ + 'depends' => [ + 'typo3' => '11.5.0-11.99.99', + 'extbase' => '11.5.0-11.99.99', + 'fluid' => '11.5.0-11.99.99', + 'frontend' => '11.5.0-11.99.99', + ], + 'conflicts' => [], + 'suggests' => [], + ], + 'state' => 'stable', + 'clearCacheOnLoad' => 1, + 'author_email' => 'typo3@ydt-global.com', + 'author' => 'YDT Global Team', + 'author_company' => 'Your Dev Team Global', + 'autoload' => [ + 'psr-4' => [ + 'Ydt\\FrontendUser\\' => 'Classes', + ], + ], +]; diff --git a/ext_localconf.php b/ext_localconf.php new file mode 100644 index 0000000..a443180 --- /dev/null +++ b/ext_localconf.php @@ -0,0 +1,46 @@ + + */ + +use TYPO3\CMS\Extbase\Utility\ExtensionUtility; +use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; +use Ydt\FrontendUser\Controller\FrontendUserController; +use Ydt\FrontendUser\Property\TypeConverter\UploadedFileReferenceConverter; + +defined('TYPO3') or die(); + +(function () { + ExtensionManagementUtility::addTypoScriptConstants( + "@import 'EXT:frontend_user/Configuration/TypoScript/constants.typoscript'" + ); + ExtensionManagementUtility::addTypoScriptSetup( + "@import 'EXT:frontend_user/Configuration/TypoScript/setup.typoscript'" + ); + + ExtensionUtility::configurePlugin( + 'FrontendUser', + 'Form', + [FrontendUserController::class => 'new, edit, create, update, delete, deleteImage'], + [FrontendUserController::class => 'new, edit, create, update, delete, deleteImage'], + ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT + ); + + ExtensionManagementUtility::addPageTSConfig( + "@import 'EXT:frontend_user/Configuration/TsConfig/Page/Mod/Wizards/NewContentElement.tsconfig'" + ); + + $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['ydt'] = ['Ydt\FrontendUser\ViewHelpers']; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride']['EXT:core/Resources/Private/Language/locallang_general.xlf'][] + = 'EXT:frontend_user/Resources/Private/Language/locallang_be.xlf'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride']['de']['EXT:core/Resources/Private/Language/locallang_general.xlf'][] + = 'EXT:frontend_user/Resources/Private/Language/de.locallang_be.xlf'; + + ExtensionUtility::registerTypeConverter(UploadedFileReferenceConverter::class); +})();