File: /var/www/payments-gateway/vendor/symfony/maker-bundle/src/Maker/MakeResetPassword.php
<?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 Doctrine\ORM\EntityManagerInterface;
use PhpParser\Builder\Param;
use Symfony\Bridge\Twig\AppVariable;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
use Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator;
use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder;
use Symfony\Bundle\MakerBundle\Doctrine\RelationManyToOne;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Maker\Common\CanGenerateTestsTrait;
use Symfony\Bundle\MakerBundle\Maker\Common\UidTrait;
use Symfony\Bundle\MakerBundle\Security\InteractiveSecurityHelper;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Bundle\MakerBundle\Util\CliOutputHelper;
use Symfony\Bundle\MakerBundle\Util\UseStatementGenerator;
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
use Symfony\Bundle\MakerBundle\Validator;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Route as RouteObject;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotCompromisedPassword;
use Symfony\Component\Validator\Constraints\PasswordStrength;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Yaml\Yaml;
use Symfony\Contracts\Translation\TranslatorInterface;
use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait;
use SymfonyCasts\Bundle\ResetPassword\Persistence\Repository\ResetPasswordRequestRepositoryTrait;
use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelper;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
use SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle;
/**
* @author Romaric Drigon <romaric.drigon@gmail.com>
* @author Jesse Rushlow <jr@rushlow.dev>
* @author Ryan Weaver <ryan@symfonycasts.com>
* @author Antoine Michelet <jean.marcel.michelet@gmail.com>
*
* @internal
*
* @final
*/
class MakeResetPassword extends AbstractMaker
{
use CanGenerateTestsTrait;
use UidTrait;
private string $fromEmailAddress;
private string $fromEmailName;
private string $controllerResetSuccessRedirect;
private ?RouteObject $controllerResetSuccessRoute = null;
private string $userClass;
private string $emailPropertyName;
private string $emailGetterMethodName;
private string $passwordSetterMethodName;
public function __construct(
private FileManager $fileManager,
private DoctrineHelper $doctrineHelper,
private EntityClassGenerator $entityClassGenerator,
private ?RouterInterface $router = null,
) {
}
public static function getCommandName(): string
{
return 'make:reset-password';
}
public static function getCommandDescription(): string
{
return 'Create controller, entity, and repositories for use with symfonycasts/reset-password-bundle';
}
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
{
$command
->setHelp($this->getHelpFileContents('MakeResetPassword.txt'))
;
$this->addWithUuidOption($command);
$this->configureCommandWithTestsOption($command);
}
public function configureDependencies(DependencyBuilder $dependencies): void
{
$dependencies->addClassDependency(SymfonyCastsResetPasswordBundle::class, 'symfonycasts/reset-password-bundle');
$dependencies->addClassDependency(MailerInterface::class, 'symfony/mailer');
$dependencies->addClassDependency(Form::class, 'symfony/form');
$dependencies->addClassDependency(Validation::class, 'symfony/validator');
$dependencies->addClassDependency(SecurityBundle::class, 'security-bundle');
$dependencies->addClassDependency(AppVariable::class, 'twig');
ORMDependencyBuilder::buildDependencies($dependencies);
// reset-password-bundle 1.6 includes the ability to generate a fake token.
// we need to check that version 1.6 is installed
if (class_exists(ResetPasswordHelper::class) && !method_exists(ResetPasswordHelper::class, 'generateFakeResetToken')) {
throw new RuntimeCommandException('Please run "composer upgrade symfonycasts/reset-password-bundle". Version 1.6 or greater of this bundle is required.');
}
}
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
{
$io->title('Let\'s make a password reset feature!');
$this->checkIsUsingUid($input);
$interactiveSecurityHelper = new InteractiveSecurityHelper();
if (!$this->fileManager->fileExists($path = 'config/packages/security.yaml')) {
throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. PHP & XML configuration formats are currently not supported.');
}
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
$securityData = $manipulator->getData();
$providersData = $securityData['security']['providers'] ?? [];
$this->userClass = $interactiveSecurityHelper->guessUserClass(
$io,
$providersData,
'What is the User entity that should be used with the "forgotten password" feature? (e.g. <fg=yellow>App\\Entity\\User</>)'
);
$this->emailPropertyName = $interactiveSecurityHelper->guessEmailField($io, $this->userClass);
$this->emailGetterMethodName = $interactiveSecurityHelper->guessEmailGetter($io, $this->userClass, $this->emailPropertyName);
$this->passwordSetterMethodName = $interactiveSecurityHelper->guessPasswordSetter($io, $this->userClass);
$io->text(\sprintf('Implementing reset password for <info>%s</info>', $this->userClass));
$io->section('- ResetPasswordController -');
$io->text('A named route is used for redirecting after a successful reset. Even a route that does not exist yet can be used here.');
$this->controllerResetSuccessRedirect = $io->ask(
'What route should users be redirected to after their password has been successfully reset?',
'app_home',
Validator::notBlank(...)
);
if ($this->router instanceof RouterInterface) {
$this->controllerResetSuccessRoute = $this->router->getRouteCollection()->get($this->controllerResetSuccessRedirect);
}
$io->section('- Email -');
$emailText[] = 'These are used to generate the email code. Don\'t worry, you can change them in the code later!';
$io->text($emailText);
$this->fromEmailAddress = $io->ask(
'What email address will be used to send reset confirmations? e.g. mailer@your-domain.com',
null,
Validator::validateEmailAddress(...)
);
$this->fromEmailName = $io->ask(
'What "name" should be associated with that email address? e.g. "Acme Mail Bot"',
null,
Validator::notBlank(...)
);
$this->interactSetGenerateTests($input, $io);
}
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
{
$userClassNameDetails = $generator->createClassNameDetails(
'\\'.$this->userClass,
'Entity\\'
);
$controllerClassNameDetails = $generator->createClassNameDetails(
'ResetPasswordController',
'Controller\\'
);
$requestClassNameDetails = $generator->createClassNameDetails(
'ResetPasswordRequest',
'Entity\\'
);
$repositoryClassNameDetails = $generator->createClassNameDetails(
'ResetPasswordRequestRepository',
'Repository\\'
);
$requestFormTypeClassNameDetails = $generator->createClassNameDetails(
'ResetPasswordRequestForm',
'Form\\'
);
$changePasswordFormTypeClassNameDetails = $generator->createClassNameDetails(
'ChangePasswordForm',
'Form\\'
);
$useStatements = new UseStatementGenerator([
AbstractController::class,
$userClassNameDetails->getFullName(),
$changePasswordFormTypeClassNameDetails->getFullName(),
$requestFormTypeClassNameDetails->getFullName(),
TemplatedEmail::class,
RedirectResponse::class,
Request::class,
Response::class,
MailerInterface::class,
Address::class,
Route::class,
ResetPasswordControllerTrait::class,
ResetPasswordExceptionInterface::class,
ResetPasswordHelperInterface::class,
UserPasswordHasherInterface::class,
EntityManagerInterface::class,
]);
// Namespace for ResetPasswordExceptionInterface was imported above
$problemValidateMessageOrConstant = \defined('SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE')
? 'ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE'
: "'There was a problem validating your password reset request'";
$problemHandleMessageOrConstant = \defined('SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE')
? 'ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE'
: "'There was a problem handling your password reset request'";
if ($isTranslatorAvailable = class_exists(Translator::class)) {
$useStatements->addUseStatement(TranslatorInterface::class);
}
$generator->generateController(
$controllerClassNameDetails->getFullName(),
'resetPassword/ResetPasswordController.tpl.php',
[
'use_statements' => $useStatements,
'user_class_name' => $userClassNameDetails->getShortName(),
'request_form_type_class_name' => $requestFormTypeClassNameDetails->getShortName(),
'reset_form_type_class_name' => $changePasswordFormTypeClassNameDetails->getShortName(),
'password_setter' => $this->passwordSetterMethodName,
'success_redirect_route' => $this->controllerResetSuccessRedirect,
'from_email' => $this->fromEmailAddress,
'from_email_name' => $this->fromEmailName,
'email_getter' => $this->emailGetterMethodName,
'email_field' => $this->emailPropertyName,
'problem_validate_message_or_constant' => $problemValidateMessageOrConstant,
'problem_handle_message_or_constant' => $problemHandleMessageOrConstant,
'translator_available' => $isTranslatorAvailable,
]
);
$this->generateRequestEntity($generator, $requestClassNameDetails, $repositoryClassNameDetails, $userClassNameDetails);
$this->setBundleConfig($io, $generator, $repositoryClassNameDetails->getFullName());
$useStatements = new UseStatementGenerator([
AbstractType::class,
EmailType::class,
FormBuilderInterface::class,
OptionsResolver::class,
NotBlank::class,
]);
$generator->generateClass(
$requestFormTypeClassNameDetails->getFullName(),
'resetPassword/ResetPasswordRequestForm.tpl.php',
[
'use_statements' => $useStatements,
'email_field' => $this->emailPropertyName,
]
);
$useStatements = new UseStatementGenerator([
AbstractType::class,
PasswordType::class,
RepeatedType::class,
FormBuilderInterface::class,
OptionsResolver::class,
Length::class,
NotBlank::class,
NotCompromisedPassword::class,
PasswordStrength::class,
]);
$generator->generateClass(
$changePasswordFormTypeClassNameDetails->getFullName(),
'resetPassword/ChangePasswordForm.tpl.php',
['use_statements' => $useStatements]
);
$generator->generateTemplate(
'reset_password/check_email.html.twig',
'resetPassword/twig_check_email.tpl.php'
);
$generator->generateTemplate(
'reset_password/email.html.twig',
'resetPassword/twig_email.tpl.php'
);
$generator->generateTemplate(
'reset_password/request.html.twig',
'resetPassword/twig_request.tpl.php',
[
'email_field' => $this->emailPropertyName,
]
);
$generator->generateTemplate(
'reset_password/reset.html.twig',
'resetPassword/twig_reset.tpl.php'
);
// Generate PHPUnit tests
if ($this->shouldGenerateTests()) {
$testClassDetails = $generator->createClassNameDetails(
'ResetPasswordControllerTest',
'Test\\',
);
$userRepositoryDetails = $generator->createClassNameDetails(
\sprintf('%sRepository', $userClassNameDetails->getShortName()),
'Repository\\'
);
$useStatements = new UseStatementGenerator([
$userClassNameDetails->getFullName(),
$userRepositoryDetails->getFullName(),
EntityManagerInterface::class,
KernelBrowser::class,
WebTestCase::class,
UserPasswordHasherInterface::class,
]);
$generator->generateFile(
targetPath: \sprintf('tests/%s.php', $testClassDetails->getShortName()),
templateName: 'resetPassword/Test.ResetPasswordController.tpl.php',
variables: [
'use_statements' => $useStatements,
'user_short_name' => $userClassNameDetails->getShortName(),
'user_repo_short_name' => $userRepositoryDetails->getShortName(),
'success_route_path' => null !== $this->controllerResetSuccessRoute ? $this->controllerResetSuccessRoute->getPath() : '/',
'from_email' => $this->fromEmailAddress,
],
);
if (!class_exists(WebTestCase::class)) {
$io->caution('You\'ll need to install the `symfony/test-pack` to execute the tests for your new controller.');
}
}
$generator->writeChanges();
$this->writeSuccessMessage($io);
$this->successMessage($io, $requestClassNameDetails->getFullName());
}
private function setBundleConfig(ConsoleStyle $io, Generator $generator, string $repositoryClassFullName): void
{
$configFileExists = $this->fileManager->fileExists($path = 'config/packages/reset_password.yaml');
/*
* reset_password.yaml does not exist, we assume flex was present when
* the bundle was installed & a customized configuration is in use.
* Remind the developer to set the repository class accordingly.
*/
if (!$configFileExists) {
$io->text(\sprintf('We can\'t find %s. That\'s ok, you probably have a customized configuration.', $path));
$io->text('Just remember to set the <fg=yellow>request_password_repository</> in your configuration.');
$io->newLine();
return;
}
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));
$data = $manipulator->getData();
$symfonyCastsKey = 'symfonycasts_reset_password';
/*
* reset_password.yaml exists, and was probably created by flex;
* Let's replace it with a "clean" file.
*/
if (1 >= (is_countable($data[$symfonyCastsKey]) ? \count($data[$symfonyCastsKey]) : 0)) {
$yaml = [
$symfonyCastsKey => [
'request_password_repository' => $repositoryClassFullName,
],
];
$generator->dumpFile($path, Yaml::dump($yaml));
return;
}
/*
* reset_password.yaml exists and appears to have been customized
* before running make:reset-password. Let's just change the repository
* value and preserve everything else.
*/
$data[$symfonyCastsKey]['request_password_repository'] = $repositoryClassFullName;
$manipulator->setData($data);
$generator->dumpFile($path, $manipulator->getContents());
}
private function successMessage(ConsoleStyle $io, string $requestClassName): void
{
$closing[] = 'Next:';
$closing[] = \sprintf(' 1) Run <fg=yellow>"%s make:migration"</> to generate a migration for the new <fg=yellow>"%s"</> entity.', CliOutputHelper::getCommandPrefix(), $requestClassName);
$closing[] = ' 2) Review forms in <fg=yellow>"src/Form"</> to customize validation and labels.';
$closing[] = ' 3) Review and customize the templates in <fg=yellow>`templates/reset_password`</>.';
$closing[] = ' 4) Make sure your <fg=yellow>MAILER_DSN</> env var has the correct settings.';
$closing[] = ' 5) Create a "forgot your password link" to the <fg=yellow>app_forgot_password_request</> route on your login form.';
$io->text($closing);
$io->newLine();
$io->text('Then open your browser, go to "/reset-password" and enjoy!');
$io->newLine();
}
private function generateRequestEntity(Generator $generator, ClassNameDetails $requestClassNameDetails, ClassNameDetails $repositoryClassNameDetails, ClassNameDetails $userClassDetails): void
{
// Generate ResetPasswordRequest Entity
$requestEntityPath = $this->entityClassGenerator->generateEntityClass(
entityClassDetails: $requestClassNameDetails,
apiResource: false,
generateRepositoryClass: false,
useUuidIdentifier: $this->getIdType()
);
$generator->writeChanges();
$manipulator = new ClassSourceManipulator(
sourceCode: $this->fileManager->getFileContents($requestEntityPath),
overwrite: false,
useAttributesForDoctrineMapping: $this->doctrineHelper->doesClassUsesAttributes($requestClassNameDetails->getFullName()),
);
$manipulator->addInterface(ResetPasswordRequestInterface::class);
$manipulator->addTrait(ResetPasswordRequestTrait::class);
$manipulator->addUseStatementIfNecessary($userClassDetails->getFullName());
$manipulator->addConstructor([
(new Param('user'))->setType($userClassDetails->getShortName())->getNode(),
(new Param('expiresAt'))->setType('\DateTimeInterface')->getNode(),
(new Param('selector'))->setType('string')->getNode(),
(new Param('hashedToken'))->setType('string')->getNode(),
], <<<'CODE'
<?php
$this->user = $user;
$this->initialize($expiresAt, $selector, $hashedToken);
CODE
);
$manipulator->addManyToOneRelation(new RelationManyToOne(
propertyName: 'user',
targetClassName: $this->userClass,
mapInverseRelation: false,
avoidSetter: true,
isCustomReturnTypeNullable: false,
customReturnType: $userClassDetails->getShortName(),
isOwning: true,
));
$this->fileManager->dumpFile($requestEntityPath, $manipulator->getSourceCode());
$this->entityClassGenerator->generateRepositoryClass(
$repositoryClassNameDetails->getFullName(),
$requestClassNameDetails->getFullName(),
false,
false
);
$generator->writeChanges();
// Generate ResetPasswordRequestRepository
$pathRequestRepository = $this->fileManager->getRelativePathForFutureClass(
$repositoryClassNameDetails->getFullName()
);
$manipulator = new ClassSourceManipulator(
sourceCode: $this->fileManager->getFileContents($pathRequestRepository)
);
$manipulator->addInterface(ResetPasswordRequestRepositoryInterface::class);
$manipulator->addTrait(ResetPasswordRequestRepositoryTrait::class);
$methodBuilder = $manipulator->createMethodBuilder(
methodName: 'createResetPasswordRequest',
returnType: ResetPasswordRequestInterface::class,
isReturnTypeNullable: false,
commentLines: [\sprintf('@param %s $user', $userClassDetails->getShortName())]
);
$manipulator->addUseStatementIfNecessary($userClassDetails->getFullName());
$manipulator->addMethodBuilder($methodBuilder, [
(new Param('user'))->setType('object')->getNode(),
(new Param('expiresAt'))->setType('\DateTimeInterface')->getNode(),
(new Param('selector'))->setType('string')->getNode(),
(new Param('hashedToken'))->setType('string')->getNode(),
], <<<'CODE'
<?php
return new ResetPasswordRequest($user, $expiresAt, $selector, $hashedToken);
CODE
);
$this->fileManager->dumpFile($pathRequestRepository, $manipulator->getSourceCode());
}
}