-
-
Notifications
You must be signed in to change notification settings - Fork 406
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added a make:forgotten-password maker
- Loading branch information
1 parent
08fabae
commit 59805d7
Showing
7 changed files
with
480 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.