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/.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/composer.json b/composer.json index 917762b9..0d18956c 100644 --- a/composer.json +++ b/composer.json @@ -28,25 +28,26 @@ "role": "Frontend developer" } ], + "prefer-stable": true, "require": { - "php": ">=8.0", + "php": ">=8.1", "ext-zip": "*", - "doctrine/orm": "<2.17", + "doctrine/orm": "~2.17.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.2.*", + "roadiz/core-bundle": "2.2.*", + "roadiz/doc-generator": "2.2.*", + "roadiz/documents": "2.2.*", + "roadiz/dts-generator": "2.2.*", + "roadiz/markdown": "2.2.*", + "roadiz/models": "2.2.*", "roadiz/nodetype-contracts": "~1.1.2", - "roadiz/openid": "2.1.*", - "roadiz/rozier-bundle": "2.1.*", + "roadiz/openid": "2.2.*", + "roadiz/rozier-bundle": "2.2.*", "symfony/asset": "5.4.*", "symfony/filesystem": "5.4.*", "symfony/form": "5.4.*", @@ -66,9 +67,9 @@ "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.2.*", + "roadiz/random": "2.2.*", + "roadiz/jwt": "2.2.*", "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.2.x-dev", + "dev-develop": "2.3.x-dev" } } } diff --git a/phpstan.neon b/phpstan.neon index 353be6b9..10dbac08 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -28,6 +28,8 @@ 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 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 0c597186..64f3941e 100644 --- a/src/AjaxControllers/AjaxAbstractFieldsController.php +++ b/src/AjaxControllers/AjaxAbstractFieldsController.php @@ -9,20 +9,17 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -/** - * @package Themes\Rozier\AjaxControllers - */ abstract class AjaxAbstractFieldsController extends AbstractAjaxController { - private HandlerFactoryInterface $handlerFactory; + public function __construct(protected readonly HandlerFactoryInterface $handlerFactory) + { + } - /** - * @param HandlerFactoryInterface $handlerFactory - */ - public function __construct(HandlerFactoryInterface $handlerFactory) + protected function findEntity(int|string $entityId): ?AbstractField { - $this->handlerFactory = $handlerFactory; + return $this->em()->find($this->getEntityClass(), (int) $entityId); } /** @@ -33,7 +30,7 @@ public function __construct(HandlerFactoryInterface $handlerFactory) * * @return null|Response */ - protected function handleFieldActions(Request $request, AbstractField $field = null) + protected function handleFieldActions(Request $request, AbstractField $field = null): ?Response { /* * Validate @@ -79,11 +76,12 @@ protected function handleFieldActions(Request $request, AbstractField $field = n */ protected function updatePosition(array $parameters, AbstractField $field = null): array { - /* - * First, we set the new parent - */ - if (!empty($parameters['newPosition']) && null !== $field) { - $field->setPosition((float) $parameters['newPosition']); + if (!empty($parameters['afterFieldId']) && is_numeric($parameters['afterFieldId'])) { + $afterField = $this->findEntity((int) $parameters['afterFieldId']); + if (null === $afterField) { + throw new BadRequestHttpException('afterFieldId does not exist'); + } + $field->setPosition($afterField->getPosition() + 0.5); // Apply position update before cleaning $this->em()->flush(); $handler = $this->handlerFactory->getHandler($field); @@ -97,12 +95,31 @@ protected function updatePosition(array $parameters, AbstractField $field = null ]), ]; } - return [ - 'statusCode' => '400', - 'status' => 'error', - 'responseText' => $this->getTranslator()->trans('field.%name%.updated', [ - '%name%' => $field->getName(), - ]), - ]; + if (!empty($parameters['beforeFieldId']) && is_numeric($parameters['beforeFieldId'])) { + $beforeField = $this->findEntity((int) $parameters['beforeFieldId']); + if (null === $beforeField) { + throw new BadRequestHttpException('beforeFieldId does not exist'); + } + $field->setPosition($beforeField->getPosition() - 0.5); + // Apply position update before cleaning + $this->em()->flush(); + $handler = $this->handlerFactory->getHandler($field); + $handler->cleanPositions(); + $this->em()->flush(); + return [ + 'statusCode' => '200', + 'status' => 'success', + 'responseText' => $this->getTranslator()->trans('field.%name%.updated', [ + '%name%' => $field->getName(), + ]), + ]; + } + + throw new BadRequestHttpException('Cannot update position for Field. Missing parameters.'); } + + /** + * @return class-string + */ + abstract protected function getEntityClass(): string; } diff --git a/src/AjaxControllers/AjaxAttributeValuesController.php b/src/AjaxControllers/AjaxAttributeValuesController.php index 73f5adf1..bd3d34f3 100644 --- a/src/AjaxControllers/AjaxAttributeValuesController.php +++ b/src/AjaxControllers/AjaxAttributeValuesController.php @@ -6,9 +6,11 @@ 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; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; final class AjaxAttributeValuesController extends AbstractAjaxController { @@ -25,61 +27,75 @@ final class AjaxAttributeValuesController extends AbstractAjaxController * * @return Response JSON response */ - public function editAction(Request $request, int $attributeValueId) + 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() : '', ]; - /* - * First, we set the new parent - */ - if (!empty($parameters['newPosition'])) { - $attributeValue->setPosition((float) $parameters['newPosition']); - // Apply position update before cleaning + + if (!empty($parameters['afterAttributeValueId']) && is_numeric($parameters['afterAttributeValueId'])) { + /** @var AttributeValue|null $afterAttributeValue */ + $afterAttributeValue = $this->em()->find(AttributeValue::class, (int) $parameters['afterAttributeValueId']); + if (null === $afterAttributeValue) { + throw new BadRequestHttpException('afterAttributeValueId does not exist'); + } + $attributeValue->setPosition($afterAttributeValue->getPosition() + 0.5); + $this->em()->flush(); + return [ + 'statusCode' => '200', + 'status' => 'success', + 'responseText' => $this->getTranslator()->trans( + 'attribute_value_translation.%name%.updated_from_node.%nodeName%', + $details + ), + ]; + } + if (!empty($parameters['beforeAttributeValueId']) && is_numeric($parameters['beforeAttributeValueId'])) { + /** @var AttributeValue|null $beforeAttributeValue */ + $beforeAttributeValue = $this->em()->find(AttributeValue::class, (int) $parameters['beforeAttributeValueId']); + if (null === $beforeAttributeValue) { + throw new BadRequestHttpException('beforeAttributeValueId does not exist'); + } + $attributeValue->setPosition($beforeAttributeValue->getPosition() - 0.5); $this->em()->flush(); return [ 'statusCode' => '200', @@ -91,13 +107,6 @@ protected function updatePosition($parameters, AttributeValue $attributeValue): ]; } - return [ - 'statusCode' => '400', - 'status' => 'error', - 'responseText' => $this->getTranslator()->trans( - 'attribute_value_translation.%name%.updated_from_node.%nodeName%', - $details - ), - ]; + throw new BadRequestHttpException('Cannot update position for AttributeValue. Missing parameters.'); } } diff --git a/src/AjaxControllers/AjaxCustomFormFieldsController.php b/src/AjaxControllers/AjaxCustomFormFieldsController.php index f6075f8d..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,19 +15,16 @@ 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 { - /* - * Validate - */ $this->validateRequest($request); $this->denyAccessUnlessGranted('ROLE_ACCESS_CUSTOMFORMS_DELETE'); - $field = $this->em()->find(CustomFormField::class, (int) $customFormFieldId); + $field = $this->findEntity((int) $customFormFieldId); if (null !== $field && null !== $response = $this->handleFieldActions($request, $field)) { return $response; @@ -43,4 +37,9 @@ public function editAction(Request $request, int $customFormFieldId) ] )); } + + protected function getEntityClass(): string + { + return CustomFormField::class; + } } 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..e7fb58ce 100644 --- a/src/AjaxControllers/AjaxEntitiesExplorerController.php +++ b/src/AjaxControllers/AjaxEntitiesExplorerController.php @@ -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; } /** @@ -121,12 +109,6 @@ 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')) { @@ -137,7 +119,7 @@ public function listAction(Request $request): JsonResponse throw new InvalidParameterException('Ids should be provided within an array'); } - $this->denyAccessUnlessGranted('ROLE_ACCESS_NODES'); + $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); /** @var EntityManager $em */ $em = $this->em(); 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 3162ecc7..b0867002 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 e795fa5c..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 { /** @@ -24,13 +21,10 @@ class AjaxNodeTypeFieldsController extends AjaxAbstractFieldsController */ public function editAction(Request $request, int $nodeTypeFieldId): Response { - /* - * Validate - */ $this->validateRequest($request); $this->denyAccessUnlessGranted('ROLE_ACCESS_NODEFIELDS_DELETE'); - $field = $this->em()->find(NodeTypeField::class, (int) $nodeTypeFieldId); + $field = $this->findEntity($nodeTypeFieldId); if (null !== $response = $this->handleFieldActions($request, $field)) { return $response; @@ -43,4 +37,9 @@ public function editAction(Request $request, int $nodeTypeFieldId): Response ] )); } + + protected function getEntityClass(): string + { + return NodeTypeField::class; + } } diff --git a/src/AjaxControllers/AjaxNodeTypesController.php b/src/AjaxControllers/AjaxNodeTypesController.php index 3f45891e..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,17 +13,14 @@ use Symfony\Component\Routing\Exception\InvalidParameterException; use Themes\Rozier\Models\NodeTypeModel; -/** - * @package Themes\Rozier\AjaxControllers - */ -class AjaxNodeTypesController extends AjaxAbstractFieldsController +class AjaxNodeTypesController extends AbstractAjaxController { /** * @param Request $request * * @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 802fa451..e459d81c 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(); } @@ -246,15 +235,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')); @@ -268,6 +253,8 @@ public function statusesAction(Request $request): JsonResponse ])); } + $this->denyAccessUnlessGranted(NodeVoter::EDIT_STATUS, $node); + $availableStatuses = [ 'visible' => 'setVisible', 'locked' => 'setLocked', @@ -306,14 +293,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(); @@ -357,7 +344,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( [ @@ -381,9 +368,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..f9914e3c 100644 --- a/src/AjaxControllers/AjaxNodesExplorerController.php +++ b/src/AjaxControllers/AjaxNodesExplorerController.php @@ -14,34 +14,26 @@ use RZ\Roadiz\CoreBundle\EntityApi\NodeTypeApi; use RZ\Roadiz\CoreBundle\SearchEngine\ClientRegistry; use RZ\Roadiz\CoreBundle\SearchEngine\NodeSourceSearchHandlerInterface; +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\Component\Security\Core\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 +53,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); @@ -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,10 +250,10 @@ 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 = []; @@ -267,12 +261,12 @@ private function normalizeNodes($nodes) if (null !== $node) { if ($node instanceof NodesSources) { if (!key_exists($node->getNode()->getId(), $nodesArray)) { - $nodeModel = new NodeSourceModel($node, $this->urlGenerator); + $nodeModel = new NodeSourceModel($node, $this->urlGenerator, $this->security); $nodesArray[$node->getNode()->getId()] = $nodeModel->toArray(); } } else { if (!key_exists($node->getId(), $nodesArray)) { - $nodeModel = new NodeModel($node, $this->urlGenerator); + $nodeModel = new NodeModel($node, $this->urlGenerator, $this->security); $nodesArray[$node->getId()] = $nodeModel->toArray(); } } diff --git a/src/AjaxControllers/AjaxSearchNodesSourcesController.php b/src/AjaxControllers/AjaxSearchNodesSourcesController.php index 894c7145..8f2e7115 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\Component\Security\Core\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 6b4b0a91..6a3d5b24 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..338f76b4 100644 --- a/src/Controllers/AbstractAdminController.php +++ b/src/Controllers/AbstractAdminController.php @@ -68,6 +68,34 @@ 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 @@ -75,7 +103,7 @@ protected function getRepository(): ObjectRepository */ public function defaultAction(Request $request) { - $this->denyAccessUnlessGranted($this->getRequiredRole()); + $this->denyAccessUnlessGranted($this->getRequiredListingRole()); $this->additionalAssignation($request); $elm = $this->createEntityListManager( @@ -109,7 +137,7 @@ public function defaultAction(Request $request) */ public function addAction(Request $request) { - $this->denyAccessUnlessGranted($this->getRequiredRole()); + $this->denyAccessUnlessGranted($this->getRequiredCreationRole()); $this->additionalAssignation($request); $item = $this->createEmptyItem($request); @@ -138,7 +166,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); } @@ -162,7 +190,7 @@ public function addAction(Request $request) */ public function editAction(Request $request, $id) { - $this->denyAccessUnlessGranted($this->getRequiredRole()); + $this->denyAccessUnlessGranted($this->getRequiredEditionRole()); $this->additionalAssignation($request); /** @var mixed|object|null $item */ @@ -199,7 +227,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 +245,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(); @@ -284,7 +312,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 +356,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 +432,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 +459,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 +498,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): mixed { 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..059d22bc 100644 --- a/src/Controllers/AbstractAdminWithBulkController.php +++ b/src/Controllers/AbstractAdminWithBulkController.php @@ -170,7 +170,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..ffc952fe 100644 --- a/src/Controllers/Attributes/AttributeController.php +++ b/src/Controllers/Attributes/AttributeController.php @@ -11,22 +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, + FormFactoryInterface $formFactory, SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator ) { - parent::__construct($serializer, $urlGenerator); + parent::__construct($formFactory, $serializer, $urlGenerator); $this->attributeImporter = $attributeImporter; } @@ -39,6 +42,11 @@ protected function supports(PersistableInterface $item): bool return $item instanceof Attribute; } + protected function getBulkDeleteRouteName(): ?string + { + return 'attributesBulkDeletePage'; + } + /** * @inheritDoc */ @@ -103,7 +111,10 @@ protected function getFormType(): string */ protected function getDefaultOrder(Request $request): array { - return ['code' => 'ASC']; + return [ + 'weight' => 'DESC', + 'code' => 'ASC', + ]; } /** @@ -136,8 +147,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'); @@ -150,6 +162,9 @@ public function importAction(Request $request) if ($file->isValid()) { $serializedData = file_get_contents($file->getPathname()); + if (false === $serializedData) { + throw new \RuntimeException('Cannot read uploaded file.'); + } $this->attributeImporter->import($serializedData); $this->em()->flush(); 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/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..42537ebf 100644 --- a/src/Controllers/CustomForms/CustomFormsController.php +++ b/src/Controllers/CustomForms/CustomFormsController.php @@ -8,96 +8,60 @@ 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; -/** - * @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..11516b8a 100644 --- a/src/Controllers/CustomForms/CustomFormsUtilsController.php +++ b/src/Controllers/CustomForms/CustomFormsUtilsController.php @@ -14,9 +14,6 @@ use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Themes\Rozier\RozierApp; -/** - * @package Themes\Rozier\Controllers - */ class CustomFormsUtilsController extends RozierApp { private CustomFormAnswerSerializer $customFormAnswerSerializer; @@ -123,7 +120,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 +131,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..0fcb8cdc 100644 --- a/src/Controllers/Documents/DocumentTranslationsController.php +++ b/src/Controllers/Documents/DocumentTranslationsController.php @@ -22,9 +22,6 @@ use Themes\Rozier\Traits\VersionedControllerTrait; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers\Documents - */ class DocumentTranslationsController extends RozierApp { use VersionedControllerTrait; @@ -179,13 +176,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 @@ -239,7 +236,7 @@ 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); } } diff --git a/src/Controllers/Documents/DocumentsController.php b/src/Controllers/Documents/DocumentsController.php index 1acffaed..b049b5d3 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; @@ -48,6 +47,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; @@ -141,20 +141,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 +336,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 +415,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 +473,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 +519,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 +578,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 +622,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 +676,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 +702,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 +718,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..92b122db 100644 --- a/src/Controllers/FoldersController.php +++ b/src/Controllers/FoldersController.php @@ -86,7 +86,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 +95,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 +137,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 +146,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 +193,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 +201,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 +277,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 +285,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..e090be63 100644 --- a/src/Controllers/GroupsUtilsController.php +++ b/src/Controllers/GroupsUtilsController.php @@ -120,6 +120,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..5714ba77 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 ) { } diff --git a/src/Controllers/NodeTypeFieldsController.php b/src/Controllers/NodeTypeFieldsController.php index 5596ab58..fad5a557 100644 --- a/src/Controllers/NodeTypeFieldsController.php +++ b/src/Controllers/NodeTypeFieldsController.php @@ -12,6 +12,7 @@ use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Exception\ResourceNotFoundException; @@ -22,10 +23,12 @@ class NodeTypeFieldsController extends RozierApp { private MessageBusInterface $messageBus; + private KernelInterface $kernel; - public function __construct(MessageBusInterface $messageBus) + public function __construct(KernelInterface $kernel, MessageBusInterface $messageBus) { $this->messageBus = $messageBus; + $this->kernel = $kernel; } /** @@ -79,21 +82,25 @@ public function editAction(Request $request, int $nodeTypeFieldId): Response $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - $this->em()->flush(); + if (!$this->kernel->isDebug()) { + $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 +137,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->kernel->isDebug() + ]); $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->kernel->isDebug()) { + $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 +198,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->kernel->isDebug()) { + $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..3926d683 100644 --- a/src/Controllers/NodeTypes/NodeTypesController.php +++ b/src/Controllers/NodeTypes/NodeTypesController.php @@ -12,22 +12,23 @@ use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Messenger\Envelope; 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; + private KernelInterface $kernel; - public function __construct(MessageBusInterface $messageBus) + public function __construct(KernelInterface $kernel, MessageBusInterface $messageBus) { $this->messageBus = $messageBus; + $this->kernel = $kernel; } public function indexAction(Request $request): Response @@ -58,12 +59,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 +81,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 +101,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->kernel->isDebug() + ]); $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->kernel->isDebug()) { + $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 +146,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 +166,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->kernel->isDebug()) { + $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..b7c07e63 100644 --- a/src/Controllers/NodeTypes/NodeTypesUtilsController.php +++ b/src/Controllers/NodeTypes/NodeTypesUtilsController.php @@ -52,9 +52,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 +71,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'), ], @@ -83,6 +83,7 @@ public function exportJsonFileAction(Request $request, int $nodeTypeId): Respons * @param Request $request * * @return BinaryFileResponse + * @throws RuntimeError */ public function exportDocumentationAction(Request $request): BinaryFileResponse { @@ -91,6 +92,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); @@ -161,6 +166,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 +220,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 +249,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..15549a66 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; @@ -34,12 +36,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 +61,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..0ed1677c 100644 --- a/src/Controllers/Nodes/HistoryController.php +++ b/src/Controllers/Nodes/HistoryController.php @@ -4,9 +4,12 @@ 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\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; @@ -14,9 +17,6 @@ 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..4ccd94bb 100644 --- a/src/Controllers/Nodes/NodesAttributesController.php +++ b/src/Controllers/Nodes/NodesAttributesController.php @@ -9,10 +9,12 @@ 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\Security\Authorization\Voter\NodeVoter; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\JsonResponse; @@ -44,8 +46,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 +55,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 +68,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,11 +129,11 @@ 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(), @@ -122,12 +144,12 @@ public function editAction(Request $request, int $nodeId, int $translationId): R /* * 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 +162,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 +177,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 +195,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 +236,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 +259,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 +287,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 +313,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 +336,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 +364,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..47fa8f37 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; @@ -19,8 +21,10 @@ 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; @@ -35,28 +39,10 @@ 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 { @@ -349,7 +330,6 @@ public function addAction(Request $request, int $nodeTypeId, ?int $translationId } $node = new Node($type); - $chroot = $this->nodeChrootResolver->getChroot($this->getUser()); if (null !== $chroot) { // If user is jailed in a node, prevent moving nodes out. @@ -374,7 +354,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 +386,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 +405,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 +444,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 +475,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 +486,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 +525,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 +555,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 +569,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 +607,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 +618,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 +648,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 +691,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 +729,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..ce28da25 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; @@ -54,8 +55,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 +72,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 +110,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 +172,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 +212,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. */ @@ -248,14 +251,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..085d4410 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,10 +28,8 @@ 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; @@ -66,25 +65,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 +179,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 +316,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, ]); @@ -515,17 +529,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 +567,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..e2e293a9 100644 --- a/src/Controllers/Nodes/NodesUtilsController.php +++ b/src/Controllers/Nodes/NodesUtilsController.php @@ -9,13 +9,11 @@ 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; @@ -36,13 +34,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 +63,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 +74,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..c8b51d90 100644 --- a/src/Controllers/Nodes/TranstypeController.php +++ b/src/Controllers/Nodes/TranstypeController.php @@ -9,6 +9,7 @@ use RZ\Roadiz\CoreBundle\Event\Node\NodeUpdatedEvent; use RZ\Roadiz\CoreBundle\Event\NodesSources\NodesSourcesUpdatedEvent; use RZ\Roadiz\CoreBundle\Node\NodeTranstyper; +use RZ\Roadiz\CoreBundle\Security\Authorization\Voter\NodeVoter; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -17,9 +18,6 @@ use Themes\Rozier\RozierApp; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers\Nodes - */ class TranstypeController extends RozierApp { private NodeTranstyper $nodeTranstyper; @@ -36,14 +34,12 @@ public function __construct(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 +48,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 +92,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..f0a5907e 100644 --- a/src/Controllers/RolesUtilsController.php +++ b/src/Controllers/RolesUtilsController.php @@ -90,6 +90,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/SettingsController.php b/src/Controllers/SettingsController.php index 81284ef6..8a57e58d 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -116,6 +116,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 +140,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 +169,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 +225,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 +280,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 +326,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..dff29f0e 100644 --- a/src/Controllers/SettingsUtilsController.php +++ b/src/Controllers/SettingsUtilsController.php @@ -103,6 +103,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..f71ddb4a 100644 --- a/src/Controllers/Tags/TagMultiCreationController.php +++ b/src/Controllers/Tags/TagMultiCreationController.php @@ -16,9 +16,6 @@ use Themes\Rozier\Forms\MultiTagType; use Themes\Rozier\RozierApp; -/** - * @package Themes\Rozier\Controllers\Tags - */ class TagMultiCreationController extends RozierApp { private TagFactory $tagFactory; @@ -78,7 +75,7 @@ public function addChildAction(Request $request, int $parentTagId) */ $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('tagsTreePage', ['tagId' => $parentTagId]); diff --git a/src/Controllers/Tags/TagsController.php b/src/Controllers/Tags/TagsController.php index dd024d97..ca50fb93 100644 --- a/src/Controllers/Tags/TagsController.php +++ b/src/Controllers/Tags/TagsController.php @@ -35,9 +35,6 @@ use Themes\Rozier\Widgets\TreeWidgetFactory; use Twig\Error\RuntimeError; -/** - * @package Themes\Rozier\Controllers\Tags - */ class TagsController extends RozierApp { use VersionedControllerTrait; @@ -68,7 +65,7 @@ public function __construct( * * @return Response */ - public function indexAction(Request $request) + public function indexAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -105,7 +102,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 +163,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 +195,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()) { + if ($isJsonRequest) { $errors = $this->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 +254,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'); @@ -299,7 +307,7 @@ public function bulkDeleteAction(Request $request) * * @return Response */ - public function addAction(Request $request) + public function addAction(Request $request): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -333,7 +341,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 */ @@ -354,7 +362,7 @@ public function addAction(Request $request) * * @return Response */ - public function editSettingsAction(Request $request, int $tagId) + public function editSettingsAction(Request $request, int $tagId): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS'); @@ -372,6 +380,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 +394,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,7 +407,7 @@ public function editSettingsAction(Request $request, int $tagId) /* * Handle errors when Ajax POST requests */ - if ($request->isXmlHttpRequest()) { + if ($isJsonRequest) { $errors = $this->getErrorsAsArray($form); return new JsonResponse([ 'status' => 'fail', @@ -419,7 +431,7 @@ public function editSettingsAction(Request $request, int $tagId) * * @return Response */ - 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'); @@ -453,7 +465,7 @@ public function treeAction(Request $request, int $tagId, ?int $translationId = n * * @return Response */ - public function deleteAction(Request $request, int $tagId) + public function deleteAction(Request $request, int $tagId): Response { $this->denyAccessUnlessGranted('ROLE_ACCESS_TAGS_DELETE'); @@ -482,8 +494,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 @@ -508,7 +524,7 @@ public function deleteAction(Request $request, int $tagId) * * @return Response */ - 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 +566,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 +596,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 +634,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, [ @@ -641,7 +657,7 @@ private function buildDeleteForm(Tag $tag) private function buildBulkDeleteForm( $referer = false, array $tagsIds = [] - ) { + ): FormInterface { $builder = $this->formFactory ->createNamedBuilder('deleteForm') ->add('tagsIds', HiddenType::class, [ @@ -667,7 +683,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 +727,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..1108a87d 100644 --- a/src/Controllers/Tags/TagsUtilsController.php +++ b/src/Controllers/Tags/TagsUtilsController.php @@ -12,9 +12,6 @@ use Symfony\Component\HttpFoundation\Response; use Themes\Rozier\RozierApp; -/** - * @package Themes\Rozier\Controllers\Tags - */ class TagsUtilsController extends RozierApp { private SerializerInterface $serializer; @@ -31,11 +28,10 @@ public function __construct(SerializerInterface $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 +43,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 +60,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 +76,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..968a268e 100644 --- a/src/Controllers/TranslationsController.php +++ b/src/Controllers/TranslationsController.php @@ -61,7 +61,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 +106,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 +144,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 +184,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..dc6999df 100644 --- a/src/Controllers/Users/UsersController.php +++ b/src/Controllers/Users/UsersController.php @@ -4,239 +4,298 @@ namespace Themes\Rozier\Controllers\Users; +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\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Exception\ResourceNotFoundException; -use Themes\Rozier\Forms\AddUserType; +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 + 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 { - $this->denyAccessUnlessGranted('ROLE_BACKEND_USER'); + // 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 + { + 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() == '') { + $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..b67c79f7 100644 --- a/src/Controllers/WebhookController.php +++ b/src/Controllers/WebhookController.php @@ -12,20 +12,22 @@ 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, + FormFactoryInterface $formFactory, SerializerInterface $serializer, UrlGeneratorInterface $urlGenerator ) { - parent::__construct($serializer, $urlGenerator); + parent::__construct($formFactory, $serializer, $urlGenerator); $this->webhookDispatcher = $webhookDispatcher; } @@ -57,7 +59,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 +135,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/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..047dc8d6 100644 --- a/src/Forms/DocumentEditType.php +++ b/src/Forms/DocumentEditType.php @@ -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/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/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/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..c85a0b4f 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..d7ad14e8 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\Component\Security\Core\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..476cc011 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\Component\Security\Core\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 c77fcff7..e9508ae8 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..867439b4 100644 --- a/src/Resources/views/settings/list.html.twig +++ b/src/Resources/views/settings/list.html.twig @@ -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 @@