diff --git a/README.md b/README.md
index 6f75d68..8460fee 100644
--- a/README.md
+++ b/README.md
@@ -17,20 +17,30 @@ Documentation is available in this repository via `.md` files but also packaged
A bundle that provides quick password protection on Contents.
-## How does it work?
+# 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
-
{{ ibexa_content_name(content) }}
+{{ 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..22572f2
--- /dev/null
+++ b/bundle/Command/CleanTokenCommand.php
@@ -0,0 +1,60 @@
+setName('novaezprotectedcontent:cleantoken')
+ ->setDescription('Remove expired token in the DB');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ $entities = $this->protectedTokenStorageRepository->findExpired();
+
+ $io->comment(sprintf('%d entities to delete', count($entities)));
+
+ foreach ($entities as $entity) {
+ $this->protectedTokenStorageRepository->remove($entity);
+ }
+
+ $this->protectedTokenStorageRepository->flush();
+
+ $io->success(sprintf('%d entities deleted', count($entities)));
+ $io->success('Done.');
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/bundle/Controller/Admin/ProtectedAccessController.php b/bundle/Controller/Admin/ProtectedAccessController.php
index 8f7a117..e57ef69 100644
--- a/bundle/Controller/Admin/ProtectedAccessController.php
+++ b/bundle/Controller/Admin/ProtectedAccessController.php
@@ -16,8 +16,11 @@
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
-use eZ\Publish\API\Repository\Values\Content\Location;
+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 Symfony\Component\Form\FormFactoryInterface;
@@ -28,20 +31,28 @@
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,
FormFactoryInterface $formFactory,
EntityManagerInterface $entityManager,
RouterInterface $router,
ContentTagInterface $responseTagger,
- ?ProtectedAccess $access = null
+ ?ProtectedAccess $access = null,
): RedirectResponse {
if ($request->isMethod('post')) {
+ $location = $this->repository->getLocationService()->loadLocation($locationId);
$now = new DateTime();
if (null === $access) {
$access = new ProtectedAccess();
@@ -56,20 +67,24 @@ public function handle(
$entityManager->flush();
$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('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,
@@ -83,11 +98,62 @@ public function remove(
$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('ibexa.content.view', ['contentId' => $location->contentId,
'locationId' => $location->id,
- ]).
+ ]).
'#ibexa-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/Core/SiteAccessAwareEntityManagerFactory.php b/bundle/Core/SiteAccessAwareEntityManagerFactory.php
deleted file mode 100644
index a8f4cd7..0000000
--- a/bundle/Core/SiteAccessAwareEntityManagerFactory.php
+++ /dev/null
@@ -1,91 +0,0 @@
-registry = $registry;
- $this->repositoryConfigurationProvider = $repositoryConfigurationProvider;
- $this->settings = $settings;
- $this->resolver = $resolver;
- }
-
- private function getConnectionName(): string
- {
- $config = $this->repositoryConfigurationProvider->getRepositoryConfig();
-
- return $config['storage']['connection'] ?? 'default';
- }
-
- public function get(): EntityManagerInterface
- {
- $connectionName = $this->getConnectionName();
- // If it is the default connection then we don't bother we can directly use the default entity Manager
- if ('default' === $connectionName) {
- return $this->registry->getManager();
- }
-
- $connection = $this->registry->getConnection($connectionName);
-
- /** @var \Doctrine\DBAL\Connection $connection */
- $cache = new ArrayAdapter();
- $config = new Configuration();
- $config->setMetadataCacheImpl(DoctrineProvider::wrap($cache));
- $driverImpl = $config->newDefaultAnnotationDriver(__DIR__.'/../Entity', false);
- $config->setMetadataDriverImpl($driverImpl);
- $config->setQueryCacheImpl(DoctrineProvider::wrap($cache));
- $config->setProxyDir($this->settings['cache_dir'].'/eZProtectedContent/');
- $config->setProxyNamespace('eZProtectedContent\Proxies');
- $config->setAutoGenerateProxyClasses($this->settings['debug']);
- $config->setEntityListenerResolver($this->resolver);
- $config->setNamingStrategy(new UnderscoreNamingStrategy());
-
- return EntityManager::create($connection, $config);
- }
-}
diff --git a/bundle/DependencyInjection/NovaeZProtectedContentExtension.php b/bundle/DependencyInjection/NovaeZProtectedContentExtension.php
index a8c2b15..4eda9df 100644
--- a/bundle/DependencyInjection/NovaeZProtectedContentExtension.php
+++ b/bundle/DependencyInjection/NovaeZProtectedContentExtension.php
@@ -16,14 +16,33 @@
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
-class NovaeZProtectedContentExtension extends Extension
+class NovaeZProtectedContentExtension extends Extension implements PrependExtensionInterface
{
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yaml');
}
+
+ public function prepend(ContainerBuilder $container)
+ {
+ $config = [
+ 'orm' => [
+ 'entity_mappings' => [
+ 'eZProtectedContentBundle' => [
+ 'type' => 'annotation',
+ 'dir' => __DIR__.'/../Entity',
+ 'prefix' => 'Novactive\Bundle\eZProtectedContentBundle\Entity',
+ 'is_bundle' => false,
+ ],
+ ],
+ ],
+ ];
+
+ $container->prependExtensionConfig('ibexa', $config);
+ }
}
diff --git a/bundle/Entity/ProtectedAccess.php b/bundle/Entity/ProtectedAccess.php
index 1806a13..808fffa 100644
--- a/bundle/Entity/ProtectedAccess.php
+++ b/bundle/Entity/ProtectedAccess.php
@@ -21,7 +21,6 @@
/**
* @ORM\Entity()
* @ORM\Table(name="novaezprotectedcontent")
- * @ORM\EntityListeners({"Novactive\Bundle\eZProtectedContentBundle\Listener\EntityContentLink"})
*/
class ProtectedAccess implements ContentInterface
{
@@ -40,8 +39,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;
@@ -53,6 +51,13 @@ class ProtectedAccess implements ContentInterface
*/
protected $enabled;
+ /**
+ * @var bool
+ *
+ * @ORM\Column(type="boolean", nullable=false)
+ */
+ protected $asEmail = false;
+
/**
* @var bool
*
@@ -60,6 +65,13 @@ class ProtectedAccess implements ContentInterface
*/
protected $protectChildren;
+ /**
+ * @var string
+ *
+ * @ORM\Column(type="string", nullable=true)
+ */
+ protected $emailMessage;
+
public function __construct()
{
$this->enabled = true;
@@ -78,12 +90,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;
@@ -111,4 +135,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..6fb99f0
--- /dev/null
+++ b/bundle/Entity/ProtectedTokenStorage.php
@@ -0,0 +1,114 @@
+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 882cdd1..426f7fd 100644
--- a/bundle/Form/ProtectedAccessType.php
+++ b/bundle/Form/ProtectedAccessType.php
@@ -18,6 +18,7 @@
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\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -33,7 +34,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..44b459c
--- /dev/null
+++ b/bundle/Form/RequestEmailProtectedAccessType.php
@@ -0,0 +1,48 @@
+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..3b71b0c
--- /dev/null
+++ b/bundle/Listener/EmailProvided.php
@@ -0,0 +1,155 @@
+messageInstance = new Swift_Message();
+ }
+
+ public function onKernelRequest(RequestEvent $event): void
+ {
+ $request = $event->getRequest();
+
+ if (!$event->isMainRequest()) {
+ return;
+ }
+
+ if (!$request->isMethod('POST')) {
+ return;
+ }
+
+ $contentId = (int) $request->attributes->get('contentId');
+
+ if (!$contentId) {
+ return;
+ }
+
+ try {
+ $content = $this->contentService->loadContent($contentId);
+ } catch (\Exception $exception) {
+ $this->logger->error($exception->getMessage(), [
+ 'here' => __METHOD__ . ' ' . __LINE__,
+ '$contentId' => $contentId,
+ ]);
+ return;
+ }
+
+ if (!$content->contentInfo->isPublished()) {
+ return;
+ }
+
+ $protections = $this->protectedAccessRepository->findByContent($content);
+
+ if (0 === count($protections)) {
+ return;
+ }
+
+ $form = $this->formFactory->create(RequestEmailProtectedAccessType::class);
+
+ $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 = sprintf(
+ '%s://%s%s%s',
+ $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/EntityContentLink.php b/bundle/Listener/EntityContentLink.php
deleted file mode 100644
index 54a3025..0000000
--- a/bundle/Listener/EntityContentLink.php
+++ /dev/null
@@ -1,46 +0,0 @@
-repository = $repository;
- }
-
- /** @PostLoad */
- public function postLoadHandler(ContentInterface $entity, LifecycleEventArgs $event): void
- {
- $content = $this->repository->getContentService()->loadContent($entity->getContentId());
- $location = $this->repository->getLocationService()->loadLocation($content->contentInfo->mainLocationId);
- $entity->setLocation($location);
- $entity->setContent($content);
- }
-}
diff --git a/bundle/Listener/PasswordProvided.php b/bundle/Listener/PasswordProvided.php
index 95011df..5265e30 100644
--- a/bundle/Listener/PasswordProvided.php
+++ b/bundle/Listener/PasswordProvided.php
@@ -26,16 +26,15 @@ class PasswordProvided
/**
* @var FormFactoryInterface
*/
- private $formFactory;
-
- public function __construct(FormFactoryInterface $formFactory)
- {
- $this->formFactory = $formFactory;
- }
+ public function __construct(private readonly FormFactoryInterface $formFactory)
+ { }
public function onKernelRequest(RequestEvent $event): void
{
- if (!$event->isMasterRequest()) {
+ if (!$event->isMainRequest()) {
+ return;
+ }
+ if (!$event->getRequest()->isMethod('POST')) {
return;
}
$form = $this->formFactory->create(RequestProtectedAccessType::class);
diff --git a/bundle/Listener/PreContentView.php b/bundle/Listener/PreContentView.php
index cbbc6a4..86e19ac 100644
--- a/bundle/Listener/PreContentView.php
+++ b/bundle/Listener/PreContentView.php
@@ -15,11 +15,14 @@
namespace Novactive\Bundle\eZProtectedContentBundle\Listener;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
+use Ibexa\Core\Helper\ContentPreviewHelper;
use Ibexa\Core\MVC\Symfony\Event\PreContentViewEvent;
use Ibexa\Core\MVC\Symfony\View\ContentView;
use Novactive\Bundle\eZProtectedContentBundle\Entity\ProtectedAccess;
+use Novactive\Bundle\eZProtectedContentBundle\Form\RequestEmailProtectedAccessType;
use Novactive\Bundle\eZProtectedContentBundle\Form\RequestProtectedAccessType;
use Novactive\Bundle\eZProtectedContentBundle\Repository\ProtectedAccessRepository;
+use Novactive\Bundle\eZProtectedContentBundle\Repository\ProtectedTokenStorageRepository;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -35,6 +38,11 @@ class PreContentView
*/
private $protectedAccessRepository;
+ /**
+ * @var ProtectedAccessRepository
+ */
+ private $protectedTokenStorageRepository;
+
/**
* @var FormFactoryInterface
*/
@@ -45,16 +53,22 @@ class PreContentView
*/
private $requestStack;
+ private ContentPreviewHelper $contentPreviewHelper;
+
public function __construct(
PermissionResolver $permissionResolver,
ProtectedAccessRepository $protectedAccessRepository,
+ ProtectedTokenStorageRepository $protectedTokenStorageRepository,
FormFactoryInterface $factory,
- RequestStack $requestStack
+ RequestStack $requestStack,
+ ContentPreviewHelper $contentPreviewHelper
) {
$this->permissionResolver = $permissionResolver;
$this->protectedAccessRepository = $protectedAccessRepository;
+ $this->protectedTokenStorageRepository = $protectedTokenStorageRepository;
$this->formFactory = $factory;
$this->requestStack = $requestStack;
+ $this->contentPreviewHelper = $contentPreviewHelper;
}
/**
@@ -78,6 +92,10 @@ public function onPreContentView(PreContentViewEvent $event): void
return;
}
+ if ($this->contentPreviewHelper->isPreviewActive()) {
+ return;
+ }
+
$protections = $this->protectedAccessRepository->findByContent($content);
if (0 === count($protections)) {
@@ -87,18 +105,37 @@ public function onPreContentView(PreContentViewEvent $event): void
$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')
+ ) {
+ $unexpiredToken = $this->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 = $this->requestStack->getCurrentRequest()->cookies;
+ foreach ($cookies as $name => $value) {
+ $cookiePrefix = substr($name, 0, \strlen(PasswordProvided::COOKIE_PREFIX));
+ if (PasswordProvided::COOKIE_PREFIX !== $cookiePrefix) {
+ continue;
+ }
+ if (str_replace(PasswordProvided::COOKIE_PREFIX, '', $name) !== $value) {
+ continue;
+ }
+ foreach ($protections as $protection) {
+ /** @var ProtectedAccess $protection */
+ if (md5($protection->getPassword()) === $value) {
+ $canRead = true;
+ }
}
}
}
@@ -106,8 +143,25 @@ public function onPreContentView(PreContentViewEvent $event): void
$contentView->addParameters(['canReadProtectedContent' => $canRead]);
if (!$canRead) {
- $form = $this->formFactory->create(RequestProtectedAccessType::class);
- $contentView->addParameters(['requestProtectedContentPasswordForm' => $form->createView()]);
+ if ('by_mail' == $this->getContentProtectionType($protections)) {
+ $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/EntityRepository.php b/bundle/Repository/EntityRepository.php
deleted file mode 100644
index ad4c97f..0000000
--- a/bundle/Repository/EntityRepository.php
+++ /dev/null
@@ -1,53 +0,0 @@
-getEntityClass());
- }
-
- public function createQueryBuilderForFilters(array $filters = []): QueryBuilder
- {
- return $this->createQueryBuilder($this->getAlias())->select($this->getAlias())->distinct();
- }
-
- /**
- * @return array|ArrayCollection
- */
- public function findByFilters(array $filters = [])
- {
- $qb = $this->createQueryBuilderForFilters($filters);
-
- return $qb->getQuery()->getResult();
- }
-
- public function countByFilters(array $filters = []): int
- {
- $qb = $this->createQueryBuilderForFilters($filters);
- $qb->select($qb->expr()->countDistinct($this->getAlias().'.id'));
-
- return (int) $qb->getQuery()->getSingleScalarResult();
- }
-}
diff --git a/bundle/Repository/ProtectedAccessRepository.php b/bundle/Repository/ProtectedAccessRepository.php
index 9c700bb..1e96b34 100644
--- a/bundle/Repository/ProtectedAccessRepository.php
+++ b/bundle/Repository/ProtectedAccessRepository.php
@@ -14,25 +14,18 @@
namespace Novactive\Bundle\eZProtectedContentBundle\Repository;
+use Doctrine\ORM\EntityManagerInterface;
use Ibexa\Contracts\Core\Repository\Repository;
use Ibexa\Contracts\Core\Repository\Values\Content\Content;
use Ibexa\Contracts\Core\Repository\Values\Content\Location;
use Novactive\Bundle\eZProtectedContentBundle\Entity\ProtectedAccess;
-class ProtectedAccessRepository extends EntityRepository
+class ProtectedAccessRepository
{
- /**
- * @var Repository
- */
- private $repository;
-
- /**
- * @required
- */
- public function setRepository(Repository $repository): void
- {
- $this->repository = $repository;
- }
+ public function __construct(
+ protected readonly Repository $repository,
+ protected readonly EntityManagerInterface $entityManager,
+ ) { }
protected function getAlias(): string
{
@@ -49,27 +42,18 @@ public function findByContent(?Content $content): array
if (null === $content) {
return [];
}
- $contentIds = $this->repository->sudo(
- function (Repository $repository) use ($content) {
- $ids = [$content->id];
- $locations = $repository->getLocationService()->loadLocations($content->contentInfo);
- foreach ($locations as $location) {
- /** @var Location $location */
- $parent = $repository->getLocationService()->loadLocation($location->parentLocationId);
- $ids[] = $parent->contentInfo->id;
- }
+ $contentIds = $this->getContentIds($content);
- return $ids;
- }
- );
+ $entityRepository = $this->entityManager->getRepository($this->getEntityClass());
- $qb = parent::createQueryBuilderForFilters();
+ $qb = $entityRepository->createQueryBuilder($this->getAlias());
$qb->where($qb->expr()->eq($this->getAlias().'.enabled', true));
$qb->andWhere(
$qb->expr()->in($this->getAlias().'.contentId', ':contentIds')
);
$qb->setParameter('contentIds', $contentIds);
$results = $qb->getQuery()->getResult();
+
$filteredResults = [];
foreach ($results as $protection) {
/** @var ProtectedAccess $protection */
@@ -86,4 +70,37 @@ function (Repository $repository) use ($content) {
return $filteredResults;
}
+
+ /**
+ * Retourne les ContentID du contenu et de tous ces descendants en prenant en compte ses multiples emplacements.
+ * @param Content $content
+ * @return array
+ */
+ protected function getContentIds(Content $content): array
+ {
+ return $this->repository->sudo(
+ function (Repository $repository) use ($content) {
+ $ids = [$content->id];
+ $locations = $repository->getLocationService()->loadLocations($content->contentInfo);
+ $ct = 0;
+ foreach ($locations as $location) {
+ /** @var Location $loc */
+ $loc = $location;
+ while ($loc->parentLocationId
+ && ($loc = $repository->getLocationService()->loadLocation($loc->parentLocationId))
+ && $loc instanceof Location
+ && $loc->parentLocationId
+ && $loc->parentLocationId !== 1
+ ) {
+ $ct++;
+ $ids[] = $loc->getContentInfo()->id;
+ if ($ct >= 15) {
+ break(2);
+ }
+ }
+ }
+ return $ids;
+ }
+ );
+ }
}
diff --git a/bundle/Repository/ProtectedTokenStorageRepository.php b/bundle/Repository/ProtectedTokenStorageRepository.php
new file mode 100644
index 0000000..66d6e0a
--- /dev/null
+++ b/bundle/Repository/ProtectedTokenStorageRepository.php
@@ -0,0 +1,77 @@
+entityManager->getRepository($this->getEntityClass());
+ $qb = $entityRepository->createQueryBuilder($this->getAlias());
+
+ $qb ->select('c')
+ ->from(ProtectedTokenStorage::class, 'c')
+ ->where('c.created >= :nowMinusOneHour')
+ ->setParameter('nowMinusOneHour', new DateTime('now - 1 hours'));
+
+ foreach ($criteria as $key => $criterion) {
+ $qb->andWhere("c.$key = '$criterion'");
+ }
+
+ return $qb->getQuery()->getResult();
+ }
+
+ public function findExpired(): array
+ {
+ $entityRepository = $this->entityManager->getRepository($this->getEntityClass());
+ $qb = $entityRepository->createQueryBuilder($this->getAlias());
+ $qb ->select('c')
+ ->from(ProtectedTokenStorage::class, 'c')
+ ->where('c.created < :nowMinusOneHour')
+ ->setParameter('nowMinusOneHour', new DateTime('now - 1 hours'));
+
+ return $qb->getQuery()->getResult();
+ }
+
+ /**
+ * @param ProtectedTokenStorage $entity
+ * @return void
+ * @see EntityManagerInterface::remove()
+ */
+ public function remove(ProtectedTokenStorage $entity): void
+ {
+ $this->entityManager->remove($entity);
+ }
+
+ /**
+ * @return void
+ * @see EntityManagerInterface::flush()
+ */
+ public function flush(): void
+ {
+ $this->entityManager->flush();
+ }
+}
diff --git a/bundle/Resources/config/services.yaml b/bundle/Resources/config/services.yaml
index 498b4ab..be3d4a0 100644
--- a/bundle/Resources/config/services.yaml
+++ b/bundle/Resources/config/services.yaml
@@ -6,7 +6,9 @@ services:
autoconfigure: true
public: false
bind:
- $entityManager: "@novaezprotectedcontent.doctrine.entity_manager"
+ $entityManager: "@ibexa.doctrine.orm.entity_manager"
+ $searchHandler: '@ibexa.spi.search'
+ $persistenceHandler: '@ibexa.api.persistence_handler'
Novactive\Bundle\eZProtectedContentBundle\Command\:
resource: '../../Command'
@@ -18,13 +20,14 @@ services:
resource: '../../Controller'
tags: ['controller.service_arguments']
- Novactive\Bundle\eZProtectedContentBundle\Core\Tab\ProtectContent:
+ Novactive\Bundle\eZProtectedContentBundle\Command\CleanTokenCommand:
tags:
- - { name: ibexa.admin_ui.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:
@@ -34,12 +37,6 @@ services:
tags:
- { name: kernel.event_listener, event: kernel.request, method: 'onKernelRequest', priority: -100}
- novaezprotectedcontent.doctrine.entity_manager:
- class: Doctrine\ORM\EntityManagerInterface
- factory: ['@Novactive\Bundle\eZProtectedContentBundle\Core\SiteAccessAwareEntityManagerFactory', 'get']
-
- Novactive\Bundle\eZProtectedContentBundle\Core\SiteAccessAwareEntityManagerFactory:
- arguments:
- $repositoryConfigurationProvider: "@Ibexa\\Bundle\\Core\\ApiLoader\\RepositoryConfigurationProvider"
- $resolver: "@doctrine.orm.default_entity_listener_resolver"
- $settings: { debug: "%kernel.debug%", cache_dir: "%kernel.cache_dir%" }
+ 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/themes/admin/tabs/protected_content.html.twig b/bundle/Resources/views/themes/admin/tabs/protected_content.html.twig
index 4b39d50..517e318 100644
--- a/bundle/Resources/views/themes/admin/tabs/protected_content.html.twig
+++ b/bundle/Resources/views/themes/admin/tabs/protected_content.html.twig
@@ -31,6 +31,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 }}