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 }}