<?php
namespace ProjectBiz\PortalBundle\Controller;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManager;
use ProjectBiz\FormBundle\Form\Type\CustomPasswordType;
use ProjectBiz\FormBundle\Form\Type\PasswordRecoveryType;
use ProjectBiz\FormBundle\Form\Type\UserCredentialsType;
use ProjectBiz\FormBundle\Form\Type\UserRegistrationType;
use ProjectBiz\FormBundle\Form\Type\UserValidationType;
use ProjectBiz\PortalBundle\Event\UserInteractionEvent;
use ProjectBiz\PortalBundle\Exceptions\DuplicateUsernameException;
use ProjectBiz\PortalBundle\Exceptions\UserNotCreatedException;
use ProjectBiz\PortalBundle\Exceptions\ValidationTokenExpiredException;
use ProjectBiz\PortalBundle\Exceptions\ValidationTokenInvalidException;
use ProjectBiz\UserBundle\Entity\BaseUserInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class LoginController extends AbstractController
{
const msgRegister = 'Absenden';
const msgNoUniqueTokenFound = 'Es konnte kein eindeutiger Token gefunden werden.';
const msgRegisterSuccess = 'Die Registrierung war erfolgreich.';
const msgPasswordChanged = 'Ihr Passwort wurde erfolgreich geändert.';
const msgValidationError = 'Die Validierung ist fehlgeschlagen.';
const msgValidationTooOldError= 'Der Validierungs-Code ist abgelaufen.';
const msgAcceptPrivacyTerms = 'Mit Anklicken des Links bestätigen Sie, die <a target="_blank" href="%s">Datenschutzerklärung</a> gelesen zu haben.';
const msgDomainNotAllowed = "Die Domain Ihrer E-Mail-Adresse ist nicht in der Whitelist enthalten.\nBitte wenden Sie sich an Ihren Verantwortlichen.\nIhre E-Mail-Adresse wurde nicht gespeichert.";
/**
* Does the user accept cookies and privacy terms?
*
* If not the user always gets back to the privacy terms acceptance page.
*
*/
#[Route('/compliance', name: 'compliance')]
public function compliance(Request $request)
{
if ($request->isMethod('POST') &&
$request->get('accept_privacy_terms') &&
$request->get('accept_cookies')) {
$session = $request->getSession();
// ❗ Session manuell starten, wenn nötig
if (!$session->isStarted()) {
$session->start();
}
// ✅ Zustimmung speichern
$session->set('compliance_accepted', true);
// ✔️ Session speichern, dass User akzeptiert hat
// 🔁 Weiterleitung zur Login-Seite
return $this->redirect($this->generateUrl('_login')); // oder: return $this->redirect('/login');
}
// 👁️ GET-Aufruf: zeige Formular
return $this->render('@ProjectBizPortal/Login/compliance.html.twig');
}
public function index(Request $request)
{
$session = $request->getSession();
// get the login error if there is one
if ($request->attributes->has(Security::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(
Security::AUTHENTICATION_ERROR
);
} else {
$error = $session->get(Security::AUTHENTICATION_ERROR);
$session->remove(Security::AUTHENTICATION_ERROR);
}
return $this->render('@ProjectBizPortal/Login/index.html.twig', [
// last username entered by the user
'features' => ['register' => $this->useRegistration()],
'last_username' => $session->get(Security::LAST_USERNAME),
'error' => $error,
'require_cookie_acceptance_at_login' => $this->getParameter('citibiz.require_cookie_acceptance_at_login'),
]);
}
/**
* @param Request $request
*
* @return array|\Symfony\Component\HttpFoundation\Response
* @throws \Exception
*/
public function recoverPassword(Request $request)
{
$form = $this->createForm(PasswordRecoveryType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
/** @var \Doctrine\ORM\EntityManager $entityManager */
$entityManager = $this->get('doctrine')->getManager();
/** @var \ProjectBiz\UserBundle\Entity\BaseUserInterface $user */
$user = $entityManager
->getRepository($this->getUserClass())
->findOneBy(['username' => $data['username']]);
if ($user && $user->getEmail()) {
$tokenType = 'recover_password';
$uniqueToken = $this->getUniqueToken($entityManager, $tokenType);
$userClass = $this->getParameter('projectbiz.user.user_entity_class');
$tokenClass = $userClass::TOKEN_CLASS;
$token = new $tokenClass(
$tokenType,
$uniqueToken,
$this->getParameter('citibiz.max_recover_password_age'),
null,
$user
);
$entityManager->persist($token);
$entityManager->flush();
// Send validation E-Mail
$sender = $this->getParameter('citibiz.email_sender');
$subject = $this->getParameter('citibiz.email_recover_password_subject');
$message = (new \Swift_Message($subject))
->setFrom($sender)
->setTo($user->getEmail())
->setBody(
$this->render(
'@ProjectBizPortal/Email/recover_password.txt.twig',
[
'token' => $token->getToken(),
'username' => $user->getUsername(),
'realname' => $user->getRealname(),
'href' => $this->getMailLinkGenerator()->generateUrl('_recover_password_token', [
'token' => $token->getToken()
], true)
]
)
)
->addPart(
$this->render(
'@ProjectBizPortal/Email/recover_password.html.twig',
[
'token' => $token->getToken(),
'username' => $user->getUsername(),
'realname' => $user->getRealname(),
'href' => $this->getMailLinkGenerator()->generateUrl('_recover_password_token', [
'token' => $token->getToken()
])
]
), 'text/html'
);
$this->get('mailer')->send($message);
}
return $this->render("@ProjectBizPortal/Login/recoverPasswordSuccess.html.twig", [
'user' => $data['username']
]);
}
return $this->render('@ProjectBizPortal/Login/recoverPassword.html.twig',
[
'form' => $form->createView()
]
);
}
public function recoverPasswordToken(Request $request)
{
$userClass = $this->getParameter('projectbiz.user.user_entity_class');
$tokenClass = $userClass::TOKEN_CLASS;
$token = $request->query->get('token');
/** @var \Doctrine\ORM\EntityManager $entityManager */
$entityManager = $this->get('doctrine')->getManager();
$tokenEntity = $entityManager->getRepository($tokenClass)->findOneBy(['token' => $token]);
if (!$tokenEntity) {
return $this->render('@ProjectBizPortal/Login/recoverPasswordToken.html.twig',
[
'missing_token' => true,
'invalid_token' => false,
]
);
}
if (!$tokenEntity->isValid() || ($tokenEntity->getType() != 'recover_password')) {
return $this->render('@ProjectBizPortal/Login/recoverPasswordToken.html.twig',
[
'missing_token' => false,
'invalid_token' => true,
]
);
}
$form = $this->createForm(CustomPasswordType::class);
$form->add('create', SubmitType::class, [
'label' => self::msgRegister,
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$user = $tokenEntity->getUser();
if ($user) {
$user->setPassword($data);
$user->setModifiedBy($user);
$user->setModifiedAt(new \DateTime());
$entityManager->persist($user);
$entityManager->remove($tokenEntity);
$entityManager->flush();
}
$this->get('session')
->getFlashBag()
->add(
'success',
$this->get('translator')->trans(self::msgPasswordChanged)
);
return $this->redirect($this->generateUrl('_login'));
}
return $this->render('@ProjectBizPortal/Login/recoverPasswordToken.html.twig',
[
'missing_token' => false,
'invalid_token' => false,
'form' => $form->createView()
]
);
}
public function register(Request $request)
{
if (!$this->useRegistration()) {
return $this->redirect($this->generateUrl('_login'));
}
$form = $this->createForm(UserRegistrationType::class, ['email' => $request->query->get('email')], [
'additional_fields' => [
'privacy_terms' => [
'label' => sprintf($this->get('translator')->trans(self::msgAcceptPrivacyTerms), $this->generateUrl('pages_privacy_terms')),
'required' => true,
'type' => \Symfony\Component\Form\Extension\Core\Type\CheckboxType::class,
'attr' => [
'autocomplete' => 'off'
]
]
]
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Create validation token (including supplied data)
$data = $form->getData();
/*
* Check if the user's e-mail domain is allowed for registering.
*/
if ($this->getParameter('citibiz.restrict_email_domains_on_registration')) {
$allowedEmailDomainsStmt = $this->get('citibiz.generic_repository_factory')->createGenericRepository('AllowedEmailDomains')->findAll(null, ['AllowedEmailDomains_Name']);
$allowedEmailDomains = [];
if ($allowedEmailDomainsStmt) {
$allowedEmailDomains = array_map(function($row) {
return $row['AllowedEmailDomains_Name'];
}, $allowedEmailDomainsStmt->fetchAll());
}
$userEmailParts = explode('@', $data['email']);
$userDomain = array_pop($userEmailParts);
if (!in_array($userDomain, $allowedEmailDomains)) {
$this->setAndTransFlashMessage(self::msgDomainNotAllowed);
return [
'form' => $form->createView()
];
}
}
/** @var \Doctrine\ORM\EntityManager $entityManager */
$entityManager = $this->get('doctrine')->getManager();
$tokenType = 'validation';
$uniqueToken = $this->getUniqueToken($entityManager, $tokenType);
$userClass = $this->getParameter('projectbiz.user.user_entity_class');
$tokenClass = $userClass::TOKEN_CLASS;
$token = new $tokenClass(
$tokenType,
$uniqueToken,
$this->getParameter('citibiz.max_validation_age'),
$data
);
$entityManager->persist($token);
$entityManager->flush();
// Send validation E-Mail
$sender = $this->getParameter('citibiz.email_sender');
$subject = $this->getParameter('citibiz.email_registration_subject');
$privacyUrl = $this->generateUrl(
'pages_privacy_terms',
['slug' => 'datenschutz'],
UrlGeneratorInterface::ABSOLUTE_URL
);
$imprintUrl = $this->generateUrl(
'pages_imprint',
['slug' => 'impressum'],
UrlGeneratorInterface::ABSOLUTE_URL
);
$message = (new \Swift_Message($subject))
->setFrom($sender)
->setTo($data['email'])
->setBody(
$this->render(
'@ProjectBizPortal/Email/validation.txt.twig',
[
'portal_name' => $this->getParameter('citibiz.portal'),
'token' => $token->getToken(),
'email' => $data["email"],
'href' => $this->getMailLinkGenerator()->generateUrl('_register_do_validate', [
'user_validation' => [
'email' => $data['email'],
'code' => $token->getToken()
]
], true)
]
)
)
->addPart(
$this->render(
'@ProjectBizPortal/Email/validation.html.twig',
[
'portal_name' => $this->getParameter('citibiz.portal'),
'token' => $token->getToken(),
'email' => $data["email"],
'imprintUrl' => $imprintUrl,
'privacyUrl' => $privacyUrl,
'href' => $this->getMailLinkGenerator()->generateUrl('_register_do_validate', [
'user_validation' => [
'email' => $data['email'],
'code' => $token->getToken()
]
], true)
]
),
'text/html'
);
$this->get('mailer')->send($message);
$this->get('projectbiz.portal.user_interaction.event_dispatcher')->dispatch(
UserInteractionEvent::EVENT,
new UserInteractionEvent(UserInteractionEvent::EVENT_REGISTRATION_BEGIN, [
'registration' => $data
])
);
return $this->redirect($this->generateUrl('_register_validate', ['email' => $data['email']]));
}
return $this->render('@ProjectBizPortal/Login/register.html.twig',
[
'form' => $form->createView()
]
);
}
/**
* @param Request $request
*
* @return array|RedirectResponse
*/
public function registerValidation(Request $request)
{
if (!$this->useRegistration()) {
return $this->redirect($this->generateUrl('_login'));
}
$data = [
'email' => $request->query->get('email')
];
$form = $this->createForm(UserValidationType::class, $data, [
'action' => $this->generateUrl('_register_do_validate'),
'method' => 'GET',
'csrf_protection' => false
]);
return $this->render('@ProjectBizPortal/Login/registerValidation.html.twig', [
'form' => $form->createView()
]);
}
/**
* @param Request $request
*
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function registerValidate(Request $request)
{
$userClass = $this->getParameter('projectbiz.user.user_entity_class');
$tokenClass = $userClass::TOKEN_CLASS;
$form = $this->createForm(UserValidationType::class, [], [
'method' => 'GET',
'csrf_protection' => false
]);
$form->handleRequest($request);
if ($form->isSubmitted() && !$form->isValid()) {
$this
->get('session')
->getFlashBag()
->add(
'danger',
$this->get('translator')->trans(self::msgValidationError)
);
return $this->redirect($this->generateUrl('_register_validate'));
}
$data = $form->getData();
/** @var \Doctrine\ORM\EntityManager $em */
$em = $this->get('doctrine')->getManager();
$token = $em->getRepository($tokenClass)->findBy(['token' => $data['code']]);
// Check a/ if $token b/ $token has correct email c/ $token is not too old
if (!((count($token) == 1) && ($token[0]->getData()['email'] == $data['email']) && $token[0]->isValid())) {
// Error - show error message
if ((count($token) == 1) && !$token[0]->isValid()) {
$this
->get('session')
->getFlashBag()
->add(
'danger',
$this->get('translator')->trans(self::msgValidationTooOldError)
);
return $this->redirect($this->generateUrl('_register', ['email' => $data['email']]));
}
$this
->get('session')
->getFlashBag()
->add(
'danger',
$this->get('translator')->trans(self::msgValidationError)
);
return $this->redirect($this->generateUrl('_register_validate', ['email' => $data['email']]));
}
// Valid - ask for username and password - token and email are stored hidden
$form = $this->createForm(UserCredentialsType::class, $data, [
'action' => $this->generateUrl('_register_credentials'),
'use_realname' => $this->getParameter('citibiz.registration_use_realname')
]);
return $this->render('@ProjectBizPortal/Login/registerCredentials.html.twig', [
'form' => $form->createView()
]);
}
/**
* @param Request $request
*
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
* @throws \Doctrine\DBAL\ConnectionException
* @throws \Exception
*/
public function registerCredentials(Request $request)
{
$form = $this->createForm(UserCredentialsType::class, ['use_realname' => true]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$success = true;
try {
$this->handleCredentialsFormData($data, $request);
$this->get('projectbiz.portal.user_interaction.event_dispatcher')->dispatch(
UserInteractionEvent::EVENT,
new UserInteractionEvent(UserInteractionEvent::EVENT_REGISTRATION_END, [
'registration' => $data
])
);
} catch (ValidationTokenExpiredException $ex) {
$this->setAndTransFlashMessage($ex->getMessage());
return $this->redirect($this->generateUrl('_register', ['email' => $data['email']]));
}
catch(ValidationTokenInvalidException $ex) {
$this->setAndTransFlashMessage($ex->getMessage());
return $this->redirect($this->generateUrl('_register_validate', ['email' => $data['email']]));
}
catch(UserNotCreatedException $ex) {
$this->setAndTransFlashMessage($ex->getMessage());
$success = false;
}
catch(DuplicateUsernameException $ex) {
$this->setAndTransFlashMessage($ex->getMessage());
$success = false;
}
catch (UniqueConstraintViolationException $ex) {
$this->setAndTransFlashMessage(DuplicateUsernameException::MSG_DUPLICATE_USERNAME);
$success = false;
}
if ($success) {
$this->setAndTransFlashMessage(self::msgRegisterSuccess, 'success');
return $this->redirect($this->generateUrl('portal'));
}
}
return $this->render('@ProjectBizPortal/Login/registerCredentials.html.twig',
[
'form' => $form->createView()
]
);
}
protected function getUserClass()
{
return $this->getParameter('projectbiz.user.user_entity_class');
}
/**
* @param EntityManager $em
* @param $tokenType
*
* @return mixed
*
* @throws \Exception
*/
private function getUniqueToken(EntityManager $em, $tokenType)
{
$userClass = $this->getParameter('projectbiz.user.user_entity_class');
$tokenClass = $userClass::TOKEN_CLASS;
$maxTrys = 1000;
while ($maxTrys > 0) {
$tokenString = $this->get('citibiz.token_factory')->createToken($tokenType);
$identicalTokens = $em
->getRepository($tokenClass)
->findBy(['token' => $tokenString]);
if (count($identicalTokens) == 0) {
return $tokenString;
}
$maxTrys--;
}
throw new \Exception(self::msgNoUniqueTokenFound);
}
private function setAndTransFlashMessage($message, $type = 'danger') {
$this
->get('session')
->getFlashBag()
->add(
$type,
$this->get('translator')->trans($message)
);
}
/**
* @return \Doctrine\ORM\EntityManager
*/
private function getEntityManager()
{
return $this->get('doctrine')->getManager();
}
private function handleCredentialsFormData($data, Request $request)
{
$userClass = $this->getParameter('projectbiz.user.user_entity_class');
$tokenClass = $userClass::TOKEN_CLASS;
$entityManager = $this->getEntityManager();
// Check token (again)
/** @var \ProjectBiz\UserBundle\Entity\Token[] $token */
$token = $entityManager
->getRepository($tokenClass)
->findBy(['token' => $data['code']]);
if (!((count($token) == 1) && ($token[0]->getData()['email'] == $data['email']) && $token[0]->isValid())) {
// Error - show error message
if ((count($token) == 1) && !$token[0]->isValid()) {
throw new ValidationTokenExpiredException();
}
throw new ValidationTokenInvalidException();
}
$this->createAndLoginUser($data, $token, $request);
}
private function createAndLoginUser($data, $token, Request $request)
{
$entityManager = $this->getEntityManager();
// Create user
$realname = null;
if ($this->getParameter('citibiz.registration_use_realname')) {
$realname = $data['realname'];
}
$className = $this->getUserClass();
/** @var BaseUserInterface $user */
$user = new $className(
$data['username'],
$data['password'],
$realname,
$data['email'],
$this->getParameter('citibiz.active_after_registration'),
$this->getParameter('citibiz.rights_after_registration')
);
$userProvider = $this->get('projectbiz.user.user_provider');
$admin = $userProvider->loadUserByUsername('admin');
// Transactional to ensure unique usernames
$entityManager->getConnection()->beginTransaction();
try {
$user->setModifiedBy($admin);
$entityManager->persist($user);
if ($user->getRights()) {
$user->getRights()->setModifiedBy($user);
$entityManager->persist($user->getRights());
}
$entityManager->remove($token[0]);
$user->setCreatedBy($user);
$entityManager->persist($user);
$entityManager->flush();
$users = $entityManager
->getRepository($this->getUserClass())
->findBy(['username' => $data['username']]);
if (count($users) == 0) {
throw new UserNotCreatedException();
}
if (count($users) > 1) {
throw new DuplicateUsernameException();
}
} catch (\Exception $ex) {
$entityManager->getConnection()->rollBack();
throw($ex);
}
$entityManager->getConnection()->commit();
// Login User
if ($this->getParameter('citibiz.login_after_registration')) {
// Here, "main" is the name of the firewall in your security.yml
$secToken = new UsernamePasswordToken($user, $user->getPassword(), "main", $user->getRoles());
$this->get("security.token_storage")->setToken($secToken);
// Fire the login event
// Logging the user in above the way we do it doesn't do this automatically
$event = new InteractiveLoginEvent($request, $secToken);
$this->get("event_dispatcher")->dispatch("security.interactive_login", $event);
$session = $request->getSession();
$session->set('_security_main', serialize($secToken));
}
}
private function useRegistration()
{
if ($useRegistration = $this->getParameter('citibiz.use_registration')) {
return $useRegistration;
}
return false;
}
/**
* @return \ProjectBiz\PortalBundle\Service\MailLinkGenerator
*/
private function getMailLinkGenerator()
{
return $this->get('projectbiz.portal.mail_link_generator');
}
}