');
+ foreach ($this->profiles as $profile) {
+ // Will build the configuration
+ $this->registry->get($profile)->purify("
");
+ }
}
/**
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index f30b6a76..8ecee7ad 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -4,6 +4,7 @@
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
+use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
class Configuration implements ConfigurationInterface
{
@@ -22,10 +23,77 @@ public function getConfigTreeBuilder()
}
$rootNode
- ->useAttributeAsKey('name')
- ->prototype('array')
- ->useAttributeAsKey('name')
- ->prototype('variable')
+ ->children()
+ ->scalarNode('default_cache_serializer_path')
+ ->defaultValue('%kernel.cache_dir%/htmlpurifier')
+ ->end()
+ ->arrayNode('html_profiles')
+ ->useAttributeAsKey('name')
+ ->normalizeKeys(false)
+ ->validate()
+ ->always(function ($profiles) {
+ foreach ($profiles as $profile => $definition) {
+ foreach ($definition['parents'] as $parent) {
+ if (!isset($profiles[$parent])) {
+ throw new InvalidConfigurationException(sprintf('Invalid parent "%s" is not defined for profile "%s".', $parent, $profile));
+ }
+ }
+ }
+
+ return $profiles;
+ })
+ ->end()
+ ->arrayPrototype()
+ ->children()
+ ->arrayNode('config')
+ ->defaultValue([])
+ ->info('An array of parameters.')
+ ->useAttributeAsKey('parameter')
+ ->normalizeKeys(false)
+ ->variablePrototype()->end()
+ ->end()
+ ->arrayNode('attributes')
+ ->defaultValue([])
+ ->info('Every key is a tag name, with arrays for rules')
+ ->normalizeKeys(false)
+ ->useAttributeAsKey('tag_name')
+ ->arrayPrototype()
+ ->info('Every key is an attribute name for a rule like "Text"')
+ ->useAttributeAsKey('attribute_name')
+ ->normalizeKeys(false)
+ ->scalarPrototype()->end()
+ ->end()
+ ->end()
+ ->arrayNode('elements')
+ ->defaultValue([])
+ ->info('Every key is a tag name, with an array of four values as definition. The fourth is an optional array of attributes rules.')
+ ->normalizeKeys(false)
+ ->useAttributeAsKey('tag_name')
+ ->info('An array represents a definition, with three required elements: a type ("Inline", "Block", ...), a content type ("Empty", "Optional: #PCDATA", ...), an attributes set ("Core", "Common", ...), a fourth optional may define attributes rules as array, and fifth for forbidden attributes.')
+ ->arrayPrototype()
+ ->validate()
+ ->ifTrue(function ($array) {
+ $count = count($array);
+
+ return 3 > $count || $count > 5;
+ })
+ ->thenInvalid('An element definition must define three to five elements: a type ("Inline", "Block", ...), a content type ("Empty", "Optional: #PCDATA", ...), an attributes set ("Core", "Common", ...), and a fourth optional may define attributes rules as array, and fifth for forbidden attributes.')
+ ->end()
+ ->variablePrototype()->end()
+ ->end()
+ ->end()
+ ->arrayNode('blank_elements')
+ ->defaultValue([])
+ ->info('An array of tag names that should purify everything.')
+ ->scalarPrototype()->end()
+ ->end()
+ ->arrayNode('parents')
+ ->defaultValue([])
+ ->info('An array of config names that should be inherited.')
+ ->scalarPrototype()->end()
+ ->end()
+ ->end()
+ ->end()
->end()
->end()
;
diff --git a/src/DependencyInjection/ExerciseHTMLPurifierExtension.php b/src/DependencyInjection/ExerciseHTMLPurifierExtension.php
index c8aae1b6..76965aaf 100644
--- a/src/DependencyInjection/ExerciseHTMLPurifierExtension.php
+++ b/src/DependencyInjection/ExerciseHTMLPurifierExtension.php
@@ -3,6 +3,7 @@
namespace Exercise\HTMLPurifierBundle\DependencyInjection;
use Exercise\HTMLPurifierBundle\DependencyInjection\Compiler\HTMLPurifierPass;
+use Exercise\HTMLPurifierBundle\HTMLPurifierConfigFactory;
use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistry;
use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistryInterface;
use Symfony\Component\Config\FileLocator;
@@ -13,59 +14,61 @@
class ExerciseHTMLPurifierExtension extends Extension
{
- /**
- * {@inheritdoc}
- */
public function load(array $configs, ContainerBuilder $container)
{
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('html_purifier.xml');
- /* Prepend the default configuration. This cannot be defined within the
- * Configuration class, since the root node's children are array
- * prototypes.
- *
- * This cache path may be suppressed by either unsetting the "default"
- * configuration (relying on canBeUnset() on the prototype node) or
- * setting the "Cache.SerializerPath" option to null.
- */
- array_unshift($configs, [
- 'default' => [
- 'Cache.SerializerPath' => '%kernel.cache_dir%/htmlpurifier',
- ],
- ]);
-
$configs = $this->processConfiguration(new Configuration(), $configs);
+ // Set default serializer cache path, while ensuring a default profile is defined
+ $configs['html_profiles']['default']['config']['Cache.SerializerPath'] = $configs['default_cache_serializer_path'];
+
$serializerPaths = [];
+ // Drop when require Symfony > 3.4
+ $registerAlias = method_exists($container, 'registerAliasForArgument');
- foreach ($configs as $name => $config) {
+ foreach ($configs['html_profiles'] as $name => $definition) {
$configId = "exercise_html_purifier.config.$name";
- $configDefinition = $container->register($configId, \HTMLPurifier_Config::class)
- ->setPublic(false)
- ;
+ $default = null;
+ $parents = []; // stores inherited configs
- if ('default' === $name) {
- $configDefinition
- ->setFactory([\HTMLPurifier_Config::class, 'create'])
- ->addArgument($config)
- ;
- } else {
- $configDefinition
- ->setFactory([\HTMLPurifier_Config::class, 'inherit'])
- ->addArgument(new Reference('exercise_html_purifier.config.default'))
- ->addMethodCall('loadArray', [$config])
- ;
+ if ('default' !== $name) {
+ $default = new Reference('exercise_html_purifier.config.default');
+ $parentNames = $definition['parents'];
+
+ unset($parentNames['default']); // default is always inherited
+ foreach ($parentNames as $parentName) {
+ self::resolveProfileInheritance($parentName, $configs['html_profiles'], $parents);
+ }
}
- $container->register("exercise_html_purifier.$name", \HTMLPurifier::class)
- ->addArgument(new Reference($configId))
+ $container->register($configId, \HTMLPurifier_Config::class)
+ ->setFactory([HTMLPurifierConfigFactory::class, 'create'])
+ ->setArguments([
+ $name,
+ $definition['config'],
+ $default,
+ self::getResolvedConfig('config', $parents),
+ self::getResolvedConfig('attributes', $parents, $definition),
+ self::getResolvedConfig('elements', $parents, $definition),
+ self::getResolvedConfig('blank_elements', $parents, $definition),
+ ])
+ ;
+
+ $id = "exercise_html_purifier.$name";
+ $container->register($id, \HTMLPurifier::class)
+ ->setArguments([new Reference($configId)])
->addTag(HTMLPurifierPass::PURIFIER_TAG, ['profile' => $name])
;
- if (isset($config['Cache.SerializerPath'])) {
- $serializerPaths[] = $config['Cache.SerializerPath'];
+ if (isset($definition['config']['Cache.SerializerPath'])) {
+ $serializerPaths[] = $definition['config']['Cache.SerializerPath'];
+ }
+
+ if ($registerAlias && $default) {
+ $container->registerAliasForArgument($id, \HTMLPurifier::class, "$name.purifier");
}
}
@@ -78,14 +81,43 @@ public function load(array $configs, ContainerBuilder $container)
$container->setAlias(\HTMLPurifier::class, 'exercise_html_purifier.default')
->setPublic(false)
;
- $container->setParameter('exercise_html_purifier.cache_warmer.serializer.paths', array_unique($serializerPaths));
+ $container->getDefinition('exercise_html_purifier.cache_warmer.serializer')
+ ->setArgument(0, array_unique($serializerPaths))
+ ->setArgument(1, array_keys($configs['html_profiles']))
+ ;
}
- /**
- * {@inheritdoc}
- */
public function getAlias()
{
return 'exercise_html_purifier';
}
+
+ private static function resolveProfileInheritance(string $parent, array $configs, array &$resolved): void
+ {
+ if (isset($resolved[$parent])) {
+ // Another profile already inherited this config, skip
+ return;
+ }
+
+ foreach ($configs[$parent]['parents'] as $grandParent) {
+ self::resolveProfileInheritance($grandParent, $configs, $resolved);
+ }
+
+ $resolved[$parent]['config'] = $configs[$parent]['config'];
+ $resolved[$parent]['attributes'] = $configs[$parent]['attributes'];
+ $resolved[$parent]['elements'] = $configs[$parent]['elements'];
+ $resolved[$parent]['blank_elements'] = $configs[$parent]['blank_elements'];
+ }
+
+ private static function getResolvedConfig(string $parameter, array $parents, array $definition = null): array
+ {
+ if (null !== $definition) {
+ return array_filter(array_merge(
+ array_column($parents, $parameter),
+ isset($definition[$parameter]) ? $definition[$parameter] : []
+ ));
+ }
+
+ return array_filter(array_column($parents, $parameter));
+ }
}
diff --git a/src/Form/Listener/HTMLPurifierListener.php b/src/Form/Listener/HTMLPurifierListener.php
index 846151fe..98d86dd1 100644
--- a/src/Form/Listener/HTMLPurifierListener.php
+++ b/src/Form/Listener/HTMLPurifierListener.php
@@ -12,16 +12,13 @@ class HTMLPurifierListener implements EventSubscriberInterface
private $registry;
private $profile;
- /**
- * @param string $profile
- */
- public function __construct(HTMLPurifiersRegistryInterface $registry, $profile)
+ public function __construct(HTMLPurifiersRegistryInterface $registry, string $profile)
{
$this->registry = $registry;
$this->profile = $profile;
}
- public function purifySubmittedData(FormEvent $event)
+ public function purifySubmittedData(FormEvent $event): void
{
if (!is_scalar($data = $event->getData())) {
// Hope there is a view transformer, otherwise an error might happen
@@ -49,10 +46,7 @@ public static function getSubscribedEvents()
];
}
- /**
- * @return \HTMLPurifier
- */
- private function getPurifier()
+ private function getPurifier(): \HTMLPurifier
{
return $this->registry->get($this->profile);
}
diff --git a/src/Form/TypeExtension/ForwardCompatTypeExtensionTrait.php b/src/Form/TypeExtension/ForwardCompatTypeExtensionTrait.php
deleted file mode 100644
index 4f1f1eeb..00000000
--- a/src/Form/TypeExtension/ForwardCompatTypeExtensionTrait.php
+++ /dev/null
@@ -1,47 +0,0 @@
- ['src' => 'URI', 'data-type' => Text']] ]
+ * @param array $elements An array of arrays by element to add or override, arrays must
+ * hold a type ("Inline, "Block", ...), a content type ("Empty",
+ * "Optional: #PCDATA", ...), an attributes set ("Core", "Common",
+ * ...), a fourth optional may define attributes rules as array, and
+ * a fifth to list forbidden attributes
+ * @param array $blankElements An array of tag names that should not have any attributes
+ */
+ public static function create(
+ string $profile,
+ array $configArray,
+ \HTMLPurifier_Config $defaultConfig = null,
+ array $parents = [],
+ array $attributes = [],
+ array $elements = [],
+ array $blankElements = []
+ ): \HTMLPurifier_Config {
+ if ($defaultConfig) {
+ $config = \HTMLPurifier_Config::inherit($defaultConfig);
+ } else {
+ $config = \HTMLPurifier_Config::createDefault();
+ }
+
+ foreach ($parents as $parent) {
+ $config->loadArray($parent);
+ }
+
+ $config->loadArray($configArray);
+
+ // Make the config unique
+ $config->set('HTML.DefinitionID', $profile);
+ $config->set('HTML.DefinitionRev', 1);
+
+ $def = $config->maybeGetRawHTMLDefinition();
+
+ // If the definition is not cached, build it
+ if ($def && ($attributes || $elements || $blankElements)) {
+ static::buildHTMLDefinition($def, $attributes, $elements, $blankElements);
+ }
+
+ return $config;
+ }
+
+ /**
+ * Builds a config definition from the given parameters.
+ *
+ * This build should never happen on runtime, since purifiers cache should
+ * be generated during warm up.
+ */
+ public static function buildHTMLDefinition(\HTMLPurifier_Definition $def, array $attributes, array $elements, array $blankElements): void
+ {
+ foreach ($attributes as $elementName => $rule) {
+ foreach ($rule as $attributeName => $definition) {
+ /* @see \HTMLPurifier_AttrTypes */
+ $def->addAttribute($elementName, $attributeName, $definition);
+ }
+ }
+
+ foreach ($elements as $elementName => $config) {
+ /* @see \HTMLPurifier_HTMLModule::addElement() */
+ $el = $def->addElement($elementName, $config[0], $config[1], $config[2], isset($config[3]) ? $config[3] : []);
+
+ if (isset($config[4])) {
+ $el->excludes = array_fill_keys($config[4], true);
+ }
+ }
+
+ foreach ($blankElements as $blankElement) {
+ /* @see \HTMLPurifier_HTMLModule::addBlankElement() */
+ $def->addBlankElement($blankElement);
+ }
+ }
+}
diff --git a/src/HTMLPurifiersRegistry.php b/src/HTMLPurifiersRegistry.php
index a96917ce..ec8f39c8 100644
--- a/src/HTMLPurifiersRegistry.php
+++ b/src/HTMLPurifiersRegistry.php
@@ -16,7 +16,7 @@ public function __construct(ContainerInterface $purifiersLocator)
/**
* {@inheritdoc}
*/
- public function has($profile)
+ public function has(string $profile): bool
{
return $this->purifiersLocator->has($profile);
}
@@ -24,7 +24,7 @@ public function has($profile)
/**
* {@inheritdoc}
*/
- public function get($profile)
+ public function get(string $profile): \HTMLPurifier
{
return $this->purifiersLocator->get($profile);
}
diff --git a/src/HTMLPurifiersRegistryInterface.php b/src/HTMLPurifiersRegistryInterface.php
index 06136d49..0f484a72 100644
--- a/src/HTMLPurifiersRegistryInterface.php
+++ b/src/HTMLPurifiersRegistryInterface.php
@@ -4,17 +4,7 @@
interface HTMLPurifiersRegistryInterface
{
- /**
- * @param string $profile
- *
- * @return bool
- */
- public function has($profile);
+ public function has(string $profile): bool;
- /**
- * @param string $profile
- *
- * @return \HTMLPurifier
- */
- public function get($profile);
+ public function get(string $profile): \HTMLPurifier;
}
diff --git a/src/Resources/config/html_purifier.xml b/src/Resources/config/html_purifier.xml
index f3e2bc63..d3bfac09 100644
--- a/src/Resources/config/html_purifier.xml
+++ b/src/Resources/config/html_purifier.xml
@@ -4,8 +4,10 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
- %exercise_html_purifier.cache_warmer.serializer.paths%
-
+
+
+
+
diff --git a/src/Twig/HTMLPurifierRuntime.php b/src/Twig/HTMLPurifierRuntime.php
index 025cf0e7..8c152528 100644
--- a/src/Twig/HTMLPurifierRuntime.php
+++ b/src/Twig/HTMLPurifierRuntime.php
@@ -22,7 +22,7 @@ public function __construct(HTMLPurifiersRegistryInterface $registry)
*
* @return string The purified html string
*/
- public function purify($string, $profile = 'default')
+ public function purify(string $string, string $profile = 'default'): string
{
return $this->getHTMLPurifierForProfile($profile)->purify($string);
}
@@ -30,13 +30,9 @@ public function purify($string, $profile = 'default')
/**
* Gets the HTMLPurifier service corresponding to the given profile.
*
- * @param string $profile
- *
- * @return \HTMLPurifier
- *
* @throws \InvalidArgumentException If the profile does not exist
*/
- private function getHTMLPurifierForProfile($profile)
+ private function getHTMLPurifierForProfile(string $profile): \HTMLPurifier
{
return $this->purifiersRegistry->get($profile);
}
diff --git a/tests/CacheWarmer/SerializerCacheWarmerTest.php b/tests/CacheWarmer/SerializerCacheWarmerTest.php
index fa20dd62..42856753 100644
--- a/tests/CacheWarmer/SerializerCacheWarmerTest.php
+++ b/tests/CacheWarmer/SerializerCacheWarmerTest.php
@@ -3,46 +3,60 @@
namespace Exercise\HTMLPurifierBundle\Tests\CacheWarmer;
use Exercise\HTMLPurifierBundle\CacheWarmer\SerializerCacheWarmer;
+use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistryInterface;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Filesystem\Filesystem;
class SerializerCacheWarmerTest extends TestCase
{
public function testShouldBeRequired()
{
- $cacheWarmer = new SerializerCacheWarmer([], new \HTMLPurifier());
+ $cacheWarmer = new SerializerCacheWarmer([], [], $this->createMock(HTMLPurifiersRegistryInterface::class), new Filesystem());
+
$this->assertFalse($cacheWarmer->isOptional());
}
- public function testFailsWhenNotWriteable()
+ public function testWarmUpShouldCreatePaths()
{
- $path = sys_get_temp_dir().'/'.uniqid('htmlpurifierbundle_fails');
+ $fs = new Filesystem();
+ $path = sys_get_temp_dir().DIRECTORY_SEPARATOR.'html_purifier';
- if (false === @mkdir($path, 0000)) {
- $this->markTestSkipped('Tmp dir is not writeable.');
+ if ($fs->exists($path)) {
+ $fs->remove($path);
}
- $this->expectException('RuntimeException');
+ $this->assertFalse($fs->exists($path));
- $cacheWarmer = new SerializerCacheWarmer([$path], new \HTMLPurifier());
+ $cacheWarmer = new SerializerCacheWarmer([$path], [], $this->createMock(HTMLPurifiersRegistryInterface::class), $fs);
$cacheWarmer->warmUp(null);
- @rmdir($path);
+ $this->assertTrue($fs->exists($path));
+
+ $fs->remove($path);
}
- public function testShouldCreatePaths()
+ public function testWarmUpShouldCallPurifyForEachProfile()
{
- if (!is_writable(sys_get_temp_dir())) {
- $this->markTestSkipped(sprintf('The system temp directory "%s" is not writeable for the current system user.', sys_get_temp_dir()));
- }
-
- $path = sys_get_temp_dir().'/'.uniqid('htmlpurifierbundle');
-
- $cacheWarmer = new SerializerCacheWarmer([$path], new \HTMLPurifier());
+ $purifier = $this->createMock(\HTMLPurifier::class);
+ $purifier->expects($this->exactly(2))
+ ->method('purify')
+ ;
+
+ $registry = $this->createMock(HTMLPurifiersRegistryInterface::class);
+ $registry->expects($this->exactly(2))
+ ->method('get')
+ ->willReturn($purifier)
+ ;
+ $registry->expects($this->at(0))
+ ->method('get')
+ ->with('first')
+ ;
+ $registry->expects($this->at(1))
+ ->method('get')
+ ->with('second')
+ ;
+
+ $cacheWarmer = new SerializerCacheWarmer([], ['first', 'second'], $registry, new Filesystem());
$cacheWarmer->warmUp(null);
-
- $this->assertTrue(is_dir($path));
- $this->assertTrue(is_writeable($path));
-
- rmdir($path);
}
}
diff --git a/tests/DependencyInjection/Compiler/HTMLPurifierPassTest.php b/tests/DependencyInjection/Compiler/HTMLPurifierPassTest.php
index b7c88e4a..7c484f2f 100644
--- a/tests/DependencyInjection/Compiler/HTMLPurifierPassTest.php
+++ b/tests/DependencyInjection/Compiler/HTMLPurifierPassTest.php
@@ -5,9 +5,9 @@
use Exercise\HTMLPurifierBundle\DependencyInjection\Compiler\HTMLPurifierPass;
use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistry;
use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistryInterface;
-use Exercise\HTMLPurifierBundle\Tests\ForwardCompatTestTrait;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
@@ -16,12 +16,10 @@
class HTMLPurifierPassTest extends TestCase
{
- use ForwardCompatTestTrait;
-
/** @var ContainerBuilder|MockObject */
private $container;
- private function doSetUp()
+ protected function setUp(): void
{
$this->container = $this->createPartialMock(ContainerBuilder::class, [
'hasAlias',
@@ -31,7 +29,7 @@ private function doSetUp()
]);
}
- private function doTearDown()
+ protected function tearDown(): void
{
$this->container = null;
}
@@ -95,6 +93,22 @@ public function testProcessDoNothingIfRegistryIsNotDefined()
$pass = new HTMLPurifierPass();
$pass->process($this->container);
}
+
+ public function testProcessFailsIfTaggedServiceMissesProfileName()
+ {
+ $container = new ContainerBuilder();
+ $container->register(DummyPurifier::class)
+ ->addTag('exercise.html_purifier')
+ ;
+ $container->register('exercise_html_purifier.purifiers_registry', HTMLPurifiersRegistry::class);
+ $container->setAlias(HTMLPurifiersRegistryInterface::class, 'exercise_html_purifier.purifiers_registry');
+
+ $this->expectException(InvalidConfigurationException::class);
+ $this->expectExceptionMessage('Tag "exercise.html_purifier" must define a "profile" attribute.');
+
+ $pass = new HTMLPurifierPass();
+ $pass->process($container);
+ }
}
class DummyPurifier extends \HTMLPurifier
diff --git a/tests/DependencyInjection/ExerciseHTMLPurifierExtensionTest.php b/tests/DependencyInjection/ExerciseHTMLPurifierExtensionTest.php
index 5e445548..274e5431 100644
--- a/tests/DependencyInjection/ExerciseHTMLPurifierExtensionTest.php
+++ b/tests/DependencyInjection/ExerciseHTMLPurifierExtensionTest.php
@@ -4,16 +4,17 @@
use Exercise\HTMLPurifierBundle\DependencyInjection\Compiler\HTMLPurifierPass;
use Exercise\HTMLPurifierBundle\DependencyInjection\ExerciseHTMLPurifierExtension;
-use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistry;
+use Exercise\HTMLPurifierBundle\HTMLPurifierConfigFactory;
use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistryInterface;
-use Exercise\HTMLPurifierBundle\Tests\ForwardCompatTestTrait;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
class ExerciseHTMLPurifierExtensionTest extends TestCase
{
- use ForwardCompatTestTrait;
+ private const DEFAULT_CACHE_PATH = '%kernel.cache_dir%/htmlpurifier';
/**
* @var ContainerBuilder
@@ -30,16 +31,17 @@ class ExerciseHTMLPurifierExtensionTest extends TestCase
*/
private $defaultConfig;
- private function doSetUp()
+ public function setUp(): void
{
$this->container = new ContainerBuilder();
+ $this->container->setParameter('kernel.cache_dir', '/tmp');
$this->extension = new ExerciseHTMLPurifierExtension();
$this->defaultConfig = [
- 'Cache.SerializerPath' => '%kernel.cache_dir%/htmlpurifier',
+ 'Cache.SerializerPath' => self::DEFAULT_CACHE_PATH,
];
}
- private function doTearDown()
+ public function tearDown(): void
{
$this->defaultConfig = null;
$this->extension = null;
@@ -51,77 +53,321 @@ public function testShouldLoadDefaultConfiguration()
$this->extension->load([], $this->container);
$this->assertDefaultConfigDefinition($this->defaultConfig);
- $this->assertCacheWarmerSerializerPaths(['%kernel.cache_dir%/htmlpurifier']);
+ $this->assertCacheWarmerSerializerArgs([self::DEFAULT_CACHE_PATH], ['default']);
$this->assertRegistryHasProfiles(['default']);
}
+ public function testInvalidParent()
+ {
+ $config = [
+ 'html_profiles' => [
+ 'custom' => [
+ 'config' => ['AutoFormat.AutoParagraph' => true],
+ ],
+ 'custom_2' => [
+ 'config' => ['AutoFormat.Linkify' => true],
+ 'parents' => ['custom', 'unknown'],
+ ],
+ ],
+ ];
+
+ $this->expectException(InvalidConfigurationException::class);
+ $this->expectExceptionMessage('Invalid parent "unknown" is not defined for profile "custom_2".');
+
+ $this->extension->load([$config], $this->container);
+ }
+
+ /**
+ * @dataProvider provideInvalidElementDefinitions
+ */
+ public function testInvalidElements(array $elementDefinition)
+ {
+ $config = [
+ 'html_profiles' => [
+ 'default' => [
+ 'elements' => ['a' => []],
+ ],
+ ],
+ ];
+
+ $this->expectException(InvalidConfigurationException::class);
+ $this->expectExceptionMessage('Invalid configuration for path "exercise_html_purifier.html_profiles.default.elements.a": An element definition must define three to five elements: a type ("Inline", "Block", ...), a content type ("Empty", "Optional: #PCDATA", ...), an attributes set ("Core", "Common", ...), and a fourth optional may define attributes rules as array, and fifth for forbidden attributes.');
+
+ $this->extension->load([$config], $this->container);
+ }
+
+ public function provideInvalidElementDefinitions(): iterable
+ {
+ yield 'empty array' => [[]];
+ yield 'only one argument' => [['']];
+ yield 'only two arguments' => [['', '']];
+ yield 'too many arguments' => [['', '', '', [], [], 'extra argument']];
+ }
+
public function testShouldAllowOverridingDefaultConfigurationCacheSerializerPath()
{
$config = [
- 'default' => [
- 'AutoFormat.AutoParagraph' => true,
- 'Cache.SerializerPath' => null,
+ 'default_cache_serializer_path' => null,
+ 'html_profiles' => [
+ 'default' => [
+ 'config' => [
+ 'AutoFormat.AutoParagraph' => true,
+ ],
+ ],
],
];
$this->extension->load([$config], $this->container);
- $this->assertDefaultConfigDefinition($config['default']);
- $this->assertCacheWarmerSerializerPaths([]);
+ $this->assertDefaultConfigDefinition(array_merge($config['html_profiles']['default']['config'], [
+ 'Cache.SerializerPath' => null,
+ ]));
+ $this->assertCacheWarmerSerializerArgs([], ['default']);
$this->assertRegistryHasProfiles(['default']);
}
public function testShouldNotDeepMergeOptions()
{
$configs = [
- ['default' => [
- 'Core.HiddenElements' => ['script' => true],
- 'Cache.SerializerPath' => null,
+ ['html_profiles' => [
+ 'default' => [
+ 'config' => [
+ 'Core.HiddenElements' => ['script' => true],
+ ],
+ ],
]],
- ['default' => [
- 'Core.HiddenElements' => ['style' => true],
+ ['html_profiles' => [
+ 'default' => [
+ 'config' => [
+ 'Core.HiddenElements' => ['style' => true],
+ ],
+ ],
]],
];
$this->extension->load($configs, $this->container);
- $this->assertDefaultConfigDefinition([
+ $this->assertDefaultConfigDefinition(array_merge([
'Core.HiddenElements' => ['style' => true],
- 'Cache.SerializerPath' => null,
- ]);
- $this->assertCacheWarmerSerializerPaths([]);
+ ], $this->defaultConfig));
+ $this->assertCacheWarmerSerializerArgs([self::DEFAULT_CACHE_PATH], ['default']);
$this->assertRegistryHasProfiles(['default']);
}
public function testShouldLoadCustomConfiguration()
{
$config = [
- 'default' => [
- 'AutoFormat.AutoParagraph' => true,
+ 'html_profiles' => [
+ 'default' => [
+ 'config' => [
+ 'AutoFormat.AutoParagraph' => true,
+ ],
+ ],
+ 'simple' => [
+ 'config' => [
+ 'Cache.DefinitionImpl' => null,
+ 'Cache.SerializerPath' => '%kernel.cache_dir%/htmlpurifier-simple',
+ 'AutoFormat.Linkify' => true,
+ 'AutoFormat.RemoveEmpty' => true,
+ 'AutoFormat.RemoveEmpty.RemoveNbsp' => true,
+ 'HTML.Allowed' => 'a[href],strong,em,p,li,ul,ol',
+ ],
+ ],
+ 'advanced' => [
+ 'config' => [
+ 'Cache.DefinitionImpl' => null,
+ ],
+ ],
+ ],
+ ];
+
+ $this->extension->load([$config], $this->container);
+
+ $profiles = ['default', 'simple', 'advanced'];
+
+ $this->assertDefaultConfigDefinition(array_merge($config['html_profiles']['default']['config'], $this->defaultConfig));
+ $this->assertConfigDefinition('simple', $config['html_profiles']['simple']['config']);
+ $this->assertConfigDefinition('advanced', $config['html_profiles']['advanced']['config']);
+ $this->assertCacheWarmerSerializerArgs([
+ self::DEFAULT_CACHE_PATH,
+ self::DEFAULT_CACHE_PATH.'-simple',
+ ], $profiles);
+ $this->assertRegistryHasProfiles($profiles);
+ }
+
+ public function testShouldLoadComplexCustomConfiguration()
+ {
+ $defaultConfig = [
+ 'AutoFormat.AutoParagraph' => true,
+ ];
+ $defaultAttributes = [
+ 'a' => ['href' => 'URI'],
+ 'span' => ['data-link' => 'URI'],
+ ];
+ $defaultBlankElements = [
+ 'figcaption',
+ 'legend',
+ ];
+ $simpleConfig = [
+ 'AutoFormat.Linkify' => true,
+ 'AutoFormat.RemoveEmpty' => true,
+ 'AutoFormat.RemoveEmpty.RemoveNbsp' => true,
+ 'HTML.Allowed' => 'a[href],strong,em,p,li,ul,ol',
+ ];
+ $videoElements = [
+ 'video' => [
+ 'Block',
+ 'Optional: (source, Flow) | (Flow, source) | Flow',
+ 'Common',
+ [
+ 'src' => 'URI',
+ 'type' => 'Text',
+ 'width' => 'Length',
+ 'height' => 'Length',
+ 'poster' => 'URI',
+ 'preload' => 'Enum#auto,metadata,none',
+ 'controls' => 'Bool',
+ ],
],
- 'simple' => [
- 'Cache.DefinitionImpl' => null,
- 'Cache.SerializerPath' => '%kernel.cache_dir%/htmlpurifier-simple',
- 'AutoFormat.Linkify' => true,
- 'AutoFormat.RemoveEmpty' => true,
- 'AutoFormat.RemoveEmpty.RemoveNbsp' => true,
- 'HTML.Allowed' => 'a[href],strong,em,p,li,ul,ol',
+ ];
+ $advancedConfig = [
+ 'Core.HiddenElements' => ['script' => true],
+ ];
+ $allParents = ['simple', 'video', 'advanced'];
+
+ $config = [
+ 'html_profiles' => [
+ 'default' => [
+ 'config' => $defaultConfig,
+ 'attributes' => $defaultAttributes,
+ 'blank_elements' => $defaultBlankElements,
+ ],
+ 'simple' => [
+ 'config' => $simpleConfig,
+ ],
+ 'video' => [
+ 'elements' => $videoElements,
+ ],
+ 'advanced' => [
+ 'config' => $advancedConfig,
+ ],
+ 'all' => [
+ 'parents' => $allParents,
+ ],
],
- 'advanced' => [
- 'Cache.DefinitionImpl' => null,
+ ];
+
+ $this->extension->load([$config], $this->container);
+
+ $profiles = ['default', 'simple', 'video', 'advanced', 'all'];
+
+ $this->assertDefaultConfigDefinition(
+ array_merge($defaultConfig, $this->defaultConfig),
+ $defaultAttributes,
+ [],
+ $defaultBlankElements
+ );
+ $this->assertConfigDefinition('simple', $simpleConfig);
+ $this->assertConfigDefinition(
+ 'video',
+ [/* config */],
+ [/* parents */],
+ [/* attributes */],
+ $videoElements
+ );
+ $this->assertConfigDefinition('advanced', $advancedConfig);
+ $this->assertConfigDefinition(
+ 'all',
+ [/* config */],
+ [$simpleConfig, /* video config is filtered */ 2 => $advancedConfig],
+ [/* attributes */],
+ [/* simple elements are filtered */ 1 => $videoElements],
+ [/* blank elements */]
+ );
+ $this->assertCacheWarmerSerializerArgs([self::DEFAULT_CACHE_PATH], $profiles);
+ $this->assertRegistryHasProfiles($profiles);
+ }
+
+ public function testShouldRegisterAliases()
+ {
+ if (!method_exists($this->container, 'registerAliasForArgument')) {
+ $this->markTestSkipped('Alias arguments binding is not available.');
+ }
+
+ $config = [
+ 'html_profiles' => [
+ 'default' => [
+ 'config' => [
+ 'AutoFormat.AutoParagraph' => true,
+ ],
+ ],
+ 'simple' => [
+ 'config' => [
+ 'HTML.Allowed' => 'a[href],strong,em,p,li,ul,ol',
+ ],
+ ],
+ 'advanced' => [
+ 'config' => [
+ 'Core.HiddenElements' => ['script' => true],
+ ],
+ ],
],
];
$this->extension->load([$config], $this->container);
- $this->assertDefaultConfigDefinition(array_replace($this->defaultConfig, $config['default']));
- $this->assertConfigDefinition('simple', $config['simple']);
- $this->assertConfigDefinition('advanced', $config['advanced']);
- $this->assertCacheWarmerSerializerPaths([
- '%kernel.cache_dir%/htmlpurifier',
- '%kernel.cache_dir%/htmlpurifier-simple',
- ]);
- $this->assertRegistryHasProfiles(['default', 'simple', 'advanced']);
+ $this->container->register(ServiceWithDefaultConfig::class)
+ ->setAutowired(true)
+ ->setPublic(true)
+ ;
+ $this->container->register(ServiceWithDefaultConfig2::class)
+ ->setAutowired(true)
+ ->setPublic(true)
+ ;
+ $this->container->register(ServiceWithSimpleConfig::class)
+ ->setAutowired(true)
+ ->setPublic(true)
+ ;
+ $this->container->register(ServiceWithAdvancedConfig::class)
+ ->setAutowired(true)
+ ->setPublic(true)
+ ;
+
+ $this->container->compile();
+
+ $defaultConfigArgument1 = $this->container->findDefinition(ServiceWithDefaultConfig::class)
+ ->getArgument(0)
+ ;
+
+ $this->assertInstanceOf(Reference::class, $defaultConfigArgument1);
+ $this->assertSame('exercise_html_purifier.default', (string) $defaultConfigArgument1);
+
+ $defaultConfigArgument2 = $this->container->findDefinition(ServiceWithDefaultConfig2::class)
+ ->getArgument(0)
+ ;
+
+ $this->assertInstanceOf(Reference::class, $defaultConfigArgument2);
+ $this->assertSame('exercise_html_purifier.default', (string) $defaultConfigArgument2);
+
+ $simpleConfigArgument = $this->container->findDefinition(ServiceWithSimpleConfig::class)
+ ->getArgument(0)
+ ;
+
+ $this->assertInstanceOf(Definition::class, $simpleConfigArgument);
+ $this->assertSame(
+ 'simple',
+ $simpleConfigArgument->getTag(HTMLPurifierPass::PURIFIER_TAG)[0]['profile'] ?? ''
+ );
+
+ $advancedConfigArgument = $this->container->findDefinition(ServiceWithAdvancedConfig::class)
+ ->getArgument(0)
+ ;
+
+ $this->assertInstanceOf(Definition::class, $advancedConfigArgument);
+ $this->assertSame(
+ 'advanced',
+ $advancedConfigArgument->getTag(HTMLPurifierPass::PURIFIER_TAG)[0]['profile'] ?? ''
+ );
}
/**
@@ -130,30 +376,39 @@ public function testShouldLoadCustomConfiguration()
*
* @param string $name
*/
- private function assertConfigDefinition($name, array $config)
+ private function assertConfigDefinition($name, array $config, array $parents = [], array $attributes = [], array $elements = [], array $blankElements = [])
{
$this->assertTrue($this->container->hasDefinition('exercise_html_purifier.config.'.$name));
$definition = $this->container->getDefinition('exercise_html_purifier.config.'.$name);
- $this->assertSame([\HTMLPurifier_Config::class, 'inherit'], $definition->getFactory());
+ $this->assertEquals([new Reference(HTMLPurifierConfigFactory::class), 'create'], $definition->getFactory());
$args = $definition->getArguments();
-
- $this->assertCount(1, $args);
- $this->assertEquals([$config], $definition->getMethodCalls()[0][1]);
+ $defaultConfig = $definition->getArgument(2);
+
+ $this->assertCount(7, $args);
+ $this->assertSame($name, $definition->getArgument(0));
+ $this->assertSame($config, $definition->getArgument(1));
+ $this->assertInstanceOf(Reference::class, $defaultConfig);
+ $this->assertSame('exercise_html_purifier.config.default', (string) $defaultConfig);
+ $this->assertSame($parents, $definition->getArgument(3));
+ $this->assertSame($attributes, $definition->getArgument(4));
+ $this->assertSame($elements, $definition->getArgument(5));
+ $this->assertSame($blankElements, $definition->getArgument(6));
}
/**
* Asserts that the default config definition loads the given options.
*/
- private function assertDefaultConfigDefinition(array $config)
+ private function assertDefaultConfigDefinition(array $config, array $attributes = [], array $elements = [], array $blankElements = []): void
{
$this->assertTrue($this->container->hasDefinition('exercise_html_purifier.config.default'));
$definition = $this->container->getDefinition('exercise_html_purifier.config.default');
- $this->assertEquals([\HTMLPurifier_Config::class, 'create'], $definition->getFactory());
- $this->assertEquals([$config], $definition->getArguments());
+
+ $this->assertEquals([new Reference(HTMLPurifierConfigFactory::class), 'create'], $definition->getFactory());
+ $this->assertSame(['default', $config, null, [], $attributes, $elements, $blankElements], $definition->getArguments(), 'Default config is invalid.');
}
/**
@@ -161,35 +416,52 @@ private function assertDefaultConfigDefinition(array $config)
*
* @param string[] $profiles
*/
- private function assertRegistryHasProfiles(array $profiles)
+ private function assertRegistryHasProfiles(array $profiles): void
{
- $this->assertTrue($this->container->hasAlias(HTMLPurifiersRegistryInterface::class), 'The registry interface alias must exist.');
-
- try {
- $registry = $this->container->findDefinition(HTMLPurifiersRegistryInterface::class);
- } catch (ServiceNotFoundException $e) {
- $this->fail(sprintf('Alias %s does not target a valid id: %s.', HTMLPurifiersRegistryInterface::class, $e->getMessage()));
+ foreach ($profiles as $profile) {
+ $this->assertTrue($this->container->hasDefinition("exercise_html_purifier.$profile"));
+ $this->assertTrue($this->container->hasDefinition("exercise_html_purifier.config.$profile"));
}
+ }
- $this->assertSame(HTMLPurifiersRegistry::class, $registry->getClass());
+ /**
+ * Assert that the cache warmer serializer paths equal the given array.
+ */
+ private function assertCacheWarmerSerializerArgs(array $paths, array $profiles): void
+ {
+ $serializer = $this->container->getDefinition('exercise_html_purifier.cache_warmer.serializer');
- foreach ($profiles as $profile) {
- $purifierId = "exercise_html_purifier.$profile";
+ $this->assertSame($serializer->getArgument(0), $paths);
+ $this->assertSame($serializer->getArgument(1), $profiles);
+ $this->assertSame((string) $serializer->getArgument(2), HTMLPurifiersRegistryInterface::class);
+ $this->assertSame((string) $serializer->getArgument(3), 'filesystem');
+ }
+}
- $this->assertTrue($this->container->has($purifierId), "The service $purifierId should be registered.");
+class ServiceWithDefaultConfig
+{
+ public function __construct(\HTMLPurifier $purifier)
+ {
+ }
+}
- $tag = ['profile' => $profile];
- $purifier = $this->container->findDefinition($purifierId);
+class ServiceWithDefaultConfig2
+{
+ public function __construct(\HTMLPurifier $htmlPurifier)
+ {
+ }
+}
- $this->assertSame([HTMLPurifierPass::PURIFIER_TAG => [$tag]], $purifier->getTags());
- }
+class ServiceWithSimpleConfig
+{
+ public function __construct(\HTMLPurifier $simplePurifier)
+ {
}
+}
- /**
- * Assert that the cache warmer serializer paths equal the given array.
- */
- private function assertCacheWarmerSerializerPaths(array $paths)
+class ServiceWithAdvancedConfig
+{
+ public function __construct(\HTMLPurifier $advancedPurifier)
{
- $this->assertEquals($paths, $this->container->getParameter('exercise_html_purifier.cache_warmer.serializer.paths'));
}
}
diff --git a/tests/Form/Listener/HTMLPurifierListenerTest.php b/tests/Form/Listener/HTMLPurifierListenerTest.php
index a712df69..14447a60 100644
--- a/tests/Form/Listener/HTMLPurifierListenerTest.php
+++ b/tests/Form/Listener/HTMLPurifierListenerTest.php
@@ -94,14 +94,14 @@ public function testPurifyDoNothingForEmptyOrNonScalarData($input)
$listener->purifySubmittedData($event);
}
- public function provideInvalidInput()
+ public function provideInvalidInput(): iterable
{
yield [''];
yield [[]];
yield [new \stdClass()];
}
- private function getFormEvent($data)
+ private function getFormEvent($data): FormEvent
{
return new FormEvent($this->createMock(FormInterface::class), $data);
}
diff --git a/tests/Form/TypeExtension/HTMLPurifierTextTypeExtensionTest.php b/tests/Form/TypeExtension/HTMLPurifierTextTypeExtensionTest.php
index d8a71df2..1bddef62 100644
--- a/tests/Form/TypeExtension/HTMLPurifierTextTypeExtensionTest.php
+++ b/tests/Form/TypeExtension/HTMLPurifierTextTypeExtensionTest.php
@@ -5,26 +5,24 @@
use Exercise\HTMLPurifierBundle\Form\Listener\HTMLPurifierListener;
use Exercise\HTMLPurifierBundle\Form\TypeExtension\HTMLPurifierTextTypeExtension;
use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistryInterface;
-use Exercise\HTMLPurifierBundle\Tests\ForwardCompatTestTrait;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Test\FormIntegrationTestCase;
+use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
class HTMLPurifierTextTypeExtensionTest extends FormIntegrationTestCase
{
- use ForwardCompatTestTrait;
-
private $registry;
- private function doSetUp()
+ protected function setUp(): void
{
$this->registry = $this->createMock(HTMLPurifiersRegistryInterface::class);
parent::setUp();
}
- private function doTearDown()
+ protected function tearDown(): void
{
parent::tearDown();
@@ -71,7 +69,7 @@ public function testPurifyOptionsNeedDefaultProfile()
->method('get')
;
- $this->expectException('Symfony\Component\OptionsResolver\Exception\InvalidOptionsException');
+ $this->expectException(InvalidOptionsException::class);
$this->expectExceptionMessage('The profile "default" is not registered.');
$this->factory->create(TextType::class, null, ['purify_html' => true]);
@@ -108,7 +106,7 @@ public function testInvalidProfile()
->method('get')
;
- $this->expectException('Symfony\Component\OptionsResolver\Exception\InvalidOptionsException');
+ $this->expectException(InvalidOptionsException::class);
$this->expectExceptionMessage('The profile "test" is not registered.');
$this->factory->create(TextType::class, null, [
@@ -117,10 +115,7 @@ public function testInvalidProfile()
]);
}
- /**
- * @return bool
- */
- private function hasPurifierListener(FormInterface $form)
+ private function hasPurifierListener(FormInterface $form): bool
{
foreach ($form->getConfig()->getEventDispatcher()->getListeners(FormEvents::PRE_SUBMIT) as $listener) {
if ($listener[0] instanceof HTMLPurifierListener) {
diff --git a/tests/ForwardCompatTestTrait.php b/tests/ForwardCompatTestTrait.php
deleted file mode 100644
index 3870ed3f..00000000
--- a/tests/ForwardCompatTestTrait.php
+++ /dev/null
@@ -1,72 +0,0 @@
-hasReturnType()) {
- eval('
- namespace Exercise\HTMLPurifierBundle\Tests;
-
- /**
- * @internal
- */
- trait ForwardCompatTestTrait
- {
- private function doSetUp(): void
- {
- }
-
- private function doTearDown(): void
- {
- }
-
- protected function setUp(): void
- {
- $this->doSetUp();
- }
- protected function tearDown(): void
- {
- $this->doTearDown();
- }
- }
-');
-} else {
- /**
- * @internal
- */
- trait ForwardCompatTestTrait
- {
- /**
- * @return void
- */
- private function doSetUp()
- {
- }
-
- /**
- * @return void
- */
- private function doTearDown()
- {
- }
-
- /**
- * @return void
- */
- protected function setUp()
- {
- $this->doSetUp();
- }
-
- /**
- * @return void
- */
- protected function tearDown()
- {
- $this->doTearDown();
- }
- }
-}
diff --git a/tests/HTMLPurifiersRegistryTest.php b/tests/HTMLPurifiersRegistryTest.php
index 378e73d8..d58d6df0 100644
--- a/tests/HTMLPurifiersRegistryTest.php
+++ b/tests/HTMLPurifiersRegistryTest.php
@@ -8,24 +8,22 @@
class HTMLPurifiersRegistryTest extends TestCase
{
- use ForwardCompatTestTrait;
-
private $locator;
private $registry;
- private function doSetUp()
+ protected function setUp(): void
{
$this->locator = $this->createMock(ContainerInterface::class);
$this->registry = new HTMLPurifiersRegistry($this->locator);
}
- private function doTearDown()
+ protected function tearDown(): void
{
$this->registry = null;
$this->locator = null;
}
- public function provideProfiles()
+ public function provideProfiles(): iterable
{
yield ['default'];
yield ['test'];
diff --git a/tests/Twig/HTMLPurifierRuntimeTest.php b/tests/Twig/HTMLPurifierRuntimeTest.php
index 7910e491..e6b03349 100644
--- a/tests/Twig/HTMLPurifierRuntimeTest.php
+++ b/tests/Twig/HTMLPurifierRuntimeTest.php
@@ -38,7 +38,7 @@ public function testPurifyFilter($profile)
$this->assertEquals($purifiedInput, $extension->purify($input, $profile));
}
- public function providePurifierProfiles()
+ public function providePurifierProfiles(): iterable
{
yield ['default'];
yield ['custom'];