Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ environment:
TEST_DATABASE_DSN: mysql://root:Password12!@127.0.0.1:3306/test_maker
matrix:
- dependencies: highest
php_ver_target: 7.2.3
php_ver_target: 7.2.5

install:
- ps: Set-Service wuauserv -StartupType Manual
Expand Down
142 changes: 138 additions & 4 deletions src/Maker/MakeRegistrationForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,22 @@
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Validation;
use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle;

/**
* @author Ryan Weaver <ryan@symfonycasts.com>
* @author Ryan Weaver <ryan@symfonycasts.com>
* @author Jesse Rushlow <jr@rushlow.dev>
*
* @internal
*/
Expand Down Expand Up @@ -77,6 +81,11 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
->addArgument('user-class')
->addArgument('username-field')
->addArgument('password-field')
->addArgument('will-verify-email')
->addArgument('id-getter')
->addArgument('email-getter')
->addArgument('from-email-address')
->addArgument('from-email-name')
->addOption('auto-login-authenticator')
->addOption('firewall-name')
->addOption('redirect-route-name')
Expand Down Expand Up @@ -124,6 +133,29 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
$addAnnotation
);

$willVerify = $io->confirm('Do you want to send an email to verify the user\'s email address after registration?', true);

$input->setArgument('will-verify-email', $willVerify);

if ($willVerify) {
$this->checkComponentsExist($io);

$input->setArgument('id-getter', $interactiveSecurityHelper->guessIdGetter($io, $userClass));
$input->setArgument('email-getter', $interactiveSecurityHelper->guessEmailGetter($io, $userClass, 'email'));

$input->setArgument('from-email-address', $io->ask(
'What email address will be used to send registration confirmations? e.g. mailer@your-domain.com',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'What email address will be used to send registration confirmations? e.g. mailer@your-domain.com',
'What email address will be used to send registration confirmations? e.g. mailer@example',

null,
[Validator::class, 'validateEmailAddress']
));

$input->setArgument('from-email-name', $io->ask(
'What "name" should be associated with that email address? e.g. "Acme Mail Bot"',
null,
[Validator::class, 'notBlank']
));
}

if ($io->confirm('Do you want to automatically authenticate the user after registration?')) {
$this->interactAuthenticatorQuestions(
$input,
Expand All @@ -132,7 +164,9 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
$securityData,
$command
);
} else {
}

if (!$input->getOption('auto-login-authenticator')) {
$routeNames = array_keys($this->router->getRouteCollection()->all());
$input->setOption(
'redirect-route-name',
Expand Down Expand Up @@ -184,6 +218,27 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
'Entity\\'
);

$verifyEmailServiceClassNameDetails = $generator->createClassNameDetails(
'EmailVerifier',
'Security\\'
);

if ($input->getArgument('will-verify-email')) {
$generator->generateClass(
$verifyEmailServiceClassNameDetails->getFullName(),
'verifyEmail/EmailVerifier.tpl.php',
[
'id_getter' => $input->getArgument('id-getter'),
'email_getter' => $input->getArgument('email-getter'),
]
);

$generator->generateTemplate(
'registration/confirmation_email.html.twig',
'registration/twig_email.tpl.php'
);
}

// 1) Generate the form class
$usernameField = $input->getArgument('username-field');
$formClassDetails = $this->generateFormClass(
Expand All @@ -210,6 +265,11 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
'user_class_name' => $userClassNameDetails->getShortName(),
'user_full_class_name' => $userClassNameDetails->getFullName(),
'password_field' => $input->getArgument('password-field'),
'will_verify_email' => $input->getArgument('will-verify-email'),
'verify_email_security_service' => $verifyEmailServiceClassNameDetails->getFullName(),
'from_email' => $input->getArgument('from-email-address'),
'from_email_name' => $input->getArgument('from-email-name'),
'email_getter' => $input->getArgument('email-getter'),
'authenticator_class_name' => $authenticatorClassName ? Str::getShortClassName($authenticatorClassName) : null,
'authenticator_full_class_name' => $authenticatorClassName,
'firewall_name' => $input->getOption('firewall-name'),
Expand Down Expand Up @@ -244,11 +304,85 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$this->fileManager->dumpFile($classDetails->getPath(), $userManipulator->getSourceCode());
}

if ($input->getArgument('will-verify-email')) {
$classDetails = new ClassDetails($userClass);
$userManipulator = new ClassSourceManipulator(
file_get_contents($classDetails->getPath())
);
$userManipulator->setIo($io);

$userManipulator->addProperty('isVerified', ['@ORM\Column(type="boolean")'], false);
$userManipulator->addAccessorMethod('isVerified', 'isVerified', 'bool', false);
$userManipulator->addSetter('isVerified', 'bool', false);

$this->fileManager->dumpFile($classDetails->getPath(), $userManipulator->getSourceCode());
}

$generator->writeChanges();

$this->writeSuccessMessage($io);
$io->text('Next: Go to /register to check out your new form!');
$io->text('Make any changes you need to the form, controller & template.');
$this->successMessage($io, $input->getArgument('will-verify-email'), $userClassNameDetails->getShortName());
}

private function successMessage(ConsoleStyle $io, bool $emailVerification, string $userClass): void
{
$closing[] = 'Next:';

if (!$emailVerification) {
$closing[] = 'Make any changes you need to the form, controller & template.';
} else {
$index = 1;
if ($missingPackagesMessage = $this->getMissingComponentsComposerMessage()) {
$closing[] = '1) Install some missing packages:';
$closing[] = sprintf(' <fg=green>%s</>', $missingPackagesMessage);
++$index;
}

$closing[] = sprintf('%d) In <fg=yellow>RegistrationController::verifyUserEmail()</>:', $index++);
$closing[] = ' * Customize the last <fg=yellow>redirectToRoute()</> after a successful email verification.';
$closing[] = ' * Make sure you\'re rendering <fg=yellow>success</> flash messages or change the <fg=yellow>$this->addFlash()</> line.';
$closing[] = sprintf('%d) Review and customize the form, controller, and templates as needed.', $index++);
$closing[] = sprintf('%d) Run <fg=yellow>"php bin/console make:migration"</> to generate a migration for the newly added <fg=yellow>%s::isVerified</> property.', $index++, $userClass);
}

$io->text($closing);
$io->newLine();
$io->text('Then open your browser, go to "/register" and enjoy your new form!');
$io->newLine();
}

private function checkComponentsExist(ConsoleStyle $io): void
{
$message = $this->getMissingComponentsComposerMessage();

if ($message) {
$io->warning([
'We\'re missing some important components. Don\'t forget to install these after you\'re finished.',
$message,
]);
}
}

private function getMissingComponentsComposerMessage(): ?string
{
$missing = false;
$composerMessage = 'composer require';

if (!class_exists(SymfonyCastsVerifyEmailBundle::class)) {
$missing = true;
$composerMessage = sprintf('%s symfonycasts/verify-email-bundle', $composerMessage);
}

if (!interface_exists(MailerInterface::class)) {
$missing = true;
$composerMessage = sprintf('%s symfony/mailer', $composerMessage);
}

if (!$missing) {
return null;
}

return $composerMessage;
}

public function configureDependencies(DependencyBuilder $dependencies)
Expand Down
55 changes: 55 additions & 0 deletions src/Resources/skeleton/registration/RegistrationController.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,41 @@

use <?= $user_full_class_name ?>;
use <?= $form_full_class_name ?>;
<?php if ($will_verify_email): ?>
use <?= $verify_email_security_service; ?>;
<?php endif; ?>
<?php if ($authenticator_full_class_name): ?>
use <?= $authenticator_full_class_name; ?>;
<?php endif; ?>
<?php if ($will_verify_email): ?>
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
<?php endif; ?>
use Symfony\Bundle\FrameworkBundle\Controller\<?= $parent_class_name; ?>;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
<?php if ($will_verify_email): ?>
use Symfony\Component\Mime\Address;
<?php endif; ?>
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
<?php if ($authenticator_full_class_name): ?>
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
<?php endif; ?>
<?php if ($will_verify_email): ?>
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
<?php endif; ?>

class <?= $class_name; ?> extends <?= $parent_class_name; ?><?= "\n" ?>
{
<?php if ($will_verify_email): ?>
private $emailVerifier;

public function __construct(EmailVerifier $emailVerifier)
{
$this->emailVerifier = $emailVerifier;
}

<?php endif; ?>
/**
* @Route("<?= $route_path ?>", name="<?= $route_name ?>")
*/
Expand All @@ -39,7 +60,17 @@ public function register(Request $request, UserPasswordEncoderInterface $passwor
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
<?php if ($will_verify_email): ?>

// generate a signed url and email it to the user
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail())
->from(new Address('<?= $from_email ?>', '<?= $from_email_name ?>'))
->to($user-><?= $email_getter ?>())
->subject('Please Confirm your Email')
->htmlTemplate('registration/confirmation_email.html.twig')
);
<?php endif; ?>
// do anything else you need here, like send an email

<?php if ($authenticator_full_class_name): ?>
Expand All @@ -58,4 +89,28 @@ public function register(Request $request, UserPasswordEncoderInterface $passwor
'registrationForm' => $form->createView(),
]);
}
<?php if ($will_verify_email): ?>

/**
* @Route("/verify/email", name="app_verify_email")
*/
public function verifyUserEmail(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');

// validate email confirmation link, sets User::isVerified=true and persists
try {
$this->emailVerifier->handleEmailConfirmation($request, $this->getUser());
} catch (VerifyEmailExceptionInterface $exception) {
$this->addFlash('verify_email_error', $exception->getReason());

return $this->redirectToRoute('<?= $route_name ?>');
}

// @TODO Change the redirect on success and handle or remove the flash message in your templates
$this->addFlash('success', 'Your email address has been verified.');

return $this->redirectToRoute('app_register');
}
<?php endif; ?>
}
11 changes: 11 additions & 0 deletions src/Resources/skeleton/registration/twig_email.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<h1>Hi! Please confirm your email!</h1>

<p>
Please confirm your email address by clicking the following link: <br><br>
<a href="{{ signedUrl|raw }}">Confirm my Email</a>.
This link will expire in {{ expiresAt|date('g') }} hour(s).
</p>

<p>
Cheers!
</p>
8 changes: 7 additions & 1 deletion src/Resources/skeleton/registration/twig_template.tpl.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
<?= $helper->getHeadPrintCode('Register'); ?>

{% block body %}
{% for flashError in app.flashes('verify_email_error') %}
<div class="alert alert-danger" role="alert">{{ flashError }}</div>
{% endfor %}

<h1>Register</h1>

{{ form_start(registrationForm) }}
{{ form_row(registrationForm.<?= $username_field ?>) }}
{{ form_row(registrationForm.plainPassword) }}
{{ form_row(registrationForm.plainPassword, {
label: 'Password'
}) }}
{{ form_row(registrationForm.agreeTerms) }}

<button type="submit" class="btn">Register</button>
Expand Down
55 changes: 55 additions & 0 deletions src/Resources/skeleton/verifyEmail/EmailVerifier.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?= "<?php\n" ?>

namespace <?= $namespace; ?>;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;

class <?= $class_name; ?><?= "\n" ?>
{
private $verifyEmailHelper;
private $mailer;
private $entityManager;

public function __construct(VerifyEmailHelperInterface $helper, MailerInterface $mailer, EntityManagerInterface $manager)
{
$this->verifyEmailHelper = $helper;
$this->mailer = $mailer;
$this->entityManager = $manager;
}

public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void
{
$signatureComponents = $this->verifyEmailHelper->generateSignature(
$verifyEmailRouteName,
$user-><?= $id_getter ?>(),
$user-><?= $email_getter ?>()
);

$context = $email->getContext();
$context['signedUrl'] = $signatureComponents->getSignedUrl();
$context['expiresAt'] = $signatureComponents->getExpiresAt();

$email->context($context);

$this->mailer->send($email);
}

/**
* @throws VerifyEmailExceptionInterface
*/
public function handleEmailConfirmation(Request $request, UserInterface $user): void
{
$this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user-><?= $id_getter ?>(), $user-><?= $email_getter?>());

$user->setIsVerified(true);

$this->entityManager->persist($user);
$this->entityManager->flush();
}
}
Loading