diff --git a/README.md b/README.md index 0cdbd9b..1ec1a57 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,27 @@ A bundle that provides quick password protection on Contents. # How it works Allows you to add 1 on N password on a Content in the Admin UI. +Once a protection is set, the Content becomes Protected. +In this situation you can have 3 new variables in the view full + - canReadProtectedContent (always) + - requestProtectedContentPasswordForm (if content is protected by password) + - requestProtectedContentEmailForm (if content is protected with email verification) -Once a Password is set, the Content becomes Protected. In this situation you will have 2 new variables in the view full. Allowing you do: ```twig

{{ ez_content_name(content) }}

{% if not canReadProtectedContent %} -

This content has been protected by a password

-
- {{ form(requestProtectedContentPasswordForm) }} -
+ {% if requestProtectedContentPasswordForm is defined %} +

This content has been protected by a password

+
+ {{ form(requestProtectedContentPasswordForm) }} +
+ {% elseif requestProtectedContentEmailForm is defined %} +

This content has been protected by an email verification

+
+ {{ form(requestProtectedContentEmailForm) }} +
+ {% endif %} {% else %} {% for field in content.fieldsByLanguage(language|default(null)) %}

{{ field.fieldDefIdentifier }}

diff --git a/bundle/Command/CleanTokenCommand.php b/bundle/Command/CleanTokenCommand.php new file mode 100644 index 0000000..32e0ffb --- /dev/null +++ b/bundle/Command/CleanTokenCommand.php @@ -0,0 +1,65 @@ +entityManager = $entityManager; + + } + + protected function configure(): void + { + $this + ->setName('novaezprotectedcontent:cleantoken') + ->setDescription('Remove expired token in the DB'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + + /** @var ProtectedTokenStorageRepository $protectedTokenStorageRepository */ + $protectedTokenStorageRepository = $this->entityManager->getRepository(ProtectedTokenStorage::class); + + $entities = $protectedTokenStorageRepository->findExpired(); + + foreach ($entities as $entity) { + $this->entityManager->remove($entity); + } + + $this->entityManager->flush(); + + $io->success(sprintf('%d entities deleted', count($entities))); + $io->success('Done.'); + } +} diff --git a/bundle/Controller/Admin/ProtectedAccessController.php b/bundle/Controller/Admin/ProtectedAccessController.php index 3a8c652..e1d605d 100644 --- a/bundle/Controller/Admin/ProtectedAccessController.php +++ b/bundle/Controller/Admin/ProtectedAccessController.php @@ -14,32 +14,43 @@ use DateTime; use Doctrine\ORM\EntityManagerInterface; -use eZ\Publish\API\Repository\Values\Content\Location; -use EzSystems\PlatformHttpCacheBundle\PurgeClient\PurgeClientInterface; +use Ibexa\Contracts\Core\Repository\Values\Content\Content; +use Ibexa\Contracts\Core\Repository\Values\Content\Location; +use Ibexa\Contracts\Core\Repository\Values\Content\Query; +use Ibexa\Contracts\HttpCache\Handler\ContentTagInterface; +use Ibexa\Core\Repository\SiteAccessAware\Repository; use Novactive\Bundle\eZProtectedContentBundle\Entity\ProtectedAccess; use Novactive\Bundle\eZProtectedContentBundle\Form\ProtectedAccessType; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; -use Symfony\Component\Form\FormFactory; +use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\RouterInterface; class ProtectedAccessController { + public function __construct( + protected readonly Repository $repository, + protected readonly \Ibexa\Contracts\Core\Search\Handler $searchHandler, + protected readonly \Ibexa\Contracts\Core\Persistence\Handler $persistenceHandler, + ) { } + /** * @Route("/handle/{locationId}/{access}", name="novaezprotectedcontent_bundle_admin_handle_form", * defaults={"accessId": null}) */ + //#[Route(path: '/handle/{locationId}/{access}', name: 'novaezprotectedcontent_bundle_admin_handle_form')] public function handle( - Location $location, + int $locationId, Request $request, - FormFactory $formFactory, + FormFactoryInterface $formFactory, EntityManagerInterface $entityManager, RouterInterface $router, - PurgeClientInterface $httpCachePurgeClient, - ?ProtectedAccess $access = null + ContentTagInterface $responseTagger, + ?ProtectedAccess $access = null, ): RedirectResponse { if ($request->isMethod('post')) { + $location = $this->repository->getLocationService()->loadLocation($locationId); $now = new DateTime(); if (null === $access) { $access = new ProtectedAccess(); @@ -52,38 +63,95 @@ public function handle( $access->setUpdated($now); $entityManager->persist($access); $entityManager->flush(); - $httpCachePurgeClient->purge( - [ - 'location-'.$location->id, - 'location-'.$location->parentLocationId, - ] - ); + $responseTagger->addLocationTags([$location->id]); + $responseTagger->addParentLocationTags([$location->parentLocationId]); + + $content = $location->getContent(); + $this->reindexContent($content); + if ($access->isProtectChildren()) { + $this->reindexChildren($content); + } } } - return new RedirectResponse($router->generate($location).'#ez-tab-location-view-protect-content#tab'); + return new RedirectResponse( + $router->generate('ibexa.content.view', ['contentId' => $location->contentId, + 'locationId' => $location->id, + ]). + '#ibexa-tab-location-view-protect-content#tab' + ); } - /** - * @Route("/remove/{locationId}/{access}", name="novaezprotectedcontent_bundle_admin_remove_protection") - */ + #[Route(path: '/remove/{locationId}/{access}', name: 'novaezprotectedcontent_bundle_admin_remove_protection')] public function remove( Location $location, EntityManagerInterface $entityManager, RouterInterface $router, - ProtectedAccess $access, - PurgeClientInterface $httpCachePurgeClient + int $access, + ContentTagInterface $responseTagger ): RedirectResponse { + $access = $entityManager->find(ProtectedAccess::class, $access); $entityManager->remove($access); $entityManager->flush(); + $responseTagger->addLocationTags([$location->id]); + $responseTagger->addParentLocationTags([$location->parentLocationId]); - $httpCachePurgeClient->purge( - [ - 'location-'.$location->id, - 'location-'.$location->parentLocationId, - ] + $content = $location->getContent(); + $this->reindexContent($content); + if ($access->isProtectChildren()) { + $this->reindexChildren($content); + } + + return new RedirectResponse( + $router->generate('ibexa.content.view', ['contentId' => $location->contentId, + 'locationId' => $location->id, + ]). + '#ibexa-tab-location-view-protect-content#tab' ); + } - return new RedirectResponse($router->generate($location).'#ez-tab-location-view-protect-content#tab'); + /** + * @param Content $content + * @return void + */ + protected function reindexContent(Content $content) + { + $contentId = $content->id; + $contentVersionNo = $content->getVersionInfo()->versionNo; + + $this->searchHandler->indexContent( + $this->persistenceHandler->contentHandler()->load($contentId, $contentVersionNo) + ); + + $locations = $this->persistenceHandler->locationHandler()->loadLocationsByContent($contentId); + foreach ($locations as $location) { + $this->searchHandler->indexLocation($location); + } + } + + protected function reindexChildren(Content $content, int $limit = 100) + { + $locations = $this->repository->getLocationService()->loadLocations($content->contentInfo); + $pathStringArray = []; + foreach ($locations as $location) { + /** @var Location $location */ + $pathStringArray[] = $location->pathString; + } + + if ($pathStringArray) { + $query = new Query(); + $query->limit = $limit; + $query->filter = new Query\Criterion\LogicalAnd([ + new Query\Criterion\Subtree($pathStringArray) + ]); + $query->sortClauses = [ + new Query\SortClause\ContentId(), + // new Query\SortClause\Visibility(), // domage.. + ]; + $searchResult = $this->repository->getSearchService()->findContent($query); + foreach ($searchResult->searchHits as $hit) { + $this->reindexContent($hit->valueObject); + } + } } } diff --git a/bundle/Entity/ProtectedAccess.php b/bundle/Entity/ProtectedAccess.php index ba2a022..575565d 100644 --- a/bundle/Entity/ProtectedAccess.php +++ b/bundle/Entity/ProtectedAccess.php @@ -38,8 +38,7 @@ class ProtectedAccess implements ContentInterface /** * @var string * - * @ORM\Column(type="string", length=255, nullable=false) - * @Assert\NotBlank() + * @ORM\Column(type="string", length=255, nullable=true) * @Assert\Length(max=255) */ protected $password; @@ -51,6 +50,13 @@ class ProtectedAccess implements ContentInterface */ protected $enabled; + /** + * @var bool + * + * @ORM\Column(type="boolean", nullable=false) + */ + protected $asEmail = false; + /** * @var bool * @@ -58,6 +64,13 @@ class ProtectedAccess implements ContentInterface */ protected $protectChildren; + /** + * @var string + * + * @ORM\Column(type="string", nullable=true) + */ + protected $emailMessage; + public function __construct() { $this->enabled = true; @@ -76,12 +89,24 @@ public function setId(int $id): self return $this; } + public function getAsEmail(): bool + { + return $this->asEmail ?? false; + } + + public function setAsEmail(bool $asEmail): self + { + $this->asEmail = $asEmail; + + return $this; + } + public function getPassword(): string { return $this->password ?? ''; } - public function setPassword(string $password): self + public function setPassword(?string $password = ''): self { $this->password = $password; @@ -109,4 +134,14 @@ public function setProtectChildren(bool $protectChildren): void { $this->protectChildren = $protectChildren; } + + public function getEmailMessage(): ?string + { + return $this->emailMessage; + } + + public function setEmailMessage(string $emailMessage): void + { + $this->emailMessage = $emailMessage; + } } diff --git a/bundle/Entity/ProtectedTokenStorage.php b/bundle/Entity/ProtectedTokenStorage.php new file mode 100644 index 0000000..0404c51 --- /dev/null +++ b/bundle/Entity/ProtectedTokenStorage.php @@ -0,0 +1,111 @@ +id; + } + + public function setId(int $id): void + { + $this->id = $id; + } + public function getCreated(): DateTime + { + return $this->created; + } + + public function setCreated(DateTime $created): void + { + $this->created = $created; + } + + public function getToken(): string + { + return $this->token; + } + + public function setToken(string $token): void + { + $this->token = $token; + } + + public function getMail(): string + { + return $this->mail; + } + + public function setMail(string $mail): void + { + $this->mail = $mail; + } + + public function getContentId(): int + { + return $this->content_id; + } + + public function setContentId(int $content_id): void + { + $this->content_id = $content_id; + } +} diff --git a/bundle/Form/ProtectedAccessType.php b/bundle/Form/ProtectedAccessType.php index 6b5d384..5fb8506 100644 --- a/bundle/Form/ProtectedAccessType.php +++ b/bundle/Form/ProtectedAccessType.php @@ -13,10 +13,12 @@ namespace Novactive\Bundle\eZProtectedContentBundle\Form; use Novactive\Bundle\eZProtectedContentBundle\Entity\ProtectedAccess; + use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -31,7 +33,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ['label' => 'tab.table.th.children_protection', 'required' => false] ) ->add('enabled', CheckboxType::class, ['label' => 'tab.table.th.enabled', 'required' => false]) - ->add('password', TextType::class, ['required' => true, 'label' => 'tab.table.th.password']); + ->add('password', TextType::class, ['required' => false, 'label' => 'tab.table.th.password']) + ->add('asEmail', CheckboxType::class, ['label' => 'tab.table.th.as_email', 'required' => false]) + ->add('emailMessage', TextareaType::class, [ + 'label' => 'tab.table.th.message_email', + 'help' => 'mail.help_message', + 'required' => false + ]) + ; } public function configureOptions(OptionsResolver $resolver): void diff --git a/bundle/Form/RequestEmailProtectedAccessType.php b/bundle/Form/RequestEmailProtectedAccessType.php new file mode 100644 index 0000000..18cfd1e --- /dev/null +++ b/bundle/Form/RequestEmailProtectedAccessType.php @@ -0,0 +1,47 @@ +add( + 'email', + EmailType::class, + [ + 'required' => true, + 'label' => 'tab.table.th.email', + ] + ); + $builder->add('content_id', HiddenType::class); + $builder->add('submit', SubmitType::class); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults( + [ + 'translation_domain' => 'ezprotectedcontent', + ] + ); + } +} diff --git a/bundle/Listener/EmailProvided.php b/bundle/Listener/EmailProvided.php new file mode 100644 index 0000000..a03b0db --- /dev/null +++ b/bundle/Listener/EmailProvided.php @@ -0,0 +1,136 @@ +formFactory = $formFactory; + $this->mailer = $mailer; + $this->entityManager = $entityManager; + $this->translator = $translator; + $this->parameterBag = $parameterBag; + $this->messageInstance = new Swift_Message(); + + } + + public function onKernelRequest(GetResponseEvent $event): void + { + if (!$event->isMasterRequest()) { + return; + } + $form = $this->formFactory->create(RequestEmailProtectedAccessType::class); + + $request = $event->getRequest(); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + $contentId = intval($data['content_id']); + $token = Uuid::uuid4()->toString(); + $access = new ProtectedTokenStorage(); + + $access->setMail($data['email']); + $access->setContentId($contentId); + $access->setCreated(new DateTime()); + $access->setToken($token); + + $this->entityManager->persist($access); + $this->entityManager->flush(); + + $currentUrl = $request->getScheme().'://'.$request->getHost().$request->getBaseUrl().$request->getRequestUri(); + $accessUrl = $currentUrl."?mail=".$data['email']."&token=".$token; + $this->sendMail($contentId, $data['email'], $accessUrl); + $response = new RedirectResponse($request->getRequestUri()."?waiting_validation=".$data['email']); + $response->setPrivate(); + $event->setResponse($response); + } + } + + /** + * @throws Exception + */ + private function sendMail(int $contentId, string $receiver, string $link): void { + /** @var ProtectedAccess $protectedAccess */ + $protectedAccess = $this->entityManager->getRepository(ProtectedAccess::class)->findOneBy(['contentId' => $contentId]); + + $mailLink = "".$this->translator->trans('mail.link', [], 'ezprotectedcontent').""; + $bodyMessage = str_replace('{{ url }}', $mailLink, $protectedAccess->getEmailMessage()); + + $message = $this->messageInstance + ->setSubject($this->translator->trans('mail.subject', [], 'ezprotectedcontent')) + ->setFrom($this->parameterBag->get('default_sender_email')) + ->setTo($receiver) + ->setContentType('text/html') + ->setBody( + $bodyMessage + ); + + try { + $this->mailer->send($message); + } catch (Exception $exception) { + throw new Exception(sprintf(self::SENDMAIL_ERROR, $receiver)); + } + } +} diff --git a/bundle/Listener/PreContentView.php b/bundle/Listener/PreContentView.php index ba47bdb..4b6d5c8 100644 --- a/bundle/Listener/PreContentView.php +++ b/bundle/Listener/PreContentView.php @@ -17,7 +17,10 @@ use eZ\Publish\Core\MVC\Symfony\Event\PreContentViewEvent; use eZ\Publish\Core\MVC\Symfony\View\ContentView; use Novactive\Bundle\eZProtectedContentBundle\Entity\ProtectedAccess; +use Novactive\Bundle\eZProtectedContentBundle\Entity\ProtectedTokenStorage; +use Novactive\Bundle\eZProtectedContentBundle\Form\RequestEmailProtectedAccessType; use Novactive\Bundle\eZProtectedContentBundle\Form\RequestProtectedAccessType; +use Novactive\Bundle\eZProtectedContentBundle\Repository\ProtectedTokenStorageRepository; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -78,18 +81,37 @@ public function onPreContentView(PreContentViewEvent $event) $canRead = $this->permissionResolver->canUser('private_content', 'read', $content); if (!$canRead) { - $cookies = $this->requestStack->getCurrentRequest()->cookies; - foreach ($cookies as $name => $value) { - if (PasswordProvided::COOKIE_PREFIX !== substr($name, 0, \strlen(PasswordProvided::COOKIE_PREFIX))) { - continue; - } - if (str_replace(PasswordProvided::COOKIE_PREFIX, '', $name) !== $value) { - continue; + $request = $this->requestStack->getCurrentRequest(); + + if ($request->query->has('mail') + && $request->query->has('token') + && !$request->query->has('waiting_validation') + ) { + /** @var ProtectedTokenStorageRepository $protectedTokenStorageRepository */ + $protectedTokenStorageRepository = $this->entityManager->getRepository(ProtectedTokenStorage::class); + $unexpiredToken = $protectedTokenStorageRepository->findUnexpiredBy([ + 'content_id' => $content->id, + 'token' => $request->get('token'), + 'mail' => $request->get('mail') + ]); + + if (count($unexpiredToken) > 0 ) { + $canRead = true; } - foreach ($protections as $protection) { - /** @var ProtectedAccess $protection */ - if (md5($protection->getPassword()) === $value) { - $canRead = true; + } else { + $cookies = $request->cookies; + foreach ($cookies as $name => $value) { + if (PasswordProvided::COOKIE_PREFIX !== substr($name, 0, \strlen(PasswordProvided::COOKIE_PREFIX))) { + continue; + } + if (str_replace(PasswordProvided::COOKIE_PREFIX, '', $name) !== $value) { + continue; + } + foreach ($protections as $protection) { + /** @var ProtectedAccess $protection */ + if (md5($protection->getPassword()) === $value) { + $canRead = true; + } } } } @@ -97,8 +119,23 @@ public function onPreContentView(PreContentViewEvent $event) $contentView->addParameters(['canReadProtectedContent' => $canRead]); if (!$canRead) { - $form = $this->formFactory->create(RequestProtectedAccessType::class); - $contentView->addParameters(['requestProtectedContentPasswordForm' => $form->createView()]); + if ($this->getContentProtectionType($protections) == 'by_mail') { + $form = $this->formFactory->create(RequestEmailProtectedAccessType::class); + $contentView->addParameters(['requestProtectedContentEmailForm' => $form->createView()]); + } else { + $form = $this->formFactory->create(RequestProtectedAccessType::class); + $contentView->addParameters(['requestProtectedContentPasswordForm' => $form->createView()]); + } + } + } + + private function getContentProtectionType(array $protections): string { + foreach ($protections as $protection) { + /** @var ProtectedAccess $protection */ + if ( !is_null($protection->getPassword()) && $protection->getPassword() != '' ) { + return 'by_password'; + } } + return 'by_mail'; } } diff --git a/bundle/Repository/ProtectedTokenStorageRepository.php b/bundle/Repository/ProtectedTokenStorageRepository.php new file mode 100644 index 0000000..b08f734 --- /dev/null +++ b/bundle/Repository/ProtectedTokenStorageRepository.php @@ -0,0 +1,46 @@ +_em->createQueryBuilder() + ->select('c') + ->from(ProtectedTokenStorage::class, 'c') + ->where('c.created >= :nowMinusOneHour') + ->setParameter('nowMinusOneHour', new \DateTime('now - 1 hours')); + + foreach ($criteria as $key => $criterion) { + $dbQuery->andWhere("c.$key = '$criterion'"); + } + + return $dbQuery->getQuery()->getResult(); + } + + public function findExpired(): array + { + $dbQuery = $this->_em->createQueryBuilder() + ->select('c') + ->from(ProtectedTokenStorage::class, 'c') + ->where('c.created < :nowMinusOneHour') + ->setParameter('nowMinusOneHour', new \DateTime('now - 1 hours')); + + return $dbQuery->getQuery()->getResult(); + } +} diff --git a/bundle/Resources/config/services.yaml b/bundle/Resources/config/services.yaml index 3aaae53..be3d4a0 100644 --- a/bundle/Resources/config/services.yaml +++ b/bundle/Resources/config/services.yaml @@ -6,7 +6,12 @@ services: autoconfigure: true public: false bind: - $httpCachePurgeClient: '@ezplatform.http_cache.purge_client' + $entityManager: "@ibexa.doctrine.orm.entity_manager" + $searchHandler: '@ibexa.spi.search' + $persistenceHandler: '@ibexa.api.persistence_handler' + + Novactive\Bundle\eZProtectedContentBundle\Command\: + resource: '../../Command' Novactive\Bundle\eZProtectedContentBundle\Repository\: resource: '../../Repository' @@ -15,13 +20,14 @@ services: resource: '../../Controller' tags: ['controller.service_arguments'] - Novactive\Bundle\eZProtectedContentBundle\Core\Tab\ProtectContent: + Novactive\Bundle\eZProtectedContentBundle\Command\CleanTokenCommand: tags: - - { name: ezplatform.tab, group: location-view } + - { name: 'novaezprotectedcontent:cleantoken', command: 'novaezprotectedcontent:cleantoken' } + - { name: ibexa.cron.job, schedule: '0 1 * * *' } - Novactive\Bundle\eZProtectedContentBundle\Listener\EntityContentLink: + Novactive\Bundle\eZProtectedContentBundle\Core\Tab\ProtectContent: tags: - - { name: doctrine.orm.entity_listener } + - { name: ibexa.admin_ui.tab, group: location-view } Novactive\Bundle\eZProtectedContentBundle\Listener\PreContentView: tags: @@ -30,3 +36,7 @@ services: Novactive\Bundle\eZProtectedContentBundle\Listener\PasswordProvided: tags: - { name: kernel.event_listener, event: kernel.request, method: 'onKernelRequest', priority: -100} + + Novactive\Bundle\eZProtectedContentBundle\Listener\EmailProvided: + tags: + - { name: kernel.event_listener, event: kernel.request, method: 'onKernelRequest', priority: -100} diff --git a/bundle/Resources/translations/ezprotectedcontent.en.yml b/bundle/Resources/translations/ezprotectedcontent.en.yml index 3c8db4b..2113894 100644 --- a/bundle/Resources/translations/ezprotectedcontent.en.yml +++ b/bundle/Resources/translations/ezprotectedcontent.en.yml @@ -7,7 +7,17 @@ tab.modal.buttons.add: "Add" tab.table.th.password: "Password" tab.table.th.children_protection: "Protect Children?" tab.table.th.enabled: "Enabled?" +tab.table.th.as_email: "Protection by mail" +tab.table.th.message_email: "Message to display in send email" +tab.table.th.email: "Your email" +tab.table.th.as_email_message: "Email message defined" tab.table.th.remove: "Remove" +mail.help_message: Add {{ url }} in your message it will be replace by "Click here" and a link +mail.link: Click here +mail.subject: "Your request for access to protected content" + tab.yes: "YES" tab.no: "NO" + +'This area is email protected.': 'This area is email protected.' diff --git a/bundle/Resources/translations/ezprotectedcontent.fr.yml b/bundle/Resources/translations/ezprotectedcontent.fr.yml index 2039255..7a03be6 100644 --- a/bundle/Resources/translations/ezprotectedcontent.fr.yml +++ b/bundle/Resources/translations/ezprotectedcontent.fr.yml @@ -7,7 +7,17 @@ tab.modal.buttons.add: "Ajouter" tab.table.th.password: "Mot de passe" tab.table.th.children_protection: "Protéger les contenus enfants?" tab.table.th.enabled: "Activer?" +tab.table.th.as_email: "Protection par courriel" +tab.table.th.message_email: "Message à afficher dans le courriel envoyé" +tab.table.th.email: "Votre adresse courriel" +tab.table.th.as_email_message: "Message courriel défini" tab.table.th.remove: "Supprimer" +mail.help_message: Ajoutez {{ url }} dans votre message, cela sera remplacé par "Cliquez ici" et un lien +mail.link: "Cliquez ici" +mail.subject: "Votre demande d'accès à un contenu protégé" + tab.yes: "OUI" tab.no: "NON" + +'This area is email protected.': 'Cette zone est protégé par une vérification de courriel' diff --git a/bundle/Resources/views/tabs/protected_content.html.twig b/bundle/Resources/views/tabs/protected_content.html.twig index 92c314e..97750b5 100644 --- a/bundle/Resources/views/tabs/protected_content.html.twig +++ b/bundle/Resources/views/tabs/protected_content.html.twig @@ -24,6 +24,9 @@ {{ form_row(form.password) }} {{ form_row(form.protectChildren) }} {{ form_row(form.enabled) }} + {{ form_row(form.asEmail) }} + {{ form_row(form.emailMessage) }} + {{ form.emailMessage.vars['help']|trans }}