From 77d9d35e75f337c5ee2de1323a4ec94cbacc6ce2 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Tue, 20 Feb 2018 14:37:54 +0100 Subject: [PATCH] EZP-28110: As a developer, I want to define attributes for custom tags (#2219) * Deprecated old SiteAccess-aware RichText custom tags configuration * [CoreBundle] Defined SA-aware RichText Custom Tags list configuration * [MVC] Injected RichText Custom Tags configuration into RichText Renderer * Injected new configuration * Used configured template to render site template for Custom Tag * Kept BC layer if new configuration is not available * [CoreBundle] Defined Semantic Configuration for RichText CustomTags * [Tests] Created tests for RichText Custom Tags * [RichText] Created Custom Tags user input validator * [CoreBundle] Disallowed merging list of choices for Custom Tag attribute --- .../DependencyInjection/Configuration.php | 104 ++++++++ .../Parser/FieldType/RichText.php | 236 +++++++++++------- .../EzPublishCoreExtension.php | 19 ++ .../Resources/config/default_settings.yml | 5 + .../Resources/config/fieldtype_services.yml | 6 + .../Parser/FieldType/RichTextTest.php | 43 +++- .../EzPublishCoreExtensionTest.php | 61 +++++ .../FieldType/RichText/ezrichtext.yml | 41 +++ .../FieldType/RichTextIntegrationTest.php | 104 ++++++++ .../custom_tags/invalid/equation.xml | 14 ++ .../custom_tags/invalid/unknown_tag.xml | 12 + .../ezrichtext/custom_tags/invalid/video.xml | 19 ++ .../ezrichtext/custom_tags/valid/equation.xml | 15 ++ .../ezrichtext/custom_tags/valid/video.xml | 18 ++ .../RichText/CustomTagsValidator.php | 98 ++++++++ eZ/Publish/Core/FieldType/RichText/Type.php | 17 +- .../RichText/CustomTagsValidatorTest.php | 228 +++++++++++++++++ .../Symfony/FieldType/RichText/Renderer.php | 16 +- .../Core/settings/fieldtype_services.yml | 5 + eZ/Publish/Core/settings/fieldtypes.yml | 1 + eZ/Publish/Core/settings/tests/common.yml | 30 +++ 21 files changed, 1003 insertions(+), 89 deletions(-) create mode 100644 eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Fixtures/FieldType/RichText/ezrichtext.yml create mode 100644 eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/equation.xml create mode 100644 eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/unknown_tag.xml create mode 100644 eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/video.xml create mode 100644 eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/valid/equation.xml create mode 100644 eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/valid/video.xml create mode 100644 eZ/Publish/Core/FieldType/RichText/CustomTagsValidator.php create mode 100644 eZ/Publish/Core/FieldType/Tests/RichText/CustomTagsValidatorTest.php diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration.php index 06dff94815e..a344f5296d1 100644 --- a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration.php +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration.php @@ -11,11 +11,14 @@ use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\ParserInterface; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\SiteAccessAware\Configuration as SiteAccessConfiguration; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\Suggestion\Collector\SuggestionCollectorInterface; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; class Configuration extends SiteAccessConfiguration { + const CUSTOM_TAG_ATTRIBUTE_TYPES = ['number', 'string', 'boolean', 'choice']; + /** * @var \eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\ParserInterface */ @@ -25,6 +28,7 @@ class Configuration extends SiteAccessConfiguration * @var Configuration\Suggestion\Collector\SuggestionCollectorInterface */ private $suggestionCollector; + /** * @var \eZ\Bundle\EzPublishCoreBundle\SiteAccess\SiteAccessConfigurationFilter[] */ @@ -57,6 +61,7 @@ public function getConfigTreeBuilder() $this->addHttpCacheSection($rootNode); $this->addPageSection($rootNode); $this->addRouterSection($rootNode); + $this->addRichTextSection($rootNode); // Delegate SiteAccess config to configuration parsers $this->mainConfigParser->addSemanticConfig($this->generateScopeBaseNode($rootNode)); @@ -464,4 +469,103 @@ private function addRouterSection(ArrayNodeDefinition $rootNode) ->end() ->end(); } + + /** + * Define global Semantic Configuration for RichText. + * + * @param \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $rootNode + */ + private function addRichTextSection(ArrayNodeDefinition $rootNode) + { + $this->addCustomTagsSection( + $rootNode->children()->arrayNode('ezrichtext')->children() + )->end()->end()->end(); + } + + /** + * Define RichText Custom Tags Semantic Configuration. + * + * The configuration is available at: + * + * ezpublish: + * ezrichtext: + * custom_tags: + * + * + * @param \Symfony\Component\Config\Definition\Builder\NodeBuilder $ezRichTextNode + * + * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition + */ + private function addCustomTagsSection(NodeBuilder $ezRichTextNode) + { + return $ezRichTextNode + ->arrayNode('custom_tags') + // workaround: take into account Custom Tag names when merging configs + ->useAttributeAsKey('tag') + ->arrayPrototype() + ->children() + ->scalarNode('template') + ->isRequired() + ->end() + ->scalarNode('icon') + ->defaultNull() + ->end() + ->arrayNode('attributes') + ->useAttributeAsKey('attribute') + ->isRequired() + ->arrayPrototype() + ->beforeNormalization() + ->always( + function ($v) { + // Workaround: set empty value to be able to unset it later on (see validation for "choices") + if (!isset($v['choices'])) { + $v['choices'] = []; + } + + return $v; + } + ) + ->end() + ->validate() + ->ifTrue( + function ($v) { + return $v['type'] === 'choice' && !empty($v['required']) && empty($v['choices']); + } + ) + ->thenInvalid('List of choices for required choice type attribute has to be non-empty') + ->end() + ->validate() + ->ifTrue( + function ($v) { + return !empty($v['choices']) && $v['type'] !== 'choice'; + } + ) + ->thenInvalid('List of choices is supported by choices type only.') + ->end() + ->children() + ->enumNode('type') + ->isRequired() + ->values(static::CUSTOM_TAG_ATTRIBUTE_TYPES) + ->end() + ->booleanNode('required') + ->defaultFalse() + ->end() + ->scalarNode('default_value') + ->defaultNull() + ->end() + ->arrayNode('choices') + ->scalarPrototype()->end() + ->performNoDeepMerging() + ->validate() + ->ifEmpty()->thenUnset() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration/Parser/FieldType/RichText.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration/Parser/FieldType/RichText.php index b046914a6f0..a45d04ea0ac 100644 --- a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration/Parser/FieldType/RichText.php +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/Configuration/Parser/FieldType/RichText.php @@ -10,8 +10,11 @@ use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\Parser\AbstractFieldTypeParser; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\SiteAccessAware\ContextualizerInterface; +use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\EzPublishCoreExtension; use Symfony\Component\Config\Definition\Builder\NodeBuilder; use Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Configuration parser handling RichText field type related config. @@ -36,93 +39,10 @@ public function getFieldTypeIdentifier() */ public function addFieldTypeSemanticConfig(NodeBuilder $nodeBuilder) { + // for BC setup deprecated configuration + $this->setupDeprecatedConfiguration($nodeBuilder); + $nodeBuilder - ->arrayNode('output_custom_tags') - ->info('Custom XSL stylesheets to use for RichText transformation to HTML5. Useful for "custom tags".') - ->example( - array( - 'path' => '%kernel.root_dir%/../src/Acme/TestBundle/Resources/myTag.xsl', - 'priority' => 10, - ) - ) - ->prototype('array') - ->children() - ->scalarNode('path') - ->info('Path of the XSL stylesheet to load.') - ->isRequired() - ->end() - ->integerNode('priority') - ->info('Priority in the loading order. A high value will have higher precedence in overriding XSL templates.') - ->defaultValue(0) - ->end() - ->end() - ->end() - ->end() - ->arrayNode('edit_custom_tags') - ->info('Custom XSL stylesheets to use for RichText transformation to HTML5. Useful for "custom tags".') - ->example( - array( - 'path' => '%kernel.root_dir%/../src/Acme/TestBundle/Resources/myTag.xsl', - 'priority' => 10, - ) - ) - ->prototype('array') - ->children() - ->scalarNode('path') - ->info('Path of the XSL stylesheet to load.') - ->isRequired() - ->end() - ->integerNode('priority') - ->info('Priority in the loading order. A high value will have higher precedence in overriding XSL templates.') - ->defaultValue(0) - ->end() - ->end() - ->end() - ->end() - ->arrayNode('input_custom_tags') - ->info('Custom XSL stylesheets to use for RichText transformation to HTML5. Useful for "custom tags".') - ->example( - array( - 'path' => '%kernel.root_dir%/../src/Acme/TestBundle/Resources/myTag.xsl', - 'priority' => 10, - ) - ) - ->prototype('array') - ->children() - ->scalarNode('path') - ->info('Path of the XSL stylesheet to load.') - ->isRequired() - ->end() - ->integerNode('priority') - ->info('Priority in the loading order. A high value will have higher precedence in overriding XSL templates.') - ->defaultValue(0) - ->end() - ->end() - ->end() - ->end() - ->arrayNode('tags') - ->info('RichText template tags configuration.') - ->useAttributeAsKey('key') - ->normalizeKeys(false) - ->prototype('array') - ->info( - "Name of RichText template tag.\n" . - "'default' and 'default_inline' tag names are reserved for fallback." - ) - ->example('math_equation') - ->children() - ->append( - $this->getTemplateNodeDefinition( - 'Template used for rendering RichText template tag.', - 'MyBundle:FieldType/RichText/tag:math_equation.html.twig' - ) - ) - ->variableNode('config') - ->info('Tag configuration, arbitrary configuration is allowed here.') - ->end() - ->end() - ->end() - ->end() ->arrayNode('embed') ->info('RichText embed tags configuration.') ->children() @@ -240,6 +160,13 @@ public function addFieldTypeSemanticConfig(NodeBuilder $nodeBuilder) ->end() ->end() ->end(); + + // RichText Custom Tags configuration (list of Custom Tags enabled for current SiteAccess scope) + $nodeBuilder + ->arrayNode('custom_tags') + ->info('List of RichText Custom Tags enabled for the current scope. The Custom Tags must be defined in ezpublish.ezrichtext.custom_tags Node.') + ->scalarPrototype()->end() + ->end(); } /** @@ -279,6 +206,18 @@ public function mapConfig(array &$scopeSettings, $currentScope, ContextualizerIn unset($scopeSettings['fieldtypes']['ezrichtext']['input_custom_tags']); } + if (isset($scopeSettings['fieldtypes']['ezrichtext']['custom_tags'])) { + $this->validateCustomTagsConfiguration( + $contextualizer->getContainer(), + $scopeSettings['fieldtypes']['ezrichtext']['custom_tags'] + ); + $contextualizer->setContextualParameter( + 'fieldtypes.ezrichtext.custom_tags', + $currentScope, + $scopeSettings['fieldtypes']['ezrichtext']['custom_tags'] + ); + } + if (isset($scopeSettings['fieldtypes']['ezrichtext']['tags'])) { foreach ($scopeSettings['fieldtypes']['ezrichtext']['tags'] as $name => $tagSettings) { $contextualizer->setContextualParameter( @@ -307,4 +246,129 @@ public function postMap(array $config, ContextualizerInterface $contextualizer) $contextualizer->mapConfigArray('fieldtypes.ezrichtext.edit_custom_xsl', $config); $contextualizer->mapConfigArray('fieldtypes.ezrichtext.input_custom_xsl', $config); } + + /** + * Validate SiteAccess-defined Custom Tags configuration against global one. + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * @param array $enabledCustomTags List of Custom Tags enabled for the current scope/SiteAccess + */ + private function validateCustomTagsConfiguration( + ContainerInterface $container, + array $enabledCustomTags + ) { + $definedCustomTags = array_keys( + $container->getParameter(EzPublishCoreExtension::RICHTEXT_CUSTOM_TAGS_PARAMETER) + ); + foreach ($enabledCustomTags as $customTagName) { + if (!in_array($customTagName, $definedCustomTags)) { + throw new InvalidConfigurationException( + "Unknown RichText Custom Tag '{$customTagName}'" + ); + } + } + } + + /** + * Add BC setup for deprecated configuration. + * + * Note: kept in separate method for readability. + * + * @param \Symfony\Component\Config\Definition\Builder\NodeBuilder $nodeBuilder + */ + private function setupDeprecatedConfiguration(NodeBuilder $nodeBuilder) + { + $nodeBuilder + ->arrayNode('output_custom_tags') + ->setDeprecated('DEPRECATED. Configure custom tags using custom_tags node') + ->info('Custom XSL stylesheets to use for RichText transformation to HTML5. Useful for "custom tags".') + ->example( + array( + 'path' => '%kernel.root_dir%/../src/Acme/TestBundle/Resources/myTag.xsl', + 'priority' => 10, + ) + ) + ->prototype('array') + ->children() + ->scalarNode('path') + ->info('Path of the XSL stylesheet to load.') + ->isRequired() + ->end() + ->integerNode('priority') + ->info('Priority in the loading order. A high value will have higher precedence in overriding XSL templates.') + ->defaultValue(0) + ->end() + ->end() + ->end() + ->end() + ->arrayNode('edit_custom_tags') + ->setDeprecated('DEPRECATED. Configure custom tags using custom_tags node') + ->info('Custom XSL stylesheets to use for RichText transformation to HTML5. Useful for "custom tags".') + ->example( + array( + 'path' => '%kernel.root_dir%/../src/Acme/TestBundle/Resources/myTag.xsl', + 'priority' => 10, + ) + ) + ->prototype('array') + ->children() + ->scalarNode('path') + ->info('Path of the XSL stylesheet to load.') + ->isRequired() + ->end() + ->integerNode('priority') + ->info('Priority in the loading order. A high value will have higher precedence in overriding XSL templates.') + ->defaultValue(0) + ->end() + ->end() + ->end() + ->end() + ->arrayNode('input_custom_tags') + ->setDeprecated('DEPRECATED. Configure custom tags using custom_tags node') + ->info('Custom XSL stylesheets to use for RichText transformation to HTML5. Useful for "custom tags".') + ->example( + array( + 'path' => '%kernel.root_dir%/../src/Acme/TestBundle/Resources/myTag.xsl', + 'priority' => 10, + ) + ) + ->prototype('array') + ->children() + ->scalarNode('path') + ->info('Path of the XSL stylesheet to load.') + ->isRequired() + ->end() + ->integerNode('priority') + ->info('Priority in the loading order. A high value will have higher precedence in overriding XSL templates.') + ->defaultValue(0) + ->end() + ->end() + ->end() + ->end() + ->arrayNode('tags') + ->setDeprecated('DEPRECATED. Configure custom tags using custom_tags node') + ->info('RichText template tags configuration.') + ->useAttributeAsKey('key') + ->normalizeKeys(false) + ->prototype('array') + ->info( + "Name of RichText template tag.\n" . + "'default' and 'default_inline' tag names are reserved for fallback." + ) + ->example('math_equation') + ->children() + ->append( + $this->getTemplateNodeDefinition( + 'Template used for rendering RichText template tag.', + 'MyBundle:FieldType/RichText/tag:math_equation.html.twig' + ) + ) + ->variableNode('config') + ->info('Tag configuration, arbitrary configuration is allowed here.') + ->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php index 71edad973bf..6bed4f71878 100644 --- a/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php +++ b/eZ/Bundle/EzPublishCoreBundle/DependencyInjection/EzPublishCoreExtension.php @@ -29,6 +29,8 @@ class EzPublishCoreExtension extends Extension implements PrependExtensionInterface { + const RICHTEXT_CUSTOM_TAGS_PARAMETER = 'ezplatform.ezrichtext.custom_tags'; + /** * @var \eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\Suggestion\Collector\SuggestionCollector */ @@ -113,6 +115,7 @@ public function load(array $configs, ContainerBuilder $container) $this->registerSiteAccessConfiguration($config, $container); $this->registerImageMagickConfiguration($config, $container); $this->registerPageConfiguration($config, $container); + $this->registerRichTextConfiguration($config, $container); // Routing $this->handleRouting($config, $container, $loader); @@ -283,6 +286,22 @@ private function registerPageConfiguration(array $config, ContainerBuilder $cont } } + /** + * Register parameters of global RichText configuration. + * + * @param array $config + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + */ + private function registerRichTextConfiguration(array $config, ContainerBuilder $container) + { + if (isset($config['ezrichtext']['custom_tags'])) { + $container->setParameter( + static::RICHTEXT_CUSTOM_TAGS_PARAMETER, + $config['ezrichtext']['custom_tags'] + ); + } + } + /** * Handle routing parameters. * diff --git a/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml index be5c7aa4e33..f30aeb4c0f4 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/default_settings.yml @@ -20,6 +20,11 @@ parameters: ezplatform.default_view_templates.content.embed_image: 'EzPublishCoreBundle:default:content/embed_image.html.twig' ezplatform.default_view_templates.block: 'EzPublishCoreBundle:default:block/block.html.twig' + # Rich Text Custom Tags global configuration + ezplatform.ezrichtext.custom_tags: {} + # Rich Text Custom Tags default scope (for SiteAccess) configuration + ezsettings.default.fieldtypes.ezrichtext.custom_tags: [] + ezsettings.default.pagelayout: 'EzPublishCoreBundle::pagelayout.html.twig' # List of content type identifiers to display as image when embedded diff --git a/eZ/Bundle/EzPublishCoreBundle/Resources/config/fieldtype_services.yml b/eZ/Bundle/EzPublishCoreBundle/Resources/config/fieldtype_services.yml index 3b7fd5cdad0..851e1f27ec5 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Resources/config/fieldtype_services.yml +++ b/eZ/Bundle/EzPublishCoreBundle/Resources/config/fieldtype_services.yml @@ -131,6 +131,7 @@ services: - "%ezpublish.fieldType.ezrichtext.tag.namespace%" - "%ezpublish.fieldType.ezrichtext.embed.namespace%" - "@?logger" + - "%ezplatform.ezrichtext.custom_tags%" ezpublish.fieldType.ezrichtext.converter.template: class: "%ezpublish.fieldType.ezrichtext.converter.template.class%" @@ -265,3 +266,8 @@ services: class: "%ezpublish.fieldType.ezselection.nameable_field.class%" tags: - {name: ezpublish.fieldType.nameable, alias: ezselection} + + # Symfony 3.4+ service definitions: + eZ\Publish\Core\FieldType\RichText\CustomTagsValidator: + public: false + arguments: ['%ezplatform.ezrichtext.custom_tags%'] diff --git a/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Configuration/Parser/FieldType/RichTextTest.php b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Configuration/Parser/FieldType/RichTextTest.php index 5f358141b00..f2dc0a65530 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Configuration/Parser/FieldType/RichTextTest.php +++ b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Configuration/Parser/FieldType/RichTextTest.php @@ -10,6 +10,7 @@ use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\EzPublishCoreExtension; use eZ\Bundle\EzPublishCoreBundle\Tests\DependencyInjection\Configuration\Parser\AbstractParserTestCase; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\Parser\FieldType\RichText as RichTextConfigParser; use Symfony\Component\Yaml\Yaml; @@ -31,7 +32,7 @@ protected function getContainerExtensions() protected function getMinimalConfiguration() { - return Yaml::parse(file_get_contents(__DIR__ . '/../../../Fixtures/ezpublish_minimal.yml')); + return Yaml::parse(file_get_contents(__DIR__ . '/../../../Fixtures/FieldType/RichText/ezrichtext.yml')); } public function testDefaultContentSettings() @@ -57,6 +58,34 @@ public function testDefaultContentSettings() ); } + /** + * Test Rich Text Custom Tags invalid settings, like enabling undefined Custom Tag. + */ + public function testRichTextCustomTagsInvalidSettings() + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Unknown RichText Custom Tag \'foo\''); + + $this->load( + [ + 'system' => [ + 'ezdemo_site' => [ + 'fieldtypes' => [ + 'ezrichtext' => [ + 'custom_tags' => ['foo'], + ], + ], + ], + ], + ] + ); + $this->assertConfigResolverParameterValue( + 'fieldtypes.ezrichtext.custom_tags', + ['foo'], + 'ezdemo_site' + ); + } + /** * @dataProvider richTextSettingsProvider */ @@ -205,6 +234,18 @@ public function richTextSettingsProvider() ), ), ), + array( + array( + 'fieldtypes' => array( + 'ezrichtext' => array( + 'custom_tags' => array('video', 'equation'), + ), + ), + ), + array( + 'fieldtypes.ezrichtext.custom_tags' => array('video', 'equation'), + ), + ), array( array( 'fieldtypes' => array( diff --git a/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/EzPublishCoreExtensionTest.php b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/EzPublishCoreExtensionTest.php index c243df0e3f9..31d1edc813f 100644 --- a/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/EzPublishCoreExtensionTest.php +++ b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/EzPublishCoreExtensionTest.php @@ -826,4 +826,65 @@ public function testRegisteredPolicies() self::assertContainerBuilderHasParameter('ezpublish.api.role.policy_map'); self::assertEquals($expectedPolicies, $this->container->getParameter('ezpublish.api.role.policy_map')); } + + /** + * Test RichText Semantic Configuration. + */ + public function testRichTextConfiguration() + { + $config = Yaml::parse( + file_get_contents(__DIR__ . '/Fixtures/FieldType/RichText/ezrichtext.yml') + ); + $this->load($config); + + // Validate Custom Tags + $this->assertTrue( + $this->container->hasParameter($this->extension::RICHTEXT_CUSTOM_TAGS_PARAMETER) + ); + $expectedCustomTagsConfig = [ + 'video' => [ + 'template' => 'MyBundle:FieldType/RichText/tag:video.html.twig', + 'icon' => '/bundles/mybundle/fieldtype/richtext/video.svg#video', + 'attributes' => [ + 'title' => [ + 'type' => 'string', + 'required' => true, + 'default_value' => 'abc', + ], + 'width' => [ + 'type' => 'number', + 'required' => true, + 'default_value' => 360, + ], + 'autoplay' => [ + 'type' => 'boolean', + 'required' => false, + 'default_value' => null, + ], + ], + ], + 'equation' => [ + 'template' => 'MyBundle:FieldType/RichText/tag:equation.html.twig', + 'icon' => '/bundles/mybundle/fieldtype/richtext/equation.svg#equation', + 'attributes' => [ + 'name' => [ + 'type' => 'string', + 'required' => true, + 'default_value' => 'Equation', + ], + 'processor' => [ + 'type' => 'choice', + 'required' => true, + 'default_value' => 'latex', + 'choices' => ['latex', 'tex'], + ], + ], + ], + ]; + + $this->assertSame( + $expectedCustomTagsConfig, + $this->container->getParameter($this->extension::RICHTEXT_CUSTOM_TAGS_PARAMETER) + ); + } } diff --git a/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Fixtures/FieldType/RichText/ezrichtext.yml b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Fixtures/FieldType/RichText/ezrichtext.yml new file mode 100644 index 00000000000..277d091d799 --- /dev/null +++ b/eZ/Bundle/EzPublishCoreBundle/Tests/DependencyInjection/Fixtures/FieldType/RichText/ezrichtext.yml @@ -0,0 +1,41 @@ +siteaccess: + default_siteaccess: ezdemo_site + list: + - ezdemo_site + groups: + ezdemo_group: + - ezdemo_site + match: + URIElement: 1 + Map\URI: + the_front: ezdemo_site + +ezrichtext: + custom_tags: + video: + template: 'MyBundle:FieldType/RichText/tag:video.html.twig' + icon: '/bundles/mybundle/fieldtype/richtext/video.svg#video' + attributes: + title: + type: 'string' + required: true + default_value: 'abc' + width: + type: 'number' + required: true + default_value: 360 + autoplay: + type: 'boolean' + equation: + template: 'MyBundle:FieldType/RichText/tag:equation.html.twig' + icon: '/bundles/mybundle/fieldtype/richtext/equation.svg#equation' + attributes: + name: + type: 'string' + required: true + default_value: 'Equation' + processor: + type: 'choice' + required: true + default_value: 'latex' + choices: ['latex', 'tex'] diff --git a/eZ/Publish/API/Repository/Tests/FieldType/RichTextIntegrationTest.php b/eZ/Publish/API/Repository/Tests/FieldType/RichTextIntegrationTest.php index 9f960db8cff..cf22c238513 100644 --- a/eZ/Publish/API/Repository/Tests/FieldType/RichTextIntegrationTest.php +++ b/eZ/Publish/API/Repository/Tests/FieldType/RichTextIntegrationTest.php @@ -8,11 +8,14 @@ */ namespace eZ\Publish\API\Repository\Tests\FieldType; +use DirectoryIterator; +use eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException; use eZ\Publish\Core\FieldType\RichText\Value as RichTextValue; use eZ\Publish\API\Repository\Values\Content\Field; use DOMDocument; use eZ\Publish\Core\Repository\Values\Content\Relation; use eZ\Publish\API\Repository\Values\Content\Content; +use eZ\Publish\SPI\FieldType\ValidationError; /** * Integration test for use field type. @@ -618,6 +621,107 @@ public function testConvertRemoteObjectIdToObjectId($test, $expected) ); } + /** + * @param string $xmlDocumentPath + * @dataProvider providerForTestCreateContentWithValidCustomTag + */ + public function testCreateContentWithValidCustomTag($xmlDocumentPath) + { + $validXmlDocument = $this->createDocument($xmlDocumentPath); + $this->createContent(new RichTextValue($validXmlDocument)); + } + + /** + * Data provider for testCreateContentWithValidCustomTag. + * + * @return array + */ + public function providerForTestCreateContentWithValidCustomTag() + { + $data = []; + $iterator = new DirectoryIterator(__DIR__ . '/_fixtures/ezrichtext/custom_tags/valid'); + foreach ($iterator as $fileInfo) { + if ($fileInfo->isFile() && $fileInfo->getExtension() === 'xml') { + $data[] = [ + $fileInfo->getRealPath(), + ]; + } + } + + return $data; + } + + /** + * @param string $xmlDocumentPath + * @param string $expectedValidationMessage + * + * @dataProvider providerForTestCreateContentWithInvalidCustomTag + */ + public function testCreateContentWithInvalidCustomTag( + $xmlDocumentPath, + $expectedValidationMessage + ) { + try { + $invalidXmlDocument = $this->createDocument($xmlDocumentPath); + $this->createContent(new RichTextValue($invalidXmlDocument)); + } catch (ContentFieldValidationException $e) { + // get first nested ValidationError + /** @var \eZ\Publish\SPI\FieldType\ValidationError $error */ + $error = current(current(current($e->getFieldErrors()))); + + self::assertEquals( + $expectedValidationMessage, + $error->getTranslatableMessage()->message + ); + + return; + } + + self::fail("Expected ValidationError '{$expectedValidationMessage}' did not occur."); + } + + /** + * Data provider for testCreateContentWithInvalidCustomTag. + * + * @return array + */ + public function providerForTestCreateContentWithInvalidCustomTag() + { + $data = [ + [ + __DIR__ . '/_fixtures/ezrichtext/custom_tags/invalid/unknown_tag.xml', + "Unknown RichText Custom Tag 'unknown_tag'", + ], + [ + __DIR__ . '/_fixtures/ezrichtext/custom_tags/invalid/equation.xml', + "The attribute 'processor' of RichText Custom Tag 'equation' cannot be empty", + ], + [ + __DIR__ . '/_fixtures/ezrichtext/custom_tags/invalid/video.xml', + "Unknown attribute 'unknown_attribute' of RichText Custom Tag 'video'", + ], + ]; + + return $data; + } + + /** + * @param string $filename + * + * @return \DOMDocument + */ + protected function createDocument($filename) + { + $document = new DOMDocument(); + + $document->preserveWhiteSpace = false; + $document->formatOutput = false; + + $document->loadXml(file_get_contents($filename), LIBXML_NOENT); + + return $document; + } + protected function checkSearchEngineSupport() { if (ltrim(get_class($this->getSetupFactory()), '\\') === 'eZ\\Publish\\API\\Repository\\Tests\\SetupFactory\\Legacy') { diff --git a/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/equation.xml b/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/equation.xml new file mode 100644 index 00000000000..aa8949c92d7 --- /dev/null +++ b/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/equation.xml @@ -0,0 +1,14 @@ + +
+ + + E = mc^2 + + + Equation + + +
diff --git a/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/unknown_tag.xml b/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/unknown_tag.xml new file mode 100644 index 00000000000..dc328b38b9e --- /dev/null +++ b/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/unknown_tag.xml @@ -0,0 +1,12 @@ + +
+ + Undefined + + Test + + +
diff --git a/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/video.xml b/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/video.xml new file mode 100644 index 00000000000..a76f1335aaf --- /dev/null +++ b/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/invalid/video.xml @@ -0,0 +1,19 @@ + +
+ + + Title: Test + Width: 640 + Autoplay: false + + + Test + 640 + false + false + + +
diff --git a/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/valid/equation.xml b/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/valid/equation.xml new file mode 100644 index 00000000000..827ae59d353 --- /dev/null +++ b/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/valid/equation.xml @@ -0,0 +1,15 @@ + +
+ + + E = mc^2 + + + Equation + Latex + + +
diff --git a/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/valid/video.xml b/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/valid/video.xml new file mode 100644 index 00000000000..635ceb4f94e --- /dev/null +++ b/eZ/Publish/API/Repository/Tests/FieldType/_fixtures/ezrichtext/custom_tags/valid/video.xml @@ -0,0 +1,18 @@ + +
+ + + Title: Test + Width: 640 + Autoplay: false + + + Test + 640 + false + + +
diff --git a/eZ/Publish/Core/FieldType/RichText/CustomTagsValidator.php b/eZ/Publish/Core/FieldType/RichText/CustomTagsValidator.php new file mode 100644 index 00000000000..0fd4ec9a403 --- /dev/null +++ b/eZ/Publish/Core/FieldType/RichText/CustomTagsValidator.php @@ -0,0 +1,98 @@ +customTagsConfiguration = $customTagsConfiguration; + } + + /** + * Validate Custom Tags found in the document. + * + * @param \DOMDocument $xmlDocument + * + * @return string[] an array of error messages + */ + public function validateDocument(DOMDocument $xmlDocument) + { + $errors = []; + + $xpath = new DOMXPath($xmlDocument); + $xpath->registerNamespace('docbook', 'http://docbook.org/ns/docbook'); + + foreach ($xpath->query('//docbook:eztemplate') as $tagElement) { + $tagName = $tagElement->getAttribute('name'); + if (empty($tagName)) { + $errors[] = 'Missing RichText Custom Tag name'; + continue; + } + + if (!isset($this->customTagsConfiguration[$tagName])) { + $errors[] = "Unknown RichText Custom Tag '{$tagName}'"; + continue; + } + + $nonEmptyAttributes = []; + $tagAttributes = $this->customTagsConfiguration[$tagName]['attributes']; + + // iterate over all attributes defined in XML document to check if their names match configuration + $configElements = $xpath->query('.//docbook:ezconfig/docbook:ezvalue', $tagElement); + foreach ($configElements as $configElement) { + $attributeName = $configElement->getAttribute('key'); + if (empty($attributeName)) { + $errors[] = "Missing attribute name for RichText Custom Tag '{$tagName}'"; + continue; + } + if (!isset($tagAttributes[$attributeName])) { + $errors[] = "Unknown attribute '{$attributeName}' of RichText Custom Tag '{$tagName}'"; + } + + // collect information about non-empty attributes + if (!empty($configElement->textContent)) { + $nonEmptyAttributes[] = $attributeName; + } + } + + // check if all required attributes are present + foreach ($tagAttributes as $attributeName => $attributeSettings) { + if (empty($attributeSettings['required'])) { + continue; + } + + if (!in_array($attributeName, $nonEmptyAttributes)) { + $errors[] = "The attribute '{$attributeName}' of RichText Custom Tag '{$tagName}' cannot be empty"; + } + } + } + + return $errors; + } +} diff --git a/eZ/Publish/Core/FieldType/RichText/Type.php b/eZ/Publish/Core/FieldType/RichText/Type.php index 0fa2d8e41f7..dda73f06315 100644 --- a/eZ/Publish/Core/FieldType/RichText/Type.php +++ b/eZ/Publish/Core/FieldType/RichText/Type.php @@ -50,25 +50,33 @@ class Type extends FieldType */ protected $internalLinkValidator; + /** + * @var null|\eZ\Publish\Core\FieldType\RichText\CustomTagsValidator + */ + private $customTagsValidator; + /** * @param \eZ\Publish\Core\FieldType\RichText\Validator $internalFormatValidator * @param \eZ\Publish\Core\FieldType\RichText\ConverterDispatcher $inputConverterDispatcher * @param null|\eZ\Publish\Core\FieldType\RichText\Normalizer $inputNormalizer * @param null|\eZ\Publish\Core\FieldType\RichText\ValidatorDispatcher $inputValidatorDispatcher * @param null|\eZ\Publish\Core\FieldType\RichText\InternalLinkValidator $internalLinkValidator + * @param null|\eZ\Publish\Core\FieldType\RichText\CustomTagsValidator $customTagsValidator */ public function __construct( Validator $internalFormatValidator, ConverterDispatcher $inputConverterDispatcher, Normalizer $inputNormalizer = null, ValidatorDispatcher $inputValidatorDispatcher = null, - InternalLinkValidator $internalLinkValidator = null + InternalLinkValidator $internalLinkValidator = null, + CustomTagsValidator $customTagsValidator = null ) { $this->internalFormatValidator = $internalFormatValidator; $this->inputConverterDispatcher = $inputConverterDispatcher; $this->inputNormalizer = $inputNormalizer; $this->inputValidatorDispatcher = $inputValidatorDispatcher; $this->internalLinkValidator = $internalLinkValidator; + $this->customTagsValidator = $customTagsValidator; } /** @@ -267,6 +275,13 @@ public function validate(FieldDefinition $fieldDefinition, SPIValue $value) } } + if ($this->customTagsValidator !== null) { + $errors = $this->customTagsValidator->validateDocument($value->xml); + foreach ($errors as $error) { + $validationErrors[] = new ValidationError($error); + } + } + return $validationErrors; } diff --git a/eZ/Publish/Core/FieldType/Tests/RichText/CustomTagsValidatorTest.php b/eZ/Publish/Core/FieldType/Tests/RichText/CustomTagsValidatorTest.php new file mode 100644 index 00000000000..578c78c3bd6 --- /dev/null +++ b/eZ/Publish/Core/FieldType/Tests/RichText/CustomTagsValidatorTest.php @@ -0,0 +1,228 @@ +validator = new CustomTagsValidator($customTagsConfiguration); + } + + /** + * Test validating DocBook document containing Custom Tags. + * + * @covers \CustomTagsValidator::validateDocument + * + * @dataProvider providerForTestValidateDocument + * + * @param \DOMDocument $document + * @param array $expectedErrors + */ + public function testValidateDocument(DOMDocument $document, array $expectedErrors) + { + self::assertEquals( + $expectedErrors, + $this->validator->validateDocument($document) + ); + } + + /** + * Data provider for testValidateDocument. + * + * @see testValidateDocument + * + * @return array + */ + public function providerForTestValidateDocument() + { + return [ + [ + $this->createDocument( + << +
+ + +
+DOCBOOK + ), + [ + 'Missing RichText Custom Tag name', + ], + ], + [ + $this->createDocument( + << +
+ + Undefined + + Test + + +
+DOCBOOK + ), + [ + "Unknown RichText Custom Tag 'undefined_tag'", + ], + ], + [ + $this->createDocument( + << +
+ + Content + + Test + Test + 360 + + +
+DOCBOOK + ), + [ + "Missing attribute name for RichText Custom Tag 'video'", + ], + ], + [ + $this->createDocument( + << +
+ + Content + + Test + Test + 360 + + +
+DOCBOOK + ), + [ + "Unknown attribute 'unknown' of RichText Custom Tag 'video'", + ], + ], + [ + $this->createDocument( + << +
+ + Content + + false + + +
+DOCBOOK + ), + [ + "The attribute 'title' of RichText Custom Tag 'video' cannot be empty", + "The attribute 'width' of RichText Custom Tag 'video' cannot be empty", + ], + ], + [ + $this->createDocument( + << +
+ + + + + + Content + + Test + + + + Content + + Test + Test + latex + + +
+DOCBOOK + ), + [ + 'Missing RichText Custom Tag name', + "Unknown RichText Custom Tag 'undefined_tag'", + "Missing attribute name for RichText Custom Tag 'video'", + "The attribute 'title' of RichText Custom Tag 'video' cannot be empty", + "The attribute 'width' of RichText Custom Tag 'video' cannot be empty", + "Unknown attribute 'unknown' of RichText Custom Tag 'equation'", + ], + ], + ]; + } + + /** + * @param string $source XML source + * + * @return \DOMDocument + */ + protected function createDocument($source) + { + $document = new DOMDocument(); + + $document->preserveWhiteSpace = false; + $document->formatOutput = false; + + $document->loadXml($source, LIBXML_NOENT); + + return $document; + } +} diff --git a/eZ/Publish/Core/MVC/Symfony/FieldType/RichText/Renderer.php b/eZ/Publish/Core/MVC/Symfony/FieldType/RichText/Renderer.php index 1417030f667..4dc6e3c2198 100644 --- a/eZ/Publish/Core/MVC/Symfony/FieldType/RichText/Renderer.php +++ b/eZ/Publish/Core/MVC/Symfony/FieldType/RichText/Renderer.php @@ -64,6 +64,11 @@ class Renderer implements RendererInterface */ protected $logger; + /** + * @var array + */ + private $customTagsConfiguration; + /** * @param \eZ\Publish\API\Repository\Repository $repository * @param \Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface $authorizationChecker @@ -72,6 +77,7 @@ class Renderer implements RendererInterface * @param string $tagConfigurationNamespace * @param string $embedConfigurationNamespace * @param null|\Psr\Log\LoggerInterface $logger + * @param array $customTagsConfiguration */ public function __construct( Repository $repository, @@ -80,7 +86,8 @@ public function __construct( EngineInterface $templateEngine, $tagConfigurationNamespace, $embedConfigurationNamespace, - LoggerInterface $logger = null + LoggerInterface $logger = null, + array $customTagsConfiguration = [] ) { $this->repository = $repository; $this->authorizationChecker = $authorizationChecker; @@ -89,6 +96,7 @@ public function __construct( $this->tagConfigurationNamespace = $tagConfigurationNamespace; $this->embedConfigurationNamespace = $embedConfigurationNamespace; $this->logger = $logger; + $this->customTagsConfiguration = $customTagsConfiguration; } public function renderTag($name, array $parameters, $isInline) @@ -281,6 +289,11 @@ protected function render($templateReference, array $parameters) */ protected function getTagTemplateName($identifier, $isInline) { + if (isset($this->customTagsConfiguration[$identifier])) { + return $this->customTagsConfiguration[$identifier]['template']; + } + + // BC layer: $configurationReference = $this->tagConfigurationNamespace . '.' . $identifier; if ($this->configResolver->hasParameter($configurationReference)) { @@ -288,6 +301,7 @@ protected function getTagTemplateName($identifier, $isInline) return $configuration['template']; } + // End of BC layer --/ if (isset($this->logger)) { $this->logger->warning( diff --git a/eZ/Publish/Core/settings/fieldtype_services.yml b/eZ/Publish/Core/settings/fieldtype_services.yml index 4c63861245a..549c86f02ca 100644 --- a/eZ/Publish/Core/settings/fieldtype_services.yml +++ b/eZ/Publish/Core/settings/fieldtype_services.yml @@ -115,3 +115,8 @@ services: class: "%ezpublish.fieldType.ezselection.nameable_field.class%" tags: - {name: ezpublish.fieldType.nameable, alias: ezselection} + + # Symfony 3.4+ service definitions: + eZ\Publish\Core\FieldType\RichText\CustomTagsValidator: + public: false + arguments: ['%ezplatform.ezrichtext.custom_tags%'] diff --git a/eZ/Publish/Core/settings/fieldtypes.yml b/eZ/Publish/Core/settings/fieldtypes.yml index 793f77a8577..61a4f1bd72a 100644 --- a/eZ/Publish/Core/settings/fieldtypes.yml +++ b/eZ/Publish/Core/settings/fieldtypes.yml @@ -413,6 +413,7 @@ services: - "@ezpublish.fieldType.ezrichtext.normalizer.input" - "@ezpublish.fieldType.ezrichtext.validator.input.dispatcher" - '@ezpublish.fieldType.ezrichtext.validator.internal_link' + - '@eZ\Publish\Core\FieldType\RichText\CustomTagsValidator' tags: - {name: ezpublish.fieldType, alias: ezrichtext} diff --git a/eZ/Publish/Core/settings/tests/common.yml b/eZ/Publish/Core/settings/tests/common.yml index 99ad516c728..f680b6eb928 100644 --- a/eZ/Publish/Core/settings/tests/common.yml +++ b/eZ/Publish/Core/settings/tests/common.yml @@ -1,3 +1,33 @@ +parameters: + ezplatform.ezrichtext.custom_tags: + video: + template: 'MyBundle:FieldType/RichText/tag:video.html.twig' + icon: '/bundles/mybundle/fieldtype/richtext/video.svg#video' + attributes: + title: + type: 'string' + required: true + default_value: 'abc' + width: + type: 'number' + required: true + default_value: 360 + autoplay: + type: 'boolean' + equation: + template: 'MyBundle:FieldType/RichText/tag:equation.html.twig' + icon: '/bundles/mybundle/fieldtype/richtext/equation.svg#equation' + attributes: + name: + type: 'string' + required: true + default_value: 'Equation' + processor: + type: 'choice' + required: true + default_value: 'latex' + choices: ['latex', 'tex'] + services: logger: class: Psr\Log\NullLogger