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