Sindbad~EG File Manager
<?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\Common\Annotations\Annotation;
use Doctrine\ORM\EntityManagerInterface;
use PhpParser\Builder\Param;
use Symfony\Bridge\Twig\AppVariable;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
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\Security\InteractiveSecurityHelper;
use Symfony\Bundle\MakerBundle\Util\ClassNameDetails;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Bundle\MakerBundle\Util\TemplateComponentGenerator;
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\Form;
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\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Translation\Translator;
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
{
private $fileManager;
private $doctrineHelper;
private $entityClassGenerator;
private $fromEmailAddress;
private $fromEmailName;
private $controllerResetSuccessRedirect;
private $userClass;
private $emailPropertyName;
private $emailGetterMethodName;
private $passwordSetterMethodName;
public function __construct(FileManager $fileManager, DoctrineHelper $doctrineHelper, EntityClassGenerator $entityClassGenerator)
{
$this->fileManager = $fileManager;
$this->doctrineHelper = $doctrineHelper;
$this->entityClassGenerator = $entityClassGenerator;
}
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(file_get_contents(__DIR__.'/../Resources/help/MakeResetPassword.txt'))
;
}
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);
$dependencies->addClassDependency(Annotation::class, 'annotations');
// 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!');
$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 reset password form.');
}
$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::class, 'notBlank']
);
$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::class, 'validateEmailAddress']
);
$this->fromEmailName = $io->ask(
'What "name" should be associated with that email address? e.g. "Acme Mail Bot"',
null,
[Validator::class, 'notBlank']
);
}
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(
'ResetPasswordRequestFormType',
'Form\\'
);
$changePasswordFormTypeClassNameDetails = $generator->createClassNameDetails(
'ChangePasswordFormType',
'Form\\'
);
/*
* @legacy Conditional can be removed when MakerBundle no longer
* supports Symfony < 5.2
*/
$passwordHasher = UserPasswordEncoderInterface::class;
if (interface_exists(UserPasswordHasherInterface::class)) {
$passwordHasher = UserPasswordHasherInterface::class;
}
$useStatements = [
Generator::getControllerBaseClass()->getFullName(), // @legacy see getControllerBaseClass comment
$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,
$passwordHasher,
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[] = TranslatorInterface::class;
}
$generator->generateController(
$controllerClassNameDetails->getFullName(),
'resetPassword/ResetPasswordController.tpl.php',
[
'use_statements' => TemplateComponentGenerator::generateUseStatements($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,
'password_hasher_class_details' => ($passwordClassDetails = $generator->createClassNameDetails($passwordHasher, '\\')),
'password_hasher_variable_name' => str_replace('Interface', '', sprintf('$%s', lcfirst($passwordClassDetails->getShortName()))), // @legacy see passwordHasher conditional above
'use_password_hasher' => UserPasswordHasherInterface::class === $passwordHasher, // @legacy see passwordHasher conditional above
'problem_validate_message_or_constant' => $problemValidateMessageOrConstant,
'problem_handle_message_or_constant' => $problemHandleMessageOrConstant,
'translator_available' => $isTranslatorAvailable,
]
);
$this->generateRequestEntity($generator, $requestClassNameDetails, $repositoryClassNameDetails);
$this->setBundleConfig($io, $generator, $repositoryClassNameDetails->getFullName());
$generator->generateClass(
$requestFormTypeClassNameDetails->getFullName(),
'resetPassword/ResetPasswordRequestFormType.tpl.php',
[
'email_field' => $this->emailPropertyName,
]
);
$generator->generateClass(
$changePasswordFormTypeClassNameDetails->getFullName(),
'resetPassword/ChangePasswordFormType.tpl.php'
);
$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'
);
$generator->writeChanges();
$this->writeSuccessMessage($io);
$this->successMessage($input, $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 >= \count($data[$symfonyCastsKey])) {
$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(InputInterface $input, ConsoleStyle $io, string $requestClassName): void
{
$closing[] = 'Next:';
$closing[] = sprintf(' 1) Run <fg=yellow>"php bin/console make:migration"</> to generate a migration for the new <fg=yellow>"%s"</> entity.', $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): void
{
$requestEntityPath = $this->entityClassGenerator->generateEntityClass($requestClassNameDetails, false, false, false);
$generator->writeChanges();
$useAttributesForDoctrineMapping = $this->doctrineHelper->isDoctrineSupportingAttributes() && $this->doctrineHelper->doesClassUsesAttributes($requestClassNameDetails->getFullName());
$manipulator = new ClassSourceManipulator(
$this->fileManager->getFileContents($requestEntityPath),
false,
!$useAttributesForDoctrineMapping,
true,
$useAttributesForDoctrineMapping
);
$manipulator->addInterface(ResetPasswordRequestInterface::class);
$manipulator->addTrait(ResetPasswordRequestTrait::class);
$manipulator->addConstructor([
(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
$this->user = $user;
$this->initialize($expiresAt, $selector, $hashedToken);
CODE
);
$manipulator->addManyToOneRelation((new RelationManyToOne())
->setPropertyName('user')
->setTargetClassName($this->userClass)
->setMapInverseRelation(false)
->setCustomReturnType('object', false)
->avoidSetter()
);
$this->fileManager->dumpFile($requestEntityPath, $manipulator->getSourceCode());
$this->entityClassGenerator->generateRepositoryClass(
$repositoryClassNameDetails->getFullName(),
$requestClassNameDetails->getFullName(),
false,
false
);
$generator->writeChanges();
$pathRequestRepository = $this->fileManager->getRelativePathForFutureClass(
$repositoryClassNameDetails->getFullName()
);
$manipulator = new ClassSourceManipulator(
$this->fileManager->getFileContents($pathRequestRepository)
);
$manipulator->addInterface(ResetPasswordRequestRepositoryInterface::class);
$manipulator->addTrait(ResetPasswordRequestRepositoryTrait::class);
$methodBuilder = $manipulator->createMethodBuilder('createResetPasswordRequest', ResetPasswordRequestInterface::class, false);
$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());
}
}
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists