diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..a1e85ac6
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,6 @@
+.gitattributes export-ignore
+.gitignore export-ignore
+.github export-ignore
+tests/ export-ignore
+.php-cs-fixer.dist.php export-ignore
+phpunit.xml.dist export-ignore
diff --git a/.github/workflows/qa.yaml b/.github/workflows/qa.yaml
new file mode 100644
index 00000000..059a2544
--- /dev/null
+++ b/.github/workflows/qa.yaml
@@ -0,0 +1,93 @@
+name: Quality and Assurance
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ qa:
+ name: Quality
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: true
+ matrix:
+ php-versions:
+ - 8.2
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP, extensions and composer with shivammathur/setup-php
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ extensions: mbstring, xml, ctype, intl, dom, filter
+ env:
+ update: true
+
+ - name: Install Composer dependencies
+ run: composer install --working-dir=qa/php-cs-fixer
+
+ - name: PHP-CS-Fixer
+ run: ./qa/php-cs-fixer/vendor/bin/php-cs-fixer fix --dry-run --using-cache=no --verbose --diff
+
+ test:
+ name: Tests
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: true
+ matrix:
+ php-versions:
+ - 8.1
+ - 8.2
+ - 8.3
+ - 8.4
+ symfony-versions:
+ - 5.4.*
+ - 6.4.*
+ - 7.3.*
+ dependencies:
+ - ''
+ include:
+ - php-versions: '8.1'
+ symfony-versions: '5.4.*'
+ dependencies: 'lowest'
+ - php-versions: '8.4'
+ symfony-versions: '7.4.*'
+ - php-versions: '8.4'
+ symfony-versions: '8.0.*'
+ exclude:
+ - php-versions: '8.1'
+ symfony-versions: '7.3.*'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP, extensions and composer with shivammathur/setup-php
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ extensions: mbstring, xml, ctype, iconv, intl, pdo, pdo_mysql, dom, filter, gd, json
+ tools: flex
+ env:
+ update: true
+
+ - name: Install Composer
+ if: "matrix.dependencies == ''"
+ run: composer update --no-progress --ansi --prefer-stable
+ env:
+ SYMFONY_REQUIRE: ${{ matrix.symfony-versions }}
+
+ - name: Install Lowest Composer dependencies
+ if: "matrix.dependencies == 'lowest'"
+ run: composer update --no-progress --ansi --prefer-stable --prefer-lowest
+ env:
+ SYMFONY_REQUIRE: ${{ matrix.symfony-versions }}
+
+ - name: Validate composer
+ run: composer validate --strict --no-check-lock
+
+ - name: Execute unit tests
+ run: ./vendor/bin/simple-phpunit
diff --git a/.gitignore b/.gitignore
index ce3aa652..55240f9e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,5 @@
+.php-cs-fixer.cache
+.phpunit.result.cache
phpunit.xml
+composer.lock
+vendor/
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 00000000..7de6fa49
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,13 @@
+exclude('Resources')
+ ->in(__DIR__)
+;
+
+return (new PhpCsFixer\Config())
+ ->setRules([
+ '@Symfony' => true,
+ ])
+ ->setFinder($finder)
+;
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..cde3b1ba
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,51 @@
+## Version 4.1 (08/2022)
+
+ * Add support of the `Cache.SerializerPermissions` for default profile
+
+## Version 4.0 (04/2022)
+
+ * [BC Break] Drop support for Symfony < 4.4
+ * add support for Symfony 6.x
+ * add support for PHPUnit 10.x
+
+## Version 3.0 (12/2019)
+
+ * [BC break] Dropped support for PHP 5.x. PHP 7.1 minimum required.
+ * [BC break] Added type hints for scalar and return type hints where possible.
+ * [BC Break] The bundle configuration has changed:
+ ```yaml
+ # Before
+ exercise_html_purifier:
+ default:
+ Cache.SerializerPath: '%kernel.cache_dir%/htmlpurifier'
+ # ...
+ custom:
+ Core.Encoding: 'ISO-8859-1'
+
+ # After
+ exercise_html_purifier:
+ default_cache_serializer_path: '%kernel.cache_dir%/htmlpurifier'
+ html_profiles:
+ default:
+ # ...
+ custom:
+ config:
+ Core.Encoding: 'ISO-8859-1'
+ ```
+ * Added an `HTMLPurifierConfigFactory` to handle cache and custom definitions.
+ * Refactored `SerializerCacheWarmer` to preload each profile configuration
+
+## Version 2.0 (08/2018)
+
+ * Added compatibility for Symfony 5 and Twig 3
+ * Updated minimum requirement of Twig to 1.35 and 2.4 to support runtime
+ * [BC break] Dropped support for Symfony 2. Symfony 3.4 minimum required.
+ * [BC break] Removed classes parameters.
+ * [BC break] Removed the form data transformer.
+ * Added an `HTMLPurifierTextTypeExtension` to add `purify_html` and
+ `purify_html_profile` options to all `TextType` children.
+ * Added an `HTMLPurifierListener` to purify submitted form data.
+ * Added an `HTMLPurifiersRegistryInterface` to lazy load purifiers by profile.
+ * Added a Twig `HTMLPurifierRuntime` to lazy load purifiers in templates.
+ * Added a pass to use custom `\HTMLPurifier` classes as custom profiles using
+ a new `exercise.html_purifier` tag.
diff --git a/CacheWarmer/SerializerCacheWarmer.php b/CacheWarmer/SerializerCacheWarmer.php
deleted file mode 100644
index 365eb6a4..00000000
--- a/CacheWarmer/SerializerCacheWarmer.php
+++ /dev/null
@@ -1,49 +0,0 @@
-
- */
-class SerializerCacheWarmer implements CacheWarmerInterface
-{
- private $paths;
-
- /**
- * Constructor.
- *
- * @param array $paths
- */
- public function __construct(array $paths)
- {
- $this->paths = $paths;
- }
-
- /**
- * @see Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface::warmUp()
- */
- public function warmUp($cacheDir)
- {
- foreach ($this->paths as $path) {
- if (!is_dir($path)) {
- if (false === @mkdir($path, 0777, true)) {
- throw new \RuntimeException(sprintf('Unable to create the HTMLPurifier Serializer cache directory "%s".', $path));
- }
- } elseif (!is_writable($path)) {
- throw new \RuntimeException(sprintf('The HTMLPurifier Serializer cache directory "%s" is not writeable for the current system user.', $path));
- }
- }
- }
-
- /**
- * @see Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface::isOptional()
- */
- public function isOptional()
- {
- return false;
- }
-}
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
deleted file mode 100644
index dc068210..00000000
--- a/DependencyInjection/Configuration.php
+++ /dev/null
@@ -1,26 +0,0 @@
-root('exercise_html_purifier');
-
- $rootNode
- ->useAttributeAsKey('name')
- ->prototype('array')
- ->useAttributeAsKey('name')
- ->prototype('variable')
- ->end()
- ->end()
- ;
-
- return $treeBuilder;
- }
-}
diff --git a/DependencyInjection/ExerciseHTMLPurifierExtension.php b/DependencyInjection/ExerciseHTMLPurifierExtension.php
deleted file mode 100644
index ab208509..00000000
--- a/DependencyInjection/ExerciseHTMLPurifierExtension.php
+++ /dev/null
@@ -1,116 +0,0 @@
-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, array(
- 'default' => array(
- 'Cache.SerializerPath' => '%kernel.cache_dir%/htmlpurifier',
- ),
- ));
-
- $configs = $this->processConfiguration(new Configuration(), $configs);
- $configs = array_map(array($this, 'resolveServices'), $configs);
- $paths = array();
-
- foreach ($configs as $name => $config) {
- $configDefinition = new Definition('%exercise_html_purifier.config.class%');
-
- // Handle Symfony >= 2.7
- if (method_exists($configDefinition, 'setFactory')) {
- if ('default' === $name) {
- $configDefinition
- ->setFactory(array('%exercise_html_purifier.config.class%', 'create'))
- ->addArgument($config);
- } else {
- $configDefinition
- ->setFactory(array('%exercise_html_purifier.config.class%', 'inherit'))
- ->addArgument(new Reference('exercise_html_purifier.config.default'))
- ->addMethodCall('loadArray', array($config));
- }
- }
- // Handle Symfony < 2.7
- else {
- $configDefinition->setFactoryClass('%exercise_html_purifier.config.class%');
-
- if ('default' === $name) {
- $configDefinition
- ->setFactoryMethod('create')
- ->addArgument($config);
- } else {
- $configDefinition
- ->setFactoryMethod('inherit')
- ->addArgument(new Reference('exercise_html_purifier.config.default'))
- ->addMethodCall('loadArray', array($config));
- }
- }
-
- $configId = 'exercise_html_purifier.config.' . $name;
- $container->setDefinition($configId, $configDefinition);
-
- $container->setDefinition(
- 'exercise_html_purifier.' . $name,
- new Definition('%exercise_html_purifier.class%', array(new Reference($configId)))
- );
-
- if (isset($config['Cache.SerializerPath'])) {
- $paths[] = $config['Cache.SerializerPath'];
- }
- }
-
- $container->setParameter('exercise_html_purifier.cache_warmer.serializer.paths', array_unique($paths));
- }
-
- public function getAlias()
- {
- return 'exercise_html_purifier';
- }
-
- private function resolveServices($value)
- {
- if (is_array($value)) {
- $value = array_map(array($this, 'resolveServices'), $value);
- } else if (is_string($value) && 0 === strpos($value, '@')) {
- if (0 === strpos($value, '@?')) {
- $value = substr($value, 2);
- $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
- } else {
- $value = substr($value, 1);
- $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
- }
-
- if ('=' === substr($value, -1)) {
- $value = substr($value, 0, -1);
- $strict = false;
- } else {
- $strict = true;
- }
-
- $value = new Reference($value, $invalidBehavior, $strict);
- }
-
- return $value;
- }
-}
diff --git a/ExerciseHTMLPurifierBundle.php b/ExerciseHTMLPurifierBundle.php
deleted file mode 100644
index 1cb01644..00000000
--- a/ExerciseHTMLPurifierBundle.php
+++ /dev/null
@@ -1,9 +0,0 @@
-purifier = $purifier;
- }
-
- /**
- * @see Symfony\Component\Form\DataTransformerInterface::transform()
- */
- public function transform($value)
- {
- return $value;
- }
-
- /**
- * @see Symfony\Component\Form\DataTransformerInterface::reverseTransform()
- */
- public function reverseTransform($value)
- {
- return $this->purifier->purify($value);
- }
-}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..0fe28c0e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+https://github.com/Exercise/HTMLPurifierBundle/graphs/contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
index e9a55e24..8a24ba2a 100644
--- a/README.md
+++ b/README.md
@@ -1,216 +1,328 @@
+[](https://packagist.org/packages/exercise/htmlpurifier-bundle)
+[](https://packagist.org/packages/exercise/htmlpurifier-bundle)
+[](https://packagist.org/packages/exercise/htmlpurifier-bundle)
+[](https://travis-ci.org/Exercise/HTMLPurifierBundle)
+
# ExerciseHTMLPurifierBundle
-This bundle integrates [HTMLPurifier][] into Symfony2.
+This bundle integrates [HTMLPurifier][] into Symfony.
[HTMLPurifier]: http://htmlpurifier.org/
## Installation
-## Symfony 2.1 and above (using Composer)
-
-Require the bundle in your composer.json file:
+Install the bundle:
-```
-{
- "require": {
- "exercise/htmlpurifier-bundle": "*",
- }
-}
+```bash
+$ composer require exercise/htmlpurifier-bundle
```
-Install the bundle:
+## Configuration
-```
-$ composer update exercise/htmlpurifier-bundle
+If you do not explicitly configure this bundle, an HTMLPurifier service will be
+defined as `exercise_html_purifier.default`. This behavior is the same as if you
+had specified the following configuration:
+
+```yaml
+# config/packages/exercise_html_purifier.yaml
+
+exercise_html_purifier:
+ default_cache_serializer_path: '%kernel.cache_dir%/htmlpurifier'
+ # 493 int => ocl "0755"
+ default_cache_serializer_permissions: 493
```
-Register the bundle:
+The `default` profile is special, it is *always* defined and its configuration
+is inherited by all custom profiles.
+`exercise_html_purifier.default` is the default service using the base
+configuration.
-``` php
-// app/AppKernel.php
+```yaml
+# config/packages/exercise_html_purifier.yaml
-public function registerBundles()
-{
- return array(
- new Exercise\HTMLPurifierBundle\ExerciseHTMLPurifierBundle(),
- // ...
- );
-}
+exercise_html_purifier:
+ default_cache_serializer_path: '%kernel.cache_dir%/htmlpurifier'
+ html_profiles:
+ custom:
+ config:
+ Core.Encoding: 'ISO-8859-1'
+ HTML.Allowed: 'a[href|target],p,br'
+ Attr.AllowedFrameTargets: '_blank'
```
-## Symfony 2.0.*
+In this example, a `exercise_html_purifier.custom` service will also be defined,
+which includes cache, encoding, HTML tags and attributes options. Available configuration
+options may be found in HTMLPurifier's [configuration documentation][].
+
+**Note:** If you define a `default` profile but omit `Cache.SerializerPath`, it
+will still default to the path above. You can specify a value of `null` for the
+option to suppress the default path.
-### Submodule Creation
+ [configuration documentation]: http://htmlpurifier.org/live/configdoc/plain.html
-Add HTMLPurifier and this bundle to your `vendor/` directory:
+## Autowiring
+By default type hinting `\HtmlPurifier` in your services will autowire
+the `exercise_html_purifier.default` service.
+To override it and use your own config as default autowired services just add
+this configuration:
+
+```yaml
+# config/services.yaml
+services:
+ #...
+
+ exercise_html_purifier.default: '@exercise_html_purifier.custom'
```
-$ git submodule add git://github.com/Exercise/HTMLPurifierBundle.git vendor/bundles/Exercise/HTMLPurifierBundle
-$ git submodule add git://github.com/ezyang/htmlpurifier.git vendor/htmlpurifier
+
+### Using a custom purifier class as default
+
+If you want to use your own class as default purifier, define the new alias as
+below:
+
+```yaml
+# config/services.yaml
+services:
+ # ...
+
+ exercise_html_purifier.default: '@App\Html\CustomHtmlPurifier'
```
-### Class Autoloading
+### Argument binding
-Register "HTMLPurifier" and the "Exercise" namespace prefix in your project's
-`autoload.php`:
+The bundle also leverages the alias argument binding for each profile. So the
+following config:
+```yaml
+ html_profiles:
+ blog:
+ # ...
+ gallery:
+ # ...
```
-# app/autoload.php
-$loader->registerNamespaces(array(
- 'Exercise' => __DIR__ . '/../vendor/bundles',
-));
+will register the following binding:
-$loader->registerPrefixes(array(
- 'HTMLPurifier' => __DIR__ . '/../vendor//htmlpurifier/library',
-));
+```php
+ // default config is bound whichever argument name is used
+public function __construct(\HTMLPurifier $purifier) {}
+public function __construct(\HTMLPurifier $htmlPurifier) {}
+public function __construct(\HTMLPurifier $blogPurifier) {} // blog config
+public function __construct(\HTMLPurifier $galleryPurifier) {} // gallery config
```
-### Application Kernel
+## Form Type Extension
-Add HTMLPurifierBundle to the `registerBundles()` method of your application
-kernel:
+This bundles provides a form type extension for filtering form fields with
+HTMLPurifier. Purification is done early during the PRE_SUBMIT event, which
+means that client data will be filtered before being bound to the form.
-```
-# app/AppKernel.php
+Two options are automatically available in all `TextType` based types:
-public function registerBundles()
+```php
+add('content', TextareaType::class, ['purify_html' => true]) // will use default profile
+ ->add('sneek_peak', TextType::class, ['purify_html' => true, 'purify_html_profile' => 'sneak_peak'])
+ // ...
+ ;
+ }
+
+ // ...
}
```
-## Configuration
-
-If you do not explicitly configure this bundle, an HTMLPurifier service will be
-defined as `exercise_html_purifier.default`. This behavior is the same as if you
-had specified the following configuration:
+Every type extending `TextType` (i.e: `TextareaType`) inherit these options.
+It also means that if you use a type such as [CKEditorType][], you will benefit
+from these options without configuring anything.
-```
-# app/config.yml
+ [CKEDitorType]: https://github.com/egeloen/IvoryCKEditorBundle/blob/master/Form/Type/CKEditorType.php#L570
-exercise_html_purifier:
- default:
- Cache.SerializerPath: '%kernel.cache_dir%/htmlpurifier'
-```
+## Twig Filter
-The `default` profile is special in that it is used as the configuration for the
-`exercise_html_purifier.default` service as well as the base configuration for
-other profiles you might define.
+This bundles registers a `purify` filter with Twig. Output from this filter is
+marked safe for HTML, much like Twig's built-in escapers. The filter may be used
+as follows:
-```
-# app/config.yml
+```twig
+{# Filters text's value through the "default" HTMLPurifier service #}
+{{ text|purify }}
-exercise_html_purifier:
- default:
- Cache.SerializerPath: '%kernel.cache_dir%/htmlpurifier'
- custom:
- Core.Encoding: 'ISO-8859-1'
+{# Filters text's value through the "custom" HTMLPurifier service #}
+{{ text|purify('custom') }}
```
-In this example, a `exercise_html_purifier.custom` service will also be defined,
-which includes both the cache and encoding options. Available configuration
-options may be found in HTMLPurifier's [configuration documentation][].
+## Purifiers Registry
-**Note:** If you define a `default` profile but omit `Cache.SerializerPath`, it
-will still default to the path above. You can specify a value of `null` for the
-option to suppress the default path.
+A `Exercise\HtmlPurifierBundle\HtmlPurifiersRegistry` class is registered by default
+as a service. To add your custom instance of purifier, and make it available to
+the form type and Twig extensions through its profile name, you can use the tag
+`exercise.html_purifier` as follow:
- [configuration documentation]: http://htmlpurifier.org/live/configdoc/plain.html
+```yaml
+# config/services.yaml
-## Cache Warming ##
+services:
+ # ...
+
+ App\HtmlPurifier\CustomPurifier:
+ tags:
+ - name: exercise.html_purifier
+ profile: custom
+```
-When a path is supplied for HTMLPurifier's `Cache.SerializerPath` configuration
-option, an error is raised if the directory is not writable. This bundle defines
-a cache warmer service that will collect all `Cache.SerializerPath` options and
-ensure those directories exist and are writeable.
+Now your purifier can be used when:
-## Form Data Transformer
+```php
+// In a form type
+$builder
+ ->add('content', TextareaType::class, [
+ 'purify_html' => true,
+ 'purify_html_profile' => 'custom',
+ ])
+ // ...
+```
-This bundles provides a data transformer class for filtering form fields with
-HTMLPurifier. Purification is done during the `reverseTransform()` method, which
-means that client data will be filtered during binding to the form.
+```twig
+{# in a template #}
+{{ html_string|purify('custom') }}
+```
-The following example demonstrates one possible way to integrate an HTMLPurifier
-transformer into a form by way of a custom field type:
+## How to Customize a Config Definition
-``` php
-purifierTransformer = $purifierTransformer;
- }
+ [HTMLPurifier_AttrTypes]: https://github.com/ezyang/htmlpurifier/blob/master/library/HTMLPurifier/AttrTypes.php
- public function buildForm(FormBuilderInterface $builder, array $options)
- {
- $builder->addViewTransformer($this->purifierTransformer);
- }
+### Whitelist Elements
- public function getParent()
- {
- return 'textarea';
- }
+In some case, you might want to set some rules for a specific tag.
+This is what the following config is about:
- public function setDefaultOptions(OptionsResolverInterface $resolver)
- {
- $resolver->setDefaults(array(
- 'compound' => false,
- ));
- }
+```yaml
+# config/packages/exercise_html_purifier.yaml
+exercise_html_purifier:
+ html_profiles:
+ default:
+ # ...
+ elements:
+ video:
+ - Block
+ - 'Optional: (source, Flow) | (Flow, source) | Flow'
+ - Common # allows a set of common attributes
+ # The 4th and 5th arguments are optional
+ - src: URI # list of type rules by attributes
+ type: Text
+ width: Length
+ height: Length
+ poster: URI
+ preload: 'Enum#auto,metadata,none'
+ controls: Bool
+ source:
+ - Block
+ - Flow
+ - Common
+ - { src: URI, type: Text }
+ - [style] # list of forbidden attributes
+```
- public function getName()
- {
- return 'purified_textarea';
- }
-}
+Would be equivalent to:
+
+```php
+$def = $config->getHTMLDefintion(true);
+$def->addElement('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',
+]);
+$source = $def->addElement('source', 'Block', 'Flow', 'Common', [
+ 'src' => 'URI',
+ 'type' => 'Text',
+]);
+$source->excludes = ['style' => true];
```
-Then define both the field type and transformer in the service container:
+See [HTMLPurifier documentation][] for more details.
+
+ [HTMLPurifier documentation]: http://htmlpurifier.org/docs/enduser-customize.html
+
+### Blank Elements
-``` xml
-