diff --git a/.github/workflows/run-test.yml b/.github/workflows/run-test.yml index 3b17d56e..189e3a55 100644 --- a/.github/workflows/run-test.yml +++ b/.github/workflows/run-test.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['8.0', '8.1'] + php-version: ['8.1', '8.2', '8.3'] steps: - uses: shivammathur/setup-php@v2 with: diff --git a/.gitignore b/.gitignore index ea8d6aa9..43469c37 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ /dev.php /install.php /clear_cache.php -/pimple.json /assets project_env.sh diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5346b623..00000000 --- a/.travis.yml +++ /dev/null @@ -1,75 +0,0 @@ -node_js: - - "14" -php: - - 8.0 - - 8.1 - - nightly -dist: bionic -stages: - - "PHP lint tests" - - "Backoffice assets tests" -branches: - except: - - l10n_develop - -jobs: - allow_failures: - - php: nightly - - include: - - stage: "Backoffice assets tests" - language: node_js - node_js: "14" - script: sh .travis/backoffice_assets.sh - - - stage: "PHP lint tests" - language: php - sudo: required - services: - - mysql - env: - - DB=mysql - - MYSQL_VERSION=5.7 - - MYSQL_PASSWORD= - php: 7.4 - install: sh .travis/composer_install.sh - script: sh .travis/php_lint.sh - - stage: "PHP lint tests" - language: php - sudo: required - services: - - mysql - env: - - DB=mysql - - MYSQL_VERSION=5.7 - - MYSQL_PASSWORD= - php: 8.0 - install: sh .travis/composer_install.sh - script: sh .travis/php_lint.sh - - stage: "PHP lint tests" - language: php - sudo: required - services: - - mysql - env: - - DB=mysql - - MYSQL_VERSION=5.7 - - MYSQL_PASSWORD= - php: 8.1 - install: sh .travis/composer_install.sh - script: sh .travis/php_lint.sh - - stage: "PHP lint tests" - language: php - sudo: required - services: - - mysql - env: - - DB=mysql - - MYSQL_VERSION=5.7 - - MYSQL_PASSWORD= - php: nightly - install: sh .travis/composer_install.sh - script: sh .travis/php_lint.sh - - - diff --git a/.travis/backoffice_assets.sh b/.travis/backoffice_assets.sh deleted file mode 100644 index 92ba88c8..00000000 --- a/.travis/backoffice_assets.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -x -cd src || exit 1; -yarn install --pure-lockfile -yarn run install -yarn run build diff --git a/.travis/composer_install.sh b/.travis/composer_install.sh deleted file mode 100644 index 628aaebb..00000000 --- a/.travis/composer_install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -x -phpenv config-rm xdebug.ini; -curl -s http://getcomposer.org/installer | php; -php composer.phar install --dev --no-interaction; diff --git a/.travis/php_lint.sh b/.travis/php_lint.sh deleted file mode 100644 index 624af292..00000000 --- a/.travis/php_lint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -x -vendor/bin/phpcs --report=full --report-file=./report.txt -p ./ || exit 1; -vendor/bin/phpstan analyse -c phpstan.neon || exit 1; -#vendor/bin/console lint:twig || exit 1; -#vendor/bin/console lint:twig src/Resources/views || exit 1; diff --git a/LICENSE.md b/LICENSE.md index 747e48b2..01cd7778 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2023 Ambroise Maupate, Julien Blanchet +Copyright © 2024 Ambroise Maupate, Julien Blanchet Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/composer.json b/composer.json index 917762b9..e5bbf990 100644 --- a/composer.json +++ b/composer.json @@ -28,47 +28,48 @@ "role": "Frontend developer" } ], + "minimum-stability": "dev", + "prefer-stable": true, "require": { - "php": ">=8.0", + "php": ">=8.1", "ext-zip": "*", - "doctrine/orm": "<2.17", + "doctrine/orm": "~2.19.0", "guzzlehttp/guzzle": "^7.2.0", "jms/serializer": "^3.9.0", "league/flysystem": "^3.0", - "pimple/pimple": "^3.3.1", "ramsey/uuid": "^4.7", - "roadiz/compat-bundle": "2.1.*", - "roadiz/core-bundle": "2.1.*", - "roadiz/doc-generator": "2.1.*", - "roadiz/documents": "2.1.*", - "roadiz/dts-generator": "2.1.*", - "roadiz/markdown": "2.1.*", - "roadiz/models": "2.1.*", + "roadiz/compat-bundle": "2.3.*", + "roadiz/core-bundle": "2.3.*", + "roadiz/doc-generator": "2.3.*", + "roadiz/documents": "2.3.*", + "roadiz/dts-generator": "2.3.*", + "roadiz/markdown": "2.3.*", + "roadiz/models": "2.3.*", "roadiz/nodetype-contracts": "~1.1.2", - "roadiz/openid": "2.1.*", - "roadiz/rozier-bundle": "2.1.*", - "symfony/asset": "5.4.*", - "symfony/filesystem": "5.4.*", - "symfony/form": "5.4.*", - "symfony/http-foundation": "5.4.*", - "symfony/http-kernel": "5.4.*", - "symfony/routing": "5.4.*", - "symfony/security-core": "5.4.*", - "symfony/security-csrf": "5.4.*", - "symfony/security-http": "5.4.*", - "symfony/translation": "5.4.*", - "symfony/validator": "5.4.*", - "symfony/workflow": "5.4.*", - "symfony/yaml": "5.4.*", + "roadiz/openid": "2.3.*", + "roadiz/rozier-bundle": "2.3.*", + "symfony/asset": "6.4.*", + "symfony/filesystem": "6.4.*", + "symfony/form": "6.4.*", + "symfony/http-foundation": "6.4.*", + "symfony/http-kernel": "6.4.*", + "symfony/routing": "6.4.*", + "symfony/security-core": "6.4.*", + "symfony/security-csrf": "6.4.*", + "symfony/security-http": "6.4.*", + "symfony/translation": "6.4.*", + "symfony/validator": "6.4.*", + "symfony/workflow": "6.4.*", + "symfony/yaml": "6.4.*", "twig/twig": "^3.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.4", "phpstan/phpstan": "^1.5.3", "phpstan/phpstan-doctrine": "^1.3", - "roadiz/entity-generator": "2.1.*", - "roadiz/random": "2.1.*", - "roadiz/jwt": "2.1.*", + "roadiz/entity-generator": "2.3.*", + "roadiz/random": "2.3.*", + "roadiz/jwt": "2.3.*", "squizlabs/php_codesniffer": "^3.5" }, "autoload": { @@ -94,8 +95,8 @@ }, "extra": { "branch-alias": { - "dev-main": "2.1.x-dev", - "dev-develop": "2.2.x-dev" + "dev-main": "2.3.x-dev", + "dev-develop": "2.4.x-dev" } } } diff --git a/phpstan.neon b/phpstan.neon index 353be6b9..6693cc61 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 6 + level: 7 paths: - src excludePaths: @@ -9,6 +9,8 @@ parameters: doctrine: repositoryClass: RZ\Roadiz\Core\Repositories\EntityRepository ignoreErrors: + - identifier: missingType.iterableValue + - identifier: missingType.generics - '#Call to an undefined method RZ\\Roadiz\\CoreBundle\\Repository#' - '#Call to an undefined method RZ\\Roadiz\\UserBundle\\Repository#' - '#Call to an undefined method Doctrine\\Persistence\\ObjectRepository#' @@ -28,10 +30,11 @@ parameters: - '#Doctrine\\ORM\\Mapping\\GeneratedValue constructor expects#' - '#type mapping mismatch: property can contain Doctrine\\Common\\Collections\\Collection]+> but database expects Doctrine\\Common\\Collections\\Collection&iterable<[^\>]+>#' - '#should return Doctrine\\Common\\Collections\\Collection]+Interface> but returns Doctrine\\Common\\Collections\\Collection]+>#' + - '#but returns Doctrine\\Common\\Collections\\ReadableCollection]+>#' + - '#does not accept Doctrine\\Common\\Collections\\ReadableCollection]+>#' reportUnmatchedIgnoredErrors: false - checkGenericClassInNonGenericObjectType: false - checkMissingIterableValueType: false + treatPhpDocTypesAsCertain: false includes: - vendor/phpstan/phpstan-doctrine/extension.neon diff --git a/src/.babelrc b/src/.babelrc index 2595c191..b007a11d 100644 --- a/src/.babelrc +++ b/src/.babelrc @@ -1,4 +1,4 @@ { - "presets": ["es2015", "stage-0"], + "presets": ["env", "stage-0"], "plugins": ["transform-runtime", "lodash"] } diff --git a/src/.editorconfig b/src/.editorconfig index c2778daa..cd986d42 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -1,7 +1,6 @@ # TheatreTheme editor config for contributors # Root is false as your theme is inside Roadiz filetree # http://editorconfig.org/ -root = false [*] indent_style = space diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 536f9132..020a951c 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -11,7 +11,7 @@ module.exports = { }, // https://github.com/standard/standard/blob/master/docs/RULES-en.md //extends: 'standard', - extends: ['prettier', 'plugin:prettier/recommended', 'plugin:vue/essential'], + extends: ['prettier', 'plugin:prettier/recommended', 'plugin:vue/base'], // required to lint *.vue files plugins: ['html', 'prettier'], // add your custom rules here diff --git a/src/AjaxControllers/AbstractAjaxController.php b/src/AjaxControllers/AbstractAjaxController.php index 2cd1c2e0..c4633eca 100644 --- a/src/AjaxControllers/AbstractAjaxController.php +++ b/src/AjaxControllers/AbstractAjaxController.php @@ -42,7 +42,7 @@ protected function getTranslation(Request $request): ?TranslationInterface * @param string $method * @param bool $requestCsrfToken * - * @return boolean Return true if request is valid, else throw exception + * @return bool Return true if request is valid, else throw exception */ protected function validateRequest(Request $request, string $method = 'POST', bool $requestCsrfToken = true): bool { diff --git a/src/AjaxControllers/AjaxAbstractFieldsController.php b/src/AjaxControllers/AjaxAbstractFieldsController.php index 00ff7757..64f3941e 100644 --- a/src/AjaxControllers/AjaxAbstractFieldsController.php +++ b/src/AjaxControllers/AjaxAbstractFieldsController.php @@ -11,19 +11,10 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -/** - * @package Themes\Rozier\AjaxControllers - */ abstract class AjaxAbstractFieldsController extends AbstractAjaxController { - private HandlerFactoryInterface $handlerFactory; - - /** - * @param HandlerFactoryInterface $handlerFactory - */ - public function __construct(HandlerFactoryInterface $handlerFactory) + public function __construct(protected readonly HandlerFactoryInterface $handlerFactory) { - $this->handlerFactory = $handlerFactory; } protected function findEntity(int|string $entityId): ?AbstractField @@ -39,7 +30,7 @@ protected function findEntity(int|string $entityId): ?AbstractField * * @return null|Response */ - protected function handleFieldActions(Request $request, AbstractField $field = null) + protected function handleFieldActions(Request $request, AbstractField $field = null): ?Response { /* * Validate diff --git a/src/AjaxControllers/AjaxAttributeValuesController.php b/src/AjaxControllers/AjaxAttributeValuesController.php index 0b93e7f6..a4bc2108 100644 --- a/src/AjaxControllers/AjaxAttributeValuesController.php +++ b/src/AjaxControllers/AjaxAttributeValuesController.php @@ -5,7 +5,7 @@ namespace Themes\Rozier\AjaxControllers; use RZ\Roadiz\CoreBundle\Entity\AttributeValue; -use RZ\Roadiz\CoreBundle\Entity\Node; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -28,49 +28,47 @@ final class AjaxAttributeValuesController extends AbstractAjaxController */ public function editAction(Request $request, int $attributeValueId): Response { + /* + * Validate + */ $this->validateRequest($request, 'POST', false); - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODE_ATTRIBUTES'); /** @var AttributeValue|null $attributeValue */ $attributeValue = $this->em()->find(AttributeValue::class, (int) $attributeValueId); - if ($attributeValue !== null) { - $responseArray = []; - /* - * Get the right update method against "_action" parameter - */ - switch ($request->get('_action')) { - case 'updatePosition': - $responseArray = $this->updatePosition($request->request->all(), $attributeValue); - break; - } + if ($attributeValue === null) { + throw $this->createNotFoundException($this->getTranslator()->trans( + 'attribute_value.%attributeValueId%.not_exists', + [ + '%attributeValueId%' => $attributeValueId + ] + )); + } + + $this->denyAccessUnlessGranted(NodeVoter::EDIT_ATTRIBUTE, $attributeValue->getAttributable()); - return new JsonResponse( - $responseArray, - Response::HTTP_PARTIAL_CONTENT - ); + $responseArray = []; + /* + * Get the right update method against "_action" parameter + */ + switch ($request->get('_action')) { + case 'updatePosition': + $responseArray = $this->updatePosition($request->request->all(), $attributeValue); + break; } - throw $this->createNotFoundException($this->getTranslator()->trans( - 'attribute_value.%attributeValueId%.not_exists', - [ - '%attributeValueId%' => $attributeValueId - ] - )); + return new JsonResponse( + $responseArray, + Response::HTTP_PARTIAL_CONTENT + ); } - /** - * @param array $parameters - * @param AttributeValue $attributeValue - * - * @return array - */ - protected function updatePosition($parameters, AttributeValue $attributeValue): array + protected function updatePosition(array $parameters, AttributeValue $attributeValue): array { $attributable = $attributeValue->getAttributable(); $details = [ '%name%' => $attributeValue->getAttribute()->getLabelOrCode(), - '%nodeName%' => $attributable instanceof Node ? $attributable->getNodeName() : '', + '%nodeName%' => $attributable->getNodeName(), ]; if (!empty($parameters['afterAttributeValueId']) && is_numeric($parameters['afterAttributeValueId'])) { diff --git a/src/AjaxControllers/AjaxCustomFormFieldsController.php b/src/AjaxControllers/AjaxCustomFormFieldsController.php index 4153b9f4..86dc7ebc 100644 --- a/src/AjaxControllers/AjaxCustomFormFieldsController.php +++ b/src/AjaxControllers/AjaxCustomFormFieldsController.php @@ -8,9 +8,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxCustomFormFieldsController extends AjaxAbstractFieldsController { /** @@ -18,11 +15,11 @@ class AjaxCustomFormFieldsController extends AjaxAbstractFieldsController * such as coming from widgets. * * @param Request $request - * @param int $customFormFieldId + * @param int $customFormFieldId * * @return Response JSON response */ - public function editAction(Request $request, int $customFormFieldId) + public function editAction(Request $request, int $customFormFieldId): Response { $this->validateRequest($request); $this->denyAccessUnlessGranted('ROLE_ACCESS_CUSTOMFORMS_DELETE'); diff --git a/src/AjaxControllers/AjaxCustomFormsExplorerController.php b/src/AjaxControllers/AjaxCustomFormsExplorerController.php index 901e6004..8c8707f2 100644 --- a/src/AjaxControllers/AjaxCustomFormsExplorerController.php +++ b/src/AjaxControllers/AjaxCustomFormsExplorerController.php @@ -13,16 +13,10 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Themes\Rozier\Models\CustomFormModel; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxCustomFormsExplorerController extends AbstractAjaxController { - private UrlGeneratorInterface $urlGenerator; - - public function __construct(UrlGeneratorInterface $urlGenerator) + public function __construct(private readonly UrlGeneratorInterface $urlGenerator) { - $this->urlGenerator = $urlGenerator; } /** @@ -30,9 +24,9 @@ public function __construct(UrlGeneratorInterface $urlGenerator) * * @return Response JSON response */ - public function indexAction(Request $request) + public function indexAction(Request $request): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + $this->denyAccessUnlessGranted('ROLE_ACCESS_CUSTOMFORMS'); $arrayFilter = []; /* @@ -68,15 +62,15 @@ public function indexAction(Request $request) * Get a CustomForm list from an array of id. * * @param Request $request - * @return JsonResponse + * @return Response */ - public function listAction(Request $request) + public function listAction(Request $request): Response { if (!$request->query->has('ids')) { throw new InvalidParameterException('Ids should be provided within an array'); } - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + $this->denyAccessUnlessGranted('ROLE_ACCESS_CUSTOMFORMS'); $cleanCustomFormsIds = array_filter($request->query->filter('ids', [], \FILTER_DEFAULT, [ 'flags' => \FILTER_FORCE_ARRAY @@ -111,7 +105,7 @@ public function listAction(Request $request) * @param array|\Traversable $customForms * @return array */ - private function normalizeCustomForms($customForms) + private function normalizeCustomForms(iterable $customForms): array { $customFormsArray = []; diff --git a/src/AjaxControllers/AjaxDocumentsExplorerController.php b/src/AjaxControllers/AjaxDocumentsExplorerController.php index c816d16a..5936a864 100644 --- a/src/AjaxControllers/AjaxDocumentsExplorerController.php +++ b/src/AjaxControllers/AjaxDocumentsExplorerController.php @@ -11,31 +11,18 @@ use RZ\Roadiz\Documents\UrlGenerators\DocumentUrlGeneratorInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Themes\Rozier\Models\DocumentModel; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxDocumentsExplorerController extends AbstractAjaxController { - private RendererInterface $renderer; - private DocumentUrlGeneratorInterface $documentUrlGenerator; - private UrlGeneratorInterface $urlGenerator; - private EmbedFinderFactory $embedFinderFactory; - public function __construct( - RendererInterface $renderer, - DocumentUrlGeneratorInterface $documentUrlGenerator, - UrlGeneratorInterface $urlGenerator, - EmbedFinderFactory $embedFinderFactory + private readonly RendererInterface $renderer, + private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly EmbedFinderFactory $embedFinderFactory ) { - $this->renderer = $renderer; - $this->documentUrlGenerator = $documentUrlGenerator; - $this->urlGenerator = $urlGenerator; - $this->embedFinderFactory = $embedFinderFactory; } public static array $thumbnailArray = [ @@ -43,12 +30,8 @@ public function __construct( "quality" => 50, "inline" => false, ]; - /** - * @param Request $request - * - * @return Response JSON response - */ - public function indexAction(Request $request) + + public function indexAction(Request $request): JsonResponse { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); @@ -111,7 +94,7 @@ public function indexAction(Request $request) * @param Request $request * @return JsonResponse */ - public function listAction(Request $request) + public function listAction(Request $request): JsonResponse { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); diff --git a/src/AjaxControllers/AjaxEntitiesExplorerController.php b/src/AjaxControllers/AjaxEntitiesExplorerController.php index 4557121d..9ce0e969 100644 --- a/src/AjaxControllers/AjaxEntitiesExplorerController.php +++ b/src/AjaxControllers/AjaxEntitiesExplorerController.php @@ -18,7 +18,7 @@ use Symfony\Component\Config\Definition\Processor; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Exception\InvalidParameterException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Yaml\Yaml; use Themes\Rozier\Explorer\ConfigurableExplorerItem; @@ -26,26 +26,14 @@ use Themes\Rozier\Explorer\SettingExplorerItem; use Themes\Rozier\Explorer\UserExplorerItem; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxEntitiesExplorerController extends AbstractAjaxController { - private RendererInterface $renderer; - private DocumentUrlGeneratorInterface $documentUrlGenerator; - private UrlGeneratorInterface $urlGenerator; - private EmbedFinderFactory $embedFinderFactory; - public function __construct( - RendererInterface $renderer, - DocumentUrlGeneratorInterface $documentUrlGenerator, - UrlGeneratorInterface $urlGenerator, - EmbedFinderFactory $embedFinderFactory + private readonly RendererInterface $renderer, + private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly EmbedFinderFactory $embedFinderFactory ) { - $this->renderer = $renderer; - $this->documentUrlGenerator = $documentUrlGenerator; - $this->urlGenerator = $urlGenerator; - $this->embedFinderFactory = $embedFinderFactory; } /** @@ -58,7 +46,7 @@ protected function getFieldConfiguration(NodeTypeField $nodeTypeField): array $nodeTypeField->getType() !== AbstractField::MANY_TO_MANY_T && $nodeTypeField->getType() !== AbstractField::MANY_TO_ONE_T ) { - throw new InvalidParameterException('nodeTypeField is not a valid entity join.'); + throw new BadRequestHttpException('nodeTypeField is not a valid entity join.'); } $configs = [ @@ -75,11 +63,16 @@ public function indexAction(Request $request): JsonResponse $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); if (!$request->query->has('nodeTypeFieldId')) { - throw new InvalidParameterException('nodeTypeFieldId parameter is missing.'); + throw new BadRequestHttpException('nodeTypeFieldId parameter is missing.'); } - /** @var NodeTypeField $nodeTypeField */ + /** @var NodeTypeField|null $nodeTypeField */ $nodeTypeField = $this->em()->find(NodeTypeField::class, $request->query->get('nodeTypeFieldId')); + + if (null === $nodeTypeField) { + throw new BadRequestHttpException('nodeTypeField does not exist.'); + } + $configuration = $this->getFieldConfiguration($nodeTypeField); /** @var class-string $className */ $className = $configuration['classname']; @@ -121,29 +114,28 @@ public function indexAction(Request $request): JsonResponse ); } - /** - * Get a Node list from an array of id. - * - * @param Request $request - * @return JsonResponse - */ public function listAction(Request $request): JsonResponse { if (!$request->query->has('nodeTypeFieldId')) { - throw new InvalidParameterException('nodeTypeFieldId parameter is missing.'); + throw new BadRequestHttpException('nodeTypeFieldId parameter is missing.'); } if (!$request->query->has('ids')) { - throw new InvalidParameterException('Ids should be provided within an array'); + throw new BadRequestHttpException('Ids should be provided within an array'); } - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); /** @var EntityManager $em */ $em = $this->em(); - /** @var NodeTypeField $nodeTypeField */ + /** @var NodeTypeField|null $nodeTypeField */ $nodeTypeField = $this->em()->find(NodeTypeField::class, $request->query->get('nodeTypeFieldId')); + + if (null === $nodeTypeField) { + throw new BadRequestHttpException('nodeTypeField does not exist.'); + } + $configuration = $this->getFieldConfiguration($nodeTypeField); /** @var class-string $className */ $className = $configuration['classname']; diff --git a/src/AjaxControllers/AjaxExplorerProviderController.php b/src/AjaxControllers/AjaxExplorerProviderController.php index b9ee2e04..3ea4fe98 100644 --- a/src/AjaxControllers/AjaxExplorerProviderController.php +++ b/src/AjaxControllers/AjaxExplorerProviderController.php @@ -4,7 +4,9 @@ namespace Themes\Rozier\AjaxControllers; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use RZ\Roadiz\CoreBundle\Explorer\AbstractExplorerProvider; use RZ\Roadiz\CoreBundle\Explorer\ExplorerItemInterface; use RZ\Roadiz\CoreBundle\Explorer\ExplorerProviderInterface; @@ -13,23 +15,17 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\InvalidParameterException; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxExplorerProviderController extends AbstractAjaxController { - private ContainerInterface $psrContainer; - - public function __construct(ContainerInterface $psrContainer) + public function __construct(private readonly ContainerInterface $psrContainer) { - $this->psrContainer = $psrContainer; } /** - * @param class-string $providerClass + * @param class-string $providerClass * @return ExplorerProviderInterface - * @throws \Psr\Container\ContainerExceptionInterface - * @throws \Psr\Container\NotFoundExceptionInterface + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ protected function getProvider(string $providerClass): ExplorerProviderInterface { @@ -38,27 +34,47 @@ protected function getProvider(string $providerClass): ExplorerProviderInterface } return new $providerClass(); } + /** - * @param Request $request - * @return Response JSON response + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function indexAction(Request $request) + protected function getProviderFromRequest(Request $request): ExplorerProviderInterface { - $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); + /** @var class-string|null $providerClass */ + $providerClass = $request->query->get('providerClass'); - if (!$request->query->has('providerClass')) { + if (!\is_string($providerClass)) { throw new InvalidParameterException('providerClass parameter is missing.'); } - - $providerClass = $request->query->get('providerClass'); - if (!class_exists($providerClass)) { + if (!\class_exists($providerClass)) { throw new InvalidParameterException('providerClass is not a valid class.'); } + $reflection = new \ReflectionClass($providerClass); + if (!$reflection->implementsInterface(ExplorerProviderInterface::class)) { + throw new InvalidParameterException('providerClass is not a valid ExplorerProviderInterface class.'); + } + $provider = $this->getProvider($providerClass); if ($provider instanceof AbstractExplorerProvider) { $provider->setContainer($this->psrContainer); } + + return $provider; + } + + /** + * @param Request $request + * @return JsonResponse + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function indexAction(Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); + + $provider = $this->getProviderFromRequest($request); $options = [ 'page' => $request->query->get('page') ?: 1, 'itemPerPage' => $request->query->get('itemPerPage') ?: 30, @@ -99,28 +115,14 @@ public function indexAction(Request $request) * * @param Request $request * @return JsonResponse + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function listAction(Request $request) + public function listAction(Request $request): JsonResponse { - if (!$request->query->has('providerClass')) { - throw new InvalidParameterException('providerClass parameter is missing.'); - } - - $providerClass = $request->query->get('providerClass'); - if (!class_exists($providerClass)) { - throw new InvalidParameterException('providerClass is not a valid class.'); - } - - if (!$request->query->has('ids')) { - throw new InvalidParameterException('Ids should be provided within an array'); - } - $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); - $provider = $this->getProvider($providerClass); - if ($provider instanceof AbstractExplorerProvider) { - $provider->setContainer($this->psrContainer); - } + $provider = $this->getProviderFromRequest($request); $entitiesArray = []; $cleanNodeIds = array_filter($request->query->filter('ids', [], \FILTER_DEFAULT, [ 'flags' => \FILTER_FORCE_ARRAY diff --git a/src/AjaxControllers/AjaxFolderTreeController.php b/src/AjaxControllers/AjaxFolderTreeController.php index c0c2dbe0..1d934a54 100644 --- a/src/AjaxControllers/AjaxFolderTreeController.php +++ b/src/AjaxControllers/AjaxFolderTreeController.php @@ -10,16 +10,10 @@ use Themes\Rozier\Widgets\FolderTreeWidget; use Themes\Rozier\Widgets\TreeWidgetFactory; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxFolderTreeController extends AbstractAjaxController { - private TreeWidgetFactory $treeWidgetFactory; - - public function __construct(TreeWidgetFactory $treeWidgetFactory) + public function __construct(private readonly TreeWidgetFactory $treeWidgetFactory) { - $this->treeWidgetFactory = $treeWidgetFactory; } public function getTreeAction(Request $request): JsonResponse diff --git a/src/AjaxControllers/AjaxFoldersController.php b/src/AjaxControllers/AjaxFoldersController.php index 9c44aa1d..ffa230ad 100644 --- a/src/AjaxControllers/AjaxFoldersController.php +++ b/src/AjaxControllers/AjaxFoldersController.php @@ -11,28 +11,17 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxFoldersController extends AbstractAjaxController { - private HandlerFactoryInterface $handlerFactory; - - public function __construct(HandlerFactoryInterface $handlerFactory) + public function __construct(private readonly HandlerFactoryInterface $handlerFactory) { - $this->handlerFactory = $handlerFactory; } - /** + /* * Handle AJAX edition requests for Folder * such as coming from tag-tree widgets. - * - * @param Request $request - * @param int $folderId - * - * @return Response JSON response */ - public function editAction(Request $request, int $folderId) + public function editAction(Request $request, int $folderId): JsonResponse { $this->validateRequest($request); $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); @@ -84,7 +73,7 @@ public function editAction(Request $request, int $folderId) * @param Request $request * @return JsonResponse */ - public function searchAction(Request $request) + public function searchAction(Request $request): JsonResponse { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); @@ -114,11 +103,7 @@ public function searchAction(Request $request) throw $this->createNotFoundException($this->getTranslator()->trans('no.folder.found')); } - /** - * @param array $parameters - * @param Folder $folder - */ - protected function updatePosition($parameters, Folder $folder): void + protected function updatePosition(array $parameters, Folder $folder): void { /* * First, we set the new parent diff --git a/src/AjaxControllers/AjaxFoldersExplorerController.php b/src/AjaxControllers/AjaxFoldersExplorerController.php index 3617a29a..8133a0a0 100644 --- a/src/AjaxControllers/AjaxFoldersExplorerController.php +++ b/src/AjaxControllers/AjaxFoldersExplorerController.php @@ -9,17 +9,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxFoldersExplorerController extends AbstractAjaxController { - /** - * @param Request $request - * - * @return Response JSON response - */ - public function indexAction(Request $request) + public function indexAction(Request $request): JsonResponse { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); diff --git a/src/AjaxControllers/AjaxNodeTreeController.php b/src/AjaxControllers/AjaxNodeTreeController.php index 88e75a43..313eff55 100644 --- a/src/AjaxControllers/AjaxNodeTreeController.php +++ b/src/AjaxControllers/AjaxNodeTreeController.php @@ -14,15 +14,12 @@ use Themes\Rozier\Widgets\NodeTreeWidget; use Themes\Rozier\Widgets\TreeWidgetFactory; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxNodeTreeController extends AbstractAjaxController { public function __construct( - private NodeChrootResolver $nodeChrootResolver, - private TreeWidgetFactory $treeWidgetFactory, - private NodeTypes $nodeTypesBag + private readonly NodeChrootResolver $nodeChrootResolver, + private readonly TreeWidgetFactory $treeWidgetFactory, + private readonly NodeTypes $nodeTypesBag ) { } @@ -96,7 +93,7 @@ public function getTreeAction(Request $request): JsonResponse $parent = $this->nodeChrootResolver->getChroot($this->getUser()); } - $nodeTree = $this->treeWidgetFactory->createNodeTree($parent, $translation); + $nodeTree = $this->treeWidgetFactory->createRootNodeTree($parent, $translation); $this->assignation['mainNodeTree'] = true; break; } diff --git a/src/AjaxControllers/AjaxNodeTypeFieldsController.php b/src/AjaxControllers/AjaxNodeTypeFieldsController.php index e2b34626..d825f480 100644 --- a/src/AjaxControllers/AjaxNodeTypeFieldsController.php +++ b/src/AjaxControllers/AjaxNodeTypeFieldsController.php @@ -8,9 +8,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxNodeTypeFieldsController extends AjaxAbstractFieldsController { /** diff --git a/src/AjaxControllers/AjaxNodeTypesController.php b/src/AjaxControllers/AjaxNodeTypesController.php index 183b482c..e2f2a736 100644 --- a/src/AjaxControllers/AjaxNodeTypesController.php +++ b/src/AjaxControllers/AjaxNodeTypesController.php @@ -5,6 +5,7 @@ namespace Themes\Rozier\AjaxControllers; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Exception\NotSupported; use RZ\Roadiz\CoreBundle\Entity\NodeType; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -12,9 +13,6 @@ use Symfony\Component\Routing\Exception\InvalidParameterException; use Themes\Rozier\Models\NodeTypeModel; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxNodeTypesController extends AbstractAjaxController { /** @@ -22,7 +20,7 @@ class AjaxNodeTypesController extends AbstractAjaxController * * @return Response JSON response */ - public function indexAction(Request $request) + public function indexAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); $arrayFilter = []; @@ -59,8 +57,9 @@ public function indexAction(Request $request) * * @param Request $request * @return JsonResponse + * @throws NotSupported */ - public function listAction(Request $request) + public function listAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); @@ -101,7 +100,7 @@ public function listAction(Request $request) * @param array|\Traversable $nodeTypes * @return array */ - private function normalizeNodeType($nodeTypes) + private function normalizeNodeType(iterable $nodeTypes): array { $nodeTypesArray = []; diff --git a/src/AjaxControllers/AjaxNodesController.php b/src/AjaxControllers/AjaxNodesController.php index e8d7bb14..fdf2e5e2 100644 --- a/src/AjaxControllers/AjaxNodesController.php +++ b/src/AjaxControllers/AjaxNodesController.php @@ -5,7 +5,6 @@ namespace Themes\Rozier\AjaxControllers; use Psr\Log\LoggerInterface; -use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Tag; use RZ\Roadiz\CoreBundle\Event\Node\NodeCreatedEvent; @@ -19,38 +18,25 @@ use RZ\Roadiz\CoreBundle\Node\NodeMover; use RZ\Roadiz\CoreBundle\Node\NodeNamePolicyInterface; use RZ\Roadiz\CoreBundle\Node\UniqueNodeGenerator; +use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Workflow\Registry; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxNodesController extends AbstractAjaxController { - private NodeNamePolicyInterface $nodeNamePolicy; - private LoggerInterface $logger; - private NodeMover $nodeMover; - private NodeChrootResolver $nodeChrootResolver; - private Registry $workflowRegistry; - private UniqueNodeGenerator $uniqueNodeGenerator; - public function __construct( - NodeNamePolicyInterface $nodeNamePolicy, - LoggerInterface $logger, - NodeMover $nodeMover, - NodeChrootResolver $nodeChrootResolver, - Registry $workflowRegistry, - UniqueNodeGenerator $uniqueNodeGenerator + private readonly NodeNamePolicyInterface $nodeNamePolicy, + private readonly LoggerInterface $logger, + private readonly NodeMover $nodeMover, + private readonly NodeChrootResolver $nodeChrootResolver, + private readonly Registry $workflowRegistry, + private readonly UniqueNodeGenerator $uniqueNodeGenerator ) { - $this->nodeNamePolicy = $nodeNamePolicy; - $this->logger = $logger; - $this->nodeMover = $nodeMover; - $this->nodeChrootResolver = $nodeChrootResolver; - $this->workflowRegistry = $workflowRegistry; - $this->uniqueNodeGenerator = $uniqueNodeGenerator; } /** @@ -58,12 +44,16 @@ public function __construct( * @param int $nodeId * @return JsonResponse */ - public function getTagsAction(Request $request, int $nodeId) + public function getTagsAction(Request $request, int $nodeId): JsonResponse { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); $tags = []; - /** @var Node $node */ + /** @var Node|null $node */ $node = $this->em()->find(Node::class, (int) $nodeId); + if (null === $node) { + throw new NotFoundHttpException('Node not found'); + } + + $this->denyAccessUnlessGranted(NodeVoter::READ, $node); /** @var Tag $tag */ foreach ($node->getTags() as $tag) { @@ -80,83 +70,77 @@ public function getTagsAction(Request $request, int $nodeId) * such as coming from node-tree widgets. * * @param Request $request - * @param int $nodeId + * @param int|string $nodeId * * @return Response JSON response */ - public function editAction(Request $request, $nodeId) + public function editAction(Request $request, int|string $nodeId): Response { - /* - * Validate - */ $this->validateRequest($request); - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); /** @var Node|null $node */ $node = $this->em()->find(Node::class, (int) $nodeId); - if ($node !== null) { - $responseArray = null; - - /* - * Get the right update method against "_action" parameter - */ - switch ($request->get('_action')) { - case 'updatePosition': - $this->updatePosition($request->request->all(), $node); - break; - case 'duplicate': - $duplicator = new NodeDuplicator( - $node, - $this->em(), - $this->nodeNamePolicy - ); - $newNode = $duplicator->duplicate(); - /* - * Dispatch event - */ - $this->dispatchEvent(new NodeCreatedEvent($newNode)); - $this->dispatchEvent(new NodeDuplicatedEvent($newNode)); - - $msg = $this->getTranslator()->trans('duplicated.node.%name%', [ - '%name%' => $node->getNodeName(), - ]); - $this->logger->info($msg, ['source' => $newNode->getNodeSources()->first()]); - - $responseArray = [ - 'statusCode' => '200', - 'status' => 'success', - 'responseText' => $msg, - ]; - break; - } - - if ($responseArray === null) { + if (null === $node) { + throw $this->createNotFoundException($this->getTranslator()->trans('node.%nodeId%.not_exists', [ + '%nodeId%' => $nodeId, + ])); + } + /* + * Get the right update method against "_action" parameter + */ + switch ($request->get('_action')) { + case 'updatePosition': + $this->denyAccessUnlessGranted(NodeVoter::EDIT_SETTING, $node); + $this->updatePosition($request->request->all(), $node); $responseArray = [ 'statusCode' => '200', 'status' => 'success', - 'responseText' => $this->getTranslator()->trans('node.%name%.updated', [ + 'responseText' => $this->getTranslator()->trans('node.%name%.was_moved', [ '%name%' => $node->getNodeName(), ]), ]; - } + break; + case 'duplicate': + $this->denyAccessUnlessGranted(NodeVoter::DUPLICATE, $node); + $duplicator = new NodeDuplicator( + $node, + $this->em(), + $this->nodeNamePolicy + ); + $newNode = $duplicator->duplicate(); + /* + * Dispatch event + */ + $this->dispatchEvent(new NodeCreatedEvent($newNode)); + $this->dispatchEvent(new NodeDuplicatedEvent($newNode)); - return new JsonResponse( - $responseArray, - Response::HTTP_PARTIAL_CONTENT - ); + $msg = $this->getTranslator()->trans('duplicated.node.%name%', [ + '%name%' => $node->getNodeName(), + ]); + $this->logger->info($msg, ['entity' => $newNode->getNodeSources()->first()]); + + $responseArray = [ + 'statusCode' => '200', + 'status' => 'success', + 'responseText' => $msg, + ]; + break; + default: + throw new BadRequestHttpException('Action is not defined.'); } - throw $this->createNotFoundException($this->getTranslator()->trans('node.%nodeId%.not_exists', [ - '%nodeId%' => $nodeId, - ])); + return new JsonResponse( + $responseArray, + Response::HTTP_PARTIAL_CONTENT + ); } /** * @param array $parameters * @param Node $node */ - protected function updatePosition($parameters, Node $node): void + protected function updatePosition(array $parameters, Node $node): void { if ($node->isLocked()) { throw new BadRequestHttpException('Locked node cannot be moved.'); @@ -195,6 +179,11 @@ protected function updatePosition($parameters, Node $node): void $this->dispatchEvent(new NodesSourcesUpdatedEvent($nodeSource)); } + $msg = $this->getTranslator()->trans('node.%name%.was_moved', [ + '%name%' => $node->getNodeName(), + ]); + $this->logger->info($msg, ['entity' => $node->getNodeSources()->first() ?: $node]); + $this->em()->flush(); } @@ -250,15 +239,11 @@ protected function parsePosition(array $parameters, float $default = 0.0): float * Update node's status. * * @param Request $request - * * @return JsonResponse - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException */ public function statusesAction(Request $request): JsonResponse { $this->validateRequest($request); - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); if ($request->get('nodeId', 0) <= 0) { throw new BadRequestHttpException($this->getTranslator()->trans('node.id.not_specified')); @@ -272,6 +257,8 @@ public function statusesAction(Request $request): JsonResponse ])); } + $this->denyAccessUnlessGranted(NodeVoter::EDIT_STATUS, $node); + $availableStatuses = [ 'visible' => 'setVisible', 'locked' => 'setLocked', @@ -310,14 +297,14 @@ public function statusesAction(Request $request): JsonResponse '%name%' => $node->getNodeName(), '%visible%' => $node->isVisible() ? $this->getTranslator()->trans('visible') : $this->getTranslator()->trans('invisible'), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); $this->dispatchEvent(new NodeVisibilityChangedEvent($node)); } else { $msg = $this->getTranslator()->trans('node.%name%.%field%.updated', [ '%name%' => $node->getNodeName(), '%field%' => $request->get('statusName'), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); } $this->dispatchEvent(new NodeUpdatedEvent($node)); $this->em()->flush(); @@ -361,7 +348,7 @@ protected function changeNodeStatus(Node $node, string $transition): JsonRespons '%name%' => $node->getNodeName(), '%status%' => $this->getTranslator()->trans(Node::getStatusLabel($node->getStatus())), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); return new JsonResponse( [ @@ -385,9 +372,9 @@ public function quickAddAction(Request $request): JsonResponse * Validate */ $this->validateRequest($request); - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); try { + // Access security is handled by UniqueNodeGenerator $source = $this->uniqueNodeGenerator->generateFromRequest($request); /* diff --git a/src/AjaxControllers/AjaxNodesExplorerController.php b/src/AjaxControllers/AjaxNodesExplorerController.php index 834fc240..7efcfcd4 100644 --- a/src/AjaxControllers/AjaxNodesExplorerController.php +++ b/src/AjaxControllers/AjaxNodesExplorerController.php @@ -14,34 +14,27 @@ use RZ\Roadiz\CoreBundle\EntityApi\NodeTypeApi; use RZ\Roadiz\CoreBundle\SearchEngine\ClientRegistry; use RZ\Roadiz\CoreBundle\SearchEngine\NodeSourceSearchHandlerInterface; +use RZ\Roadiz\CoreBundle\SearchEngine\SolrSearchResultItem; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Bundle\SecurityBundle\Security; use Themes\Rozier\Models\NodeModel; use Themes\Rozier\Models\NodeSourceModel; -class AjaxNodesExplorerController extends AbstractAjaxController +final class AjaxNodesExplorerController extends AbstractAjaxController { - private SerializerInterface $serializer; - private ClientRegistry $clientRegistry; - private NodeSourceSearchHandlerInterface $nodeSourceSearchHandler; - private NodeTypeApi $nodeTypeApi; - private UrlGeneratorInterface $urlGenerator; - public function __construct( - SerializerInterface $serializer, - ClientRegistry $clientRegistry, - NodeSourceSearchHandlerInterface $nodeSourceSearchHandler, - NodeTypeApi $nodeTypeApi, - UrlGeneratorInterface $urlGenerator + private readonly SerializerInterface $serializer, + private readonly ClientRegistry $clientRegistry, + private readonly NodeSourceSearchHandlerInterface $nodeSourceSearchHandler, + private readonly NodeTypeApi $nodeTypeApi, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly Security $security, ) { - $this->nodeSourceSearchHandler = $nodeSourceSearchHandler; - $this->nodeTypeApi = $nodeTypeApi; - $this->serializer = $serializer; - $this->urlGenerator = $urlGenerator; - $this->clientRegistry = $clientRegistry; } protected function getItemPerPage(): int @@ -61,7 +54,8 @@ protected function isSearchEngineAvailable(Request $request): bool */ public function indexAction(Request $request): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + // Only requires Search permission for nodes + $this->denyAccessUnlessGranted(NodeVoter::SEARCH); $criteria = $this->parseFilterFromRequest($request); $sorting = $this->parseSortingFromRequest($request); @@ -191,7 +185,6 @@ protected function getSolrSearchResults( $arrayFilter, $this->getItemPerPage(), true, - 2, (int) $currentPage ); $pageCount = ceil($results->getResultCount() / $this->getItemPerPage()); @@ -221,7 +214,8 @@ protected function getSolrSearchResults( */ public function listAction(Request $request): JsonResponse { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + // Only requires Search permission for nodes + $this->denyAccessUnlessGranted(NodeVoter::SEARCH); if (!$request->query->has('ids')) { throw new InvalidParameterException('Ids should be provided within an array'); @@ -256,32 +250,38 @@ public function listAction(Request $request): JsonResponse /** * Normalize response Node list result. * - * @param array|\Traversable $nodes + * @param iterable $nodes * @return array */ - private function normalizeNodes($nodes) + private function normalizeNodes(iterable $nodes): array { $nodesArray = []; foreach ($nodes as $node) { - if (null !== $node) { - if ($node instanceof NodesSources) { - if (!key_exists($node->getNode()->getId(), $nodesArray)) { - $nodeModel = new NodeSourceModel($node, $this->urlGenerator); - $nodesArray[$node->getNode()->getId()] = $nodeModel->toArray(); - } - } else { - if (!key_exists($node->getId(), $nodesArray)) { - $nodeModel = new NodeModel($node, $this->urlGenerator); - $nodesArray[$node->getId()] = $nodeModel->toArray(); - } + if ($node instanceof SolrSearchResultItem) { + $item = $node->getItem(); + if ($item instanceof NodesSources || $item instanceof Node) { + $this->normalizeItem($item, $nodesArray); } + } else { + $this->normalizeItem($node, $nodesArray); } } return array_values($nodesArray); } + private function normalizeItem(NodesSources|Node $item, array &$nodesArray): void + { + if ($item instanceof NodesSources && !key_exists($item->getNode()->getId(), $nodesArray)) { + $nodeSourceModel = new NodeSourceModel($item, $this->urlGenerator, $this->security); + $nodesArray[$item->getNode()->getId()] = $nodeSourceModel->toArray(); + } elseif ($item instanceof Node && !key_exists($item->getId(), $nodesArray)) { + $nodeModel = new NodeModel($item, $this->urlGenerator, $this->security); + $nodesArray[$item->getId()] = $nodeModel->toArray(); + } + } + /** * @param array $data * @return JsonResponse diff --git a/src/AjaxControllers/AjaxSearchNodesSourcesController.php b/src/AjaxControllers/AjaxSearchNodesSourcesController.php index 894c7145..b8c34594 100644 --- a/src/AjaxControllers/AjaxSearchNodesSourcesController.php +++ b/src/AjaxControllers/AjaxSearchNodesSourcesController.php @@ -8,36 +8,35 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\SearchEngine\GlobalNodeSourceSearchHandler; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use RZ\Roadiz\Documents\UrlGenerators\DocumentUrlGeneratorInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Bundle\SecurityBundle\Security; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxSearchNodesSourcesController extends AbstractAjaxController { - public const RESULT_COUNT = 8; - private DocumentUrlGeneratorInterface $documentUrlGenerator; + public const RESULT_COUNT = 10; - public function __construct(DocumentUrlGeneratorInterface $documentUrlGenerator) - { - $this->documentUrlGenerator = $documentUrlGenerator; + public function __construct( + private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, + private readonly Security $security + ) { } /** * Handle AJAX edition requests for Node - * such as coming from nodetree widgets. + * such as coming from node-tree widgets. * * @param Request $request * * @return Response JSON response */ - public function searchAction(Request $request) + public function searchAction(Request $request): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + $this->denyAccessUnlessGranted(NodeVoter::SEARCH); if (!$request->query->has('searchTerms') || $request->query->get('searchTerms') == '') { throw new BadRequestHttpException('searchTerms parameter is missing.'); @@ -46,13 +45,13 @@ public function searchAction(Request $request) $searchHandler = new GlobalNodeSourceSearchHandler($this->em()); $searchHandler->setDisplayNonPublishedNodes(true); - /** @var array $nodesSources */ + /** @var array $nodesSources */ $nodesSources = $searchHandler->getNodeSourcesBySearchTerm( $request->get('searchTerms'), static::RESULT_COUNT ); - if (null !== $nodesSources && count($nodesSources) > 0) { + if (count($nodesSources) > 0) { $responseArray = [ 'statusCode' => Response::HTTP_OK, 'status' => 'success', @@ -63,6 +62,7 @@ public function searchAction(Request $request) foreach ($nodesSources as $source) { if ( $source instanceof NodesSources && + $this->security->isGranted(NodeVoter::READ, $source) && !key_exists($source->getNode()->getId(), $responseArray['data']) ) { $responseArray['data'][$source->getNode()->getId()] = $this->getNodeSourceData($source); diff --git a/src/AjaxControllers/AjaxTagTreeController.php b/src/AjaxControllers/AjaxTagTreeController.php index f56f1f47..4712b013 100644 --- a/src/AjaxControllers/AjaxTagTreeController.php +++ b/src/AjaxControllers/AjaxTagTreeController.php @@ -10,12 +10,9 @@ use Themes\Rozier\Widgets\TagTreeWidget; use Themes\Rozier\Widgets\TreeWidgetFactory; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxTagTreeController extends AbstractAjaxController { - public function __construct(private TreeWidgetFactory $treeWidgetFactory) + public function __construct(private readonly TreeWidgetFactory $treeWidgetFactory) { } diff --git a/src/AjaxControllers/AjaxTagsController.php b/src/AjaxControllers/AjaxTagsController.php index 493b55e0..08a4a2cb 100644 --- a/src/AjaxControllers/AjaxTagsController.php +++ b/src/AjaxControllers/AjaxTagsController.php @@ -19,18 +19,12 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Themes\Rozier\Models\TagModel; -/** - * @package Themes\Rozier\AjaxControllers - */ class AjaxTagsController extends AbstractAjaxController { - private HandlerFactoryInterface $handlerFactory; - private UrlGeneratorInterface $urlGenerator; - - public function __construct(HandlerFactoryInterface $handlerFactory, UrlGeneratorInterface $urlGenerator) - { - $this->handlerFactory = $handlerFactory; - $this->urlGenerator = $urlGenerator; + public function __construct( + private readonly HandlerFactoryInterface $handlerFactory, + private readonly UrlGeneratorInterface $urlGenerator + ) { } /** diff --git a/src/Controllers/AbstractAdminController.php b/src/Controllers/AbstractAdminController.php index e6574ce3..2d73a9ba 100644 --- a/src/Controllers/AbstractAdminController.php +++ b/src/Controllers/AbstractAdminController.php @@ -8,33 +8,25 @@ use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializerInterface; use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; +use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; use Symfony\Component\Form\Exception\InvalidConfigurationException; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\String\UnicodeString; use Symfony\Contracts\EventDispatcher\Event; use Themes\Rozier\RozierApp; -use Themes\Rozier\Utils\SessionListFilters; abstract class AbstractAdminController extends RozierApp { public const ITEM_PER_PAGE = 20; - protected SerializerInterface $serializer; - protected UrlGeneratorInterface $urlGenerator; - - /** - * @param SerializerInterface $serializer - * @param UrlGeneratorInterface $urlGenerator - */ - public function __construct(SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator) - { - $this->serializer = $serializer; - $this->urlGenerator = $urlGenerator; + public function __construct( + protected readonly SerializerInterface $serializer, + protected readonly UrlGeneratorInterface $urlGenerator + ) { } /** @@ -68,14 +60,42 @@ protected function getRepository(): ObjectRepository return $this->em()->getRepository($this->getEntityClass()); } + /** + * @return string + */ + protected function getRequiredDeletionRole(): string + { + return $this->getRequiredRole(); + } + + protected function getRequiredListingRole(): string + { + return $this->getRequiredRole(); + } + + protected function getRequiredCreationRole(): string + { + return $this->getRequiredRole(); + } + + protected function getRequiredEditionRole(): string + { + return $this->getRequiredRole(); + } + + protected function getRequiredExportRole(): string + { + return $this->getRequiredRole(); + } + /** * @param Request $request * @return Response|null * @throws \Twig\Error\RuntimeError */ - public function defaultAction(Request $request) + public function defaultAction(Request $request): ?Response { - $this->denyAccessUnlessGranted($this->getRequiredRole()); + $this->denyAccessUnlessGranted($this->getRequiredListingRole()); $this->additionalAssignation($request); $elm = $this->createEntityListManager( @@ -104,12 +124,12 @@ public function defaultAction(Request $request) /** * @param Request $request - * @return RedirectResponse|Response|null + * @return Response|null * @throws \Twig\Error\RuntimeError */ - public function addAction(Request $request) + public function addAction(Request $request): ?Response { - $this->denyAccessUnlessGranted($this->getRequiredRole()); + $this->denyAccessUnlessGranted($this->getRequiredCreationRole()); $this->additionalAssignation($request); $item = $this->createEmptyItem($request); @@ -138,7 +158,7 @@ public function addAction(Request $request) '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) ] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $item); return $this->getPostSubmitResponse($item, true, $request); } @@ -160,9 +180,9 @@ public function addAction(Request $request) * @return Response|null * @throws \Twig\Error\RuntimeError */ - public function editAction(Request $request, $id) + public function editAction(Request $request, $id): ?Response { - $this->denyAccessUnlessGranted($this->getRequiredRole()); + $this->denyAccessUnlessGranted($this->getRequiredEditionRole()); $this->additionalAssignation($request); /** @var mixed|object|null $item */ @@ -199,7 +219,7 @@ public function editAction(Request $request, $id) '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) ] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $item); return $this->getPostSubmitResponse($item, false, $request); } @@ -217,7 +237,7 @@ public function editAction(Request $request, $id) public function exportAction(Request $request): JsonResponse { - $this->denyAccessUnlessGranted($this->getRequiredRole()); + $this->denyAccessUnlessGranted($this->getRequiredExportRole()); $this->additionalAssignation($request); $items = $this->getRepository()->findAll(); @@ -243,10 +263,10 @@ public function exportAction(Request $request): JsonResponse /** * @param Request $request * @param int|string $id Numeric ID or UUID - * @return RedirectResponse|Response|null + * @return Response|null * @throws \Twig\Error\RuntimeError */ - public function deleteAction(Request $request, $id) + public function deleteAction(Request $request, $id): ?Response { $this->denyAccessUnlessGranted($this->getRequiredDeletionRole()); $this->additionalAssignation($request); @@ -284,7 +304,7 @@ public function deleteAction(Request $request, $id) '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) ] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $item); return $this->getPostDeleteResponse($item); } @@ -328,15 +348,7 @@ abstract protected function getTemplateFolder(): string; abstract protected function getRequiredRole(): string; /** - * @return string - */ - protected function getRequiredDeletionRole(): string - { - return $this->getRequiredRole(); - } - - /** - * @return class-string + * @return class-string */ abstract protected function getEntityClass(): string; @@ -412,14 +424,26 @@ protected function getPostSubmitResponse( bool $forceDefaultEditRoute = false, ?Request $request = null ): Response { + if (null === $request) { + // Redirect to default route if no request provided + return $this->redirect($this->urlGenerator->generate( + $this->getEditRouteName(), + $this->getEditRouteParameters($item) + )); + } + + $route = $request->attributes->get('_route'); + $referrer = $request->query->get('referer'); + /* * Force redirect to avoid resending form when refreshing page */ if ( - null !== $request && $request->query->has('referer') && - (new UnicodeString($request->query->get('referer')))->startsWith('/') + \is_string($referrer) && + $referrer !== '' && + (new UnicodeString($referrer))->trim()->startsWith('/') ) { - return $this->redirect($request->query->get('referer')); + return $this->redirect($referrer); } /* @@ -427,8 +451,8 @@ protected function getPostSubmitResponse( */ if ( false === $forceDefaultEditRoute && - null !== $request && - null !== $route = $request->attributes->get('_route') + \is_string($route) && + $route !== '' ) { return $this->redirect($this->urlGenerator->generate( $route, @@ -466,21 +490,27 @@ protected function getPostDeleteResponse(PersistableInterface $item): Response } /** - * @param Event|Event[]|mixed|null $event - * @return object|object[]|null + * @template T of object|Event + * @param T|iterable|array|null $event + * @return T|iterable|array|null */ - protected function dispatchSingleOrMultipleEvent($event) + protected function dispatchSingleOrMultipleEvent(mixed $event): object|array|null { if (null === $event) { return null; } if ($event instanceof Event) { + // @phpstan-ignore-next-line return $this->dispatchEvent($event); } - if (is_iterable($event)) { + if (\is_iterable($event)) { $events = []; + /** @var T|null $singleEvent */ foreach ($event as $singleEvent) { - $events[] = $this->dispatchSingleOrMultipleEvent($singleEvent); + $returningEvent = $this->dispatchSingleOrMultipleEvent($singleEvent); + if ($returningEvent instanceof Event) { + $events[] = $returningEvent; + } } return $events; } diff --git a/src/Controllers/AbstractAdminWithBulkController.php b/src/Controllers/AbstractAdminWithBulkController.php index bbd1ed32..1efa45a5 100644 --- a/src/Controllers/AbstractAdminWithBulkController.php +++ b/src/Controllers/AbstractAdminWithBulkController.php @@ -17,15 +17,12 @@ abstract class AbstractAdminWithBulkController extends AbstractAdminController { - protected FormFactoryInterface $formFactory; - public function __construct( - FormFactoryInterface $formFactory, + protected readonly FormFactoryInterface $formFactory, SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator ) { parent::__construct($serializer, $urlGenerator); - $this->formFactory = $formFactory; } protected function additionalAssignation(Request $request): void @@ -170,7 +167,7 @@ protected function bulkAction( '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) ] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $item); } } $this->em()->flush(); diff --git a/src/Controllers/Attributes/AttributeController.php b/src/Controllers/Attributes/AttributeController.php index dcfe7c0d..49883ec6 100644 --- a/src/Controllers/Attributes/AttributeController.php +++ b/src/Controllers/Attributes/AttributeController.php @@ -11,26 +11,25 @@ use RZ\Roadiz\CoreBundle\Form\AttributeType; use RZ\Roadiz\CoreBundle\Importer\AttributeImporter; use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Themes\Rozier\Controllers\AbstractAdminController; +use Themes\Rozier\Controllers\AbstractAdminWithBulkController; +use Twig\Error\RuntimeError; -class AttributeController extends AbstractAdminController +class AttributeController extends AbstractAdminWithBulkController { - private AttributeImporter $attributeImporter; - public function __construct( - AttributeImporter $attributeImporter, + private readonly AttributeImporter $attributeImporter, + FormFactoryInterface $formFactory, SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator ) { - parent::__construct($serializer, $urlGenerator); - $this->attributeImporter = $attributeImporter; + parent::__construct($formFactory, $serializer, $urlGenerator); } - /** * @inheritDoc */ @@ -39,6 +38,11 @@ protected function supports(PersistableInterface $item): bool return $item instanceof Attribute; } + protected function getBulkDeleteRouteName(): ?string + { + return 'attributesBulkDeletePage'; + } + /** * @inheritDoc */ @@ -103,7 +107,10 @@ protected function getFormType(): string */ protected function getDefaultOrder(Request $request): array { - return ['code' => 'ASC']; + return [ + 'weight' => 'DESC', + 'code' => 'ASC', + ]; } /** @@ -136,8 +143,9 @@ protected function getEntityName(PersistableInterface $item): string /** * @param Request $request * @return Response + * @throws RuntimeError */ - public function importAction(Request $request) + public function importAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_ATTRIBUTES'); @@ -149,10 +157,21 @@ public function importAction(Request $request) $file = $form->get('file')->getData(); if ($file->isValid()) { - $serializedData = file_get_contents($file->getPathname()); + $serializedData = \file_get_contents($file->getPathname()); + if (false === $serializedData) { + throw new \RuntimeException('Cannot read uploaded file.'); + } $this->attributeImporter->import($serializedData); $this->em()->flush(); + + $msg = $this->getTranslator()->trans( + '%namespace%.imported', + [ + '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) + ] + ); + $this->publishConfirmMessage($request, $msg); return $this->redirectToRoute('attributesHomePage'); } $form->addError(new FormError($this->getTranslator()->trans('file.not_uploaded'))); diff --git a/src/Controllers/Attributes/AttributeGroupController.php b/src/Controllers/Attributes/AttributeGroupController.php index 69eeb70f..5fc541c3 100644 --- a/src/Controllers/Attributes/AttributeGroupController.php +++ b/src/Controllers/Attributes/AttributeGroupController.php @@ -10,9 +10,6 @@ use Symfony\Component\HttpFoundation\Request; use Themes\Rozier\Controllers\AbstractAdminController; -/** - * @package Themes\Rozier\Controllers\Attributes - */ class AttributeGroupController extends AbstractAdminController { /** diff --git a/src/Controllers/CacheController.php b/src/Controllers/CacheController.php index ead8047e..e71f35a2 100644 --- a/src/Controllers/CacheController.php +++ b/src/Controllers/CacheController.php @@ -14,15 +14,10 @@ final class CacheController extends RozierApp { - private LoggerInterface $logger; - private CacheClearerInterface $cacheClearer; - public function __construct( - CacheClearerInterface $cacheClearer, - LoggerInterface $logger + private readonly CacheClearerInterface $cacheClearer, + private readonly LoggerInterface $logger ) { - $this->logger = $logger; - $this->cacheClearer = $cacheClearer; } public function deleteDoctrineCache(Request $request): Response diff --git a/src/Controllers/CustomForms/CustomFormAnswersController.php b/src/Controllers/CustomForms/CustomFormAnswersController.php index 6a015479..7aea9cfe 100644 --- a/src/Controllers/CustomForms/CustomFormAnswersController.php +++ b/src/Controllers/CustomForms/CustomFormAnswersController.php @@ -16,9 +16,6 @@ use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers - */ class CustomFormAnswersController extends RozierApp { /** @@ -87,7 +84,7 @@ public function deleteAction(Request $request, int $customFormAnswerId): Respons $this->em()->flush(); $msg = $this->getTranslator()->trans('customFormAnswer.%id%.deleted', ['%id%' => $customFormAnswer->getId()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $customFormAnswer); /* * Redirect to update schema page */ diff --git a/src/Controllers/CustomForms/CustomFormFieldAttributesController.php b/src/Controllers/CustomForms/CustomFormFieldAttributesController.php index a6fac366..e5747a84 100644 --- a/src/Controllers/CustomForms/CustomFormFieldAttributesController.php +++ b/src/Controllers/CustomForms/CustomFormFieldAttributesController.php @@ -11,9 +11,6 @@ use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers - */ class CustomFormFieldAttributesController extends RozierApp { /** @@ -54,8 +51,8 @@ protected function getAnswersByGroups(iterable $answers): array /** @var CustomFormFieldAttribute $answer */ foreach ($answers as $answer) { $groupName = $answer->getCustomFormField()->getGroupName(); - if ($groupName != '') { - if (!isset($fieldsArray[$groupName])) { + if (\is_string($groupName) && $groupName !== '') { + if (!isset($fieldsArray[$groupName]) || !\is_array($fieldsArray[$groupName])) { $fieldsArray[$groupName] = []; } $fieldsArray[$groupName][] = $answer; diff --git a/src/Controllers/CustomForms/CustomFormFieldsController.php b/src/Controllers/CustomForms/CustomFormFieldsController.php index 0897579d..66e63255 100644 --- a/src/Controllers/CustomForms/CustomFormFieldsController.php +++ b/src/Controllers/CustomForms/CustomFormFieldsController.php @@ -18,9 +18,6 @@ use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers - */ class CustomFormFieldsController extends RozierApp { /** @@ -64,6 +61,7 @@ public function editAction(Request $request, int $customFormFieldId): Response /** @var CustomFormField|null $field */ $field = $this->em()->find(CustomFormField::class, $customFormFieldId); + if ($field === null) { throw new ResourceNotFoundException(); } @@ -77,7 +75,7 @@ public function editAction(Request $request, int $customFormFieldId): Response $this->em()->flush(); $msg = $this->getTranslator()->trans('customFormField.%name%.updated', ['%name%' => $field->getName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $field); /* * Redirect to update schema page @@ -108,14 +106,14 @@ public function addAction(Request $request, int $customFormId): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_CUSTOMFORMS'); - $field = new CustomFormField(); $customForm = $this->em()->find(CustomForm::class, $customFormId); - $field->setCustomForm($customForm); - if ($customForm === null) { throw new ResourceNotFoundException(); } + $field = new CustomFormField(); + $field->setCustomForm($customForm); + $this->assignation['customForm'] = $customForm; $this->assignation['field'] = $field; $form = $this->createForm(CustomFormFieldType::class, $field); @@ -130,7 +128,7 @@ public function addAction(Request $request, int $customFormId): Response 'customFormField.%name%.created', ['%name%' => $field->getName()] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $field); /* * Redirect to update schema page @@ -143,7 +141,7 @@ public function addAction(Request $request, int $customFormId): Response ); } catch (Exception $e) { $msg = $e->getMessage(); - $this->publishErrorMessage($request, $msg); + $this->publishErrorMessage($request, $msg, $field); /* * Redirect to add page */ diff --git a/src/Controllers/CustomForms/CustomFormsController.php b/src/Controllers/CustomForms/CustomFormsController.php index 7956bec1..042983c8 100644 --- a/src/Controllers/CustomForms/CustomFormsController.php +++ b/src/Controllers/CustomForms/CustomFormsController.php @@ -6,98 +6,62 @@ use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Entity\CustomForm; -use RZ\Roadiz\RozierBundle\Form\CustomFormType; use Symfony\Component\HttpFoundation\Request; -use Themes\Rozier\Controllers\AbstractAdminController; +use Themes\Rozier\Controllers\AbstractAdminWithBulkController; +use Themes\Rozier\Forms\CustomFormType; -/** - * @package Themes\Rozier\Controllers - */ -class CustomFormsController extends AbstractAdminController +class CustomFormsController extends AbstractAdminWithBulkController { - /** - * @inheritDoc - */ protected function supports(PersistableInterface $item): bool { return $item instanceof CustomForm; } - /** - * @inheritDoc - */ protected function getNamespace(): string { return 'custom-form'; } - /** - * @inheritDoc - */ protected function createEmptyItem(Request $request): PersistableInterface { return new CustomForm(); } - /** - * @inheritDoc - */ protected function getTemplateFolder(): string { return '@RoadizRozier/custom-forms'; } - /** - * @inheritDoc - */ protected function getRequiredRole(): string { return 'ROLE_ACCESS_CUSTOMFORMS'; } - /** - * @inheritDoc - */ protected function getEntityClass(): string { return CustomForm::class; } - /** - * @inheritDoc - */ protected function getFormType(): string { return CustomFormType::class; } - /** - * @inheritDoc - */ protected function getDefaultOrder(Request $request): array { return ['createdAt' => 'DESC']; } - /** - * @inheritDoc - */ protected function getDefaultRouteName(): string { return 'customFormsHomePage'; } - /** - * @inheritDoc - */ protected function getEditRouteName(): string { return 'customFormsEditPage'; } - /** - * @inheritDoc - */ protected function getEntityName(PersistableInterface $item): string { if ($item instanceof CustomForm) { @@ -105,4 +69,9 @@ protected function getEntityName(PersistableInterface $item): string } throw new \InvalidArgumentException('Item should be instance of ' . $this->getEntityClass()); } + + protected function getBulkDeleteRouteName(): ?string + { + return 'customFormsBulkDeletePage'; + } } diff --git a/src/Controllers/CustomForms/CustomFormsUtilsController.php b/src/Controllers/CustomForms/CustomFormsUtilsController.php index ba24ba04..5c293ccb 100644 --- a/src/Controllers/CustomForms/CustomFormsUtilsController.php +++ b/src/Controllers/CustomForms/CustomFormsUtilsController.php @@ -14,19 +14,10 @@ use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Themes\Rozier\RozierApp; -/** - * @package Themes\Rozier\Controllers - */ class CustomFormsUtilsController extends RozierApp { - private CustomFormAnswerSerializer $customFormAnswerSerializer; - - /** - * @param CustomFormAnswerSerializer $customFormAnswerSerializer - */ - public function __construct(CustomFormAnswerSerializer $customFormAnswerSerializer) + public function __construct(private readonly CustomFormAnswerSerializer $customFormAnswerSerializer) { - $this->customFormAnswerSerializer = $customFormAnswerSerializer; } /** @@ -123,7 +114,7 @@ public function duplicateAction(Request $request, int $id): Response '%name%' => $existingCustomForm->getDisplayName(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $newCustomForm); return $this->redirectToRoute( 'customFormsEditPage', @@ -134,9 +125,10 @@ public function duplicateAction(Request $request, int $id): Response $request, $this->getTranslator()->trans("impossible.duplicate.custom.form.%name%", [ '%name%' => $existingCustomForm->getDisplayName(), - ]) + ]), + $newCustomForm ); - $this->publishErrorMessage($request, $e->getMessage()); + $this->publishErrorMessage($request, $e->getMessage(), $existingCustomForm); return $this->redirectToRoute( 'customFormsEditPage', diff --git a/src/Controllers/DashboardController.php b/src/Controllers/DashboardController.php index 81178887..5a28614c 100644 --- a/src/Controllers/DashboardController.php +++ b/src/Controllers/DashboardController.php @@ -4,15 +4,12 @@ namespace Themes\Rozier\Controllers; -use RZ\Roadiz\CoreBundle\Entity\Log; +use RZ\Roadiz\CoreBundle\Logger\Entity\Log; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers - */ class DashboardController extends RozierApp { /** diff --git a/src/Controllers/Documents/DocumentTranslationsController.php b/src/Controllers/Documents/DocumentTranslationsController.php index 70e81f4d..e8c1138f 100644 --- a/src/Controllers/Documents/DocumentTranslationsController.php +++ b/src/Controllers/Documents/DocumentTranslationsController.php @@ -12,6 +12,7 @@ use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\Event\Document\DocumentTranslationUpdatedEvent; use Symfony\Component\Form\Extension\Core\Type\HiddenType; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; @@ -22,22 +23,19 @@ use Themes\Rozier\Traits\VersionedControllerTrait; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers\Documents - */ class DocumentTranslationsController extends RozierApp { use VersionedControllerTrait; /** * @param Request $request - * @param int $documentId - * @param int|null $translationId + * @param int $documentId + * @param int|null $translationId * * @return Response * @throws RuntimeError */ - public function editAction(Request $request, int $documentId, ?int $translationId = null) + public function editAction(Request $request, int $documentId, ?int $translationId = null): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); @@ -65,69 +63,65 @@ public function editAction(Request $request, int $documentId, ?int $translationI $documentTr = $this->createDocumentTranslation($document, $translation); } - if ($documentTr !== null && $document !== null) { - $this->assignation['document'] = $document; - $this->assignation['translation'] = $translation; - $this->assignation['documentTr'] = $documentTr; + if ($documentTr === null || $document === null) { + throw new ResourceNotFoundException(); + } - /** - * Versioning - */ - if ($this->isGranted('ROLE_ACCESS_VERSIONS')) { - if (null !== $response = $this->handleVersions($request, $documentTr)) { - return $response; - } - } + $this->assignation['document'] = $document; + $this->assignation['translation'] = $translation; + $this->assignation['documentTr'] = $documentTr; - /* - * Handle main form - */ - $form = $this->createForm(DocumentTranslationType::class, $documentTr, [ - 'referer' => $this->getRequest()->get('referer'), - 'disabled' => $this->isReadOnly, - ]); - $form->handleRequest($request); + /** + * Versioning + */ + if ($this->isGranted('ROLE_ACCESS_VERSIONS')) { + if (null !== $response = $this->handleVersions($request, $documentTr)) { + return $response; + } + } - if ($form->isSubmitted() && $form->isValid()) { - $this->onPostUpdate($documentTr, $request); + /* + * Handle main form + */ + $form = $this->createForm(DocumentTranslationType::class, $documentTr, [ + 'referer' => $this->getRequest()->get('referer'), + 'disabled' => $this->isReadOnly, + ]); + $form->handleRequest($request); - $routeParams = [ - 'documentId' => $document->getId(), - 'translationId' => $translationId, - ]; + if ($form->isSubmitted() && $form->isValid()) { + $this->onPostUpdate($documentTr, $request); - if ($form->get('referer')->getData()) { - $routeParams = array_merge($routeParams, [ - 'referer' => $form->get('referer')->getData() - ]); - } + $routeParams = [ + 'documentId' => $document->getId(), + 'translationId' => $translationId, + ]; - /* - * Force redirect to avoid resending form when refreshing page - */ - return $this->redirectToRoute( - 'documentsMetaPage', - $routeParams - ); + if ($form->get('referer')->getData()) { + $routeParams = array_merge($routeParams, [ + 'referer' => $form->get('referer')->getData() + ]); } - $this->assignation['form'] = $form->createView(); - $this->assignation['readOnly'] = $this->isReadOnly; - - return $this->render('@RoadizRozier/document-translations/edit.html.twig', $this->assignation); + /* + * Force redirect to avoid resending form when refreshing page + */ + return $this->redirectToRoute( + 'documentsMetaPage', + $routeParams + ); } - throw new ResourceNotFoundException(); + $this->assignation['form'] = $form->createView(); + $this->assignation['readOnly'] = $this->isReadOnly; + + return $this->render('@RoadizRozier/document-translations/edit.html.twig', $this->assignation); } - /** - * @param Document $document - * @param TranslationInterface $translation - * - * @return DocumentTranslation - */ - protected function createDocumentTranslation(Document $document, TranslationInterface $translation) - { + protected function createDocumentTranslation( + Document $document, + TranslationInterface $translation + ): DocumentTranslation { $dt = new DocumentTranslation(); $dt->setDocument($document); $dt->setTranslation($translation); @@ -147,7 +141,7 @@ protected function createDocumentTranslation(Document $document, TranslationInte * @return Response * @throws RuntimeError */ - public function deleteAction(Request $request, int $documentId, int $translationId) + public function deleteAction(Request $request, int $documentId, int $translationId): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS_DELETE'); @@ -179,13 +173,13 @@ public function deleteAction(Request $request, int $documentId, int $translation 'document.translation.%name%.deleted', ['%name%' => (string) $document] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $document); } catch (Exception $e) { $msg = $this->getTranslator()->trans( 'document.translation.%name%.cannot_delete', ['%name%' => (string) $document] ); - $this->publishErrorMessage($request, $msg); + $this->publishErrorMessage($request, $msg, $document); } /* * Force redirect to avoid resending form when refreshing page @@ -204,12 +198,7 @@ public function deleteAction(Request $request, int $documentId, int $translation throw new ResourceNotFoundException(); } - /** - * @param DocumentTranslation $doc - * - * @return \Symfony\Component\Form\FormInterface - */ - private function buildDeleteForm(DocumentTranslation $doc) + private function buildDeleteForm(DocumentTranslation $doc): FormInterface { $defaults = [ 'documentTranslationId' => $doc->getId(), @@ -239,15 +228,10 @@ protected function onPostUpdate(PersistableInterface $entity, Request $request): $msg = $this->getTranslator()->trans('document.translation.%name%.updated', [ '%name%' => (string) $entity->getDocument(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $entity); } } - /** - * @param PersistableInterface $entity - * - * @return Response - */ protected function getPostUpdateRedirection(PersistableInterface $entity): ?Response { if ( diff --git a/src/Controllers/Documents/DocumentsController.php b/src/Controllers/Documents/DocumentsController.php index 1acffaed..2aa339c6 100644 --- a/src/Controllers/Documents/DocumentsController.php +++ b/src/Controllers/Documents/DocumentsController.php @@ -7,7 +7,6 @@ use GuzzleHttp\Exception\RequestException; use League\Flysystem\FilesystemException; use League\Flysystem\FilesystemOperator; -use League\Flysystem\UnableToMoveFile; use Psr\Log\LoggerInterface; use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Document\DocumentFactory; @@ -20,6 +19,7 @@ use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\EntityHandler\DocumentHandler; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; +use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; use RZ\Roadiz\Documents\Events\DocumentCreatedEvent; use RZ\Roadiz\Documents\Events\DocumentDeletedEvent; use RZ\Roadiz\Documents\Events\DocumentFileUpdatedEvent; @@ -48,6 +48,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\String\UnicodeString; use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; @@ -55,23 +56,10 @@ use Themes\Rozier\Forms\DocumentEmbedType; use Themes\Rozier\Models\DocumentModel; use Themes\Rozier\RozierApp; -use Themes\Rozier\Utils\SessionListFilters; use Twig\Error\RuntimeError; class DocumentsController extends RozierApp { - private array $documentPlatforms; - private DocumentFactory $documentFactory; - private HandlerFactoryInterface $handlerFactory; - private LoggerInterface $logger; - private RandomImageFinder $randomImageFinder; - private RendererInterface $renderer; - private DocumentUrlGeneratorInterface $documentUrlGenerator; - private UrlGeneratorInterface $urlGenerator; - private FilesystemOperator $documentsStorage; - private ?string $googleServerId; - private ?string $soundcloudClientId; - protected array $thumbnailFormat = [ 'quality' => 50, 'fit' => '128x128', @@ -81,34 +69,21 @@ class DocumentsController extends RozierApp 'controls' => false, 'loading' => 'lazy', ]; - private EmbedFinderFactory $embedFinderFactory; public function __construct( - array $documentPlatforms, - FilesystemOperator $documentsStorage, - HandlerFactoryInterface $handlerFactory, - LoggerInterface $logger, - RandomImageFinder $randomImageFinder, - DocumentFactory $documentFactory, - RendererInterface $renderer, - DocumentUrlGeneratorInterface $documentUrlGenerator, - UrlGeneratorInterface $urlGenerator, - EmbedFinderFactory $embedFinderFactory, - ?string $googleServerId = null, - ?string $soundcloudClientId = null + private readonly array $documentPlatforms, + private readonly FilesystemOperator $documentsStorage, + private readonly HandlerFactoryInterface $handlerFactory, + private readonly LoggerInterface $logger, + private readonly RandomImageFinder $randomImageFinder, + private readonly DocumentFactory $documentFactory, + private readonly RendererInterface $renderer, + private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly EmbedFinderFactory $embedFinderFactory, + private readonly ?string $googleServerId = null, + private readonly ?string $soundcloudClientId = null ) { - $this->documentPlatforms = $documentPlatforms; - $this->handlerFactory = $handlerFactory; - $this->logger = $logger; - $this->randomImageFinder = $randomImageFinder; - $this->documentFactory = $documentFactory; - $this->renderer = $renderer; - $this->documentUrlGenerator = $documentUrlGenerator; - $this->urlGenerator = $urlGenerator; - $this->googleServerId = $googleServerId; - $this->soundcloudClientId = $soundcloudClientId; - $this->documentsStorage = $documentsStorage; - $this->embedFinderFactory = $embedFinderFactory; } /** @@ -141,20 +116,16 @@ public function indexAction(Request $request, ?int $folderId = null): Response $this->assignation['folder'] = $folder; } - if ( - $request->query->has('type') && - $request->query->get('type', '') !== '' - ) { - $prefilters['mimeType'] = trim($request->query->get('type', '')); - $this->assignation['mimeType'] = trim($request->query->get('type', '')); + $type = $request->query->get('type', null); + if (\is_string($type) && trim($type) !== '') { + $prefilters['mimeType'] = trim($type); + $this->assignation['mimeType'] = trim($type); } - if ( - $request->query->has('embedPlatform') && - $request->query->get('embedPlatform', '') !== '' - ) { - $prefilters['embedPlatform'] = trim($request->query->get('embedPlatform', '')); - $this->assignation['embedPlatform'] = trim($request->query->get('embedPlatform', '')); + $embedPlatform = $request->query->get('embedPlatform', null); + if (\is_string($embedPlatform) && trim($embedPlatform) !== '') { + $prefilters['embedPlatform'] = trim($embedPlatform); + $this->assignation['embedPlatform'] = trim($embedPlatform); } $this->assignation['availablePlatforms'] = $this->documentPlatforms; @@ -340,13 +311,13 @@ public function editAction(Request $request, int $documentId): Response $this->dispatchEvent( new DocumentFileUpdatedEvent($document) ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $document); } $msg = $this->getTranslator()->trans('document.%name%.updated', [ '%name%' => (string) $document, ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $document); $this->em()->flush(); // Event must be dispatched AFTER flush for async concurrency matters $this->dispatchEvent( @@ -419,13 +390,13 @@ public function deleteAction(Request $request, int $documentId): Response $msg = $this->getTranslator()->trans('document.%name%.deleted', [ '%name%' => (string) $document ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $document); } catch (\Exception $e) { $msg = $this->getTranslator()->trans('document.%name%.cannot_delete', [ '%name%' => (string) $document ]); $this->logger->error($e->getMessage()); - $this->publishErrorMessage($request, $msg); + $this->publishErrorMessage($request, $msg, $document); } /* * Force redirect to avoid resending form when refreshing page @@ -477,7 +448,7 @@ public function bulkDeleteAction(Request $request): Response 'document.%name%.deleted', ['%name%' => (string) $document] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $document); } $this->em()->flush(); @@ -523,18 +494,18 @@ public function embedAction(Request $request, ?int $folderId = null): Response if (is_iterable($document)) { foreach ($document as $singleDocument) { $msg = $this->getTranslator()->trans('document.%name%.uploaded', [ - '%name%' => (string) $singleDocument, + '%name%' => (new UnicodeString((string) $singleDocument))->truncate(50, '...')->toString(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $singleDocument); $this->dispatchEvent( new DocumentCreatedEvent($singleDocument) ); } } else { $msg = $this->getTranslator()->trans('document.%name%.uploaded', [ - '%name%' => (string) $document, + '%name%' => (new UnicodeString((string) $document))->truncate(50, '...')->toString(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $document); $this->dispatchEvent( new DocumentCreatedEvent($document) ); @@ -582,9 +553,9 @@ public function randomAction(Request $request, ?int $folderId = null): Response $document = $this->randomDocument($folderId); $msg = $this->getTranslator()->trans('document.%name%.uploaded', [ - '%name%' => (string) $document, + '%name%' => (new UnicodeString((string) $document))->truncate(50, '...')->toString(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $document); $this->dispatchEvent( new DocumentCreatedEvent($document) @@ -626,6 +597,31 @@ public function downloadAction(Request $request, int $documentId): Response throw new ResourceNotFoundException(); } + /** + * Download document file inline. + * + * @param Request $request + * @param int $documentId + * @return Response + * @throws FilesystemException + */ + public function downloadInlineAction(Request $request, int $documentId): Response + { + $this->denyAccessUnlessGranted('ROLE_ACCESS_DOCUMENTS'); + + /** @var Document|null $document */ + $document = $this->em()->find(Document::class, $documentId); + + if ($document !== null) { + /** @var DocumentHandler $handler */ + $handler = $this->handlerFactory->getHandler($document); + + return $handler->getDownloadResponse(false); + } + + throw new ResourceNotFoundException(); + } + /** * @param Request $request * @param int|null $folderId @@ -655,9 +651,9 @@ public function uploadAction(Request $request, ?int $folderId = null, string $_f if (null !== $document) { $msg = $this->getTranslator()->trans('document.%name%.uploaded', [ - '%name%' => (string) $document, + '%name%' => (new UnicodeString((string) $document))->truncate(50, '...')->toString(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $document); // Event must be dispatched AFTER flush for async concurrency matters $this->dispatchEvent( @@ -681,7 +677,7 @@ public function uploadAction(Request $request, ?int $folderId = null, string $_f } } else { $msg = $this->getTranslator()->trans('document.cannot_persist'); - $this->publishErrorMessage($request, $msg); + $this->publishErrorMessage($request, $msg, $document); if ($_format === 'json' || $request->isXmlHttpRequest()) { throw $this->createNotFoundException($msg); @@ -697,6 +693,7 @@ public function uploadAction(Request $request, ?int $folderId = null, string $_f /** @var Form $child */ foreach ($form as $child) { if ($child->isSubmitted() && !$child->isValid()) { + /** @var FormError $error */ foreach ($child->getErrors() as $error) { $errorPerForm[$child->getName()][] = $this->getTranslator()->trans($error->getMessage()); } diff --git a/src/Controllers/FoldersController.php b/src/Controllers/FoldersController.php index 44f671d1..c3e866b5 100644 --- a/src/Controllers/FoldersController.php +++ b/src/Controllers/FoldersController.php @@ -25,11 +25,8 @@ class FoldersController extends RozierApp { - private DocumentArchiver $documentArchiver; - - public function __construct(DocumentArchiver $documentArchiver) + public function __construct(private readonly DocumentArchiver $documentArchiver) { - $this->documentArchiver = $documentArchiver; } public function indexAction(Request $request): Response @@ -86,7 +83,7 @@ public function addAction(Request $request, ?int $parentFolderId = null): Respon 'folder.%name%.created', ['%name%' => $folder->getFolderName()] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $folder); /* * Dispatch event @@ -95,7 +92,7 @@ public function addAction(Request $request, ?int $parentFolderId = null): Respon new FolderCreatedEvent($folder) ); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage()); + $this->publishErrorMessage($request, $e->getMessage(), $folder); } return $this->redirectToRoute('foldersHomePage'); @@ -137,7 +134,7 @@ public function deleteAction(Request $request, int $folderId): Response 'folder.%name%.deleted', ['%name%' => $folder->getFolderName()] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $folder); /* * Dispatch event @@ -146,7 +143,7 @@ public function deleteAction(Request $request, int $folderId): Response new FolderDeletedEvent($folder) ); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage()); + $this->publishErrorMessage($request, $e->getMessage(), $folder); } return $this->redirectToRoute('foldersHomePage'); @@ -193,7 +190,7 @@ public function editAction(Request $request, int $folderId): Response 'folder.%name%.updated', ['%name%' => $folder->getFolderName()] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $folder); /* * Dispatch event */ @@ -201,7 +198,7 @@ public function editAction(Request $request, int $folderId): Response new FolderUpdatedEvent($folder) ); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage()); + $this->publishErrorMessage($request, $e->getMessage(), $folder); } return $this->redirectToRoute('foldersEditPage', ['folderId' => $folderId]); @@ -277,7 +274,7 @@ public function editTranslationAction(Request $request, int $folderId, int $tran 'folder.%name%.updated', ['%name%' => $folder->getFolderName()] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $folder); /* * Dispatch event */ @@ -285,7 +282,7 @@ public function editTranslationAction(Request $request, int $folderId, int $tran new FolderUpdatedEvent($folder) ); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage()); + $this->publishErrorMessage($request, $e->getMessage(), $folder); } return $this->redirectToRoute('foldersEditTranslationPage', [ diff --git a/src/Controllers/GroupsController.php b/src/Controllers/GroupsController.php index 61cbc768..c4b9e949 100644 --- a/src/Controllers/GroupsController.php +++ b/src/Controllers/GroupsController.php @@ -20,9 +20,6 @@ use Themes\Rozier\Forms\GroupType; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers - */ class GroupsController extends AbstractAdminController { /** @@ -150,7 +147,7 @@ public function editRolesAction(Request $request, int $id): Response '%group%' => $item->getName(), '%role%' => $role->getRole(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $role); return $this->redirectToRoute( 'groupsEditRolesPage', @@ -206,7 +203,7 @@ public function removeRolesAction(Request $request, int $id, int $roleId): Respo '%role%' => $role->getRole(), '%group%' => $item->getName(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $role); return $this->redirectToRoute( 'groupsEditRolesPage', @@ -254,7 +251,7 @@ public function editUsersAction(Request $request, int $id): Response '%group%' => $item->getName(), '%user%' => $user->getUserName(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $user); return $this->redirectToRoute( 'groupsEditUsersPage', @@ -309,7 +306,7 @@ public function removeUsersAction(Request $request, int $id, int $userId): Respo '%user%' => $user->getUserName(), '%group%' => $item->getName(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $user); return $this->redirectToRoute( 'groupsEditUsersPage', diff --git a/src/Controllers/GroupsUtilsController.php b/src/Controllers/GroupsUtilsController.php index f15b6499..25e6be3e 100644 --- a/src/Controllers/GroupsUtilsController.php +++ b/src/Controllers/GroupsUtilsController.php @@ -20,17 +20,10 @@ class GroupsUtilsController extends RozierApp { - private SerializerInterface $serializer; - private GroupsImporter $groupsImporter; - - /** - * @param SerializerInterface $serializer - * @param GroupsImporter $groupsImporter - */ - public function __construct(SerializerInterface $serializer, GroupsImporter $groupsImporter) - { - $this->serializer = $serializer; - $this->groupsImporter = $groupsImporter; + public function __construct( + private readonly SerializerInterface $serializer, + private readonly GroupsImporter $groupsImporter + ) { } /** @@ -120,6 +113,9 @@ public function importJsonFileAction(Request $request): Response if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); + if (false === $serializedData) { + throw new RuntimeError('Cannot read uploaded file.'); + } if (null !== \json_decode($serializedData)) { $this->groupsImporter->import($serializedData); diff --git a/src/Controllers/HistoryController.php b/src/Controllers/HistoryController.php index 56e7a727..acc480ca 100644 --- a/src/Controllers/HistoryController.php +++ b/src/Controllers/HistoryController.php @@ -4,12 +4,12 @@ namespace Themes\Rozier\Controllers; -use RZ\Roadiz\CoreBundle\Entity\Log; +use Monolog\Logger; use RZ\Roadiz\CoreBundle\Entity\User; +use RZ\Roadiz\CoreBundle\Logger\Entity\Log; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; -use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; @@ -19,14 +19,14 @@ class HistoryController extends RozierApp { public static array $levelToHuman = [ - Log::EMERGENCY => "emergency", - Log::CRITICAL => "critical", - Log::ALERT => "alert", - Log::ERROR => "error", - Log::WARNING => "warning", - Log::NOTICE => "notice", - Log::INFO => "info", - Log::DEBUG => "debug", + Logger::EMERGENCY => "emergency", + Logger::CRITICAL => "critical", + Logger::ALERT => "alert", + Logger::ERROR => "error", + Logger::WARNING => "warning", + Logger::NOTICE => "notice", + Logger::INFO => "info", + Logger::DEBUG => "debug", ]; /** @@ -64,20 +64,20 @@ public function indexAction(Request $request): Response * List user logs action. * * @param Request $request - * @param int $userId + * @param int|string $userId * * @return Response * @throws \Doctrine\ORM\ORMException * @throws \Doctrine\ORM\OptimisticLockException * @throws \Doctrine\ORM\TransactionRequiredException */ - public function userAction(Request $request, int $userId): Response + public function userAction(Request $request, int|string $userId): Response { $this->denyAccessUnlessGranted(['ROLE_BACKEND_USER', 'ROLE_ACCESS_LOGS']); if ( !($this->isGranted(['ROLE_ACCESS_USERS', 'ROLE_ACCESS_LOGS']) - || ($this->getUser() instanceof User && $this->getUser()->getId() == $userId)) + || ($this->getUser() instanceof User && $this->getUser()->getId() === $userId)) ) { throw $this->createAccessDeniedException("You don't have access to this page: ROLE_ACCESS_USERS"); } @@ -94,7 +94,7 @@ public function userAction(Request $request, int $userId): Response */ $listManager = $this->createEntityListManager( Log::class, - ['user' => $user], + ['userId' => $user->getId()], ['datetime' => 'DESC'] ); $listManager->setDisplayingNotPublishedNodes(true); diff --git a/src/Controllers/LoginController.php b/src/Controllers/LoginController.php index fe26ca9c..7dea01cb 100644 --- a/src/Controllers/LoginController.php +++ b/src/Controllers/LoginController.php @@ -16,9 +16,9 @@ class LoginController extends RozierApp { public function __construct( - private DocumentUrlGeneratorInterface $documentUrlGenerator, - private RandomImageFinder $randomImageFinder, - private Settings $settingsBag + private readonly DocumentUrlGeneratorInterface $documentUrlGenerator, + private readonly RandomImageFinder $randomImageFinder, + private readonly Settings $settingsBag ) { } @@ -41,10 +41,9 @@ public function imageAction(Request $request): Response 'quality' => 80, 'sharpen' => 5, ]); - $response->setData([ + return $response->setData([ 'url' => $this->documentUrlGenerator->getUrl() ]); - return $response; } } @@ -54,9 +53,8 @@ public function imageAction(Request $request): Response if (null !== $feed) { $url = $feed['url'] ?? $feed['urls']['regular'] ?? $feed['urls']['full'] ?? $feed['urls']['raw'] ?? null; } - $response->setData([ - 'url' => '/themes/Rozier/static/assets/img/default_login.jpg' + return $response->setData([ + 'url' => $url ?? '/themes/Rozier/static/assets/img/default_login.jpg' ]); - return $response; } } diff --git a/src/Controllers/NodeTypeFieldsController.php b/src/Controllers/NodeTypeFieldsController.php index 5596ab58..4bdcd44b 100644 --- a/src/Controllers/NodeTypeFieldsController.php +++ b/src/Controllers/NodeTypeFieldsController.php @@ -21,11 +21,10 @@ class NodeTypeFieldsController extends RozierApp { - private MessageBusInterface $messageBus; - - public function __construct(MessageBusInterface $messageBus) - { - $this->messageBus = $messageBus; + public function __construct( + private readonly bool $allowNodeTypeEdition, + private readonly MessageBusInterface $messageBus + ) { } /** @@ -79,21 +78,25 @@ public function editAction(Request $request, int $nodeTypeFieldId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->em()->flush(); + if (!$this->allowNodeTypeEdition) { + $form->addError(new FormError('You cannot edit node-type fields in production.')); + } else { + $this->em()->flush(); - /** @var NodeType $nodeType */ - $nodeType = $field->getNodeType(); - $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); + /** @var NodeType $nodeType */ + $nodeType = $field->getNodeType(); + $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); - $msg = $this->getTranslator()->trans('nodeTypeField.%name%.updated', ['%name%' => $field->getName()]); - $this->publishConfirmMessage($request, $msg); + $msg = $this->getTranslator()->trans('nodeTypeField.%name%.updated', ['%name%' => $field->getName()]); + $this->publishConfirmMessage($request, $msg, $field); - return $this->redirectToRoute( - 'nodeTypeFieldsEditPage', - [ - 'nodeTypeFieldId' => $nodeTypeFieldId, - ] - ); + return $this->redirectToRoute( + 'nodeTypeFieldsEditPage', + [ + 'nodeTypeFieldId' => $nodeTypeFieldId, + ] + ); + } } $this->assignation['form'] = $form->createView(); @@ -130,31 +133,37 @@ public function addAction(Request $request, int $nodeTypeId): Response $this->assignation['nodeType'] = $nodeType; $this->assignation['field'] = $field; - $form = $this->createForm(NodeTypeFieldType::class, $field); + $form = $this->createForm(NodeTypeFieldType::class, $field, [ + 'disabled' => !$this->allowNodeTypeEdition + ]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - try { - $this->em()->persist($field); - $this->em()->flush(); - $this->em()->refresh($nodeType); - - $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); - - $msg = $this->getTranslator()->trans( - 'nodeTypeField.%name%.created', - ['%name%' => $field->getName()] - ); - $this->publishConfirmMessage($request, $msg); - - return $this->redirectToRoute( - 'nodeTypeFieldsListPage', - [ - 'nodeTypeId' => $nodeTypeId, - ] - ); - } catch (Exception $e) { - $form->addError(new FormError($e->getMessage())); + if (!$this->allowNodeTypeEdition) { + $form->addError(new FormError('You cannot add node-type fields in production.')); + } else { + try { + $this->em()->persist($field); + $this->em()->flush(); + $this->em()->refresh($nodeType); + + $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); + + $msg = $this->getTranslator()->trans( + 'nodeTypeField.%name%.created', + ['%name%' => $field->getName()] + ); + $this->publishConfirmMessage($request, $msg, $field); + + return $this->redirectToRoute( + 'nodeTypeFieldsListPage', + [ + 'nodeTypeId' => $nodeTypeId, + ] + ); + } catch (Exception $e) { + $form->addError(new FormError($e->getMessage())); + } } } @@ -185,26 +194,30 @@ public function deleteAction(Request $request, int $nodeTypeFieldId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - /** @var NodeType $nodeType */ - $nodeType = $field->getNodeType(); - $nodeTypeId = $nodeType->getId(); - $this->em()->remove($field); - $this->em()->flush(); - - $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeTypeId))); - - $msg = $this->getTranslator()->trans( - 'nodeTypeField.%name%.deleted', - ['%name%' => $field->getName()] - ); - $this->publishConfirmMessage($request, $msg); - - return $this->redirectToRoute( - 'nodeTypeFieldsListPage', - [ - 'nodeTypeId' => $nodeTypeId, - ] - ); + if (!$this->allowNodeTypeEdition) { + $form->addError(new FormError('You cannot delete node-type fields in production.')); + } else { + /** @var NodeType $nodeType */ + $nodeType = $field->getNodeType(); + $nodeTypeId = $nodeType->getId(); + $this->em()->remove($field); + $this->em()->flush(); + + $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeTypeId))); + + $msg = $this->getTranslator()->trans( + 'nodeTypeField.%name%.deleted', + ['%name%' => $field->getName()] + ); + $this->publishConfirmMessage($request, $msg, $field); + + return $this->redirectToRoute( + 'nodeTypeFieldsListPage', + [ + 'nodeTypeId' => $nodeTypeId, + ] + ); + } } $this->assignation['field'] = $field; diff --git a/src/Controllers/NodeTypes/NodeTypesController.php b/src/Controllers/NodeTypes/NodeTypesController.php index c4de8822..58720914 100644 --- a/src/Controllers/NodeTypes/NodeTypesController.php +++ b/src/Controllers/NodeTypes/NodeTypesController.php @@ -6,6 +6,7 @@ use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; +use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; use RZ\Roadiz\CoreBundle\Message\DeleteNodeTypeMessage; use RZ\Roadiz\CoreBundle\Message\UpdateNodeTypeSchemaMessage; use Symfony\Component\Form\Extension\Core\Type\FormType; @@ -16,18 +17,14 @@ use Symfony\Component\Messenger\MessageBusInterface; use Themes\Rozier\Forms\NodeTypeType; use Themes\Rozier\RozierApp; -use Themes\Rozier\Utils\SessionListFilters; +use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers\NodeTypes - */ class NodeTypesController extends RozierApp { - private MessageBusInterface $messageBus; - - public function __construct(MessageBusInterface $messageBus) - { - $this->messageBus = $messageBus; + public function __construct( + private readonly bool $allowNodeTypeEdition, + private readonly MessageBusInterface $messageBus + ) { } public function indexAction(Request $request): Response @@ -58,12 +55,10 @@ public function indexAction(Request $request): Response } /** - * Return an edition form for requested node-type. - * * @param Request $request - * @param int $nodeTypeId - * + * @param int $nodeTypeId * @return Response + * @throws RuntimeError */ public function editAction(Request $request, int $nodeTypeId): Response { @@ -82,11 +77,10 @@ public function editAction(Request $request, int $nodeTypeId): Response if ($form->isSubmitted() && $form->isValid()) { try { $this->em()->flush(); - $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); $msg = $this->getTranslator()->trans('nodeType.%name%.updated', ['%name%' => $nodeType->getName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $nodeType); return $this->redirectToRoute('nodeTypesEditPage', [ 'nodeTypeId' => $nodeTypeId @@ -103,35 +97,40 @@ public function editAction(Request $request, int $nodeTypeId): Response } /** - * Return an creation form for requested node-type. - * * @param Request $request * * @return Response + * @throws RuntimeError */ public function addAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_NODETYPES'); $nodeType = new NodeType(); - $form = $this->createForm(NodeTypeType::class, $nodeType); + $form = $this->createForm(NodeTypeType::class, $nodeType, [ + 'disabled' => !$this->allowNodeTypeEdition + ]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - try { - $this->em()->persist($nodeType); - $this->em()->flush(); - - $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); - - $msg = $this->getTranslator()->trans('nodeType.%name%.created', ['%name%' => $nodeType->getName()]); - $this->publishConfirmMessage($request, $msg); - - return $this->redirectToRoute('nodeTypesEditPage', [ - 'nodeTypeId' => $nodeType->getId() - ]); - } catch (EntityAlreadyExistsException $e) { - $form->addError(new FormError($e->getMessage())); + if (!$this->allowNodeTypeEdition) { + $form->addError(new FormError('You cannot create a node-type in production mode.')); + } else { + try { + $this->em()->persist($nodeType); + $this->em()->flush(); + + $this->messageBus->dispatch(new Envelope(new UpdateNodeTypeSchemaMessage($nodeType->getId()))); + + $msg = $this->getTranslator()->trans('nodeType.%name%.created', ['%name%' => $nodeType->getName()]); + $this->publishConfirmMessage($request, $msg, $nodeType); + + return $this->redirectToRoute('nodeTypesEditPage', [ + 'nodeTypeId' => $nodeType->getId() + ]); + } catch (EntityAlreadyExistsException $e) { + $form->addError(new FormError($e->getMessage())); + } } } @@ -143,9 +142,10 @@ public function addAction(Request $request): Response /** * @param Request $request - * @param int $nodeTypeId + * @param int $nodeTypeId * * @return Response + * @throws RuntimeError */ public function deleteAction(Request $request, int $nodeTypeId): Response { @@ -162,12 +162,16 @@ public function deleteAction(Request $request, int $nodeTypeId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->messageBus->dispatch(new Envelope(new DeleteNodeTypeMessage($nodeType->getId()))); + if (!$this->allowNodeTypeEdition) { + $form->addError(new FormError('You cannot delete a node-type in production mode.')); + } else { + $this->messageBus->dispatch(new Envelope(new DeleteNodeTypeMessage($nodeType->getId()))); - $msg = $this->getTranslator()->trans('nodeType.%name%.deleted', ['%name%' => $nodeType->getName()]); - $this->publishConfirmMessage($request, $msg); + $msg = $this->getTranslator()->trans('nodeType.%name%.deleted', ['%name%' => $nodeType->getName()]); + $this->publishConfirmMessage($request, $msg, $nodeType); - return $this->redirectToRoute('nodeTypesHomePage'); + return $this->redirectToRoute('nodeTypesHomePage'); + } } $this->assignation['form'] = $form->createView(); diff --git a/src/Controllers/NodeTypes/NodeTypesUtilsController.php b/src/Controllers/NodeTypes/NodeTypesUtilsController.php index f0e656ca..059a6b13 100644 --- a/src/Controllers/NodeTypes/NodeTypesUtilsController.php +++ b/src/Controllers/NodeTypes/NodeTypesUtilsController.php @@ -29,21 +29,12 @@ class NodeTypesUtilsController extends RozierApp { - private SerializerInterface $serializer; - private NodeTypes $nodeTypesBag; - private NodeTypesImporter $nodeTypesImporter; - private MessageBusInterface $messageBus; - public function __construct( - SerializerInterface $serializer, - NodeTypes $nodeTypesBag, - NodeTypesImporter $nodeTypesImporter, - MessageBusInterface $messageBus + private readonly SerializerInterface $serializer, + private readonly NodeTypes $nodeTypesBag, + private readonly NodeTypesImporter $nodeTypesImporter, + private readonly MessageBusInterface $messageBus ) { - $this->serializer = $serializer; - $this->nodeTypesBag = $nodeTypesBag; - $this->nodeTypesImporter = $nodeTypesImporter; - $this->messageBus = $messageBus; } /** @@ -52,9 +43,9 @@ public function __construct( * @param Request $request * @param int $nodeTypeId * - * @return Response + * @return JsonResponse */ - public function exportJsonFileAction(Request $request, int $nodeTypeId): Response + public function exportJsonFileAction(Request $request, int $nodeTypeId): JsonResponse { $this->denyAccessUnlessGranted('ROLE_ACCESS_NODETYPES'); @@ -71,7 +62,7 @@ public function exportJsonFileAction(Request $request, int $nodeTypeId): Respons 'json', SerializationContext::create()->setGroups(['node_type', 'position']) ), - JsonResponse::HTTP_OK, + Response::HTTP_OK, [ 'Content-Disposition' => sprintf('attachment; filename="%s"', $nodeType->getName() . '.json'), ], @@ -81,8 +72,8 @@ public function exportJsonFileAction(Request $request, int $nodeTypeId): Respons /** * @param Request $request - * * @return BinaryFileResponse + * @throws RuntimeError */ public function exportDocumentationAction(Request $request): BinaryFileResponse { @@ -91,6 +82,10 @@ public function exportDocumentationAction(Request $request): BinaryFileResponse $documentationGenerator = new DocumentationGenerator($this->nodeTypesBag, $this->getTranslator()); $tmpfname = tempnam(sys_get_temp_dir(), date('Y-m-d-H-i-s') . '.zip'); + if (false === $tmpfname) { + throw new RuntimeError('Unable to create temporary file.'); + } + unlink($tmpfname); // Deprecated: ZipArchive::open(): Using empty file as ZipArchive is deprecated $zipArchive = new ZipArchive(); $zipArchive->open($tmpfname, ZipArchive::CREATE); @@ -147,10 +142,6 @@ public function exportTypeScriptDeclarationAction(Request $request): Response return $response; } - /** - * @param Request $request - * @return BinaryFileResponse - */ public function exportAllAction(Request $request): BinaryFileResponse { $this->denyAccessUnlessGranted('ROLE_ACCESS_NODETYPES'); @@ -161,6 +152,9 @@ public function exportAllAction(Request $request): BinaryFileResponse $zipArchive = new ZipArchive(); $tmpfname = tempnam(sys_get_temp_dir(), date('Y-m-d-H-i-s') . '.zip'); + if (false === $tmpfname) { + throw new RuntimeError('Unable to create temporary file.'); + } unlink($tmpfname); // Deprecated: ZipArchive::open(): Using empty file as ZipArchive is deprecated $zipArchive->open($tmpfname, ZipArchive::CREATE); @@ -212,6 +206,9 @@ public function importJsonFileAction(Request $request): Response if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); + if (false === $serializedData) { + throw new RuntimeError('Unable to read uploaded file.'); + } if (null !== json_decode($serializedData)) { $this->nodeTypesImporter->import($serializedData); @@ -238,7 +235,7 @@ public function importJsonFileAction(Request $request): Response /** * @return FormInterface */ - private function buildImportJsonFileForm() + private function buildImportJsonFileForm(): FormInterface { $builder = $this->createFormBuilder() ->add('node_type_file', FileType::class, [ diff --git a/src/Controllers/Nodes/ExportController.php b/src/Controllers/Nodes/ExportController.php index 61be7980..4ba71fd0 100644 --- a/src/Controllers/Nodes/ExportController.php +++ b/src/Controllers/Nodes/ExportController.php @@ -4,9 +4,11 @@ namespace Themes\Rozier\Controllers\Nodes; +use PhpOffice\PhpSpreadsheet\Writer\Exception; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use RZ\Roadiz\CoreBundle\Xlsx\NodeSourceXlsxSerializer; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -15,14 +17,8 @@ class ExportController extends RozierApp { - private NodeSourceXlsxSerializer $xlsxSerializer; - - /** - * @param NodeSourceXlsxSerializer $xlsxSerializer - */ - public function __construct(NodeSourceXlsxSerializer $xlsxSerializer) + public function __construct(private readonly NodeSourceXlsxSerializer $xlsxSerializer) { - $this->xlsxSerializer = $xlsxSerializer; } /** @@ -34,12 +30,10 @@ public function __construct(NodeSourceXlsxSerializer $xlsxSerializer) * * @return Response * @throws \PhpOffice\PhpSpreadsheet\Exception - * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + * @throws Exception */ public function exportAllXlsxAction(Request $request, int $translationId, ?int $parentNodeId = null): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); - /* * Get translation */ @@ -61,8 +55,11 @@ public function exportAllXlsxAction(Request $request, int $translationId, ?int $ if (null === $parentNode) { throw $this->createNotFoundException(); } + $this->denyAccessUnlessGranted(NodeVoter::READ, $parentNode); $criteria['node.parent'] = $parentNode; $filename = $parentNode->getNodeName() . '-' . date("YmdHis") . '.' . $translation->getLocale() . '.xlsx'; + } else { + $this->denyAccessUnlessGranted(NodeVoter::READ_AT_ROOT); } $sources = $this->em() diff --git a/src/Controllers/Nodes/HistoryController.php b/src/Controllers/Nodes/HistoryController.php index 54ef618d..6915d67e 100644 --- a/src/Controllers/Nodes/HistoryController.php +++ b/src/Controllers/Nodes/HistoryController.php @@ -4,19 +4,19 @@ namespace Themes\Rozier\Controllers\Nodes; -use RZ\Roadiz\CoreBundle\Entity\Log; +use Doctrine\ORM\QueryBuilder; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\CoreBundle\ListManager\QueryBuilderListManager; +use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; +use RZ\Roadiz\CoreBundle\Logger\Entity\Log; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Themes\Rozier\RozierApp; -use Themes\Rozier\Utils\SessionListFilters; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers\Nodes - */ class HistoryController extends RozierApp { /** @@ -27,21 +27,26 @@ class HistoryController extends RozierApp */ public function historyAction(Request $request, int $nodeId): Response { - $this->denyAccessUnlessGranted(['ROLE_ACCESS_NODES', 'ROLE_ACCESS_LOGS']); /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); if (null === $node) { throw new ResourceNotFoundException(); } + $this->denyAccessUnlessGranted(NodeVoter::READ_LOGS, $node); - $listManager = $this->createEntityListManager( - Log::class, - [ - 'nodeSource' => $node->getNodeSources()->toArray(), - ], - ['datetime' => 'DESC'] - ); + $qb = $this->em() + ->getRepository(Log::class) + ->getAllRelatedToNodeQueryBuilder($node); + + $listManager = new QueryBuilderListManager($request, $qb, 'obj'); + $listManager->setSearchingCallable(function (QueryBuilder $queryBuilder, string $search) { + $queryBuilder->andWhere($queryBuilder->expr()->orX( + $queryBuilder->expr()->like('obj.message', ':search'), + $queryBuilder->expr()->like('obj.channel', ':search') + )); + $queryBuilder->setParameter('search', '%' . $search . '%'); + }); $listManager->setDisplayingNotPublishedNodes(true); $listManager->setDisplayingAllNodesStatuses(true); /* diff --git a/src/Controllers/Nodes/NodesAttributesController.php b/src/Controllers/Nodes/NodesAttributesController.php index f11dad81..f1dfe96e 100644 --- a/src/Controllers/Nodes/NodesAttributesController.php +++ b/src/Controllers/Nodes/NodesAttributesController.php @@ -9,10 +9,13 @@ use RZ\Roadiz\CoreBundle\Entity\AttributeValueTranslation; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodesSources; +use RZ\Roadiz\CoreBundle\Entity\NodeType; use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\Event\NodesSources\NodesSourcesUpdatedEvent; use RZ\Roadiz\CoreBundle\Form\AttributeValueTranslationType; use RZ\Roadiz\CoreBundle\Form\AttributeValueType; +use RZ\Roadiz\CoreBundle\Form\Error\FormErrorSerializer; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\JsonResponse; @@ -24,14 +27,10 @@ class NodesAttributesController extends RozierApp { - private FormFactoryInterface $formFactory; - - /** - * @param FormFactoryInterface $formFactory - */ - public function __construct(FormFactoryInterface $formFactory) - { - $this->formFactory = $formFactory; + public function __construct( + private readonly FormFactoryInterface $formFactory, + private readonly FormErrorSerializer $formErrorSerializer + ) { } /** @@ -44,8 +43,6 @@ public function __construct(FormFactoryInterface $formFactory) */ public function editAction(Request $request, int $nodeId, int $translationId): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODE_ATTRIBUTES'); - /** @var Translation|null $translation */ $translation = $this->em()->find(Translation::class, $translationId); /** @var Node|null $node */ @@ -55,6 +52,8 @@ public function editAction(Request $request, int $nodeId, int $translationId): R throw $this->createNotFoundException('Node-source does not exist'); } + $this->denyAccessUnlessGranted(NodeVoter::EDIT_ATTRIBUTE, $node); + /** @var NodesSources|null $nodeSource */ $nodeSource = $this->em() ->getRepository(NodesSources::class) @@ -66,12 +65,32 @@ public function editAction(Request $request, int $nodeId, int $translationId): R throw $this->createNotFoundException('Node-source does not exist'); } + if (!$this->isAttributable($node)) { + throw $this->createNotFoundException('Node type is not attributable'); + } + if (null !== $response = $this->handleAddAttributeForm($request, $node, $translation)) { return $response; } + $isJson = + $request->isXmlHttpRequest() || + $request->getRequestFormat('html') === 'json' || + \in_array( + 'application/json', + $request->getAcceptableContentTypes() + ); + $this->assignation['attribute_value_translation_forms'] = []; - $attributeValues = $node->getAttributeValues(); + $nodeType = $node->getNodeType(); + $orderByWeight = false; + if ($nodeType instanceof NodeType) { + $orderByWeight = $nodeType->isSortingAttributesByWeight(); + } + $attributeValues = $this->em()->getRepository(AttributeValue::class)->findByAttributable( + $node, + $orderByWeight + ); /** @var AttributeValue $attributeValue */ foreach ($attributeValues as $attributeValue) { $name = $node->getNodeName() . '_attribute_' . $attributeValue->getId(); @@ -107,27 +126,27 @@ public function editAction(Request $request, int $nodeId, int $translationId): R ); $this->publishConfirmMessage($request, $msg, $nodeSource); - if ($request->isXmlHttpRequest() || $request->getRequestFormat('html') === 'json') { + if ($isJson) { return new JsonResponse([ 'status' => 'success', 'message' => $msg, - ], JsonResponse::HTTP_ACCEPTED); + ], Response::HTTP_ACCEPTED); } return $this->redirectToRoute('nodesEditAttributesPage', [ 'nodeId' => $node->getId(), 'translationId' => $translation->getId(), ]); } else { - $errors = $this->getErrorsAsArray($attributeValueTranslationForm); + $errors = $this->formErrorSerializer->getErrorsAsArray($attributeValueTranslationForm); /* * Handle errors when Ajax POST requests */ - if ($request->isXmlHttpRequest() || $request->getRequestFormat('html') === 'json') { + if ($isJson) { return new JsonResponse([ 'status' => 'fail', 'errors' => $errors, 'message' => $this->getTranslator()->trans('form_has_errors.check_you_fields'), - ], JsonResponse::HTTP_BAD_REQUEST); + ], Response::HTTP_BAD_REQUEST); } foreach ($errors as $error) { $this->publishErrorMessage($request, $error); @@ -140,6 +159,7 @@ public function editAction(Request $request, int $nodeId, int $translationId): R $this->assignation['source'] = $nodeSource; $this->assignation['translation'] = $translation; + $this->assignation['order_by_weight'] = $orderByWeight; $availableTranslations = $this->em() ->getRepository(Translation::class) ->findAvailableTranslationsForNode($node); @@ -154,6 +174,15 @@ protected function hasAttributes(): bool return $this->em()->getRepository(Attribute::class)->countBy([]) > 0; } + protected function isAttributable(Node $node): bool + { + $nodeType = $node->getNodeType(); + if ($nodeType instanceof NodeType) { + return $nodeType->isAttributable(); + } + return false; + } + /** * @param Request $request * @param Node $node @@ -163,6 +192,9 @@ protected function hasAttributes(): bool */ protected function handleAddAttributeForm(Request $request, Node $node, Translation $translation): ?RedirectResponse { + if (!$this->isAttributable($node)) { + return null; + } if (!$this->hasAttributes()) { return null; } @@ -201,16 +233,15 @@ protected function handleAddAttributeForm(Request $request, Node $node, Translat /** * @param Request $request - * @param int $nodeId - * @param int $translationId - * @param int $attributeValueId + * @param int $nodeId + * @param int $translationId + * @param int $attributeValueId * * @return Response + * @throws RuntimeError */ - public function deleteAction(Request $request, $nodeId, $translationId, $attributeValueId): Response + public function deleteAction(Request $request, int $nodeId, int $translationId, int $attributeValueId): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_ATTRIBUTES_DELETE'); - /** @var AttributeValue|null $item */ $item = $this->em()->find(AttributeValue::class, $attributeValueId); if ($item === null) { @@ -225,6 +256,8 @@ public function deleteAction(Request $request, $nodeId, $translationId, $attribu throw $this->createNotFoundException('Node-source does not exist'); } + $this->denyAccessUnlessGranted(NodeVoter::EDIT_ATTRIBUTE, $node); + /** @var NodesSources|null $nodeSource */ $nodeSource = $this->em() ->getRepository(NodesSources::class) @@ -251,9 +284,9 @@ public function deleteAction(Request $request, $nodeId, $translationId, $attribu '%nodeName%' => $nodeSource->getTitle(), ] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $item); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage()); + $this->publishErrorMessage($request, $e->getMessage(), $item); } return $this->redirectToRoute('nodesEditAttributesPage', [ @@ -277,11 +310,10 @@ public function deleteAction(Request $request, $nodeId, $translationId, $attribu * @param int $translationId * @param int $attributeValueId * @return Response + * @throws RuntimeError */ public function resetAction(Request $request, int $nodeId, int $translationId, int $attributeValueId): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_ATTRIBUTES_DELETE'); - /** @var AttributeValueTranslation|null $item */ $item = $this->em() ->getRepository(AttributeValueTranslation::class) @@ -301,6 +333,8 @@ public function resetAction(Request $request, int $nodeId, int $translationId, i throw $this->createNotFoundException('Node-source does not exist'); } + $this->denyAccessUnlessGranted(NodeVoter::EDIT_ATTRIBUTE, $node); + /** @var NodesSources|null $nodeSource */ $nodeSource = $this->em() ->getRepository(NodesSources::class) @@ -327,9 +361,9 @@ public function resetAction(Request $request, int $nodeId, int $translationId, i '%nodeName%' => $nodeSource->getTitle(), ] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $item); } catch (\RuntimeException $e) { - $this->publishErrorMessage($request, $e->getMessage()); + $this->publishErrorMessage($request, $e->getMessage(), $item); } return $this->redirectToRoute('nodesEditAttributesPage', [ diff --git a/src/Controllers/Nodes/NodesController.php b/src/Controllers/Nodes/NodesController.php index e304e24f..482e097f 100644 --- a/src/Controllers/Nodes/NodesController.php +++ b/src/Controllers/Nodes/NodesController.php @@ -4,6 +4,8 @@ namespace Themes\Rozier\Controllers\Nodes; +use Doctrine\ORM\OptimisticLockException; +use Doctrine\ORM\ORMException; use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\Node; use RZ\Roadiz\CoreBundle\Entity\NodeType; @@ -16,11 +18,14 @@ use RZ\Roadiz\CoreBundle\Event\Node\NodeUndeletedEvent; use RZ\Roadiz\CoreBundle\Event\Node\NodeUpdatedEvent; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; +use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; use RZ\Roadiz\CoreBundle\Node\Exception\SameNodeUrlException; use RZ\Roadiz\CoreBundle\Node\NodeFactory; use RZ\Roadiz\CoreBundle\Node\NodeMover; +use RZ\Roadiz\CoreBundle\Node\NodeOffspringResolverInterface; use RZ\Roadiz\CoreBundle\Node\UniqueNodeGenerator; use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormError; @@ -32,31 +37,12 @@ use Symfony\Component\Workflow\Registry; use Themes\Rozier\RozierApp; use Themes\Rozier\Traits\NodesTrait; -use Themes\Rozier\Utils\SessionListFilters; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers\Nodes - */ -class NodesController extends RozierApp +final class NodesController extends RozierApp { use NodesTrait; - private NodeChrootResolver $nodeChrootResolver; - private NodeMover $nodeMover; - private Registry $workflowRegistry; - private HandlerFactoryInterface $handlerFactory; - private UniqueNodeGenerator $uniqueNodeGenerator; - private NodeFactory $nodeFactory; - /** - * @var class-string - */ - private string $nodeFormTypeClass; - /** - * @var class-string - */ - private string $addNodeFormTypeClass; - /** * @param NodeChrootResolver $nodeChrootResolver * @param NodeMover $nodeMover @@ -64,27 +50,21 @@ class NodesController extends RozierApp * @param HandlerFactoryInterface $handlerFactory * @param UniqueNodeGenerator $uniqueNodeGenerator * @param NodeFactory $nodeFactory + * @param NodeOffspringResolverInterface $nodeOffspringResolver * @param class-string $nodeFormTypeClass * @param class-string $addNodeFormTypeClass */ public function __construct( - NodeChrootResolver $nodeChrootResolver, - NodeMover $nodeMover, - Registry $workflowRegistry, - HandlerFactoryInterface $handlerFactory, - UniqueNodeGenerator $uniqueNodeGenerator, - NodeFactory $nodeFactory, - string $nodeFormTypeClass, - string $addNodeFormTypeClass + private readonly NodeChrootResolver $nodeChrootResolver, + private readonly NodeMover $nodeMover, + private readonly Registry $workflowRegistry, + private readonly HandlerFactoryInterface $handlerFactory, + private readonly UniqueNodeGenerator $uniqueNodeGenerator, + private readonly NodeFactory $nodeFactory, + private readonly NodeOffspringResolverInterface $nodeOffspringResolver, + private readonly string $nodeFormTypeClass, + private readonly string $addNodeFormTypeClass ) { - $this->nodeChrootResolver = $nodeChrootResolver; - $this->nodeMover = $nodeMover; - $this->workflowRegistry = $workflowRegistry; - $this->handlerFactory = $handlerFactory; - $this->nodeFormTypeClass = $nodeFormTypeClass; - $this->addNodeFormTypeClass = $addNodeFormTypeClass; - $this->uniqueNodeGenerator = $uniqueNodeGenerator; - $this->nodeFactory = $nodeFactory; } protected function getNodeFactory(): NodeFactory @@ -188,14 +168,14 @@ public function indexAction(Request $request, ?string $filter = null): Response */ public function editAction(Request $request, int $nodeId, ?int $translationId = null): Response { - $this->validateNodeAccessForRole('ROLE_ACCESS_NODES_SETTING', $nodeId); - /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); if (null === $node) { throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); } + $this->denyAccessUnlessGranted(NodeVoter::EDIT_SETTING, $node); + $this->em()->refresh($node); /* * Handle StackTypes form @@ -213,7 +193,7 @@ public function editAction(Request $request, int $nodeId, ?int $translationId = '%type%' => $type->getDisplayName(), ] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $node); return $this->redirectToRoute( 'nodesEditPage', ['nodeId' => $node->getId()] @@ -254,7 +234,7 @@ public function editAction(Request $request, int $nodeId, ?int $translationId = $msg = $this->getTranslator()->trans('node.%name%.updated', [ '%name%' => $node->getNodeName(), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); return $this->redirectToRoute( 'nodesEditPage', ['nodeId' => $node->getId()] @@ -289,13 +269,13 @@ public function editAction(Request $request, int $nodeId, ?int $translationId = */ public function removeStackTypeAction(Request $request, int $nodeId, int $typeId): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); - /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); if (null === $node) { throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); } + $this->denyAccessUnlessGranted(NodeVoter::EDIT_SETTING, $node); + /** @var NodeType|null $type */ $type = $this->em()->find(NodeType::class, $typeId); if (null === $type) { @@ -312,7 +292,7 @@ public function removeStackTypeAction(Request $request, int $nodeId, int $typeId '%type%' => $type->getDisplayName(), ] ); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: null); return $this->redirectToRoute('nodesEditPage', ['nodeId' => $node->getId()]); } @@ -321,12 +301,13 @@ public function removeStackTypeAction(Request $request, int $nodeId, int $typeId * Handle node creation pages. * * @param Request $request - * @param int $nodeTypeId + * @param int $nodeTypeId * @param int|null $translationId * * @return Response - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException + * @throws RuntimeError + * @throws ORMException + * @throws OptimisticLockException */ public function addAction(Request $request, int $nodeTypeId, ?int $translationId = null): Response { @@ -348,7 +329,9 @@ public function addAction(Request $request, int $nodeTypeId, ?int $translationId throw new ResourceNotFoundException(sprintf('Translation #%s does not exist.', $translationId)); } - $node = new Node($type); + $node = new Node(); + $node->setNodeType($type); + $node->setTtl($type->getDefaultTtl()); $chroot = $this->nodeChrootResolver->getChroot($this->getUser()); if (null !== $chroot) { @@ -374,7 +357,7 @@ public function addAction(Request $request, int $nodeTypeId, ?int $translationId 'node.%name%.created', ['%name%' => $node->getNodeName()] ); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: null); return $this->redirectToRoute( 'nodesEditSourcePage', @@ -406,15 +389,12 @@ public function addAction(Request $request, int $nodeTypeId, ?int $translationId * @param int|null $translationId * * @return Response - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException - * @throws \Twig\Error\RuntimeError + * @throws ORMException + * @throws OptimisticLockException + * @throws RuntimeError */ public function addChildAction(Request $request, ?int $nodeId = null, ?int $translationId = null): Response { - // include CHRoot to enable creating node in it - $this->validateNodeAccessForRole('ROLE_ACCESS_NODES', $nodeId, true); - /** @var Translation|null $translation */ $translation = $this->em()->getRepository(Translation::class)->findDefault(); @@ -428,15 +408,19 @@ public function addChildAction(Request $request, ?int $nodeId = null, ?int $tran } if (null === $translation) { - throw new ResourceNotFoundException(sprintf('Translation does not exist')); + throw new ResourceNotFoundException('Translation does not exist'); } if (null !== $nodeId && $nodeId > 0) { - /** @var Node $parentNode */ - $parentNode = $this->em() - ->find(Node::class, $nodeId); + /** @var Node|null $parentNode */ + $parentNode = $this->em()->find(Node::class, $nodeId); + if (null === $parentNode) { + throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); + } + $this->denyAccessUnlessGranted(NodeVoter::CREATE, $parentNode); } else { $parentNode = null; + $this->denyAccessUnlessGranted(NodeVoter::CREATE_AT_ROOT); } $node = new Node(); @@ -463,7 +447,7 @@ public function addChildAction(Request $request, ?int $nodeId = null, ?int $tran 'child_node.%name%.created', ['%name%' => $node->getNodeName()] ); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: null); return $this->redirectToRoute( 'nodesEditSourcePage', @@ -494,12 +478,10 @@ public function addChildAction(Request $request, ?int $nodeId = null, ?int $tran * @param int $nodeId * * @return Response - * @throws \Twig\Error\RuntimeError + * @throws RuntimeError */ public function deleteAction(Request $request, int $nodeId): Response { - $this->validateNodeAccessForRole('ROLE_ACCESS_NODES_DELETE', $nodeId); - /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); @@ -507,9 +489,11 @@ public function deleteAction(Request $request, int $nodeId): Response throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); } + $this->denyAccessUnlessGranted(NodeVoter::DELETE, $node); + $workflow = $this->workflowRegistry->get($node); if (!$workflow->can($node, 'delete')) { - $this->publishErrorMessage($request, sprintf('Node #%s cannot be deleted.', $nodeId)); + $this->publishErrorMessage($request, sprintf('Node #%s cannot be deleted.', $nodeId), $node); return $this->redirectToRoute( 'nodesEditSourcePage', [ @@ -544,13 +528,14 @@ public function deleteAction(Request $request, int $nodeId): Response 'node.%name%.deleted', ['%name%' => $node->getNodeName()] ); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); + $referrer = $request->query->get('referer'); if ( - $request->query->has('referer') && - (new UnicodeString($request->query->get('referer')))->startsWith('/') + \is_string($referrer) && + (new UnicodeString($referrer))->trim()->startsWith('/') ) { - return $this->redirect($request->query->get('referer')); + return $this->redirect($referrer); } if (null !== $parent) { return $this->redirectToRoute( @@ -573,11 +558,11 @@ public function deleteAction(Request $request, int $nodeId): Response * @param Request $request * * @return Response - * @throws \Twig\Error\RuntimeError + * @throws RuntimeError */ public function emptyTrashAction(Request $request): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES_DELETE'); + $this->denyAccessUnlessGranted(NodeVoter::EMPTY_TRASH); $form = $this->buildEmptyTrashForm(); $form->handleRequest($request); @@ -587,10 +572,7 @@ public function emptyTrashAction(Request $request): Response /** @var Node|null $chroot */ $chroot = $this->nodeChrootResolver->getChroot($this->getUser()); if ($chroot !== null) { - /** @var NodeHandler $nodeHandler */ - $nodeHandler = $this->handlerFactory->getHandler($chroot); - $ids = $nodeHandler->getAllOffspringId(); - $criteria["parent"] = $ids; + $criteria["parent"] = $this->nodeOffspringResolver->getAllOffspringIds($chroot); } $nodes = $this->em() @@ -628,12 +610,10 @@ public function emptyTrashAction(Request $request): Response * @param int $nodeId * * @return Response - * @throws \Twig\Error\RuntimeError + * @throws RuntimeError */ public function undeleteAction(Request $request, int $nodeId): Response { - $this->validateNodeAccessForRole('ROLE_ACCESS_NODES_DELETE', $nodeId); - /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); @@ -641,9 +621,11 @@ public function undeleteAction(Request $request, int $nodeId): Response throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); } + $this->denyAccessUnlessGranted(NodeVoter::DELETE, $node); + $workflow = $this->workflowRegistry->get($node); if (!$workflow->can($node, 'undelete')) { - $this->publishErrorMessage($request, sprintf('Node #%s cannot be undeleted.', $nodeId)); + $this->publishErrorMessage($request, sprintf('Node #%s cannot be undeleted.', $nodeId), $node); return $this->redirectToRoute( 'nodesEditSourcePage', [ @@ -669,7 +651,7 @@ public function undeleteAction(Request $request, int $nodeId): Response 'node.%name%.undeleted', ['%name%' => $node->getNodeName()] ); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); /* * Force redirect to avoid resending form when refreshing page */ @@ -712,24 +694,26 @@ public function generateAndAddNodeAction(Request $request): RedirectResponse throw new ResourceNotFoundException($msg); } } + /** - * @param Request $request - * @param int $nodeId + * @param Request $request + * @param int $nodeId * @return Response + * @throws RuntimeError */ public function publishAllAction(Request $request, int $nodeId): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES_STATUS'); /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); if (null === $node) { throw new ResourceNotFoundException(sprintf('Node #%s does not exist.', $nodeId)); } + $this->denyAccessUnlessGranted(NodeVoter::EDIT_STATUS, $node); $workflow = $this->workflowRegistry->get($node); if (!$workflow->can($node, 'publish')) { - $this->publishErrorMessage($request, sprintf('Node #%s cannot be published.', $nodeId)); + $this->publishErrorMessage($request, sprintf('Node #%s cannot be published.', $nodeId), $node); return $this->redirectToRoute( 'nodesEditSourcePage', [ @@ -748,11 +732,13 @@ public function publishAllAction(Request $request, int $nodeId): Response $this->em()->flush(); $msg = $this->getTranslator()->trans('node.offspring.published'); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $node); return $this->redirectToRoute('nodesEditSourcePage', [ 'nodeId' => $nodeId, - 'translationId' => $node->getNodeSources()->first()->getTranslation()->getId(), + 'translationId' => $node->getNodeSources()->first() ? + $node->getNodeSources()->first()->getTranslation()->getId() : + null, ]); } diff --git a/src/Controllers/Nodes/NodesSourcesController.php b/src/Controllers/Nodes/NodesSourcesController.php index 84012609..980ed5f3 100644 --- a/src/Controllers/Nodes/NodesSourcesController.php +++ b/src/Controllers/Nodes/NodesSourcesController.php @@ -13,6 +13,7 @@ use RZ\Roadiz\CoreBundle\Event\NodesSources\NodesSourcesUpdatedEvent; use RZ\Roadiz\CoreBundle\Form\Error\FormErrorSerializer; use RZ\Roadiz\CoreBundle\Routing\NodeRouter; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use RZ\Roadiz\CoreBundle\TwigExtension\JwtExtension; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormError; @@ -33,13 +34,10 @@ class NodesSourcesController extends RozierApp { use VersionedControllerTrait; - private JwtExtension $jwtExtension; - private FormErrorSerializer $formErrorSerializer; - - public function __construct(JwtExtension $jwtExtension, FormErrorSerializer $formErrorSerializer) - { - $this->jwtExtension = $jwtExtension; - $this->formErrorSerializer = $formErrorSerializer; + public function __construct( + private readonly JwtExtension $jwtExtension, + private readonly FormErrorSerializer $formErrorSerializer + ) { } /** @@ -54,8 +52,6 @@ public function __construct(JwtExtension $jwtExtension, FormErrorSerializer $for */ public function editSourceAction(Request $request, int $nodeId, int $translationId): Response { - $this->validateNodeAccessForRole('ROLE_ACCESS_NODES', $nodeId); - /** @var Translation|null $translation */ $translation = $this->em()->find(Translation::class, $translationId); @@ -73,6 +69,8 @@ public function editSourceAction(Request $request, int $nodeId, int $translation throw new ResourceNotFoundException('Node does not exist'); } + $this->denyAccessUnlessGranted(NodeVoter::EDIT_CONTENT, $gNode); + /** @var NodesSources|null $source */ $source = $this->em() ->getRepository(NodesSources::class) @@ -109,12 +107,16 @@ public function editSourceAction(Request $request, int $nodeId, int $translation ] ); $form->handleRequest($request); + $isJsonRequest = + $request->isXmlHttpRequest() || + \in_array('application/json', $request->getAcceptableContentTypes()) + ; if ($form->isSubmitted()) { if ($form->isValid() && !$this->isReadOnly) { $this->onPostUpdate($source, $request); - if (!$request->isXmlHttpRequest()) { + if (!$isJsonRequest) { return $this->getPostUpdateRedirection($source); } @@ -167,7 +169,7 @@ public function editSourceAction(Request $request, int $nodeId, int $translation /* * Handle errors when Ajax POST requests */ - if ($request->isXmlHttpRequest()) { + if ($isJsonRequest) { $errors = $this->formErrorSerializer->getErrorsAsArray($form); return new JsonResponse([ 'status' => 'fail', @@ -207,12 +209,10 @@ public function removeAction(Request $request, int $nodeSourceId): Response if (null === $ns) { throw new ResourceNotFoundException('Node source does not exist'); } - /** @var Node $node */ + $this->denyAccessUnlessGranted(NodeVoter::DELETE, $ns); $node = $ns->getNode(); $this->em()->refresh($ns->getNode()); - $this->validateNodeAccessForRole('ROLE_ACCESS_NODES_DELETE', $node->getId()); - /* * Prevent deleting last node-source available in node. */ @@ -238,7 +238,6 @@ public function removeAction(Request $request, int $nodeSourceId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - /** @var Node $node */ $node = $ns->getNode(); /* * Dispatch event @@ -248,14 +247,18 @@ public function removeAction(Request $request, int $nodeSourceId): Response $this->em()->remove($ns); $this->em()->flush(); - $ns = $node->getNodeSources()->first(); + $ns = $node->getNodeSources()->first() ?: null; + + if (null === $ns) { + throw new ResourceNotFoundException('No more node-source available for this node.'); + } $msg = $this->getTranslator()->trans('node_source.%node_source%.deleted.%translation%', [ '%node_source%' => $node->getNodeName(), '%translation%' => $ns->getTranslation()->getName(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $node); return $this->redirectToRoute( 'nodesEditSourcePage', diff --git a/src/Controllers/Nodes/NodesTagsController.php b/src/Controllers/Nodes/NodesTagsController.php deleted file mode 100644 index 87f69398..00000000 --- a/src/Controllers/Nodes/NodesTagsController.php +++ /dev/null @@ -1,104 +0,0 @@ -nodeFactory = $nodeFactory; - } - - protected function getNodeFactory(): NodeFactory - { - return $this->nodeFactory; - } - - /** - * Return tags form for requested node. - * - * @param Request $request - * @param int $nodeId - * - * @return Response - * @throws \Twig\Error\RuntimeError - */ - public function editTagsAction(Request $request, int $nodeId): Response - { - $this->validateNodeAccessForRole('ROLE_ACCESS_NODES', $nodeId); - - /** @var NodesSources|null $source */ - $source = $this->em() - ->getRepository(NodesSources::class) - ->setDisplayingAllNodesStatuses(true) - ->setDisplayingNotPublishedNodes(true) - ->findOneBy([ - 'node.id' => $nodeId, - 'translation' => $this->em()->getRepository(Translation::class)->findDefault() - ]); - if (null === $source) { - /** @var NodesSources|null $source */ - $source = $this->em() - ->getRepository(NodesSources::class) - ->setDisplayingAllNodesStatuses(true) - ->setDisplayingNotPublishedNodes(true) - ->findOneBy([ - 'node.id' => $nodeId, - ]); - } - - if (null !== $source) { - $node = $source->getNode(); - $form = $this->createForm(NodeTagsType::class, $node); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - /* - * Dispatch event - */ - $this->dispatchEvent(new NodeTaggedEvent($node)); - $this->em()->flush(); - - $msg = $this->getTranslator()->trans('node.%node%.linked.tags', [ - '%node%' => $node->getNodeName(), - ]); - $this->publishConfirmMessage($request, $msg, $source); - - return $this->redirectToRoute( - 'nodesEditTagsPage', - ['nodeId' => $node->getId()] - ); - } - - $this->assignation['translation'] = $source->getTranslation(); - $this->assignation['node'] = $node; - $this->assignation['source'] = $source; - $this->assignation['form'] = $form->createView(); - - return $this->render('@RoadizRozier/nodes/editTags.html.twig', $this->assignation); - } - - throw new ResourceNotFoundException(); - } -} diff --git a/src/Controllers/Nodes/NodesTreesController.php b/src/Controllers/Nodes/NodesTreesController.php index 0e2f157f..7604d5b6 100644 --- a/src/Controllers/Nodes/NodesTreesController.php +++ b/src/Controllers/Nodes/NodesTreesController.php @@ -10,6 +10,7 @@ use RZ\Roadiz\CoreBundle\Entity\Translation; use RZ\Roadiz\CoreBundle\EntityHandler\NodeHandler; use RZ\Roadiz\CoreBundle\Security\Authorization\Chroot\NodeChrootResolver; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Form\ClickableInterface; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; @@ -27,37 +28,17 @@ use Symfony\Component\Workflow\Registry; use Themes\Rozier\RozierApp; use Themes\Rozier\Widgets\TreeWidgetFactory; +use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers\Nodes - */ class NodesTreesController extends RozierApp { - private NodeChrootResolver $nodeChrootResolver; - private TreeWidgetFactory $treeWidgetFactory; - private FormFactoryInterface $formFactory; - private HandlerFactoryInterface $handlerFactory; - private Registry $workflowRegistry; - - /** - * @param NodeChrootResolver $nodeChrootResolver - * @param TreeWidgetFactory $treeWidgetFactory - * @param FormFactoryInterface $formFactory - * @param HandlerFactoryInterface $handlerFactory - * @param Registry $workflowRegistry - */ public function __construct( - NodeChrootResolver $nodeChrootResolver, - TreeWidgetFactory $treeWidgetFactory, - FormFactoryInterface $formFactory, - HandlerFactoryInterface $handlerFactory, - Registry $workflowRegistry + private readonly NodeChrootResolver $nodeChrootResolver, + private readonly TreeWidgetFactory $treeWidgetFactory, + private readonly FormFactoryInterface $formFactory, + private readonly HandlerFactoryInterface $handlerFactory, + private readonly Registry $workflowRegistry ) { - $this->nodeChrootResolver = $nodeChrootResolver; - $this->treeWidgetFactory = $treeWidgetFactory; - $this->formFactory = $formFactory; - $this->handlerFactory = $handlerFactory; - $this->workflowRegistry = $workflowRegistry; } /** @@ -66,25 +47,29 @@ public function __construct( * @param int|null $translationId * * @return Response + * @throws RuntimeError */ - public function treeAction(Request $request, ?int $nodeId = null, ?int $translationId = null) + public function treeAction(Request $request, ?int $nodeId = null, ?int $translationId = null): Response { if (null !== $nodeId) { - $this->validateNodeAccessForRole('ROLE_ACCESS_NODES', $nodeId, true); /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); - if (null === $node) { throw new ResourceNotFoundException(); } - $this->em()->refresh($node); - } elseif (null !== $this->getUser()) { - $node = $this->nodeChrootResolver->getChroot($this->getUser()); + } elseif (null !== $user = $this->getUser()) { + $node = $this->nodeChrootResolver->getChroot($user); } else { $node = null; } + if (null !== $node) { + $this->denyAccessUnlessGranted(NodeVoter::READ, $node); + } else { + $this->denyAccessUnlessGranted(NodeVoter::READ_AT_ROOT); + } + if (null !== $translationId) { /** @var Translation $translation */ $translation = $this->em() @@ -176,120 +161,131 @@ public function treeAction(Request $request, ?int $nodeId = null, ?int $translat /** * @param Request $request * @return Response + * @throws RuntimeError */ - public function bulkDeleteAction(Request $request) + public function bulkDeleteAction(Request $request): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES_DELETE'); + if (empty($request->get('deleteForm')['nodesIds'])) { + throw new ResourceNotFoundException(); + } - if (!empty($request->get('deleteForm')['nodesIds'])) { - $nodesIds = trim($request->get('deleteForm')['nodesIds']); - $nodesIds = explode(',', $nodesIds); - array_filter($nodesIds); + $nodesIds = trim($request->get('deleteForm')['nodesIds']); + $nodesIds = explode(',', $nodesIds); + array_filter($nodesIds); - /** @var Node[] $nodes */ - $nodes = $this->em() - ->getRepository(Node::class) - ->setDisplayingNotPublishedNodes(true) - ->findBy([ - 'id' => $nodesIds, - ]); + /** @var Node[] $nodes */ + $nodes = $this->em() + ->getRepository(Node::class) + ->setDisplayingNotPublishedNodes(true) + ->findBy([ + 'id' => $nodesIds, + ]); - if (count($nodes) > 0) { - $form = $this->buildBulkDeleteForm( - $request->get('deleteForm')['referer'], - $nodesIds - ); - $form->handleRequest($request); - if ($request->get('confirm') && $form->isSubmitted() && $form->isValid()) { - $msg = $this->bulkDeleteNodes($form->getData()); - $this->publishConfirmMessage($request, $msg); - - if (!empty($form->getData()['referer'])) { - return $this->redirect($form->getData()['referer']); - } else { - return $this->redirectToRoute('nodesHomePage'); - } - } + if (count($nodes) === 0) { + throw new ResourceNotFoundException(); + } - $this->assignation['nodes'] = $nodes; - $this->assignation['form'] = $form->createView(); + foreach ($nodes as $node) { + $this->denyAccessUnlessGranted(NodeVoter::DELETE, $node); + } - if (!empty($request->get('deleteForm')['referer'])) { - $this->assignation['referer'] = $request->get('deleteForm')['referer']; - } + $form = $this->buildBulkDeleteForm( + $request->get('deleteForm')['referer'], + $nodesIds + ); + $form->handleRequest($request); + if ($request->get('confirm') && $form->isSubmitted() && $form->isValid()) { + $msg = $this->bulkDeleteNodes($form->getData()); + $this->publishConfirmMessage($request, $msg); - return $this->render('@RoadizRozier/nodes/bulkDelete.html.twig', $this->assignation); + if (!empty($form->getData()['referer'])) { + return $this->redirect($form->getData()['referer']); + } else { + return $this->redirectToRoute('nodesHomePage'); } } - throw new ResourceNotFoundException(); + $this->assignation['nodes'] = $nodes; + $this->assignation['form'] = $form->createView(); + + if (!empty($request->get('deleteForm')['referer'])) { + $this->assignation['referer'] = $request->get('deleteForm')['referer']; + } + + return $this->render('@RoadizRozier/nodes/bulkDelete.html.twig', $this->assignation); } /** * @param Request $request * * @return Response + * @throws RuntimeError */ - public function bulkStatusAction(Request $request) + public function bulkStatusAction(Request $request): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES_STATUS'); + if (empty($request->get('statusForm')['nodesIds'])) { + throw new ResourceNotFoundException(); + } - if (!empty($request->get('statusForm')['nodesIds'])) { - $nodesIds = trim($request->get('statusForm')['nodesIds']); - $nodesIds = explode(',', $nodesIds); - array_filter($nodesIds); + $nodesIds = trim($request->get('statusForm')['nodesIds']); + $nodesIds = explode(',', $nodesIds); + array_filter($nodesIds); - /** @var Node[] $nodes */ - $nodes = $this->em() - ->getRepository(Node::class) - ->setDisplayingNotPublishedNodes(true) - ->findBy([ - 'id' => $nodesIds, - ]); - if (count($nodes) > 0) { - $form = $this->buildBulkStatusForm( - $request->get('statusForm')['referer'], - $nodesIds, - (string) $request->get('statusForm')['status'] - ); + /** @var Node[] $nodes */ + $nodes = $this->em() + ->getRepository(Node::class) + ->setDisplayingNotPublishedNodes(true) + ->findBy([ + 'id' => $nodesIds, + ]); - $form->handleRequest($request); + if (count($nodes) === 0) { + throw new ResourceNotFoundException(); + } - if ($form->isSubmitted() && $form->isValid()) { - $msg = $this->bulkStatusNodes($form->getData()); - $this->publishConfirmMessage($request, $msg); + foreach ($nodes as $node) { + $this->denyAccessUnlessGranted(NodeVoter::EDIT_STATUS, $node); + } - if (!empty($form->getData()['referer'])) { - return $this->redirect($form->getData()['referer']); - } else { - return $this->redirectToRoute('nodesHomePage'); - } - } + $form = $this->buildBulkStatusForm( + $request->get('statusForm')['referer'], + $nodesIds, + (string) $request->get('statusForm')['status'] + ); - $this->assignation['nodes'] = $nodes; - $this->assignation['form'] = $form->createView(); + $form->handleRequest($request); - if (!empty($request->get('statusForm')['referer'])) { - $this->assignation['referer'] = $request->get('statusForm')['referer']; - } + if ($form->isSubmitted() && $form->isValid()) { + $msg = $this->bulkStatusNodes($form->getData()); + $this->publishConfirmMessage($request, $msg); - return $this->render('@RoadizRozier/nodes/bulkStatus.html.twig', $this->assignation); + if (!empty($form->getData()['referer'])) { + return $this->redirect($form->getData()['referer']); + } else { + return $this->redirectToRoute('nodesHomePage'); } } - throw new ResourceNotFoundException(); + $this->assignation['nodes'] = $nodes; + $this->assignation['form'] = $form->createView(); + + if (!empty($request->get('statusForm')['referer'])) { + $this->assignation['referer'] = $request->get('statusForm')['referer']; + } + + return $this->render('@RoadizRozier/nodes/bulkStatus.html.twig', $this->assignation); } /** - * @param false|string $referer + * @param null|string $referer * @param array $nodesIds * * @return FormInterface */ private function buildBulkDeleteForm( - $referer = false, - $nodesIds = [] - ) { + ?string $referer = null, + array $nodesIds = [] + ): FormInterface { /** @var FormBuilder $builder */ $builder = $this->formFactory ->createNamedBuilder('deleteForm') @@ -302,7 +298,7 @@ private function buildBulkDeleteForm( ], ]); - if (false !== $referer && (new UnicodeString($referer))->startsWith('/')) { + if (null !== $referer && (new UnicodeString($referer))->startsWith('/')) { $builder->add('referer', HiddenType::class, [ 'data' => $referer, ]); @@ -345,12 +341,7 @@ private function bulkDeleteNodes(array $data) return $this->getTranslator()->trans('wrong.request'); } - /** - * @param array $data - * - * @return string - */ - private function bulkStatusNodes(array $data) + private function bulkStatusNodes(array $data): string { if (!empty($data['nodesIds'])) { $nodesIds = trim($data['nodesIds']); @@ -379,10 +370,7 @@ private function bulkStatusNodes(array $data) return $this->getTranslator()->trans('wrong.request'); } - /** - * @return FormInterface - */ - private function buildBulkTagForm() + private function buildBulkTagForm(): FormInterface { /** @var FormBuilder $builder */ $builder = $this->formFactory @@ -515,17 +503,17 @@ private function untagNodes(array $data) } /** - * @param false|string $referer + * @param null|string $referer * @param array $nodesIds * @param string $status * * @return FormInterface */ private function buildBulkStatusForm( - $referer = false, - $nodesIds = [], - $status = 'reject' - ) { + ?string $referer = null, + array $nodesIds = [], + string $status = 'reject' + ): FormInterface { /** @var FormBuilder $builder */ $builder = $this->formFactory ->createNamedBuilder('statusForm') @@ -553,7 +541,7 @@ private function buildBulkStatusForm( ]) ; - if (false !== $referer && (new UnicodeString($referer))->startsWith('/')) { + if (null !== $referer && (new UnicodeString($referer))->startsWith('/')) { $builder->add('referer', HiddenType::class, [ 'data' => $referer, ]); diff --git a/src/Controllers/Nodes/NodesUtilsController.php b/src/Controllers/Nodes/NodesUtilsController.php index a25fa9bd..cf73e9fc 100644 --- a/src/Controllers/Nodes/NodesUtilsController.php +++ b/src/Controllers/Nodes/NodesUtilsController.php @@ -9,23 +9,15 @@ use RZ\Roadiz\CoreBundle\Event\Node\NodeDuplicatedEvent; use RZ\Roadiz\CoreBundle\Node\NodeDuplicator; use RZ\Roadiz\CoreBundle\Node\NodeNamePolicyInterface; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Themes\Rozier\RozierApp; -/** - * @package Themes\Rozier\Controllers\Nodes - */ class NodesUtilsController extends RozierApp { - private NodeNamePolicyInterface $nodeNamePolicy; - - /** - * @param NodeNamePolicyInterface $nodeNamePolicy - */ - public function __construct(NodeNamePolicyInterface $nodeNamePolicy) + public function __construct(private readonly NodeNamePolicyInterface $nodeNamePolicy) { - $this->nodeNamePolicy = $nodeNamePolicy; } /** @@ -36,13 +28,17 @@ public function __construct(NodeNamePolicyInterface $nodeNamePolicy) * * @return Response */ - public function duplicateAction(Request $request, int $nodeId) + public function duplicateAction(Request $request, int $nodeId): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); - - /** @var Node $existingNode */ + /** @var Node|null $existingNode */ $existingNode = $this->em()->find(Node::class, $nodeId); + if (null === $existingNode) { + throw $this->createNotFoundException(); + } + + $this->denyAccessUnlessGranted(NodeVoter::DUPLICATE, $existingNode); + try { $duplicator = new NodeDuplicator( $existingNode, @@ -61,7 +57,7 @@ public function duplicateAction(Request $request, int $nodeId) '%name%' => $existingNode->getNodeName(), ]); - $this->publishConfirmMessage($request, $msg, $newNode->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $newNode->getNodeSources()->first() ?: $newNode); return $this->redirectToRoute( 'nodesEditPage', @@ -72,7 +68,8 @@ public function duplicateAction(Request $request, int $nodeId) $request, $this->getTranslator()->trans("impossible.duplicate.node.%name%", [ '%name%' => $existingNode->getNodeName(), - ]) + ]), + $existingNode ); return $this->redirectToRoute( diff --git a/src/Controllers/Nodes/TranstypeController.php b/src/Controllers/Nodes/TranstypeController.php index de1a7f3b..7e3326ae 100644 --- a/src/Controllers/Nodes/TranstypeController.php +++ b/src/Controllers/Nodes/TranstypeController.php @@ -9,7 +9,7 @@ use RZ\Roadiz\CoreBundle\Event\Node\NodeUpdatedEvent; use RZ\Roadiz\CoreBundle\Event\NodesSources\NodesSourcesUpdatedEvent; use RZ\Roadiz\CoreBundle\Node\NodeTranstyper; -use Symfony\Component\HttpFoundation\RedirectResponse; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; @@ -17,33 +17,22 @@ use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers\Nodes - */ class TranstypeController extends RozierApp { - private NodeTranstyper $nodeTranstyper; - - /** - * @param NodeTranstyper $nodeTranstyper - */ - public function __construct(NodeTranstyper $nodeTranstyper) + public function __construct(private readonly NodeTranstyper $nodeTranstyper) { - $this->nodeTranstyper = $nodeTranstyper; } /** * @param Request $request * @param int $nodeId * - * @return RedirectResponse|Response + * @return Response * @throws RuntimeError * @throws \Exception */ - public function transtypeAction(Request $request, int $nodeId) + public function transtypeAction(Request $request, int $nodeId): Response { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); - /** @var Node|null $node */ $node = $this->em()->find(Node::class, $nodeId); $this->em()->refresh($node); @@ -52,6 +41,11 @@ public function transtypeAction(Request $request, int $nodeId) throw new ResourceNotFoundException(); } + /* + * Transtype is only available for higher rank users + */ + $this->denyAccessUnlessGranted(NodeVoter::EDIT_SETTING, $node); + $form = $this->createForm(TranstypeType::class, null, [ 'currentType' => $node->getNodeType(), ]); @@ -91,13 +85,15 @@ public function transtypeAction(Request $request, int $nodeId) '%node%' => $node->getNodeName(), '%type%' => $newNodeType->getName(), ]); - $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first()); + $this->publishConfirmMessage($request, $msg, $node->getNodeSources()->first() ?: $node); return $this->redirectToRoute( 'nodesEditSourcePage', [ 'nodeId' => $node->getId(), - 'translationId' => $node->getNodeSources()->first()->getTranslation()->getId(), + 'translationId' => $node->getNodeSources()->first() ? + $node->getNodeSources()->first()->getTranslation()->getId() : + null, ] ); } diff --git a/src/Controllers/Nodes/UrlAliasesController.php b/src/Controllers/Nodes/UrlAliasesController.php deleted file mode 100644 index a9966545..00000000 --- a/src/Controllers/Nodes/UrlAliasesController.php +++ /dev/null @@ -1,380 +0,0 @@ -formFactory = $formFactory; - } - - /** - * Return aliases form for requested node. - * - * @param Request $request - * @param int $nodeId - * @param int|null $translationId - * - * @return Response - * @throws RuntimeError - */ - public function editAliasesAction(Request $request, int $nodeId, ?int $translationId = null): Response - { - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); - - if (null === $translationId || $translationId < 1) { - $translation = $this->em()->getRepository(Translation::class)->findDefault(); - } else { - $translation = $this->em()->find(Translation::class, $translationId); - } - /** @var NodesSources|null $source */ - $source = $this->em() - ->getRepository(NodesSources::class) - ->setDisplayingAllNodesStatuses(true) - ->setDisplayingNotPublishedNodes(true) - ->findOneBy(['translation' => $translation, 'node.id' => $nodeId]); - - if ($source !== null && null !== $node = $source->getNode()) { - $redirections = $this->em() - ->getRepository(Redirection::class) - ->findBy([ - 'redirectNodeSource' => $node->getNodeSources()->toArray() - ]); - $uas = $this->em() - ->getRepository(UrlAlias::class) - ->findAllFromNode($node->getId()); - $availableTranslations = $this->em() - ->getRepository(Translation::class) - ->findAvailableTranslationsForNode($node); - - $this->assignation['node'] = $node; - $this->assignation['source'] = $source; - $this->assignation['aliases'] = []; - $this->assignation['redirections'] = []; - $this->assignation['translation'] = $translation; - $this->assignation['available_translations'] = $availableTranslations; - - /* - * SEO Form - */ - $seoForm = $this->createForm(NodeSourceSeoType::class, $source); - $seoForm->handleRequest($request); - if ($seoForm->isSubmitted() && $seoForm->isValid()) { - $this->em()->flush(); - $msg = $this->getTranslator()->trans('node.seo.updated'); - $this->publishConfirmMessage($request, $msg, $source); - /* - * Dispatch event - */ - $this->dispatchEvent(new NodesSourcesUpdatedEvent($source)); - return $this->redirectToRoute( - 'nodesEditSEOPage', - ['nodeId' => $node->getId(), 'translationId' => $translationId] - ); - } - - if (null !== $response = $this->handleAddRedirection($source, $request)) { - return $response; - } - /* - * each url alias edit form - */ - /** @var UrlAlias $alias */ - foreach ($uas as $alias) { - if (null !== $response = $this->handleSingleUrlAlias($alias, $request)) { - return $response; - } - } - - /** @var Redirection $redirection */ - foreach ($redirections as $redirection) { - if (null !== $response = $this->handleSingleRedirection($redirection, $request)) { - return $response; - } - } - - /* - * Main ADD url alias form - */ - $alias = new UrlAlias(); - $addAliasForm = $this->formFactory->createNamed( - 'add_urlalias_' . $node->getId(), - UrlAliasType::class, - $alias, - [ - 'with_translation' => true - ] - ); - $addAliasForm->handleRequest($request); - if ($addAliasForm->isSubmitted() && $addAliasForm->isValid()) { - try { - $alias = $this->addNodeUrlAlias($alias, $node, $addAliasForm->get('translation')->getData()); - $msg = $this->getTranslator()->trans('url_alias.%alias%.created.%translation%', [ - '%alias%' => $alias->getAlias(), - '%translation%' => $alias->getNodeSource()->getTranslation()->getName(), - ]); - $this->publishConfirmMessage($request, $msg, $source); - /* - * Dispatch event - */ - $this->dispatchEvent(new UrlAliasCreatedEvent($alias)); - - return $this->redirect($this->generateUrl( - 'nodesEditSEOPage', - ['nodeId' => $node->getId(), 'translationId' => $translationId] - ) . '#manage-aliases'); - } catch (EntityAlreadyExistsException $e) { - $addAliasForm->addError(new FormError($e->getMessage())); - } catch (NoTranslationAvailableException $e) { - $addAliasForm->addError(new FormError($e->getMessage())); - } - } - - $this->assignation['form'] = $addAliasForm->createView(); - $this->assignation['seoForm'] = $seoForm->createView(); - - return $this->render('@RoadizRozier/nodes/editAliases.html.twig', $this->assignation); - } - - throw new ResourceNotFoundException(); - } - - /** - * @param UrlAlias $alias - * @param Node $node - * @param Translation $translation - * @return UrlAlias - */ - private function addNodeUrlAlias(UrlAlias $alias, Node $node, Translation $translation): UrlAlias - { - /** @var NodesSources|null $nodeSource */ - $nodeSource = $this->em() - ->getRepository(NodesSources::class) - ->setDisplayingAllNodesStatuses(true) - ->setDisplayingNotPublishedNodes(true) - ->findOneBy(['node' => $node, 'translation' => $translation]); - - if ($nodeSource !== null) { - $alias->setNodeSource($nodeSource); - $this->em()->persist($alias); - $this->em()->flush(); - - return $alias; - } else { - $msg = $this->getTranslator()->trans('url_alias.no_translation.%translation%', [ - '%translation%' => $translation->getName() - ]); - throw new NoTranslationAvailableException($msg); - } - } - - /** - * @param UrlAlias $alias - * @param Request $request - * - * @return RedirectResponse|null - */ - private function handleSingleUrlAlias(UrlAlias $alias, Request $request): ?RedirectResponse - { - $editForm = $this->formFactory->createNamed( - 'edit_urlalias_' . $alias->getId(), - UrlAliasType::class, - $alias - ); - $deleteForm = $this->formFactory->createNamed('delete_urlalias_' . $alias->getId()); - // Match edit - $editForm->handleRequest($request); - if ($editForm->isSubmitted() && $editForm->isValid()) { - try { - try { - $this->em()->flush(); - $msg = $this->getTranslator()->trans( - 'url_alias.%alias%.updated', - ['%alias%' => $alias->getAlias()] - ); - $this->publishConfirmMessage($request, $msg, $alias->getNodeSource()); - /* - * Dispatch event - */ - $this->dispatchEvent(new UrlAliasUpdatedEvent($alias)); - /** @var Translation $translation */ - $translation = $alias->getNodeSource()->getTranslation(); - - return $this->redirect($this->generateUrl( - 'nodesEditSEOPage', - [ - 'nodeId' => $alias->getNodeSource()->getNode()->getId(), - 'translationId' => $translation->getId() - ] - ) . '#manage-aliases'); - } catch (\RuntimeException $exception) { - $editForm->addError(new FormError($exception->getMessage())); - } - } catch (EntityAlreadyExistsException $e) { - $editForm->addError(new FormError($e->getMessage())); - } - } - - // Match delete - $deleteForm->handleRequest($request); - if ($deleteForm->isSubmitted() && $deleteForm->isValid()) { - $this->em()->remove($alias); - $this->em()->flush(); - $msg = $this->getTranslator()->trans('url_alias.%alias%.deleted', ['%alias%' => $alias->getAlias()]); - $this->publishConfirmMessage($request, $msg, $alias->getNodeSource()); - - /* - * Dispatch event - */ - $this->dispatchEvent(new UrlAliasDeletedEvent($alias)); - - /** @var Translation $translation */ - $translation = $alias->getNodeSource()->getTranslation(); - - return $this->redirect($this->generateUrl( - 'nodesEditSEOPage', - [ - 'nodeId' => $alias->getNodeSource()->getNode()->getId(), - 'translationId' => $translation->getId() - ] - ) . '#manage-aliases'); - } - - $this->assignation['aliases'][] = [ - 'alias' => $alias, - 'editForm' => $editForm->createView(), - 'deleteForm' => $deleteForm->createView(), - ]; - - return null; - } - - /** - * @param NodesSources $source - * @param Request $request - * @return RedirectResponse|null - */ - private function handleAddRedirection(NodesSources $source, Request $request): ?RedirectResponse - { - $redirection = new Redirection(); - $redirection->setRedirectNodeSource($source); - $redirection->setType(Response::HTTP_MOVED_PERMANENTLY); - - $addForm = $this->formFactory->createNamed( - 'add_redirection', - RedirectionType::class, - $redirection, - [ - 'placeholder' => $this->generateUrl($source), - 'only_query' => true - ] - ); - - $addForm->handleRequest($request); - if ($addForm->isSubmitted() && $addForm->isValid()) { - $this->em()->persist($redirection); - $this->em()->flush(); - - /** @var Translation $translation */ - $translation = $redirection->getRedirectNodeSource()->getTranslation(); - - return $this->redirect($this->generateUrl( - 'nodesEditSEOPage', - [ - 'nodeId' => $redirection->getRedirectNodeSource()->getNode()->getId(), - 'translationId' => $translation->getId() - ] - ) . '#manage-redirections'); - } - - $this->assignation['addRedirection'] = $addForm->createView(); - - return null; - } - - /** - * @param Redirection $redirection - * @param Request $request - * @return RedirectResponse|null - */ - private function handleSingleRedirection(Redirection $redirection, Request $request): ?RedirectResponse - { - $editForm = $this->formFactory->createNamed( - 'edit_redirection_' . $redirection->getId(), - RedirectionType::class, - $redirection, - [ - 'only_query' => true - ] - ); - - /** @var Translation $translation */ - $translation = $redirection->getRedirectNodeSource()->getTranslation(); - - $deleteForm = $this->formFactory->createNamed('delete_redirection_' . $redirection->getId()); - - $editForm->handleRequest($request); - if ($editForm->isSubmitted() && $editForm->isValid()) { - $this->em()->flush(); - return $this->redirect($this->generateUrl( - 'nodesEditSEOPage', - [ - 'nodeId' => $redirection->getRedirectNodeSource()->getNode()->getId(), - 'translationId' => $translation->getId() - ] - ) . '#manage-redirections'); - } - - // Match delete - $deleteForm->handleRequest($request); - if ($deleteForm->isSubmitted() && $deleteForm->isValid()) { - $this->em()->remove($redirection); - $this->em()->flush(); - return $this->redirect($this->generateUrl( - 'nodesEditSEOPage', - [ - 'nodeId' => $redirection->getRedirectNodeSource()->getNode()->getId(), - 'translationId' => $translation->getId() - ] - ) . '#manage-redirections'); - } - $this->assignation['redirections'][] = [ - 'redirection' => $redirection, - 'editForm' => $editForm->createView(), - 'deleteForm' => $deleteForm->createView(), - ]; - - return null; - } -} diff --git a/src/Controllers/PingController.php b/src/Controllers/PingController.php deleted file mode 100644 index 898e2a31..00000000 --- a/src/Controllers/PingController.php +++ /dev/null @@ -1,26 +0,0 @@ -denyAccessUnlessGranted('ROLE_BACKEND_USER'); - return $this->renderJson(['Pong']); - } -} diff --git a/src/Controllers/RedirectionsController.php b/src/Controllers/RedirectionsController.php index cd787e20..b2819ef5 100644 --- a/src/Controllers/RedirectionsController.php +++ b/src/Controllers/RedirectionsController.php @@ -6,10 +6,14 @@ use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Entity\Redirection; +use RZ\Roadiz\CoreBundle\Event\Redirection\PostCreatedRedirectionEvent; +use RZ\Roadiz\CoreBundle\Event\Redirection\PostDeletedRedirectionEvent; +use RZ\Roadiz\CoreBundle\Event\Redirection\PostUpdatedRedirectionEvent; +use RZ\Roadiz\CoreBundle\Event\Redirection\RedirectionEvent; use Symfony\Component\HttpFoundation\Request; use Themes\Rozier\Forms\RedirectionType; -class RedirectionsController extends AbstractAdminController +class RedirectionsController extends AbstractAdminWithBulkController { /** * @inheritDoc @@ -101,4 +105,33 @@ protected function getDefaultOrder(Request $request): array { return ['query' => 'ASC']; } + + protected function createPostCreateEvent(PersistableInterface $item): RedirectionEvent + { + if (!($item instanceof Redirection)) { + throw new \InvalidArgumentException('Item should be instance of ' . Redirection::class); + } + return new PostCreatedRedirectionEvent($item); + } + + protected function createPostUpdateEvent(PersistableInterface $item): RedirectionEvent + { + if (!($item instanceof Redirection)) { + throw new \InvalidArgumentException('Item should be instance of ' . Redirection::class); + } + return new PostUpdatedRedirectionEvent($item); + } + + protected function createDeleteEvent(PersistableInterface $item): RedirectionEvent + { + if (!($item instanceof Redirection)) { + throw new \InvalidArgumentException('Item should be instance of ' . Redirection::class); + } + return new PostDeletedRedirectionEvent($item); + } + + protected function getBulkDeleteRouteName(): ?string + { + return 'redirectionsBulkDeletePage'; + } } diff --git a/src/Controllers/RolesUtilsController.php b/src/Controllers/RolesUtilsController.php index 382baaf3..4cc08dc7 100644 --- a/src/Controllers/RolesUtilsController.php +++ b/src/Controllers/RolesUtilsController.php @@ -20,17 +20,10 @@ class RolesUtilsController extends RozierApp { - private SerializerInterface $serializer; - private RolesImporter $rolesImporter; - - /** - * @param SerializerInterface $serializer - * @param RolesImporter $rolesImporter - */ - public function __construct(SerializerInterface $serializer, RolesImporter $rolesImporter) - { - $this->serializer = $serializer; - $this->rolesImporter = $rolesImporter; + public function __construct( + private readonly SerializerInterface $serializer, + private readonly RolesImporter $rolesImporter + ) { } /** @@ -90,6 +83,9 @@ public function importJsonFileAction(Request $request): Response if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); + if (false === $serializedData) { + throw new RuntimeError('Cannot read uploaded file.'); + } if (null !== \json_decode($serializedData)) { if ($this->rolesImporter->import($serializedData)) { diff --git a/src/Controllers/SearchController.php b/src/Controllers/SearchController.php index d6d4aecf..1cff1ec9 100644 --- a/src/Controllers/SearchController.php +++ b/src/Controllers/SearchController.php @@ -68,7 +68,7 @@ public function notBlank(mixed $var): bool * * @return array */ - protected function appendDateTimeCriteria(array &$data, string $fieldName) + protected function appendDateTimeCriteria(array &$data, string $fieldName): array { $date = $data[$fieldName]['compareDatetime']; if ($date instanceof DateTime) { @@ -86,12 +86,10 @@ protected function appendDateTimeCriteria(array &$data, string $fieldName) * @param string $prefix * @return mixed */ - protected function processCriteria($data, string $prefix = "") + protected function processCriteria($data, string $prefix = ""): mixed { if (!empty($data[$prefix . "nodeName"])) { - if (isset($data[$prefix . "nodeName_exact"]) && $data[$prefix . "nodeName_exact"] === true) { - $data[$prefix . "nodeName"] = $data[$prefix . "nodeName"]; - } else { + if (!isset($data[$prefix . "nodeName_exact"]) || $data[$prefix . "nodeName_exact"] !== true) { $data[$prefix . "nodeName"] = ["LIKE", "%" . $data[$prefix . "nodeName"] . "%"]; } } @@ -139,11 +137,11 @@ protected function processCriteria($data, string $prefix = "") } /** - * @param array|\Traversable $data + * @param array $data * @param NodeType $nodetype - * @return mixed + * @return array */ - protected function processCriteriaNodetype($data, NodeType $nodetype) + protected function processCriteriaNodetype(array $data, NodeType $nodetype): array { $fields = $nodetype->getFields(); foreach ($data as $key => $value) { @@ -197,7 +195,7 @@ protected function processCriteriaNodetype($data, NodeType $nodetype) * @return Response * @throws RuntimeError */ - public function searchNodeAction(Request $request) + public function searchNodeAction(Request $request): Response { $builder = $this->buildSimpleForm(''); $form = $this->addButtons($builder)->getForm(); @@ -253,12 +251,12 @@ public function searchNodeAction(Request $request) * @param Request $request * @param int $nodetypeId * - * @return null|RedirectResponse|Response + * @return Response * @throws Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception * @throws RuntimeError */ - public function searchNodeSourceAction(Request $request, int $nodetypeId) + public function searchNodeSourceAction(Request $request, int $nodetypeId): Response { /** @var NodeType|null $nodetype */ $nodetype = $this->em()->find(NodeType::class, $nodetypeId); @@ -346,7 +344,7 @@ protected function addButtons(FormBuilderInterface $builder, bool $exportXlsx = * * @return null|RedirectResponse */ - protected function handleNodeTypeForm(FormInterface $nodeTypeForm) + protected function handleNodeTypeForm(FormInterface $nodeTypeForm): ?RedirectResponse { if ($nodeTypeForm->isSubmitted() && $nodeTypeForm->isValid()) { if (empty($nodeTypeForm->getData()['nodetype'])) { @@ -451,7 +449,7 @@ protected function handleNodeForm(FormInterface $form, NodeType $nodetype): ?Res * @throws Exception * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ - protected function getXlsxResults(NodeType $nodetype, $entities): string + protected function getXlsxResults(NodeType $nodetype, iterable $entities): string { $fields = $nodetype->getFields(); $keys = []; diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 81284ef6..4a12ba8d 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -15,6 +15,7 @@ use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; use RZ\Roadiz\CoreBundle\Form\Error\FormErrorSerializer; use RZ\Roadiz\CoreBundle\Form\SettingType; +use RZ\Roadiz\CoreBundle\ListManager\SessionListFilters; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormFactoryInterface; @@ -23,18 +24,14 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Themes\Rozier\RozierApp; -use Themes\Rozier\Utils\SessionListFilters; use Twig\Error\RuntimeError; class SettingsController extends RozierApp { - private FormFactoryInterface $formFactory; - private FormErrorSerializer $formErrorSerializer; - - public function __construct(FormFactoryInterface $formFactory, FormErrorSerializer $formErrorSerializer) - { - $this->formFactory = $formFactory; - $this->formErrorSerializer = $formErrorSerializer; + public function __construct( + private readonly FormFactoryInterface $formFactory, + private readonly FormErrorSerializer $formErrorSerializer + ) { } /** @@ -116,6 +113,13 @@ protected function commonSettingList(Request $request, SettingGroup $settingGrou $this->assignation['filters'] = $listManager->getAssignation(); $settings = $listManager->getEntities(); $this->assignation['settings'] = []; + $isJson = + $request->isXmlHttpRequest() || + $request->getRequestFormat('html') === 'json' || + \in_array( + 'application/json', + $request->getAcceptableContentTypes() + ); /** @var Setting $setting */ foreach ($settings as $setting) { @@ -133,9 +137,9 @@ protected function commonSettingList(Request $request, SettingGroup $settingGrou 'setting.%name%.updated', ['%name%' => $setting->getName()] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $setting); - if ($request->isXmlHttpRequest() || $request->getRequestFormat('html') === 'json') { + if ($isJson) { return new JsonResponse([ 'status' => 'success', 'message' => $msg, @@ -162,7 +166,7 @@ protected function commonSettingList(Request $request, SettingGroup $settingGrou /* * Do not publish any message, it may lead to flushing invalid form */ - if ($request->isXmlHttpRequest() || $request->getRequestFormat('html') === 'json') { + if ($isJson) { return new JsonResponse([ 'status' => 'failed', 'errors' => $errors, @@ -218,7 +222,7 @@ public function editAction(Request $request, int $settingId): Response $this->dispatchEvent(new SettingUpdatedEvent($setting)); $this->em()->flush(); $msg = $this->getTranslator()->trans('setting.%name%.updated', ['%name%' => $setting->getName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $setting); /* * Force redirect to avoid resending form when refreshing page */ @@ -273,7 +277,7 @@ public function addAction(Request $request): Response $this->em()->persist($setting); $this->em()->flush(); $msg = $this->getTranslator()->trans('setting.%name%.created', ['%name%' => $setting->getName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $setting); return $this->redirectToRoute('settingsHomePage'); } catch (EntityAlreadyExistsException $e) { @@ -319,7 +323,7 @@ public function deleteAction(Request $request, int $settingId): Response $this->em()->flush(); $msg = $this->getTranslator()->trans('setting.%name%.deleted', ['%name%' => $setting->getName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $setting); /* * Force redirect to avoid resending form when refreshing page diff --git a/src/Controllers/SettingsUtilsController.php b/src/Controllers/SettingsUtilsController.php index 7ddb148c..8d963eb4 100644 --- a/src/Controllers/SettingsUtilsController.php +++ b/src/Controllers/SettingsUtilsController.php @@ -6,9 +6,9 @@ use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializerInterface; -use RZ\Roadiz\CoreBundle\Importer\SettingsImporter; use RZ\Roadiz\CoreBundle\Entity\Setting; use RZ\Roadiz\CoreBundle\Entity\SettingGroup; +use RZ\Roadiz\CoreBundle\Importer\SettingsImporter; use RZ\Roadiz\Utils\StringHandler; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\FormError; @@ -21,17 +21,10 @@ class SettingsUtilsController extends RozierApp { - private SerializerInterface $serializer; - private SettingsImporter $settingsImporter; - - /** - * @param SerializerInterface $serializer - * @param SettingsImporter $settingsImporter - */ - public function __construct(SerializerInterface $serializer, SettingsImporter $settingsImporter) - { - $this->serializer = $serializer; - $this->settingsImporter = $settingsImporter; + public function __construct( + private readonly SerializerInterface $serializer, + private readonly SettingsImporter $settingsImporter + ) { } /** @@ -103,6 +96,10 @@ public function importJsonFileAction(Request $request): Response if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); + if (!\is_string($serializedData)) { + throw new RuntimeError('Imported file is not a string.'); + } + if (null !== \json_decode($serializedData)) { if ($this->settingsImporter->import($serializedData)) { $msg = $this->getTranslator()->trans('setting.imported'); diff --git a/src/Controllers/Tags/TagMultiCreationController.php b/src/Controllers/Tags/TagMultiCreationController.php index 630fba1c..fa8632b9 100644 --- a/src/Controllers/Tags/TagMultiCreationController.php +++ b/src/Controllers/Tags/TagMultiCreationController.php @@ -9,91 +9,81 @@ use RZ\Roadiz\CoreBundle\Event\Tag\TagCreatedEvent; use RZ\Roadiz\CoreBundle\Tag\TagFactory; use Symfony\Component\Form\FormError; -use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Themes\Rozier\Forms\MultiTagType; use Themes\Rozier\RozierApp; -/** - * @package Themes\Rozier\Controllers\Tags - */ class TagMultiCreationController extends RozierApp { - private TagFactory $tagFactory; - - /** - * @param TagFactory $tagFactory - */ - public function __construct(TagFactory $tagFactory) + public function __construct(private readonly TagFactory $tagFactory) { - $this->tagFactory = $tagFactory; } /** * @param Request $request * @param int $parentTagId - * @return RedirectResponse|Response|null + * @return Response * @throws \Twig\Error\RuntimeError */ - public function addChildAction(Request $request, int $parentTagId) + public function addChildAction(Request $request, int $parentTagId): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); $translation = $this->em()->getRepository(Translation::class)->findDefault(); $parentTag = $this->em()->find(Tag::class, $parentTagId); - if (null !== $parentTag) { - $form = $this->createForm(MultiTagType::class); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - try { - $data = $form->getData(); - $names = explode(',', $data['names']); - $names = array_map('trim', $names); - $names = array_filter($names); - $names = array_unique($names); - - /* - * Get latest position to add tags after. - */ - $latestPosition = $this->em() - ->getRepository(Tag::class) - ->findLatestPositionInParent($parentTag); + if (null === $parentTag) { + throw new ResourceNotFoundException(); + } - $tagsArray = []; - foreach ($names as $name) { - $tagsArray[] = $this->tagFactory->create($name, $translation, $parentTag, $latestPosition); - $this->em()->flush(); - } + $form = $this->createForm(MultiTagType::class); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + try { + $data = $form->getData(); + $names = explode(',', $data['names']); + $names = array_map('trim', $names); + $names = array_filter($names); + $names = array_unique($names); + + /* + * Get latest position to add tags after. + */ + $latestPosition = $this->em() + ->getRepository(Tag::class) + ->findLatestPositionInParent($parentTag); + + $tagsArray = []; + foreach ($names as $name) { + $tagsArray[] = $this->tagFactory->create($name, $translation, $parentTag, $latestPosition); + $this->em()->flush(); + } + /* + * Dispatch event and msg + */ + foreach ($tagsArray as $tag) { /* - * Dispatch event and msg + * Dispatch event */ - foreach ($tagsArray as $tag) { - /* - * Dispatch event - */ - $this->dispatchEvent(new TagCreatedEvent($tag)); - $msg = $this->getTranslator()->trans('child.tag.%name%.created', ['%name%' => $tag->getTagName()]); - $this->publishConfirmMessage($request, $msg); - } - - return $this->redirectToRoute('tagsTreePage', ['tagId' => $parentTagId]); - } catch (\InvalidArgumentException $e) { - $form->addError(new FormError($e->getMessage())); + $this->dispatchEvent(new TagCreatedEvent($tag)); + $msg = $this->getTranslator()->trans('child.tag.%name%.created', ['%name%' => $tag->getTagName()]); + $this->publishConfirmMessage($request, $msg, $tag); } - } - $this->assignation['translation'] = $translation; - $this->assignation['form'] = $form->createView(); - $this->assignation['tag'] = $parentTag; - - return $this->render('@RoadizRozier/tags/add-multiple.html.twig', $this->assignation); + return $this->redirectToRoute('tagsTreePage', ['tagId' => $parentTagId]); + } catch (\InvalidArgumentException $e) { + $form->addError(new FormError($e->getMessage())); + } } - throw new ResourceNotFoundException(); + $this->assignation['translation'] = $translation; + $this->assignation['form'] = $form->createView(); + $this->assignation['tag'] = $parentTag; + + return $this->render('@RoadizRozier/tags/add-multiple.html.twig', $this->assignation); } } diff --git a/src/Controllers/Tags/TagsController.php b/src/Controllers/Tags/TagsController.php index dd024d97..9a766e71 100644 --- a/src/Controllers/Tags/TagsController.php +++ b/src/Controllers/Tags/TagsController.php @@ -15,6 +15,7 @@ use RZ\Roadiz\CoreBundle\Event\Tag\TagDeletedEvent; use RZ\Roadiz\CoreBundle\Event\Tag\TagUpdatedEvent; use RZ\Roadiz\CoreBundle\Exception\EntityAlreadyExistsException; +use RZ\Roadiz\CoreBundle\Form\Error\FormErrorSerializer; use RZ\Roadiz\CoreBundle\Repository\TranslationRepository; use RZ\Roadiz\Utils\StringHandler; use Symfony\Component\Form\Extension\Core\Type\HiddenType; @@ -35,30 +36,16 @@ use Themes\Rozier\Widgets\TreeWidgetFactory; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers\Tags - */ class TagsController extends RozierApp { use VersionedControllerTrait; - private HandlerFactoryInterface $handlerFactory; - private FormFactoryInterface $formFactory; - private TreeWidgetFactory $treeWidgetFactory; - - /** - * @param FormFactoryInterface $formFactory - * @param HandlerFactoryInterface $handlerFactory - * @param TreeWidgetFactory $treeWidgetFactory - */ public function __construct( - FormFactoryInterface $formFactory, - HandlerFactoryInterface $handlerFactory, - TreeWidgetFactory $treeWidgetFactory + private readonly FormFactoryInterface $formFactory, + private readonly FormErrorSerializer $formErrorSerializer, + private readonly HandlerFactoryInterface $handlerFactory, + private readonly TreeWidgetFactory $treeWidgetFactory ) { - $this->handlerFactory = $handlerFactory; - $this->formFactory = $formFactory; - $this->treeWidgetFactory = $treeWidgetFactory; } /** @@ -68,7 +55,7 @@ public function __construct( * * @return Response */ - public function indexAction(Request $request) + public function indexAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -105,7 +92,7 @@ public function indexAction(Request $request) * @return Response * @throws RuntimeError */ - public function editTranslatedAction(Request $request, int $tagId, ?int $translationId = null) + public function editTranslatedAction(Request $request, int $tagId, ?int $translationId = null): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -166,6 +153,10 @@ public function editTranslatedAction(Request $request, int $tagId, ?int $transla 'disabled' => $this->isReadOnly, ]); $form->handleRequest($request); + $isJsonRequest = + $request->isXmlHttpRequest() || + \in_array('application/json', $request->getAcceptableContentTypes()) + ; if ($form->isSubmitted()) { if ($form->isValid()) { @@ -194,24 +185,31 @@ public function editTranslatedAction(Request $request, int $tagId, ?int $transla $msg = $this->getTranslator()->trans('tag.%name%.updated', [ '%name%' => $tagTranslation->getName(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $tag); /* * Force redirect to avoid resending form when refreshing page */ - return $this->getPostUpdateRedirection($tagTranslation); + if (!$isJsonRequest) { + return $this->getPostUpdateRedirection($tagTranslation); + } + + return new JsonResponse([ + 'status' => 'success', + 'errors' => [], + ], Response::HTTP_PARTIAL_CONTENT); } /* * Handle errors when Ajax POST requests */ - if ($request->isXmlHttpRequest()) { - $errors = $this->getErrorsAsArray($form); + if ($isJsonRequest) { + $errors = $this->formErrorSerializer->getErrorsAsArray($form); return new JsonResponse([ 'status' => 'fail', 'errors' => $errors, 'message' => $this->getTranslator()->trans('form_has_errors.check_you_fields'), - ], JsonResponse::HTTP_BAD_REQUEST); + ], Response::HTTP_BAD_REQUEST); } } /** @var TranslationRepository $translationRepository */ @@ -246,7 +244,7 @@ protected function tagNameExists(string $name): bool * @return Response * @throws RuntimeError */ - public function bulkDeleteAction(Request $request) + public function bulkDeleteAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS_DELETE'); @@ -296,10 +294,10 @@ public function bulkDeleteAction(Request $request) /** * @param Request $request - * * @return Response + * @throws RuntimeError */ - public function addAction(Request $request) + public function addAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -333,7 +331,7 @@ public function addAction(Request $request) $this->dispatchEvent(new TagCreatedEvent($tag)); $msg = $this->getTranslator()->trans('tag.%name%.created', ['%name%' => $tag->getTagName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $tag); /* * Force redirect to avoid resending form when refreshing page */ @@ -353,8 +351,9 @@ public function addAction(Request $request) * @param int $tagId * * @return Response + * @throws RuntimeError */ - public function editSettingsAction(Request $request, int $tagId) + public function editSettingsAction(Request $request, int $tagId): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -372,6 +371,10 @@ public function editSettingsAction(Request $request, int $tagId) ]); $form->handleRequest($request); + $isJsonRequest = + $request->isXmlHttpRequest() || + \in_array('application/json', $request->getAcceptableContentTypes()) + ; if ($form->isSubmitted()) { if ($form->isValid()) { @@ -382,7 +385,7 @@ public function editSettingsAction(Request $request, int $tagId) $this->dispatchEvent(new TagUpdatedEvent($tag)); $msg = $this->getTranslator()->trans('tag.%name%.updated', ['%name%' => $tag->getTagName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $tag); /* * Force redirect to avoid resending form when refreshing page @@ -395,8 +398,8 @@ public function editSettingsAction(Request $request, int $tagId) /* * Handle errors when Ajax POST requests */ - if ($request->isXmlHttpRequest()) { - $errors = $this->getErrorsAsArray($form); + if ($isJsonRequest) { + $errors = $this->formErrorSerializer->getErrorsAsArray($form); return new JsonResponse([ 'status' => 'fail', 'errors' => $errors, @@ -418,8 +421,9 @@ public function editSettingsAction(Request $request, int $tagId) * @param int|null $translationId * * @return Response + * @throws RuntimeError */ - public function treeAction(Request $request, int $tagId, ?int $translationId = null) + public function treeAction(Request $request, int $tagId, ?int $translationId = null): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -449,11 +453,12 @@ public function treeAction(Request $request, int $tagId, ?int $translationId = n * Return a deletion form for requested tag. * * @param Request $request - * @param int $tagId + * @param int $tagId * * @return Response + * @throws RuntimeError */ - public function deleteAction(Request $request, int $tagId) + public function deleteAction(Request $request, int $tagId): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS_DELETE'); @@ -482,8 +487,12 @@ public function deleteAction(Request $request, int $tagId) $this->em()->remove($tag); $this->em()->flush(); - $msg = $this->getTranslator()->trans('tag.%name%.deleted', ['%name%' => $tag->getTranslatedTags()->first()->getName()]); - $this->publishConfirmMessage($request, $msg); + $msg = $this->getTranslator()->trans('tag.%name%.deleted', [ + '%name%' => $tag->getTranslatedTags()->first() ? + $tag->getTranslatedTags()->first()->getName() : + $tag->getTagName(), + ]); + $this->publishConfirmMessage($request, $msg, $tag); /* * Force redirect to avoid resending form when refreshing page @@ -507,8 +516,9 @@ public function deleteAction(Request $request, int $tagId) * @param int|null $translationId * * @return Response + * @throws RuntimeError */ - public function addChildAction(Request $request, int $tagId, ?int $translationId = null) + public function addChildAction(Request $request, int $tagId, ?int $translationId = null): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -550,7 +560,7 @@ public function addChildAction(Request $request, int $tagId, ?int $translationId $this->dispatchEvent(new TagCreatedEvent($tag)); $msg = $this->getTranslator()->trans('child.tag.%name%.created', ['%name%' => $tag->getTagName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $tag); return $this->redirectToRoute( 'tagsEditPage', @@ -580,7 +590,7 @@ public function addChildAction(Request $request, int $tagId, ?int $translationId * @return Response * @throws RuntimeError */ - public function editNodesAction(Request $request, int $tagId) + public function editNodesAction(Request $request, int $tagId): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -618,7 +628,7 @@ public function editNodesAction(Request $request, int $tagId) * * @return FormInterface */ - private function buildDeleteForm(Tag $tag) + private function buildDeleteForm(Tag $tag): FormInterface { $builder = $this->createFormBuilder() ->add('tagId', HiddenType::class, [ @@ -633,15 +643,15 @@ private function buildDeleteForm(Tag $tag) } /** - * @param false|string $referer + * @param null|string $referer * @param array $tagsIds * * @return FormInterface */ private function buildBulkDeleteForm( - $referer = false, + ?string $referer = null, array $tagsIds = [] - ) { + ): FormInterface { $builder = $this->formFactory ->createNamedBuilder('deleteForm') ->add('tagsIds', HiddenType::class, [ @@ -653,7 +663,7 @@ private function buildBulkDeleteForm( ], ]); - if (false !== $referer && (new UnicodeString($referer))->startsWith('/')) { + if (null !== $referer && (new UnicodeString($referer))->startsWith('/')) { $builder->add('referer', HiddenType::class, [ 'data' => $referer, ]); @@ -667,7 +677,7 @@ private function buildBulkDeleteForm( * * @return string */ - private function bulkDeleteTags(array $data) + private function bulkDeleteTags(array $data): string { if (!empty($data['tagsIds'])) { $tagsIds = trim($data['tagsIds']); @@ -711,7 +721,7 @@ protected function onPostUpdate(PersistableInterface $entity, Request $request): $msg = $this->getTranslator()->trans('tag.%name%.updated', [ '%name%' => $entity->getName(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $entity); } } diff --git a/src/Controllers/Tags/TagsUtilsController.php b/src/Controllers/Tags/TagsUtilsController.php index c60cfb7a..71f96693 100644 --- a/src/Controllers/Tags/TagsUtilsController.php +++ b/src/Controllers/Tags/TagsUtilsController.php @@ -12,30 +12,20 @@ use Symfony\Component\HttpFoundation\Response; use Themes\Rozier\RozierApp; -/** - * @package Themes\Rozier\Controllers\Tags - */ class TagsUtilsController extends RozierApp { - private SerializerInterface $serializer; - - /** - * @param SerializerInterface $serializer - */ - public function __construct(SerializerInterface $serializer) + public function __construct(private readonly SerializerInterface $serializer) { - $this->serializer = $serializer; } /** * Export a Tag in a Json file * * @param Request $request - * @param int $tagId - * - * @return Response + * @param int $tagId + * @return JsonResponse */ - public function exportAction(Request $request, int $tagId) + public function exportAction(Request $request, int $tagId): JsonResponse { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -47,7 +37,7 @@ public function exportAction(Request $request, int $tagId) 'json', SerializationContext::create()->setGroups(['tag', 'position']) ), - JsonResponse::HTTP_OK, + Response::HTTP_OK, [ 'Content-Disposition' => sprintf( 'attachment; filename="%s"', @@ -64,9 +54,9 @@ public function exportAction(Request $request, int $tagId) * @param Request $request * @param int $tagId * - * @return Response + * @return JsonResponse */ - public function exportAllAction(Request $request, int $tagId) + public function exportAllAction(Request $request, int $tagId): JsonResponse { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -80,7 +70,7 @@ public function exportAllAction(Request $request, int $tagId) 'json', SerializationContext::create()->setGroups(['tag', 'position']) ), - JsonResponse::HTTP_OK, + Response::HTTP_OK, [ 'Content-Disposition' => sprintf( 'attachment; filename="%s"', diff --git a/src/Controllers/TranslationsController.php b/src/Controllers/TranslationsController.php index 94aacc18..601c24b1 100644 --- a/src/Controllers/TranslationsController.php +++ b/src/Controllers/TranslationsController.php @@ -4,12 +4,12 @@ namespace Themes\Rozier\Controllers; +use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\CoreBundle\EntityHandler\TranslationHandler; use RZ\Roadiz\CoreBundle\Event\Translation\TranslationCreatedEvent; use RZ\Roadiz\CoreBundle\Event\Translation\TranslationDeletedEvent; use RZ\Roadiz\CoreBundle\Event\Translation\TranslationUpdatedEvent; -use RZ\Roadiz\Core\Handlers\HandlerFactoryInterface; -use RZ\Roadiz\CoreBundle\EntityHandler\TranslationHandler; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Request; @@ -23,11 +23,8 @@ class TranslationsController extends RozierApp { public const ITEM_PER_PAGE = 5; - private HandlerFactoryInterface $handlerFactory; - - public function __construct(HandlerFactoryInterface $handlerFactory) + public function __construct(private readonly HandlerFactoryInterface $handlerFactory) { - $this->handlerFactory = $handlerFactory; } /** @@ -61,7 +58,7 @@ public function indexAction(Request $request): Response $handler = $this->handlerFactory->getHandler($translation); $handler->makeDefault(); $msg = $this->getTranslator()->trans('translation.%name%.made_default', ['%name%' => $translation->getName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $translation); $this->dispatchEvent(new TranslationUpdatedEvent($translation)); /* * Force redirect to avoid resending form when refreshing page @@ -106,7 +103,7 @@ public function editAction(Request $request, int $translationId): Response if ($form->isSubmitted() && $form->isValid()) { $this->em()->flush(); $msg = $this->getTranslator()->trans('translation.%name%.updated', ['%name%' => $translation->getName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $translation); $this->dispatchEvent(new TranslationUpdatedEvent($translation)); /* @@ -144,7 +141,7 @@ public function addAction(Request $request): Response $this->em()->flush(); $msg = $this->getTranslator()->trans('translation.%name%.created', ['%name%' => $translation->getName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $translation); $this->dispatchEvent(new TranslationCreatedEvent($translation)); /* @@ -184,7 +181,7 @@ public function deleteAction(Request $request, int $translationId): Response $this->em()->remove($translation); $this->em()->flush(); $msg = $this->getTranslator()->trans('translation.%name%.deleted', ['%name%' => $translation->getName()]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $translation); $this->dispatchEvent(new TranslationDeletedEvent($translation)); return $this->redirectToRoute('translationsHomePage'); diff --git a/src/Controllers/Users/UsersController.php b/src/Controllers/Users/UsersController.php index 62732744..a2ded208 100644 --- a/src/Controllers/Users/UsersController.php +++ b/src/Controllers/Users/UsersController.php @@ -4,239 +4,311 @@ namespace Themes\Rozier\Controllers\Users; +use JMS\Serializer\SerializerInterface; +use RZ\Roadiz\Core\AbstractEntities\PersistableInterface; use RZ\Roadiz\CoreBundle\Entity\Role; use RZ\Roadiz\CoreBundle\Entity\User; -use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Exception\ResourceNotFoundException; -use Themes\Rozier\Forms\AddUserType; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Themes\Rozier\Controllers\AbstractAdminWithBulkController; use Themes\Rozier\Forms\UserDetailsType; use Themes\Rozier\Forms\UserType; -use Themes\Rozier\RozierApp; -use Themes\Rozier\Utils\SessionListFilters; use Twig\Error\RuntimeError; -class UsersController extends RozierApp +class UsersController extends AbstractAdminWithBulkController { - /** - * @param Request $request - * - * @return Response - * @throws RuntimeError - */ - public function indexAction(Request $request): Response + public function __construct( + FormFactoryInterface $formFactory, + SerializerInterface $serializer, + UrlGeneratorInterface $urlGenerator, + private readonly bool $useGravatar + ) { + parent::__construct($formFactory, $serializer, $urlGenerator); + } + + + protected function supports(PersistableInterface $item): bool { - $this->denyAccessUnlessGranted('ROLE_ACCESS_USERS'); + return $item instanceof User; + } - /* - * Manage get request to filter list - */ - $listManager = $this->createEntityListManager( - User::class, - [], - ['username' => 'ASC'] - ); - $listManager->setDisplayingNotPublishedNodes(true); - /* - * Stored in session - */ - $sessionListFilter = new SessionListFilters('user_item_per_page'); - $sessionListFilter->handleItemPerPage($request, $listManager); - $listManager->handle(); + protected function getNamespace(): string + { + return 'user'; + } + + protected function createEmptyItem(Request $request): User + { + $user = new User(); + $user->sendCreationConfirmationEmail(true); + return $user; + } - $this->assignation['filters'] = $listManager->getAssignation(); - $this->assignation['users'] = $listManager->getEntities(); + protected function getTemplateFolder(): string + { + return '@RoadizRozier/users'; + } - return $this->render('@RoadizRozier/users/list.html.twig', $this->assignation); + protected function getRequiredRole(): string + { + return 'ROLE_ACCESS_USERS'; } - /** - * @param Request $request - * @param int $userId - * - * @return Response - * @throws RuntimeError - */ - public function editAction(Request $request, int $userId): Response + protected function getRequiredEditionRole(): string + { + // Allow any backoffice user to access user edition before + // checking if current editing item is the same as current user. + return 'ROLE_BACKEND_USER'; + } + + protected function getRequiredDeletionRole(): string + { + return 'ROLE_ACCESS_USERS_DELETE'; + } + + protected function getEntityClass(): string + { + return User::class; + } + + protected function getFormType(): string + { + return UserType::class; + } + + protected function getDefaultRouteName(): string + { + return 'usersHomePage'; + } + + protected function getEditRouteName(): string { - $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); + return 'usersEditPage'; + } + + protected function getBulkDeleteRouteName(): ?string + { + return 'usersBulkDeletePage'; + } + protected function denyAccessUnlessItemGranted(PersistableInterface $item): void + { + parent::denyAccessUnlessItemGranted($item); + + if (!$item instanceof User) { + throw new \RuntimeException('Invalid item type.'); + } + $requestUser = $this->getUser(); if ( !( - $this->isGranted('ROLE_ACCESS_USERS') || - ($this->getUser() instanceof User && $this->getUser()->getId() == $userId) + $this->isGranted('ROLE_ACCESS_USERS') || + ($requestUser instanceof User && $requestUser->getId() === $item->getId()) ) ) { throw $this->createAccessDeniedException("You don't have access to this page: ROLE_ACCESS_USERS"); } - $user = $this->em()->find(User::class, $userId); - if ($user === null) { - throw new ResourceNotFoundException(); - } - if (!$this->isGranted(Role::ROLE_SUPERADMIN) && $user->isSuperAdmin()) { + if (!$this->isGranted(Role::ROLE_SUPERADMIN) && $item->isSuperAdmin()) { throw $this->createAccessDeniedException("You cannot edit a super admin."); } + } - $form = $this->createForm(UserType::class, $user); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $this->em()->flush(); - $msg = $this->getTranslator()->trans( - 'user.%name%.updated', - ['%name%' => $user->getUsername()] - ); - $this->publishConfirmMessage($request, $msg); - /* - * Force redirect to avoid resending form when refreshing page - */ - return $this->redirectToRoute( - 'usersEditPage', - ['userId' => $user->getId()] - ); + protected function getEntityName(PersistableInterface $item): string + { + if (!$item instanceof User) { + throw new \RuntimeException('Invalid item type.'); } + return $item->getUsername(); + } - $this->assignation['user'] = $user; - $this->assignation['form'] = $form->createView(); + protected function getDefaultOrder(Request $request): array + { + return ['username' => 'ASC']; + } - return $this->render('@RoadizRozier/users/edit.html.twig', $this->assignation); + protected function createUpdateEvent(PersistableInterface $item) + { + if (!$item instanceof User) { + throw new \RuntimeException('Invalid item type.'); + } + /* + * If pictureUrl is empty, use default Gravatar image. + */ + if ($item->getPictureUrl() == '' && $this->useGravatar) { + $item->setPictureUrl($item->getGravatarUrl()); + } + + return parent::createUpdateEvent($item); // TODO: Change the autogenerated stub } /** * @param Request $request - * @param int $userId + * @param int $id * * @return Response * @throws RuntimeError */ - public function editDetailsAction(Request $request, int $userId): Response + public function editDetailsAction(Request $request, int $id): Response { - $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); + $this->denyAccessUnlessGranted($this->getRequiredEditionRole()); + $this->additionalAssignation($request); - if ( - !( - $this->isGranted('ROLE_ACCESS_USERS') || - ($this->getUser() instanceof User && $this->getUser()->getId() === $userId) - ) - ) { - throw $this->createAccessDeniedException("You don't have access to this page: ROLE_ACCESS_USERS"); + /** @var mixed|object|null $item */ + $item = $this->em()->find($this->getEntityClass(), $id); + if (!($item instanceof PersistableInterface)) { + throw $this->createNotFoundException(); } - $user = $this->em()->find(User::class, $userId); - if ($user === null) { - throw new ResourceNotFoundException(); - } - if (!$this->isGranted(Role::ROLE_SUPERADMIN) && $user->isSuperAdmin()) { - throw $this->createAccessDeniedException("You cannot edit a super admin."); - } + $this->prepareWorkingItem($item); + $this->denyAccessUnlessItemGranted($item); - $form = $this->createForm(UserDetailsType::class, $user); + $form = $this->createForm(UserDetailsType::class, $item); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { /* - * If pictureUrl is empty, use default Gravatar image. + * Events are dispatched before entity manager is flushed + * to be able to throw exceptions before it is persisted. */ - if ($user->getPictureUrl() == '') { - $user->setPictureUrl($user->getGravatarUrl()); - } - + $event = $this->createUpdateEvent($item); + $this->dispatchSingleOrMultipleEvent($event); $this->em()->flush(); + /* + * Event that requires that EM is flushed + */ + $postEvent = $this->createPostUpdateEvent($item); + $this->dispatchSingleOrMultipleEvent($postEvent); + $msg = $this->getTranslator()->trans( - 'user.%name%.updated', - ['%name%' => $user->getUsername()] + '%namespace%.%item%.was_updated', + [ + '%item%' => $this->getEntityName($item), + '%namespace%' => $this->getTranslator()->trans($this->getNamespace()) + ] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $item); /* * Force redirect to avoid resending form when refreshing page */ return $this->redirectToRoute( 'usersEditDetailsPage', - ['userId' => $user->getId()] + ['id' => $item->getId()] ); } - $this->assignation['user'] = $user; $this->assignation['form'] = $form->createView(); + $this->assignation['item'] = $item; - return $this->render('@RoadizRozier/users/editDetails.html.twig', $this->assignation); + return $this->render( + $this->getTemplateFolder() . '/editDetails.html.twig', + $this->assignation, + null, + $this->getTemplateNamespace() + ); } - /** - * @param Request $request - * - * @return Response - * @throws RuntimeError - */ - public function addAction(Request $request): Response - { - $this->denyAccessUnlessGranted('ROLE_ACCESS_USERS'); - - $user = new User(); - $user->sendCreationConfirmationEmail(true); - $this->assignation['user'] = $user; - $form = $this->createForm(AddUserType::class, $user); - $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $this->em()->persist($user); - $this->em()->flush(); - - $msg = $this->getTranslator()->trans('user.%name%.created', ['%name%' => $user->getUsername()]); - $this->publishConfirmMessage($request, $msg); + protected function additionalAssignation(Request $request): void + { + parent::additionalAssignation($request); - return $this->redirectToRoute('usersHomePage'); + if (null !== $this->getBulkEnableRouteName()) { + $bulkEnableForm = $this->createEnableBulkForm(true); + $this->assignation['bulkEnableForm'] = $bulkEnableForm->createView(); + $this->assignation['hasBulkActions'] = true; } - $this->assignation['form'] = $form->createView(); - - return $this->render('@RoadizRozier/users/add.html.twig', $this->assignation); + if (null !== $this->getBulkDisableRouteName()) { + $bulkDisableForm = $this->createDisableBulkForm(true); + $this->assignation['bulkDisableForm'] = $bulkDisableForm->createView(); + $this->assignation['hasBulkActions'] = true; + } } - /** - * @param Request $request - * @param int $userId - * - * @return Response - * @throws RuntimeError + /* + * User specific bulk actions */ - public function deleteAction(Request $request, int $userId): Response - { - $this->denyAccessUnlessGranted('ROLE_ACCESS_USERS_DELETE'); - $user = $this->em()->find(User::class, (int) $userId); - if ($user === null) { - throw new ResourceNotFoundException(); - } + protected function createEnableBulkForm(bool $get = false, ?array $data = null): FormInterface + { + return $this->createBulkForm( + $this->getBulkEnableRouteName(), + 'bulk-enable', + $get, + $data + ); + } - if (!$this->isGranted(Role::ROLE_SUPERADMIN) && $user->isSuperAdmin()) { - throw $this->createAccessDeniedException("You cannot edit a super admin."); - } + protected function createDisableBulkForm(bool $get = false, ?array $data = null): FormInterface + { + return $this->createBulkForm( + $this->getBulkDisableRouteName(), + 'bulk-disable', + $get, + $data + ); + } - $form = $this->createForm(FormType::class); - $form->handleRequest($request); + private function getBulkEnableRouteName(): string + { + return 'usersBulkEnablePage'; + } - if ($form->isSubmitted() && $form->isValid()) { - $this->em()->remove($user); - $this->em()->flush(); - $msg = $this->getTranslator()->trans( - 'user.%name%.deleted', - ['%name%' => $user->getUsername()] - ); - $this->publishConfirmMessage($request, $msg); - /* - * Force redirect to avoid resending form when refreshing page - */ - return $this->redirectToRoute('usersHomePage'); - } + private function getBulkDisableRouteName(): string + { + return 'usersBulkDisablePage'; + } - $this->assignation['user'] = $user; - $this->assignation['form'] = $form->createView(); + public function bulkEnableAction(Request $request): Response + { + return $this->bulkAction( + $request, + $this->getRequiredRole(), + $this->createEnableBulkForm(true), + $this->createEnableBulkForm(), + function (string $ids) { + return $this->createEnableBulkForm(false, [ + 'id' => $ids, + ]); + }, + $this->getTemplateFolder() . '/bulk_enable.html.twig', + '%namespace%.%item%.was_enabled', + function (PersistableInterface $item) { + if (!$item instanceof User) { + throw new \RuntimeException('Invalid item type.'); + } + $item->setEnabled(true); + }, + 'bulkEnableForm' + ); + } - return $this->render('@RoadizRozier/users/delete.html.twig', $this->assignation); + public function bulkDisableAction(Request $request): Response + { + return $this->bulkAction( + $request, + $this->getRequiredRole(), + $this->createDisableBulkForm(true), + $this->createDisableBulkForm(), + function (string $ids) { + return $this->createDisableBulkForm(false, [ + 'id' => $ids, + ]); + }, + $this->getTemplateFolder() . '/bulk_disable.html.twig', + '%namespace%.%item%.was_disabled', + function (PersistableInterface $item) { + if (!$item instanceof User) { + throw new \RuntimeException('Invalid item type.'); + } + $item->setEnabled(false); + }, + 'bulkDisableForm' + ); } } diff --git a/src/Controllers/Users/UsersGroupsController.php b/src/Controllers/Users/UsersGroupsController.php index 4c5ba264..62103ffc 100644 --- a/src/Controllers/Users/UsersGroupsController.php +++ b/src/Controllers/Users/UsersGroupsController.php @@ -27,53 +27,52 @@ public function editGroupsAction(Request $request, int $userId): Response /** @var User|null $user */ $user = $this->em()->find(User::class, $userId); + if ($user === null) { + throw new ResourceNotFoundException(); + } - if ($user !== null) { - $this->assignation['user'] = $user; - - $form = $this->buildEditGroupsForm($user); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $data = $form->getData(); - if ($data['userId'] == $user->getId()) { - if (array_key_exists('group', $data) && $data['group'][0] instanceof Group) { - $group = $data['group'][0]; - } elseif (array_key_exists('group', $data) && is_numeric($data['group'])) { - $group = $this->em()->find(Group::class, $data['group']); - } else { - $group = null; - } - - if ($group !== null) { - $user->addGroup($group); - $this->em()->flush(); - - $this->dispatchEvent(new UserJoinedGroupEvent($user, $group)); - - $msg = $this->getTranslator()->trans('user.%user%.group.%group%.linked', [ - '%user%' => $user->getUserName(), - '%group%' => $group->getName(), - ]); - $this->publishConfirmMessage($request, $msg); - - /* - * Force redirect to avoid resending form when refreshing page - */ - return $this->redirectToRoute( - 'usersEditGroupsPage', - ['userId' => $user->getId()] - ); - } - } - } + $this->assignation['user'] = $user; + + $form = $this->buildEditGroupsForm($user); + $form->handleRequest($request); - $this->assignation['form'] = $form->createView(); + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + if ($data['userId'] == $user->getId()) { + if (array_key_exists('group', $data) && $data['group'][0] instanceof Group) { + $group = $data['group'][0]; + } elseif (array_key_exists('group', $data) && is_numeric($data['group'])) { + $group = $this->em()->find(Group::class, $data['group']); + } else { + $group = null; + } - return $this->render('@RoadizRozier/users/groups.html.twig', $this->assignation); + if ($group !== null) { + $user->addGroup($group); + $this->em()->flush(); + + $this->dispatchEvent(new UserJoinedGroupEvent($user, $group)); + + $msg = $this->getTranslator()->trans('user.%user%.group.%group%.linked', [ + '%user%' => $user->getUserName(), + '%group%' => $group->getName(), + ]); + $this->publishConfirmMessage($request, $msg, $user); + + /* + * Force redirect to avoid resending form when refreshing page + */ + return $this->redirectToRoute( + 'usersEditGroupsPage', + ['userId' => $user->getId()] + ); + } + } } - throw new ResourceNotFoundException(); + $this->assignation['form'] = $form->createView(); + + return $this->render('@RoadizRozier/users/groups.html.twig', $this->assignation); } public function removeGroupAction(Request $request, int $userId, int $groupId): Response @@ -85,44 +84,47 @@ public function removeGroupAction(Request $request, int $userId, int $groupId): /** @var Group|null $group */ $group = $this->em()->find(Group::class, $groupId); + if ($user === null) { + throw new ResourceNotFoundException(); + } + if ($group === null) { + throw new ResourceNotFoundException(); + } + if (!$this->isGranted($group)) { throw $this->createAccessDeniedException(); } - if ($user !== null) { - $this->assignation['user'] = $user; - $this->assignation['group'] = $group; + $this->assignation['user'] = $user; + $this->assignation['group'] = $group; - $form = $this->createForm(FormType::class); - $form->handleRequest($request); + $form = $this->createForm(FormType::class); + $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $user->removeGroup($group); - $this->em()->flush(); + if ($form->isSubmitted() && $form->isValid()) { + $user->removeGroup($group); + $this->em()->flush(); - $this->dispatchEvent(new UserLeavedGroupEvent($user, $group)); + $this->dispatchEvent(new UserLeavedGroupEvent($user, $group)); - $msg = $this->getTranslator()->trans('user.%user%.group.%group%.removed', [ - '%user%' => $user->getUserName(), - '%group%' => $group->getName(), - ]); - $this->publishConfirmMessage($request, $msg); + $msg = $this->getTranslator()->trans('user.%user%.group.%group%.removed', [ + '%user%' => $user->getUserName(), + '%group%' => $group->getName(), + ]); + $this->publishConfirmMessage($request, $msg, $user); - /* - * Force redirect to avoid resending form when refreshing page - */ - return $this->redirectToRoute( - 'usersEditGroupsPage', - ['userId' => $user->getId()] - ); - } - - $this->assignation['form'] = $form->createView(); - - return $this->render('@RoadizRozier/users/removeGroup.html.twig', $this->assignation); + /* + * Force redirect to avoid resending form when refreshing page + */ + return $this->redirectToRoute( + 'usersEditGroupsPage', + ['userId' => $user->getId()] + ); } - throw new ResourceNotFoundException(); + $this->assignation['form'] = $form->createView(); + + return $this->render('@RoadizRozier/users/removeGroup.html.twig', $this->assignation); } /** diff --git a/src/Controllers/Users/UsersRolesController.php b/src/Controllers/Users/UsersRolesController.php index 04b0a57a..ba80f0af 100644 --- a/src/Controllers/Users/UsersRolesController.php +++ b/src/Controllers/Users/UsersRolesController.php @@ -53,7 +53,7 @@ public function editRolesAction(Request $request, int $userId): Response '%role%' => $role->getRole(), ]); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $user); /* * Force redirect to avoid resending form when refreshing page @@ -114,7 +114,7 @@ public function removeRoleAction(Request $request, int $userId, int $roleId): Re 'user.%name%.role_removed', ['%name%' => $role->getRole()] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $user); /* * Force redirect to avoid resending form when refreshing page diff --git a/src/Controllers/Users/UsersSecurityController.php b/src/Controllers/Users/UsersSecurityController.php index 27c178c4..35949690 100644 --- a/src/Controllers/Users/UsersSecurityController.php +++ b/src/Controllers/Users/UsersSecurityController.php @@ -45,7 +45,7 @@ public function securityAction(Request $request, int $userId): Response ['%name%' => $user->getUsername()] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $user); /* * Force redirect to avoid resending form when refreshing page diff --git a/src/Controllers/WebhookController.php b/src/Controllers/WebhookController.php index 1c60d4ce..60bccc70 100644 --- a/src/Controllers/WebhookController.php +++ b/src/Controllers/WebhookController.php @@ -12,21 +12,20 @@ use RZ\Roadiz\CoreBundle\Webhook\WebhookDispatcher; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -final class WebhookController extends AbstractAdminController +final class WebhookController extends AbstractAdminWithBulkController { - private WebhookDispatcher $webhookDispatcher; - public function __construct( - WebhookDispatcher $webhookDispatcher, + private readonly WebhookDispatcher $webhookDispatcher, + FormFactoryInterface $formFactory, SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator ) { - parent::__construct($serializer, $urlGenerator); - $this->webhookDispatcher = $webhookDispatcher; + parent::__construct($formFactory, $serializer, $urlGenerator); } public function triggerAction(Request $request, string $id): Response @@ -57,7 +56,7 @@ public function triggerAction(Request $request, string $id): Response '%seconds%' => $item->getThrottleSeconds(), ] ); - $this->publishConfirmMessage($request, $msg); + $this->publishConfirmMessage($request, $msg, $item); return $this->redirect($this->urlGenerator->generate( $this->getDefaultRouteName(), @@ -133,4 +132,8 @@ protected function getEntityName(PersistableInterface $item): string } return ''; } + protected function getBulkDeleteRouteName(): ?string + { + return 'webhooksBulkDeletePage'; + } } diff --git a/src/Event/UserActionsMenuEvent.php b/src/Event/UserActionsMenuEvent.php new file mode 100644 index 00000000..f5284c61 --- /dev/null +++ b/src/Event/UserActionsMenuEvent.php @@ -0,0 +1,29 @@ +actions[] = [ + 'label' => $label, + 'path' => $path, + 'icon' => $icon, + ]; + } + + public function getActions(): array + { + return $this->actions; + } +} diff --git a/src/Explorer/ConfigurableExplorerItem.php b/src/Explorer/ConfigurableExplorerItem.php index 4fb67bfa..bc86157e 100644 --- a/src/Explorer/ConfigurableExplorerItem.php +++ b/src/Explorer/ConfigurableExplorerItem.php @@ -55,9 +55,12 @@ public function getAlternativeDisplayable(): ?string { $alt = $this->configuration['classname']; if (!empty($this->configuration['alt_displayable'])) { - $alt = call_user_func([$this->entity, $this->configuration['alt_displayable']]); - if ($alt instanceof \DateTimeInterface) { - $alt = $alt->format('c'); + $altDisplayableCallable = [$this->entity, $this->configuration['alt_displayable']]; + if (\is_callable($altDisplayableCallable)) { + $alt = call_user_func($altDisplayableCallable); + if ($alt instanceof \DateTimeInterface) { + $alt = $alt->format('c'); + } } } return (new UnicodeString($alt ?? ''))->truncate(30, '…')->toString(); @@ -68,9 +71,12 @@ public function getAlternativeDisplayable(): ?string */ public function getDisplayable(): string { - $displayable = call_user_func([$this->entity, $this->configuration['displayable']]); - if ($displayable instanceof \DateTimeInterface) { - $displayable = $displayable->format('c'); + $displayableCallable = [$this->entity, $this->configuration['displayable']]; + if (\is_callable($displayableCallable)) { + $displayable = call_user_func($displayableCallable); + if ($displayable instanceof \DateTimeInterface) { + $displayable = $displayable->format('c'); + } } return (new UnicodeString($displayable ?? ''))->truncate(30, '…')->toString(); } @@ -88,11 +94,14 @@ protected function getThumbnail(): ?array /** @var DocumentInterface|null $thumbnail */ $thumbnail = null; if (!empty($this->configuration['thumbnail'])) { - $thumbnail = call_user_func([$this->entity, $this->configuration['thumbnail']]); - if ($thumbnail instanceof Collection && $thumbnail->count() > 0 && $thumbnail->first() instanceof DocumentInterface) { - $thumbnail = $thumbnail->first(); - } elseif (is_array($thumbnail) && count($thumbnail) > 0 && $thumbnail[0] instanceof DocumentInterface) { - $thumbnail = $thumbnail[0]; + $thumbnailCallable = [$this->entity, $this->configuration['thumbnail']]; + if (\is_callable($thumbnailCallable)) { + $thumbnail = call_user_func($thumbnailCallable); + if ($thumbnail instanceof Collection && $thumbnail->count() > 0 && $thumbnail->first() instanceof DocumentInterface) { + $thumbnail = $thumbnail->first(); + } elseif (is_array($thumbnail) && count($thumbnail) > 0 && $thumbnail[0] instanceof DocumentInterface) { + $thumbnail = $thumbnail[0]; + } } } diff --git a/src/Explorer/FolderExplorerItem.php b/src/Explorer/FolderExplorerItem.php index 10588ae0..9a85e7ae 100644 --- a/src/Explorer/FolderExplorerItem.php +++ b/src/Explorer/FolderExplorerItem.php @@ -35,7 +35,9 @@ public function getAlternativeDisplayable(): ?string /** @var Folder|null $parent */ $parent = $this->folder->getParent(); if (null !== $parent) { - return $parent->getTranslatedFolders()->first()->getName(); + return $parent->getTranslatedFolders()->first() ? + $parent->getTranslatedFolders()->first()->getName() : + $parent->getName(); } return ''; } @@ -45,7 +47,9 @@ public function getAlternativeDisplayable(): ?string */ public function getDisplayable(): string { - return $this->folder->getTranslatedFolders()->first()->getName(); + return $this->folder->getTranslatedFolders()->first() ? + $this->folder->getTranslatedFolders()->first()->getName() : + $this->folder->getName(); } /** diff --git a/src/Explorer/UserExplorerItem.php b/src/Explorer/UserExplorerItem.php index eb63dc4c..b7012f16 100644 --- a/src/Explorer/UserExplorerItem.php +++ b/src/Explorer/UserExplorerItem.php @@ -62,7 +62,7 @@ public function getOriginal(): User protected function getEditItemPath(): ?string { return $this->urlGenerator->generate('usersEditPage', [ - 'userId' => $this->user->getId() + 'id' => $this->user->getId() ]); } } diff --git a/src/Forms/AddUserType.php b/src/Forms/AddUserType.php index debb57c3..2cbff0d7 100644 --- a/src/Forms/AddUserType.php +++ b/src/Forms/AddUserType.php @@ -7,9 +7,6 @@ use RZ\Roadiz\CoreBundle\Form\GroupsType; use Symfony\Component\Form\FormBuilderInterface; -/** - * @package Themes\Rozier\Forms - */ class AddUserType extends UserType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Forms/CustomFormFieldType.php b/src/Forms/CustomFormFieldType.php index 998ad6f6..0fc5675a 100644 --- a/src/Forms/CustomFormFieldType.php +++ b/src/Forms/CustomFormFieldType.php @@ -9,13 +9,11 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +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; -/** - * @package Themes\Rozier\Forms - */ class CustomFormFieldType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void @@ -50,9 +48,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]) ->add( 'defaultValues', - TextType::class, + TextareaType::class, [ - 'label' => 'defaultValues', + 'label' => 'customFormField.defaultValues', 'required' => false, 'attr' => [ 'placeholder' => 'enter_values_comma_separated', diff --git a/src/Forms/CustomFormType.php b/src/Forms/CustomFormType.php new file mode 100644 index 00000000..57858f3b --- /dev/null +++ b/src/Forms/CustomFormType.php @@ -0,0 +1,121 @@ +security = $security; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('displayName', TextType::class, [ + 'label' => 'customForm.displayName', + 'empty_data' => '', + ]) + ->add('description', MarkdownType::class, [ + 'label' => 'description', + 'required' => false, + ]) + ->add('email', TextType::class, [ + 'label' => 'email', + 'required' => false, + 'constraints' => [ + new Callback(function ($value, ExecutionContextInterface $context) { + $emails = array_filter( + array_map('trim', explode(',', $value ?? '')) + ); + foreach ($emails as $email) { + if (false === filter_var($email, FILTER_VALIDATE_EMAIL)) { + $context->buildViolation('{{ value }} is not a valid email address.') + ->setParameter('{{ value }}', $email) + ->setCode(Email::INVALID_FORMAT_ERROR) + ->addViolation(); + } + } + }), + ], + ]) + ; + if ($this->security->isGranted('ROLE_ACCESS_CUSTOMFORMS_RETENTION')) { + $builder->add('retentionTime', ChoiceType::class, [ + 'label' => 'customForm.retentionTime', + 'help' => 'customForm.retentionTime.help', + 'required' => false, + 'placeholder' => 'customForm.retentionTime.always', + 'choices' => [ + 'customForm.retentionTime.one_week' => 'P7D', + 'customForm.retentionTime.two_weeks' => 'P14D', + 'customForm.retentionTime.one_month' => 'P1M', + 'customForm.retentionTime.three_months' => 'P3M', + 'customForm.retentionTime.six_months' => 'P6M', + 'customForm.retentionTime.one_year' => 'P1Y', + 'customForm.retentionTime.two_years' => 'P2Y', + ] + ]); + } + $builder->add('open', CheckboxType::class, [ + 'label' => 'customForm.open', + 'required' => false, + ]) + ->add('closeDate', DateTimeType::class, [ + 'label' => 'customForm.closeDate', + 'required' => false, + 'date_widget' => 'single_text', + 'date_format' => 'yyyy-MM-dd', + 'attr' => [ + 'class' => 'rz-datetime-field', + ], + 'placeholder' => [ + 'hour' => 'hour', + 'minute' => 'minute', + ], + ]) + ->add('color', ColorType::class, [ + 'label' => 'customForm.color', + 'required' => false, + ]); + } + + public function getBlockPrefix(): string + { + return 'customform'; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'label' => false, + 'name' => '', + 'data_class' => CustomForm::class, + 'attr' => [ + 'class' => 'uk-form custom-form-form', + ], + ]); + $resolver->setAllowedTypes('name', 'string'); + } +} diff --git a/src/Forms/DataTransformer/TagTransformer.php b/src/Forms/DataTransformer/TagTransformer.php index a5519023..f90162b2 100644 --- a/src/Forms/DataTransformer/TagTransformer.php +++ b/src/Forms/DataTransformer/TagTransformer.php @@ -4,16 +4,11 @@ namespace Themes\Rozier\Forms\DataTransformer; -use Doctrine\Common\Collections\Collection; use Doctrine\Persistence\ObjectManager; -use RZ\Roadiz\Core\AbstractEntities\AbstractEntity; use RZ\Roadiz\CoreBundle\Entity\Tag; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; -/** - * @package Themes\Rozier\Forms\DataTransformer - */ class TagTransformer implements DataTransformerInterface { private ObjectManager $manager; diff --git a/src/Forms/DocumentEditType.php b/src/Forms/DocumentEditType.php index 09bf4c1b..b016ee93 100644 --- a/src/Forms/DocumentEditType.php +++ b/src/Forms/DocumentEditType.php @@ -17,7 +17,7 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\Constraints\LessThanOrEqual; use Symfony\Component\Validator\Constraints\NotBlank; @@ -118,6 +118,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]); } + if ($document->isProcessable()) { + $builder->add('imageCropAlignment', ImageCropAlignmentType::class, [ + 'label' => 'document.imageCropAlignment', + 'help' => 'document.imageCropAlignment.help', + 'required' => false, + ]); + } + /* * Display thumbnails only if current Document is original. */ diff --git a/src/Forms/DocumentTranslationType.php b/src/Forms/DocumentTranslationType.php index 946c5821..ea1fd10a 100644 --- a/src/Forms/DocumentTranslationType.php +++ b/src/Forms/DocumentTranslationType.php @@ -4,8 +4,8 @@ namespace Themes\Rozier\Forms; -use RZ\Roadiz\CoreBundle\Form\MarkdownType; use RZ\Roadiz\CoreBundle\Entity\DocumentTranslation; +use RZ\Roadiz\CoreBundle\Form\MarkdownType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -29,7 +29,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, ]) ->add('copyright', TextType::class, [ - 'label' => 'copyright', + 'label' => 'document.copyrightHolder', + 'required' => false, + ]) + ->add('externalUrl', TextType::class, [ + 'label' => 'document.externalUrl', 'required' => false, ]); } diff --git a/src/Forms/DynamicType.php b/src/Forms/DynamicType.php index 3a65e897..dbaf6999 100644 --- a/src/Forms/DynamicType.php +++ b/src/Forms/DynamicType.php @@ -9,11 +9,6 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\OptionsResolver\OptionsResolver; -/** - * Class DynamicType - * - * @package Themes\Rozier\Forms - */ class DynamicType extends AbstractType { /** diff --git a/src/Forms/FolderTranslationType.php b/src/Forms/FolderTranslationType.php index 16b25967..ba030249 100644 --- a/src/Forms/FolderTranslationType.php +++ b/src/Forms/FolderTranslationType.php @@ -10,9 +10,6 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -/** - * @package Themes\Rozier\Forms - */ class FolderTranslationType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Forms/FolderType.php b/src/Forms/FolderType.php index 83ab4f95..d05469dc 100644 --- a/src/Forms/FolderType.php +++ b/src/Forms/FolderType.php @@ -12,9 +12,6 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -/** - * @package Themes\Rozier\Forms - */ class FolderType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Forms/ImageCropAlignmentType.php b/src/Forms/ImageCropAlignmentType.php new file mode 100644 index 00000000..5a79bd2d --- /dev/null +++ b/src/Forms/ImageCropAlignmentType.php @@ -0,0 +1,44 @@ +setDefaults([ + 'label' => 'image_crop_alignment', + 'required' => false, + 'placeholder' => 'image_crop_alignment.none', + 'expanded' => true, + 'choices' => [ + 'image_crop_alignment.top-left' => 'top-left', + 'image_crop_alignment.top' => 'top', + 'image_crop_alignment.top-right' => 'top-right', + 'image_crop_alignment.left' => 'left', + 'image_crop_alignment.center' => 'center', + 'image_crop_alignment.right' => 'right', + 'image_crop_alignment.bottom-left' => 'bottom-left', + 'image_crop_alignment.bottom' => 'bottom', + 'image_crop_alignment.bottom-right' => 'bottom-right', + ] + ]); + } + + public function getBlockPrefix(): string + { + return 'image_crop_alignment'; + } + + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/Forms/LoginType.php b/src/Forms/LoginType.php index 37b0804e..3f88811c 100644 --- a/src/Forms/LoginType.php +++ b/src/Forms/LoginType.php @@ -65,7 +65,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ], ]); - if ($this->requestStack->getMasterRequest()->query->has('_home')) { + if ($this->requestStack->getMainRequest()?->query->has('_home')) { $builder->add('_target_path', HiddenType::class, [ 'data' => $this->urlGenerator->generate('adminHomePage') ]); diff --git a/src/Forms/Node/AddNodeType.php b/src/Forms/Node/AddNodeType.php index 336d4dea..ffc863f6 100644 --- a/src/Forms/Node/AddNodeType.php +++ b/src/Forms/Node/AddNodeType.php @@ -9,22 +9,17 @@ use RZ\Roadiz\CoreBundle\Form\DataTransformer\NodeTypeTransformer; use RZ\Roadiz\CoreBundle\Form\NodeTypesType; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\CallbackTransformer; -use Symfony\Component\Form\Event\PostSubmitEvent; +use Symfony\Component\Form\Event\SubmitEvent; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; -use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\Event\SubmitEvent; -/** - * @package Themes\Rozier\Forms\Node - */ class AddNodeType extends AbstractType { protected ManagerRegistry $managerRegistry; diff --git a/src/Forms/NodeSource/NodeSourceCustomFormType.php b/src/Forms/NodeSource/NodeSourceCustomFormType.php index b46f3a90..3b6fa151 100644 --- a/src/Forms/NodeSource/NodeSourceCustomFormType.php +++ b/src/Forms/NodeSource/NodeSourceCustomFormType.php @@ -85,7 +85,7 @@ public function onPreSetData(FormEvent $event): void $event->setData($this->managerRegistry ->getRepository(CustomForm::class) - ->findByNodeAndField($nodeSource->getNode(), $nodeTypeField)); + ->findByNodeAndFieldName($nodeSource->getNode(), $nodeTypeField->getName())); } /** diff --git a/src/Forms/NodeSource/NodeSourceDocumentType.php b/src/Forms/NodeSource/NodeSourceDocumentType.php index 0278872f..1e1aeba2 100644 --- a/src/Forms/NodeSource/NodeSourceDocumentType.php +++ b/src/Forms/NodeSource/NodeSourceDocumentType.php @@ -88,9 +88,9 @@ public function onPreSetData(FormEvent $event): void $event->setData($this->managerRegistry ->getRepository(Document::class) - ->findByNodeSourceAndField( + ->findByNodeSourceAndFieldName( $nodeSource, - $nodeTypeField + $nodeTypeField->getName() )); } diff --git a/src/Forms/NodeSource/NodeSourceJoinType.php b/src/Forms/NodeSource/NodeSourceJoinType.php index 27b90477..b7810c44 100644 --- a/src/Forms/NodeSource/NodeSourceJoinType.php +++ b/src/Forms/NodeSource/NodeSourceJoinType.php @@ -55,8 +55,9 @@ public function buildView(FormView $view, FormInterface $form, array $options): $configuration = $this->getFieldConfiguration($options); $displayableData = []; - - $entities = call_user_func([$options['nodeSource'], $options['nodeTypeField']->getGetterName()]); + /** @var callable $callable */ + $callable = [$options['nodeSource'], $options['nodeTypeField']->getGetterName()]; + $entities = call_user_func($callable); if ($entities instanceof \Traversable) { /** @var PersistableInterface $entity */ @@ -68,8 +69,9 @@ public function buildView(FormView $view, FormInterface $form, array $options): 'id' => $entity->getId(), 'classname' => $configuration['classname'], ]; - if (is_callable([$entity, $configuration['displayable']])) { - $data['name'] = call_user_func([$entity, $configuration['displayable']]); + $displayableCallable = [$entity, $configuration['displayable']]; + if (\is_callable($displayableCallable)) { + $data['name'] = call_user_func($displayableCallable); } $displayableData[] = $data; } @@ -81,8 +83,9 @@ public function buildView(FormView $view, FormInterface $form, array $options): 'id' => $entities->getId(), 'classname' => $configuration['classname'], ]; - if (is_callable([$entities, $configuration['displayable']])) { - $data['name'] = call_user_func([$entities, $configuration['displayable']]); + $displayableCallable = [$entities, $configuration['displayable']]; + if (\is_callable($displayableCallable)) { + $data['name'] = call_user_func($displayableCallable); } $displayableData[] = $data; } diff --git a/src/Forms/NodeSource/NodeSourceProviderType.php b/src/Forms/NodeSource/NodeSourceProviderType.php index 3471cfdb..9132793c 100644 --- a/src/Forms/NodeSource/NodeSourceProviderType.php +++ b/src/Forms/NodeSource/NodeSourceProviderType.php @@ -105,7 +105,9 @@ public function buildView(FormView $view, FormInterface $form, array $options): $provider = $this->getProvider($configuration, $options); $displayableData = []; - $ids = call_user_func([$options['nodeSource'], $options['nodeTypeField']->getGetterName()]); + /** @var callable $callable */ + $callable = [$options['nodeSource'], $options['nodeTypeField']->getGetterName()]; + $ids = call_user_func($callable); if (!is_array($ids)) { $entities = $provider->getItemsById([$ids]); } else { diff --git a/src/Forms/NodeSource/NodeSourceType.php b/src/Forms/NodeSource/NodeSourceType.php index 366408a4..59d4fbf5 100644 --- a/src/Forms/NodeSource/NodeSourceType.php +++ b/src/Forms/NodeSource/NodeSourceType.php @@ -18,6 +18,7 @@ use RZ\Roadiz\CoreBundle\Form\MultipleEnumerationType; use RZ\Roadiz\CoreBundle\Form\YamlType; use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeTypeFieldVoter; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CountryType; @@ -31,7 +32,6 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Security\Core\Security; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Type; diff --git a/src/Forms/NodeTagsType.php b/src/Forms/NodeTagsType.php deleted file mode 100644 index cecf98ac..00000000 --- a/src/Forms/NodeTagsType.php +++ /dev/null @@ -1,63 +0,0 @@ -managerRegistry = $managerRegistry; - } - - /** - * {@inheritdoc} - * - * @param FormBuilderInterface $builder - * @param array $options - */ - public function buildForm(FormBuilderInterface $builder, array $options): void - { - $builder->add('tags', TagsType::class, [ - 'by_reference' => false - ]); - $builder->get('tags') - ->addModelTransformer(new TagTransformer($this->managerRegistry->getManager())); - } - - /** - * {@inheritdoc} - * - * @param OptionsResolver $resolver - */ - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefault('data_class', Node::class); - } - - /** - * {@inheritdoc} - */ - public function getBlockPrefix(): string - { - return 'node_tags'; - } -} diff --git a/src/Forms/NodeType.php b/src/Forms/NodeType.php index d3585452..9cf491af 100644 --- a/src/Forms/NodeType.php +++ b/src/Forms/NodeType.php @@ -5,7 +5,6 @@ namespace Themes\Rozier\Forms; use RZ\Roadiz\CoreBundle\Entity\Node; -use RZ\Roadiz\CoreBundle\Form\Constraint\UniqueNodeName; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -22,11 +21,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'nodeName', 'empty_data' => '', 'help' => 'node.nodeName.help', - 'constraints' => [ - new UniqueNodeName([ - 'currentValue' => $options['nodeName'], - ]), - ] ]) ->add('dynamicNodeName', CheckboxType::class, [ 'label' => 'node.dynamicNodeName', @@ -35,7 +29,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]) ; - if (null !== $builder->getData() && $builder->getData()->getNodeType()->isReachable()) { + /** @var Node|null $node */ + $node = $builder->getData(); + $isReachable = null !== $node && $node->getNodeType()->isReachable(); + if ($isReachable) { $builder->add('home', CheckboxType::class, [ 'label' => 'node.isHome', 'required' => false, @@ -56,7 +53,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]) ; - if (null !== $builder->getData() && $builder->getData()->getNodeType()->isReachable()) { + if ($isReachable) { $builder->add('ttl', IntegerType::class, [ 'label' => 'node.ttl', 'help' => 'node_time_to_live_cache_on_front_controller', @@ -80,7 +77,6 @@ public function configureOptions(OptionsResolver $resolver): void 'class' => 'uk-form node-form', ], ]); - - $resolver->setAllowedTypes('nodeName', 'string'); + $resolver->setAllowedTypes('nodeName', ['string', 'null']); } } diff --git a/src/Forms/NodeTypeFieldType.php b/src/Forms/NodeTypeFieldType.php index 46d15fa1..2d779998 100644 --- a/src/Forms/NodeTypeFieldType.php +++ b/src/Forms/NodeTypeFieldType.php @@ -13,9 +13,6 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -/** - * @package Themes\Rozier\Forms - */ class NodeTypeFieldType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void @@ -75,7 +72,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, ]) ->add('defaultValues', DynamicType::class, [ - 'label' => 'defaultValues', + 'label' => 'nodeTypeField.defaultValues', 'required' => false, 'help' => 'for_children_node_and_node_references_enter_node_type_names_comma_separated', 'attr' => [ diff --git a/src/Forms/NodeTypeType.php b/src/Forms/NodeTypeType.php index 3601c424..beb37c1a 100644 --- a/src/Forms/NodeTypeType.php +++ b/src/Forms/NodeTypeType.php @@ -41,6 +41,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, 'help' => 'enables_published_at_field_for_time_based_publication', ]) + ->add('attributable', CheckboxType::class, [ + 'label' => 'attributable', + 'required' => false, + 'help' => 'enables_node_attributes_for_this_type', + ]) + ->add('sortingAttributesByWeight', CheckboxType::class, [ + 'label' => 'sortingAttributesByWeight', + 'required' => false, + 'help' => 'sort_attributes_by_weight_for_this_type', + ]) ->add('reachable', CheckboxType::class, [ 'label' => 'reachable', 'required' => false, diff --git a/src/Forms/RedirectionType.php b/src/Forms/RedirectionType.php index b0d22a9b..1839e8ce 100644 --- a/src/Forms/RedirectionType.php +++ b/src/Forms/RedirectionType.php @@ -13,9 +13,6 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\OptionsResolver\OptionsResolver; -/** - * @package Themes\Rozier\Forms - */ class RedirectionType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Forms/TranstypeType.php b/src/Forms/TranstypeType.php index 2ba0eba5..bb32d3b5 100644 --- a/src/Forms/TranstypeType.php +++ b/src/Forms/TranstypeType.php @@ -14,9 +14,6 @@ use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotNull; -/** - * @package Themes\Rozier\Forms - */ class TranstypeType extends AbstractType { protected ManagerRegistry $managerRegistry; diff --git a/src/Forms/UserType.php b/src/Forms/UserType.php index 65bd4b39..0f4f8c9b 100644 --- a/src/Forms/UserType.php +++ b/src/Forms/UserType.php @@ -12,9 +12,6 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -/** - * @package Themes\Rozier\Forms - */ class UserType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Models/CustomFormModel.php b/src/Models/CustomFormModel.php index fe4f00f5..7724af37 100644 --- a/src/Models/CustomFormModel.php +++ b/src/Models/CustomFormModel.php @@ -8,9 +8,6 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Translation\TranslatorInterface; -/** - * @package Themes\Rozier\Models - */ final class CustomFormModel implements ModelInterface { private CustomForm $customForm; diff --git a/src/Models/DocumentModel.php b/src/Models/DocumentModel.php index 237c4376..ad4e6f98 100644 --- a/src/Models/DocumentModel.php +++ b/src/Models/DocumentModel.php @@ -13,9 +13,6 @@ use RZ\Roadiz\Documents\UrlGenerators\DocumentUrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -/** - * @package Themes\Rozier\Models - */ final class DocumentModel implements ModelInterface { public static array $thumbnailArray = [ @@ -112,10 +109,10 @@ public function toArray(): array $editUrl = null; } - $embedFinder = $this->embedFinderFactory->createForPlatform( + $embedFinder = $this->embedFinderFactory?->createForPlatform( $this->document->getEmbedPlatform(), $this->document->getEmbedId() - ); + ) ?? null; return [ 'id' => $id, @@ -143,7 +140,7 @@ public function toArray(): array : $this->document->getShortType(), 'shortMimeType' => $this->document->getShortMimeType(), 'thumbnail_80' => $thumbnail80Url, - 'url' => $previewUrl ?? $thumbnail80Url ?? null, + 'url' => $previewUrl ?? $thumbnail80Url, ]; } } diff --git a/src/Models/ModelInterface.php b/src/Models/ModelInterface.php index 60726121..9670e57c 100644 --- a/src/Models/ModelInterface.php +++ b/src/Models/ModelInterface.php @@ -4,9 +4,6 @@ namespace Themes\Rozier\Models; -/** - * @package Themes\Rozier\Models - */ interface ModelInterface { /** diff --git a/src/Models/NodeModel.php b/src/Models/NodeModel.php index f90bb280..1ff17d4d 100644 --- a/src/Models/NodeModel.php +++ b/src/Models/NodeModel.php @@ -9,25 +9,20 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Bundle\SecurityBundle\Security; /** - * @package Themes\Rozier\Models * @Serializer\ExclusionPolicy("all") */ final class NodeModel implements ModelInterface { - private Node $node; - private UrlGeneratorInterface $urlGenerator; - - /** - * @param Node $node - * @param UrlGeneratorInterface $urlGenerator - */ - public function __construct(Node $node, UrlGeneratorInterface $urlGenerator) - { - $this->node = $node; - $this->urlGenerator = $urlGenerator; + public function __construct( + private Node $node, + private UrlGeneratorInterface $urlGenerator, + private Security $security + ) { } public function toArray(): array @@ -36,18 +31,21 @@ public function toArray(): array $nodeSource = $this->node->getNodeSources()->first(); if (false === $nodeSource) { - return [ + $result = [ 'id' => $this->node->getId(), 'title' => $this->node->getNodeName(), 'nodeName' => $this->node->getNodeName(), 'isPublished' => $this->node->isPublished(), - 'nodesEditPage' => $this->urlGenerator->generate('nodesEditPage', [ - 'nodeId' => $this->node->getId(), - ]), 'nodeType' => [ - 'color' => $this->node->getNodeType()->getColor() + 'color' => $this->node->getNodeType()->getColor() ?? '#000000', ] ]; + if ($this->security->isGranted(NodeVoter::EDIT_SETTING, $this->node)) { + $result['nodesEditPage'] = $this->urlGenerator->generate('nodesEditPage', [ + 'nodeId' => $this->node->getId(), + ]); + } + return $result; } /** @var NodesSourcesDocuments|false $thumbnail */ @@ -61,25 +59,32 @@ public function toArray(): array 'thumbnail' => $thumbnail ? $thumbnail->getDocument() : null, 'nodeName' => $this->node->getNodeName(), 'isPublished' => $this->node->isPublished(), - 'nodesEditPage' => $this->urlGenerator->generate('nodesEditSourcePage', [ - 'nodeId' => $this->node->getId(), - 'translationId' => $translation->getId(), - ]), 'nodeType' => [ - 'color' => $this->node->getNodeType()->getColor() + 'color' => $this->node->getNodeType()->getColor() ?? '#000000', ] ]; + if ($this->security->isGranted(NodeVoter::EDIT_CONTENT, $nodeSource)) { + $result['nodesEditPage'] = $this->urlGenerator->generate('nodesEditSourcePage', [ + 'nodeId' => $this->node->getId(), + 'translationId' => $translation->getId(), + ]); + } + $parent = $this->node->getParent(); if ($parent instanceof Node) { $result['parent'] = [ - 'title' => $parent->getNodeSources()->first()->getTitle() + 'title' => $parent->getNodeSources()->first() ? + $parent->getNodeSources()->first()->getTitle() : + $parent->getNodeName() ]; $subParent = $parent->getParent(); if ($subParent instanceof Node) { $result['subparent'] = [ - 'title' => $subParent->getNodeSources()->first()->getTitle() + 'title' => $subParent->getNodeSources()->first() ? + $subParent->getNodeSources()->first()->getTitle() : + $subParent->getNodeName() ]; } } diff --git a/src/Models/NodeSourceModel.php b/src/Models/NodeSourceModel.php index 73fef8c1..c8208a4c 100644 --- a/src/Models/NodeSourceModel.php +++ b/src/Models/NodeSourceModel.php @@ -8,28 +8,25 @@ use RZ\Roadiz\CoreBundle\Entity\NodesSources; use RZ\Roadiz\CoreBundle\Entity\NodesSourcesDocuments; use RZ\Roadiz\CoreBundle\Entity\Translation; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Bundle\SecurityBundle\Security; /** * @Serializer\ExclusionPolicy("all") */ final class NodeSourceModel implements ModelInterface { - private NodesSources $nodeSource; - private UrlGeneratorInterface $urlGenerator; - - public function __construct(NodesSources $nodeSource, UrlGeneratorInterface $urlGenerator) - { - $this->nodeSource = $nodeSource; - $this->urlGenerator = $urlGenerator; + public function __construct( + private NodesSources $nodeSource, + private UrlGeneratorInterface $urlGenerator, + private Security $security + ) { } public function toArray(): array { $node = $this->nodeSource->getNode(); - if (null === $node) { - throw new \RuntimeException('Node-source does not have a Node.'); - } /** @var NodesSourcesDocuments|false $thumbnail */ $thumbnail = $this->nodeSource->getDocumentsByFields()->first(); @@ -42,15 +39,18 @@ public function toArray(): array 'nodeName' => $node->getNodeName(), 'thumbnail' => $thumbnail ? $thumbnail->getDocument() : null, 'isPublished' => $node->isPublished(), - 'nodesEditPage' => $this->urlGenerator->generate('nodesEditSourcePage', [ - 'nodeId' => $node->getId(), - 'translationId' => $translation->getId(), - ]), 'nodeType' => [ - 'color' => $node->getNodeType()->getColor() + 'color' => $node->getNodeType()->getColor() ?? '#000000', ] ]; + if ($this->security->isGranted(NodeVoter::EDIT_CONTENT, $node)) { + $result['nodesEditPage'] = $this->urlGenerator->generate('nodesEditSourcePage', [ + 'nodeId' => $node->getId(), + 'translationId' => $translation->getId(), + ]); + } + $parent = $this->nodeSource->getParent(); if ($parent instanceof NodesSources) { diff --git a/src/Models/NodeTypeModel.php b/src/Models/NodeTypeModel.php index bf9e93b0..6c26967f 100644 --- a/src/Models/NodeTypeModel.php +++ b/src/Models/NodeTypeModel.php @@ -6,9 +6,6 @@ use RZ\Roadiz\CoreBundle\Entity\NodeType; -/** - * @package Themes\Rozier\Models - */ final class NodeTypeModel implements ModelInterface { private NodeType $nodeType; diff --git a/src/Models/TagModel.php b/src/Models/TagModel.php index 10fe026c..d22864d9 100644 --- a/src/Models/TagModel.php +++ b/src/Models/TagModel.php @@ -7,9 +7,6 @@ use RZ\Roadiz\CoreBundle\Entity\Tag; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -/** - * @package Themes\Rozier\Models - */ final class TagModel implements ModelInterface { private Tag $tag; diff --git a/src/Resources/app/Lazyload.js b/src/Resources/app/Lazyload.js index 0e1f3d36..1b6ddfe9 100644 --- a/src/Resources/app/Lazyload.js +++ b/src/Resources/app/Lazyload.js @@ -6,10 +6,8 @@ import TagsBulk from './components/bulk-edits/TagsBulk' import DocumentUploader from './components/documents/DocumentUploader' import NodeTypeFieldsPosition from './components/node-type-fields/NodeTypeFieldsPosition' import AttributeValuePosition from './components/attribute-values/AttributeValuePosition' -import NodeTypeFieldEdit from './components/node-type-fields/NodeTypeFieldEdit' import CustomFormFieldsPosition from './components/custom-form-fields/CustomFormFieldsPosition' import NodeTreeContextActions from './components/trees/NodeTreeContextActions' -import Import from './components/import/Import' import NodeEditSource from './components/node/NodeEditSource' import InputLengthWatcher from './widgets/InputLengthWatcher' import ChildrenNodesField from './widgets/ChildrenNodesField' @@ -29,9 +27,6 @@ import MultiLeafletGeotagField from './widgets/MultiLeafletGeotagField' import TagEdit from './components/tag/TagEdit' import MainTreeTabs from './components/tabs/MainTreeTabs' -/** - * Lazyload - */ export default class Lazyload { constructor() { this.$linksSelector = null @@ -54,7 +49,6 @@ export default class Lazyload { this.attributeValuesPosition = null this.customFormFieldsPosition = null this.settingsSaveButtons = null - this.nodeTypeFieldEdit = null this.nodeEditSource = null this.tagEdit = null this.markdownEditors = [] @@ -116,14 +110,10 @@ export default class Lazyload { ) { event.preventDefault() - if (this.clickTimeout) { - clearTimeout(this.clickTimeout) - } - - this.clickTimeout = window.setTimeout(() => { + window.requestAnimationFrame(() => { window.history.pushState({}, null, $link.attr('href')) this.onPopState(null) - }, 0) + }) return false } @@ -160,10 +150,10 @@ export default class Lazyload { * Delay loading if user is click like devil */ if (this.currentTimeout) { - clearTimeout(this.currentTimeout) + window.cancelAnimationFrame(this.currentTimeout) } - this.currentTimeout = window.setTimeout(() => { + this.currentTimeout = window.requestAnimationFrame(async () => { /* * Trigger event on window to notify open * widgets to close. @@ -171,48 +161,71 @@ export default class Lazyload { let pageChangeEvent = new CustomEvent('pagechange') window.dispatchEvent(pageChangeEvent) - this.currentRequest = $.ajax({ - url: location.href, - type: 'get', - dataType: 'html', - cache: false, - data: state.headerData, - beforeSend: (xhr) => { - xhr.setRequestHeader('X-Partial', true) - }, - }) - .done((data) => { - this.applyContent(data) - this.canvasLoader.hide() - let pageLoadEvent = new CustomEvent('pageload', { detail: data }) - window.dispatchEvent(pageLoadEvent) - }) - .fail((data) => { - if (typeof data.responseText !== 'undefined') { - try { - let exception = JSON.parse(data.responseText) - window.UIkit.notify({ - message: exception.message, - status: 'danger', - timeout: 3000, - pos: 'top-center', + try { + let url = '' + const path = location.href.split('?')[0] + const params = new URLSearchParams(location.href.split('?')[1]) + if (state.headerData) { + /** + * @param {string} key + * @param {string|number|Array} value + */ + for (let [key, value] of Object.entries(state.headerData)) { + if (Array.isArray(value)) { + value.forEach((v, i) => { + params.append(key + '[' + i + ']', v) }) - } catch (e) { - // No valid JsonResponse, need to refresh page - window.location.href = location.href + } else { + params.set(key, value) } - } else { + } + } + if (params.toString() !== '') { + url = path + '?' + params.toString() + } else { + url = path + } + + const response = await fetch(url, { + method: 'GET', + headers: { + 'X-Partial': true, + Accept: 'text/html', + }, + }) + if (!response.ok) { + throw response + } + const data = await response.text() + this.applyContent(data) + let pageLoadEvent = new CustomEvent('pageload', { detail: data }) + window.dispatchEvent(pageLoadEvent) + } catch (response) { + const data = await response.text() + if (data) { + try { + let exception = JSON.parse(data) window.UIkit.notify({ - message: window.Rozier.messages.forbiddenPage, + message: exception.message, status: 'danger', timeout: 3000, pos: 'top-center', }) + } catch (e) { + // No valid JsonResponse, need to refresh page + window.location.href = location.href } - - this.canvasLoader.hide() - }) - }, 100) + } else { + window.UIkit.notify({ + message: window.Rozier.messages.forbiddenPage, + status: 'danger', + timeout: 3000, + pos: 'top-center', + }) + } + } + this.canvasLoader.hide() + }) } refreshCodemirrorEditor() { @@ -264,10 +277,10 @@ export default class Lazyload { $tempData = $container.find('.new-content-global') - $old.fadeOut(100, () => { + $old.eq(0).fadeOut(100, () => { $old.remove() this.generalBind() - $tempData.fadeIn(200, () => { + $tempData.eq(0).fadeIn(200, () => { $tempData.removeClass('new-content-global') let pageShowEndEvent = new CustomEvent('pageshowend') window.dispatchEvent(pageShowEndEvent) @@ -304,7 +317,6 @@ export default class Lazyload { this.attributeValuesPosition, this.customFormFieldsPosition, this.settingsSaveButtons, - this.nodeTypeFieldEdit, this.nodeEditSource, this.tagEdit, this.nodeTree, @@ -339,7 +351,6 @@ export default class Lazyload { this.customFormFieldsPosition = new CustomFormFieldsPosition() this.nodeTreeContextActions = new NodeTreeContextActions() this.settingsSaveButtons = new SettingsSaveButtons() - this.nodeTypeFieldEdit = new NodeTypeFieldEdit() this.nodeEditSource = new NodeEditSource() this.tagEdit = new TagEdit() this.nodeTree = new NodeTree() @@ -364,13 +375,7 @@ export default class Lazyload { // Switch checkboxes this.initBootstrapSwitches() - window.Rozier.getMessages() - - if (typeof window.Rozier.importRoutes !== 'undefined' && window.Rozier.importRoutes !== null) { - window.Rozier.import = new Import(window.Rozier.importRoutes) - window.Rozier.importRoutes = null - } } generalUnbind(objects) { diff --git a/src/Resources/app/Rozier.js b/src/Resources/app/Rozier.js index c13e2dc6..0f6f998c 100644 --- a/src/Resources/app/Rozier.js +++ b/src/Resources/app/Rozier.js @@ -6,6 +6,7 @@ import { PointerEventsPolyfill } from './utils/plugins' import { TweenLite, Expo } from 'gsap' import NodeTreeContextActions from './components/trees/NodeTreeContextActions' import RozierMobile from './RozierMobile' +import bulkActions from './widgets/GenericBulkActions' require('gsap/ScrollToPlugin') /** @@ -20,6 +21,7 @@ export default class Rozier { this.windowHeight = null this.resizeFirst = true this.mobile = null + this.ajaxToken = null this.nodeTrees = [] this.treeTrees = [] @@ -153,6 +155,14 @@ export default class Rozier { this.refreshMainNodeTree() this.refreshMainTagTree() this.refreshMainFolderTree() + + /* + * init generic bulk actions widget + */ + bulkActions() + window.addEventListener('pageshowend', () => { + bulkActions() + }) } saveCollapsedNestableState(state = null) { @@ -223,16 +233,22 @@ export default class Rozier { bindMainTrees() { // TREES let $nodeTree = $('.nodetree-widget .root-tree') - $nodeTree.off('change.uk.nestable') - $nodeTree.on('change.uk.nestable', this.onNestableNodeTreeChange) + if ($nodeTree.length) { + $nodeTree.off('change.uk.nestable') + $nodeTree.on('change.uk.nestable', this.onNestableNodeTreeChange) + } let $tagTree = $('.tagtree-widget .root-tree') - $tagTree.off('change.uk.nestable') - $tagTree.on('change.uk.nestable', this.onNestableTagTreeChange) + if ($tagTree.length) { + $tagTree.off('change.uk.nestable') + $tagTree.on('change.uk.nestable', this.onNestableTagTreeChange) + } let $folderTree = $('.foldertree-widget .root-tree') - $folderTree.off('change.uk.nestable') - $folderTree.on('change.uk.nestable', this.onNestableFolderTreeChange) + if ($folderTree.length) { + $folderTree.off('change.uk.nestable') + $folderTree.on('change.uk.nestable', this.onNestableFolderTreeChange) + } // Tree element name this.$mainTreeElementName = this.$mainTrees.find('.tree-element-name') @@ -272,61 +288,71 @@ export default class Rozier { }) } - /** - * Get messages. - */ - getMessages() { - $.ajax({ - url: this.routes.ajaxSessionMessages, - type: 'GET', - dataType: 'json', - cache: false, - data: { + fetchSessionMessages() { + return new Promise(async (resolve, reject) => { + const query = new URLSearchParams({ _action: 'messages', _token: this.ajaxToken, - }, - }) - .done((data) => { - if (typeof data.messages !== 'undefined') { - if (typeof data.messages.confirm !== 'undefined' && data.messages.confirm.length > 0) { - for (let i = data.messages.confirm.length - 1; i >= 0; i--) { - window.UIkit.notify({ - message: data.messages.confirm[i], - status: 'success', - timeout: 2000, - pos: 'top-center', - }) - } - } - - if (typeof data.messages.error !== 'undefined' && data.messages.error.length > 0) { - for (let j = data.messages.error.length - 1; j >= 0; j--) { - window.UIkit.notify({ - message: data.messages.error[j], - status: 'error', - timeout: 2000, - pos: 'top-center', - }) - } - } - } - }) - .fail(() => { - console.log('[Rozier.getMessages] error') }) + const url = this.routes.ajaxSessionMessages + '?' + query.toString() + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }) + const data = await response.json() + if (!data.messages) { + reject() + } + resolve(data.messages) + } catch (e) { + reject() + } + }) + } + /** + * Get messages. + */ + async getMessages() { + const messages = await this.fetchSessionMessages() + if (typeof messages.confirm !== 'undefined' && messages.confirm.length > 0) { + for (let i = messages.confirm.length - 1; i >= 0; i--) { + window.UIkit.notify({ + message: messages.confirm[i], + status: 'success', + timeout: 2000, + pos: 'top-center', + }) + } + } + + if (typeof messages.error !== 'undefined' && messages.error.length > 0) { + for (let j = data.messages.error.length - 1; j >= 0; j--) { + window.UIkit.notify({ + message: data.messages.error[j], + status: 'error', + timeout: 2000, + pos: 'top-center', + }) + } + } } /** * @param translationId */ refreshAllNodeTrees(translationId) { - this.refreshMainNodeTree(translationId) + const promises = [] + promises.push(this.refreshMainNodeTree(translationId)) /* * Stack trees */ if (this.lazyload.stackNodeTrees.treeAvailable()) { - this.lazyload.stackNodeTrees.refreshNodeTree() + promises.push(this.lazyload.stackNodeTrees.refreshNodeTree()) } /* @@ -335,9 +361,10 @@ export default class Rozier { if (this.lazyload.childrenNodesFields.treeAvailable()) { for (let i = this.lazyload.childrenNodesFields.$nodeTrees.length - 1; i >= 0; i--) { let $nodeTree = this.lazyload.childrenNodesFields.$nodeTrees.eq(i) - this.lazyload.childrenNodesFields.refreshNodeTree($nodeTree) + promises.push(this.lazyload.childrenNodesFields.refreshNodeTree($nodeTree)) } } + return Promise.all(promises) } /** @@ -345,151 +372,163 @@ export default class Rozier { * * @param {Number|null|undefined} translationId */ - refreshMainNodeTree(translationId) { - let $currentNodeTree = $('#tree-container').find('.nodetree-widget') + async refreshMainNodeTree(translationId = undefined) { + let $currentNodeTree = $('#tree-container').find('.nodetree-widget').eq(0) + if (!$currentNodeTree.length) { + console.debug('No main node-tree available.') + return + } + let $currentRootTree = $currentNodeTree.find('.root-tree').eq(0) if ($currentRootTree.length && !translationId) { translationId = $currentRootTree.attr('data-translation-id') } - if ($currentNodeTree.length) { - let postData = { + try { + const query = new URLSearchParams({ _token: this.ajaxToken, _action: 'requestMainNodeTree', translationId: translationId || null, - } - let url = this.routes.nodesTreeAjax - - $.ajax({ - url: url, - type: 'get', - cache: false, - dataType: 'json', - data: postData, }) - .done((data) => { - if ($currentNodeTree.length && typeof data.nodeTree !== 'undefined') { - $currentNodeTree.fadeOut('slow', () => { - $currentNodeTree.replaceWith(data.nodeTree) - $currentNodeTree = $('#tree-container').find('.nodetree-widget') - $currentNodeTree.fadeIn() - this.initNestables() - this.bindMainTrees() - this.resize() - this.lazyload.bindAjaxLink() - - if (this.lazyload.nodeTreeContextActions) { - this.lazyload.nodeTreeContextActions.unbind() - } - - this.lazyload.nodeTreeContextActions = new NodeTreeContextActions() - }) - } - }) - .fail(() => { - console.log('[Rozier.refreshMainNodeTree] Retrying in 3 seconds') - // Wait for background jobs to be done - setTimeout(() => { - this.refreshMainNodeTree(translationId) - }, 3000) - }) - .always(() => { - this.lazyload.canvasLoader.hide() - }) - } else { - console.debug('No main node-tree available.') + const url = this.routes.nodesTreeAjax + '?' + query.toString() + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }) + if (!response.ok) { + throw response + } + const data = await response.json() + if (typeof data.nodeTree !== 'undefined') { + await this.fadeOut($currentNodeTree) + $currentNodeTree.replaceWith(data.nodeTree) + $currentNodeTree = $('#tree-container').find('.nodetree-widget') + + await this.fadeIn($currentNodeTree) + this.initNestables() + this.bindMainTrees() + this.resize() + this.lazyload.bindAjaxLink() + + if (this.lazyload.nodeTreeContextActions) { + this.lazyload.nodeTreeContextActions.unbind() + } + + this.lazyload.nodeTreeContextActions = new NodeTreeContextActions() + } + } catch (e) { + console.log('[Rozier.refreshMainNodeTree] Retrying in 3 seconds') + // Wait for background jobs to be done + window.setTimeout(() => { + this.refreshMainNodeTree(translationId) + }, 3000) } + + this.lazyload.canvasLoader.hide() } /** * Refresh only main tagTree. * */ - refreshMainTagTree() { + async refreshMainTagTree() { let $currentTagTree = $('#tree-container').find('.tagtree-widget') - if ($currentTagTree.length) { - let postData = { - _token: this.ajaxToken, - _action: 'requestMainTagTree', - } - - let url = this.routes.tagsTreeAjax - - $.ajax({ - url: url, - type: 'get', - cache: false, - dataType: 'json', - data: postData, - }) - .done((data) => { - if ($currentTagTree.length && typeof data.tagTree !== 'undefined') { - $currentTagTree.fadeOut('slow', () => { - $currentTagTree.replaceWith(data.tagTree) - $currentTagTree = $('#tree-container').find('.tagtree-widget') - $currentTagTree.fadeIn() - this.initNestables() - this.bindMainTrees() - this.resize() - this.lazyload.bindAjaxLink() - }) - } - }) - .always(() => { - this.lazyload.canvasLoader.hide() - }) - } else { + if (!$currentTagTree.length) { console.debug('No main tag-tree available.') + return } + + const query = new URLSearchParams({ + _token: this.ajaxToken, + _action: 'requestMainTagTree', + }) + const url = this.routes.tagsTreeAjax + '?' + query.toString() + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }) + const data = await response.json() + if (typeof data.tagTree !== 'undefined') { + await this.fadeOut($currentTagTree) + $currentTagTree.replaceWith(data.tagTree) + $currentTagTree = $('#tree-container').find('.tagtree-widget') + await this.fadeIn($currentTagTree) + this.initNestables() + this.bindMainTrees() + this.resize() + this.lazyload.bindAjaxLink() + } + this.lazyload.canvasLoader.hide() } /** * Refresh only main folderTree. */ - refreshMainFolderTree() { + async refreshMainFolderTree() { let $currentFolderTree = $('#tree-container').find('.foldertree-widget') - if ($currentFolderTree.length) { - let postData = { - _token: this.ajaxToken, - _action: 'requestMainFolderTree', - } + if (!$currentFolderTree.length) { + console.debug('No main folder-tree available.') + return + } - let url = this.routes.foldersTreeAjax + const query = new URLSearchParams({ + _token: this.ajaxToken, + _action: 'requestMainFolderTree', + }) + const url = this.routes.foldersTreeAjax + '?' + query.toString() + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }) + const data = await response.json() + if (typeof data.folderTree !== 'undefined') { + await this.fadeOut($currentFolderTree) + $currentFolderTree.replaceWith(data.folderTree) + $currentFolderTree = $('#tree-container').find('.foldertree-widget') + await this.fadeIn($currentFolderTree) + this.initNestables() + this.bindMainTrees() + this.resize() + this.lazyload.bindAjaxLink() + } + + this.lazyload.canvasLoader.hide() + } - $.ajax({ - url: url, - type: 'get', - cache: false, - dataType: 'json', - data: postData, + /** + * @param {jQuery} element + * @return {Promise} + */ + async fadeIn(element) { + return new Promise((resolve, reject) => { + element.fadeIn(() => { + resolve() }) - .done((data) => { - if ($currentFolderTree.length && typeof data.folderTree !== 'undefined') { - $currentFolderTree.fadeOut('slow', () => { - $currentFolderTree.replaceWith(data.folderTree) - $currentFolderTree = $('#tree-container').find('.foldertree-widget') - $currentFolderTree.fadeIn() - this.initNestables() - this.bindMainTrees() - this.resize() - this.lazyload.bindAjaxLink() - }) - } - }) - .always(() => { - this.lazyload.canvasLoader.hide() - }) - } else { - console.debug('No main folder-tree available.') - } + }) + } + + /** + * @param {jQuery} element + * @return {Promise} + */ + async fadeOut(element) { + return new Promise((resolve, reject) => { + element.fadeOut('slow', () => { + resolve() + }) + }) } /** * Toggle trees panel - * @param {[type]} event [description] - * @return {[type]} [description] */ toggleTreesPanel() { $('#main-container-inner').toggleClass('trees-panel--minified') @@ -510,8 +549,6 @@ export default class Rozier { /** * Toggle user panel - * @param {[type]} event [description] - * @return {[type]} [description] */ toggleUserPanel() { $('#user-panel').toggleClass('minified') @@ -565,12 +602,12 @@ export default class Rozier { /** * @param event - * @param rootEl - * @param el - * @param status - * @returns {boolean} + * @param {HTMLElement} rootEl + * @param {HTMLElement} el + * @param {string|null|undefined} status + * @returns {false|undefined} */ - onNestableNodeTreeChange(event, rootEl, el, status) { + async onNestableNodeTreeChange(event, rootEl, el, status) { let element = $(el) /* * If node removed, do not do anything, the other change.uk.nestable nodeTree will be triggered @@ -606,7 +643,7 @@ export default class Rozier { return false } - let postData = { + const postData = { _token: this.ajaxToken, _action: 'updatePosition', nodeId: nodeId, @@ -622,39 +659,43 @@ export default class Rozier { postData.prevNodeId = parseInt(element.prev().attr('data-node-id')) } - $.ajax({ - url: this.routes.nodeAjaxEdit.replace('%nodeId%', nodeId), - type: 'POST', - dataType: 'json', - data: postData, - }) - .done((data) => { - window.UIkit.notify({ - message: data.responseText, - status: data.status, - timeout: 3000, - pos: 'top-center', - }) + try { + const response = await fetch(this.routes.nodeAjaxEdit.replace('%nodeId%', nodeId), { + method: 'POST', + headers: { + Accept: 'application/json', + }, + body: new URLSearchParams(postData), }) - .fail((data) => { - data = JSON.parse(data.responseText) - window.UIkit.notify({ - message: data.error_message, - status: 'danger', - timeout: 3000, - pos: 'top-center', - }) + if (!response.ok) { + throw response + } + const data = await response.json() + window.UIkit.notify({ + message: data.responseText || data.detail, + status: data.status, + timeout: 3000, + pos: 'top-center', + }) + } catch (response) { + const data = await response.json() + window.UIkit.notify({ + message: data.error_message || data.detail, + status: 'danger', + timeout: 3000, + pos: 'top-center', }) + } } /** * @param event - * @param rootEl - * @param el - * @param status - * @returns {boolean} + * @param {HTMLElement} rootEl + * @param {HTMLElement} el + * @param {string|null|undefined} status + * @returns {false|undefined} */ - onNestableTagTreeChange(event, rootEl, el, status) { + async onNestableTagTreeChange(event, rootEl, el, status) { let element = $(el) /* @@ -706,29 +747,43 @@ export default class Rozier { postData.prevTagId = parseInt(element.prev().attr('data-tag-id')) } - $.ajax({ - url: this.routes.tagAjaxEdit.replace('%tagId%', tagId), - type: 'POST', - dataType: 'json', - data: postData, - }).done((data) => { + try { + const response = await fetch(this.routes.tagAjaxEdit.replace('%tagId%', tagId), { + method: 'POST', + headers: { + Accept: 'application/json', + }, + body: new URLSearchParams(postData), + }) + if (!response.ok) { + throw response + } + const data = await response.json() window.UIkit.notify({ message: data.responseText, status: data.status, timeout: 3000, pos: 'top-center', }) - }) + } catch (response) { + const data = await response.json() + window.UIkit.notify({ + message: data.error_message || data.detail, + status: 'danger', + timeout: 3000, + pos: 'top-center', + }) + } } /** - * * @param event - * @param element - * @param status - * @returns {boolean} + * @param {HTMLElement} rootEl + * @param {HTMLElement} el + * @param {string|null|undefined} status + * @returns {false|undefined} */ - onNestableFolderTreeChange(event, rootEl, el, status) { + async onNestableFolderTreeChange(event, rootEl, el, status) { let element = $(el) /* * If folder removed, do not do anything, the other folderTree will be triggered @@ -781,19 +836,33 @@ export default class Rozier { postData.prevFolderId = parseInt(element.prev().attr('data-folder-id')) } - $.ajax({ - url: this.routes.folderAjaxEdit.replace('%folderId%', folderId), - type: 'POST', - dataType: 'json', - data: postData, - }).done((data) => { + try { + const response = await fetch(this.routes.folderAjaxEdit.replace('%folderId%', folderId), { + method: 'POST', + headers: { + Accept: 'application/json', + }, + body: new URLSearchParams(postData), + }) + if (!response.ok) { + throw response + } + const data = await response.json() window.UIkit.notify({ message: data.responseText, status: data.status, timeout: 3000, pos: 'top-center', }) - }) + } catch (response) { + const data = await response.json() + window.UIkit.notify({ + message: data.error_message || data.detail, + status: 'danger', + timeout: 3000, + pos: 'top-center', + }) + } } /** @@ -817,9 +886,9 @@ export default class Rozier { if (this.windowWidth >= 768 && this.windowWidth <= 1200 && this.$mainTrees.length && this.resizeFirst) { this.$mainTrees[0].style.display = 'none' this.$minifyTreePanelButton.trigger('click') - window.setTimeout(() => { + window.requestAnimationFrame(() => { this.$mainTrees[0].style.display = 'table-cell' - }, 1000) + }) } // Check if mobile @@ -866,9 +935,6 @@ export default class Rozier { this.lazyload.resize() this.entriesPanel.replaceSubNavs() - // Documents list - // if(this.lazyload !== null && !this.resizeFirst) this.lazyload.documentsList.resize(); - // Set resize first to false if (this.resizeFirst) this.resizeFirst = false } diff --git a/src/Resources/app/api/DocumentApi.js b/src/Resources/app/api/DocumentApi.js index 9729e172..e76828c9 100644 --- a/src/Resources/app/api/DocumentApi.js +++ b/src/Resources/app/api/DocumentApi.js @@ -29,7 +29,6 @@ export function getDocumentsByIds({ ids = [] }) { } }) .catch((error) => { - // TODO // Log request error or display a message throw new Error(error.response.data.humanMessage) }) @@ -77,7 +76,6 @@ export function getDocuments({ searchTerms, filters, filterExplorerSelection, mo } }) .catch((error) => { - // TODO // Log request error or display a message throw new Error(error) }) diff --git a/src/Resources/app/components/Dropzone.vue b/src/Resources/app/components/Dropzone.vue index d6b06dc3..0babde35 100644 --- a/src/Resources/app/components/Dropzone.vue +++ b/src/Resources/app/components/Dropzone.vue @@ -183,7 +183,7 @@ export default { */ if (file.previewElement) { let $preview = $(file.previewElement) - setTimeout(function () { + window.setTimeout(function () { $preview.fadeOut(500) }, 3000) } diff --git a/src/Resources/app/components/RzTextarea.vue b/src/Resources/app/components/RzTextarea.vue index ec8ea488..5638efae 100644 --- a/src/Resources/app/components/RzTextarea.vue +++ b/src/Resources/app/components/RzTextarea.vue @@ -1,5 +1,5 @@ + - + diff --git a/src/Resources/views/partials/simple-js-inject.html.twig b/src/Resources/views/partials/simple-js-inject.html.twig index f4310832..89eb5041 100644 --- a/src/Resources/views/partials/simple-js-inject.html.twig +++ b/src/Resources/views/partials/simple-js-inject.html.twig @@ -1,3 +1,3 @@ - + diff --git a/src/Resources/views/redirections/add.html.twig b/src/Resources/views/redirections/add.html.twig index de268d6b..39253b55 100644 --- a/src/Resources/views/redirections/add.html.twig +++ b/src/Resources/views/redirections/add.html.twig @@ -24,7 +24,7 @@
{% apply spaceless %} +
+ {{ form_end(form) }} + + {% endif %} +{% endblock %} diff --git a/src/Resources/views/redirections/edit.html.twig b/src/Resources/views/redirections/edit.html.twig index 214bd60a..891a55bd 100644 --- a/src/Resources/views/redirections/edit.html.twig +++ b/src/Resources/views/redirections/edit.html.twig @@ -24,7 +24,7 @@
{% apply spaceless %} diff --git a/src/Resources/views/roles/edit.html.twig b/src/Resources/views/roles/edit.html.twig index d9efcc6c..9ce227af 100644 --- a/src/Resources/views/roles/edit.html.twig +++ b/src/Resources/views/roles/edit.html.twig @@ -20,7 +20,7 @@ {{ form_widget(form) }}
{% apply spaceless %} - diff --git a/src/Resources/views/roles/list.html.twig b/src/Resources/views/roles/list.html.twig index 110b8321..ff21a251 100644 --- a/src/Resources/views/roles/list.html.twig +++ b/src/Resources/views/roles/list.html.twig @@ -69,7 +69,7 @@ href="{{ path('rolesExportPage', { id: item.getId }) }}" title="{% trans %}export{% endtrans %}" data-uk-tooltip="{animation:true}"> - diff --git a/src/Resources/views/settingGroups/add.html.twig b/src/Resources/views/settingGroups/add.html.twig index 6a043446..8c2109b2 100644 --- a/src/Resources/views/settingGroups/add.html.twig +++ b/src/Resources/views/settingGroups/add.html.twig @@ -22,7 +22,7 @@ {{ form_start(form, { attr: { id: 'add-settingGroup-form'}}) }} {{ form_widget(form) }}
- diff --git a/src/Resources/views/settingGroups/edit.html.twig b/src/Resources/views/settingGroups/edit.html.twig index f95a60fe..a3bca2b3 100644 --- a/src/Resources/views/settingGroups/edit.html.twig +++ b/src/Resources/views/settingGroups/edit.html.twig @@ -25,7 +25,7 @@ {{ form_widget(form) }}
diff --git a/src/Resources/views/settings/edit.html.twig b/src/Resources/views/settings/edit.html.twig index d8e390b8..0f51429e 100644 --- a/src/Resources/views/settings/edit.html.twig +++ b/src/Resources/views/settings/edit.html.twig @@ -19,7 +19,7 @@ {{ form_start(form, { attr: { id: 'edit-setting-form'}}) }} {{ form_widget(form) }}
- diff --git a/src/Resources/views/settings/list.html.twig b/src/Resources/views/settings/list.html.twig index 824ed098..5df80eb1 100644 --- a/src/Resources/views/settings/list.html.twig +++ b/src/Resources/views/settings/list.html.twig @@ -57,7 +57,7 @@

- {%- if setting.setting.Encrypted -%} {% endif %}{{ setting.setting.name|trans|inlineMarkdown -}} + {{ setting.setting.name|trans|inlineMarkdown -}}

{% if setting.setting.description %} @@ -76,9 +76,9 @@ {% apply spaceless %} - + - + {% endapply %} diff --git a/src/Resources/views/tags/add.html.twig b/src/Resources/views/tags/add.html.twig index 3ddf468e..9dc9062f 100644 --- a/src/Resources/views/tags/add.html.twig +++ b/src/Resources/views/tags/add.html.twig @@ -19,7 +19,7 @@ {{ form_widget(form) }}
{% apply spaceless %} - diff --git a/src/Resources/views/tags/edit.html.twig b/src/Resources/views/tags/edit.html.twig index 5fc466f9..82ff225f 100644 --- a/src/Resources/views/tags/edit.html.twig +++ b/src/Resources/views/tags/edit.html.twig @@ -51,7 +51,7 @@
{% if not readOnly %} {% apply spaceless %} - diff --git a/src/Resources/views/tags/list.html.twig b/src/Resources/views/tags/list.html.twig index 5bdbe3a7..0079b274 100644 --- a/src/Resources/views/tags/list.html.twig +++ b/src/Resources/views/tags/list.html.twig @@ -54,7 +54,7 @@ {% endif %} {% if is_granted('ROLE_ACCESS_TAGS_DELETE') and not tag.locked %} - + {% endif %} @@ -75,7 +75,7 @@ {% if is_granted('ROLE_ACCESS_TAGS_DELETE') and not tag.locked %} - + {% endif %} diff --git a/src/Resources/views/tags/settings.html.twig b/src/Resources/views/tags/settings.html.twig index d738357b..416465d1 100644 --- a/src/Resources/views/tags/settings.html.twig +++ b/src/Resources/views/tags/settings.html.twig @@ -21,7 +21,7 @@ {{ form_widget(form) }}
{% apply spaceless %} - @@ -32,15 +32,15 @@