Skip to content

Commit

Permalink
Added a make:forgotten-password maker
Browse files Browse the repository at this point in the history
  • Loading branch information
romaricdrigon committed Feb 1, 2019
1 parent 08fabae commit 59805d7
Show file tree
Hide file tree
Showing 7 changed files with 480 additions and 0 deletions.
320 changes: 320 additions & 0 deletions src/Maker/MakeForgottenPassword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Maker;

use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
use Symfony\Bundle\MakerBundle\Util\ClassDetails;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
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\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Renderer\FormTypeRenderer;
use Symfony\Component\Validator\Validation;
use Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle;

/**
* @author Romaric Drigon <romaric.drigon@gmail.com>
*
* @internal
*/
final class MakeForgottenPassword extends AbstractMaker
{
private $fileManager;

private $formTypeRenderer;

private $router;

public function __construct(FileManager $fileManager, FormTypeRenderer $formTypeRenderer, RouterInterface $router)
{
$this->fileManager = $fileManager;
$this->formTypeRenderer = $formTypeRenderer;
$this->router = $router;
}

public static function getCommandName(): string
{
return 'make:forgotten-password';
}

public function configureCommand(Command $command, InputConfiguration $inputConfig)
{
$command
->setDescription('Creates a "forgotten password" mechanism')
//->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeRegistrationForm.txt'))
;
}

public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
{
// initialize arguments & commands that are internal (i.e. meant only to be asked)
$command
->addArgument('user-class')
->addArgument('username-field')
->addArgument('password-field')
;

$interactiveSecurityHelper = new InteractiveSecurityHelper();

if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. This command needs that file to accurately build the forgotten password form.');
}

$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
$securityData = $manipulator->getData();
$providersData = $securityData['security']['providers'] ?? [];

$input->setArgument(
'user-class',
$userClass = $interactiveSecurityHelper->guessUserClass(
$io,
$providersData,
'Enter the User class which should be able to use "forgotten password" feature (e.g. <fg=yellow>App\\Entity\\User</>)'
)
);
$io->text(sprintf('Implementing forgotten password for <info>%s</info>', $userClass));

$input->setArgument(
'username-field',
$interactiveSecurityHelper->guessUserNameField($io, $userClass, $providersData)
);
$input->setArgument(
'password-field',
$interactiveSecurityHelper->guessPasswordField($io, $userClass)
);
}

public function configureDependencies(DependencyBuilder $dependencies)
{
// This recipe depends upon Doctrine ORM, to save the token and update the user
ORMDependencyBuilder::buildDependencies($dependencies);

$dependencies->addClassDependency(
AbstractType::class,
'form'
);
$dependencies->addClassDependency(
Validation::class,
'validator'
);
$dependencies->addClassDependency(
TwigBundle::class,
'twig-bundle'
);
$dependencies->addClassDependency(
SecurityBundle::class,
'security'
);
$dependencies->addClassDependency(
SwiftmailerBundle::class,
'mail'
);
}

public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
{
$userClass = $input->getArgument('user-class');
$userClassNameDetails = $generator->createClassNameDetails(
'\\'.$userClass,
'Entity\\'
);

// 1) Update the User class with a token we will use later
$classDetails = new ClassDetails($userClass);
$userManipulator = new ClassSourceManipulator(
file_get_contents($classDetails->getPath())
);
$userManipulator->setIo($io);
$userManipulator->addEntityField(
'resetPasswordToken',
[
'type' => 'string',
'nullable' => true,
]
);
$userManipulator->addEntityField(
'passwordRequestedAt',
[
'type' => 'datetime',
'nullable' => true,
]
);
$this->fileManager->dumpFile($classDetails->getPath(), $userManipulator->getSourceCode());

// 2) Generate the "username" form class
$usernameField = $input->getArgument('username-field');
$usernameFormClassDetails = $this->generateUsernameFormClass(
$userClassNameDetails,
$generator,
$usernameField
);

// 3) Generate the "new password" form class
$resettingFormClassDetails = $this->generateResettingFormClass(
$userClassNameDetails,
$generator
);

// 4) Generate the controller
$controllerClassNameDetails = $generator->createClassNameDetails(
'ForgottenPasswordController',
'Controller\\'
);

// We assume User entity has some password setter, and e-mail getter
// TODO: display a warning if not?
$passwordSetter = 'set'.ucfirst($input->getArgument('password-field'));
$emailGetter = 'getEmail';

$generator->generateController(
$controllerClassNameDetails->getFullName(),
'forgottenPassword/ForgottenPasswordController.tpl.php',
[
'username_form_class_name' => $usernameFormClassDetails->getShortName(),
'username_form_full_class_name' => $usernameFormClassDetails->getFullName(),
'resetting_form_class_name' => $resettingFormClassDetails->getShortName(),
'resetting_form_full_class_name' => $resettingFormClassDetails->getFullName(),
'user_class_name' => $userClassNameDetails->getShortName(),
'user_full_class_name' => $userClassNameDetails->getFullName(),
'username_field' => $input->getArgument('username-field'),
'email_getter' => $emailGetter,
'password_setter' => $passwordSetter,
'login_route' => 'app_login', // TODO: get this from security config
]
);

// 5) Generate the "request" template
$generator->generateFile(
'templates/forgottenPassword/request.html.twig',
'forgottenPassword/twig_request.tpl.php',
[
'username_field' => $usernameField,
]
);

// 6) Generate the reset e-mail template
$generator->generateFile(
'templates/forgottenPassword/email.txt.twig',
'forgottenPassword/twig_email.tpl.php',
[]
);

// 7) Generate the "checkEmail" template
$generator->generateFile(
'templates/forgottenPassword/checkEmail.html.twig',
'forgottenPassword/twig_check_email.tpl.php',
[]
);

// 8) Generate the "reset" template
$generator->generateFile(
'templates/forgottenPassword/reset.html.twig',
'forgottenPassword/twig_reset.tpl.php',
[]
);

$generator->writeChanges();
$this->writeSuccessMessage($io);

$io->text('Done! You should now generate a migration (make:mig) and run it to update your database.');
$io->text('Next: You can add a link to "app_forgotten_password" path anywhere you like, typically below your login form!');
}

private function generateUsernameFormClass(ClassNameDetails $userClassDetails, Generator $generator, string $usernameField)
{
$formClassDetails = $generator->createClassNameDetails(
'UsernameFormType',
'Form\\'
);

$formFields = [
$usernameField => [
'type' => TextType::class,
'options_code' => <<<EOF
'constraints' => [
new NotBlank([
'message' => 'Please enter your $usernameField',
]),
],
EOF
],
];

$this->formTypeRenderer->render(
$formClassDetails,
$formFields,
null,
[
'Symfony\Component\Validator\Constraints\NotBlank',
]
);

return $formClassDetails;
}

private function generateResettingFormClass(ClassNameDetails $userClassDetails, Generator $generator)
{
$formClassDetails = $generator->createClassNameDetails(
'ResettingFormType',
'Form\\'
);

$formFields = [
'plainPassword' => [
'type' => PasswordType::class,
'options_code' => <<<EOF
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
],
'label' => 'New password',
EOF
],
];

$this->formTypeRenderer->render(
$formClassDetails,
$formFields,
$userClassDetails,
[
'Symfony\Component\Validator\Constraints\NotBlank',
'Symfony\Component\Validator\Constraints\Length',
]
);

return $formClassDetails;
}
}
7 changes: 7 additions & 0 deletions src/Resources/config/makers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@
<tag name="maker.command" />
</service>

<service id="maker.maker.make_forgotten_password" class="Symfony\Bundle\MakerBundle\Maker\MakeForgottenPassword">
<argument type="service" id="maker.file_manager" />
<argument type="service" id="maker.renderer.form_type_renderer" />
<argument type="service" id="router" />
<tag name="maker.command" />
</service>

<service id="maker.maker.make_form" class="Symfony\Bundle\MakerBundle\Maker\MakeForm">
<argument type="service" id="maker.doctrine_helper" />
<argument type="service" id="maker.renderer.form_type_renderer" />
Expand Down
Loading

0 comments on commit 59805d7

Please sign in to comment.