Skip to content

Commit

Permalink
Upgraded to v2
Browse files Browse the repository at this point in the history
- Require PHP 5.5 or 7.0 and Symfony 3.4 minimum
- Refactored extension and enabled autowiring
- [BC Break] Removed classes parameters
- [BC Break] Removed the form data transformer
- added a new text form type extension with a purifier listener to purify submitted data in all text based fields, using opt-in and custom profile thanks to dedicated options
- added a new "exercise.html_purifier" tag to make custom purifier implementations available as profile through form options and Twig filter
- added a purifiers registry to lazy load purifiers everywhere
- added a Twig HTMLPurifierRuntime for better performances
- upgraded the LICENSE and README files
  • Loading branch information
HeahDude committed Jul 28, 2018
1 parent 22e67b9 commit 5d96bbc
Show file tree
Hide file tree
Showing 23 changed files with 961 additions and 416 deletions.
17 changes: 7 additions & 10 deletions CacheWarmer/SerializerCacheWarmer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Exercise\HTMLPurifierBundle\CacheWarmer;

use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use HTMLPurifier;

/**
* Cache warmer for creating HTMLPurifier's cache directory and contents.
Expand All @@ -17,23 +16,20 @@
class SerializerCacheWarmer implements CacheWarmerInterface
{
private $paths;

/** @var HTMLPurifier used to build cache within bundle runtime */
private $htmlPurifier;

/**
* Constructor.
*
* @param array $paths
* @param string[] $paths
* @param \HTMLPurifier $htmlPurifier Used to build cache within bundle runtime
*/
public function __construct(array $paths, HTMLPurifier $htmlPurifier)
public function __construct(array $paths, \HTMLPurifier $htmlPurifier)
{
$this->paths = $paths;
$this->htmlPurifier = $htmlPurifier;
}

/**
* @see \Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface::warmUp()
* {@inheritdoc}
*/
public function warmUp($cacheDir)
{
Expand All @@ -47,13 +43,14 @@ public function warmUp($cacheDir)
}
}

// build htmlPurifier cache for HTML/CSS & URIs with the other Symfony cache warmups. Fixes issue #22
// build htmlPurifier cache for HTML/CSS & URIs with the other Symfony cache warmups.
// see https://github.com/Exercise/HTMLPurifierBundle/issues/22
$this->htmlPurifier->purify('<div style="border: thick">-2</div>');
$this->htmlPurifier->purify('<div style="background:url(\'http://www.example.com/x.gif\');">');
}

/**
* @see \Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface::isOptional()
* {@inheritdoc}
*/
public function isOptional()
{
Expand Down
56 changes: 56 additions & 0 deletions DependencyInjection/Compiler/HTMLPurifierPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Exercise\HTMLPurifierBundle\DependencyInjection\Compiler;

use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistryInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Reference;

class HTMLPurifierPass implements CompilerPassInterface
{
const PURIFIER_TAG = 'exercise.html_purifier';

/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasAlias(HTMLPurifiersRegistryInterface::class)) {
return;
}

try {
$registry = $container->findDefinition(HTMLPurifiersRegistryInterface::class);
} catch (ServiceNotFoundException $e) {
return;
}

$purifiers = [];

foreach ($container->findTaggedServiceIds(self::PURIFIER_TAG) as $id => $tags) {
if (empty($tags[0]['profile'])) {
throw new InvalidConfigurationException(sprintf('Tag "%s" must define a "profile" attribute.', self::PURIFIER_TAG));
}

$profile = $tags[0]['profile'];
$purifier = $container->getDefinition($id);

if (empty($purifier->getArguments())) {
$configId = "exercise_html_purifier.config.$profile";
$config = $container->hasDefinition($configId) ? $configId : 'exercise_html_purifier.config.default';

$purifier->addArgument(new Reference($config));
}

$purifiers[$profile] = new Reference($id);
}

$registry->setArguments([
ServiceLocatorTagPass::register($container, $purifiers),
]);
}
}
3 changes: 3 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

class Configuration implements ConfigurationInterface
{
/**
* {@inheritdoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
Expand Down
92 changes: 41 additions & 51 deletions DependencyInjection/ExerciseHTMLPurifierExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@

namespace Exercise\HTMLPurifierBundle\DependencyInjection;

use Exercise\HTMLPurifierBundle\DependencyInjection\Compiler\HTMLPurifierPass;
use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistry;
use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistryInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerInterface;

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
Expand All @@ -25,77 +30,62 @@ public function load(array $configs, ContainerBuilder $container)
* configuration (relying on canBeUnset() on the prototype node) or
* setting the "Cache.SerializerPath" option to null.
*/
array_unshift($configs, array(
'default' => array(
array_unshift($configs, [
'default' => [
'Cache.SerializerPath' => '%kernel.cache_dir%/htmlpurifier',
),
));
],
]);

$configs = $this->processConfiguration(new Configuration(), $configs);
$configs = array_map(array($this, 'resolveServices'), $configs);
$paths = array();

$serializerPaths = [];

foreach ($configs as $name => $config) {
$configDefinition = new Definition('%exercise_html_purifier.config.class%');
$configId = "exercise_html_purifier.config.$name";
$configDefinition = $container->register($configId, \HTMLPurifier_Config::class)
->setPublic(false)
;

if ('default' === $name) {
$configDefinition
->setFactory(array('%exercise_html_purifier.config.class%', 'create'))
->addArgument($config);
->setFactory([\HTMLPurifier_Config::class, 'create'])
->addArgument($config)
;
} else {
$configDefinition
->setFactory(array('%exercise_html_purifier.config.class%', 'inherit'))
->setFactory([\HTMLPurifier_Config::class, 'inherit'])
->addArgument(new Reference('exercise_html_purifier.config.default'))
->addMethodCall('loadArray', array($config));
->addMethodCall('loadArray', [$config])
;
}

$configId = 'exercise_html_purifier.config.' . $name;
$container->setDefinition($configId, $configDefinition);

$purifierDefinition = new Definition('%exercise_html_purifier.class%', array(new Reference($configId)));
$purifierDefinition->setPublic(true);

$container->setDefinition(
'exercise_html_purifier.' . $name,
$purifierDefinition
);
$container->register("exercise_html_purifier.$name", \HTMLPurifier::class)
->addArgument(new Reference($configId))
->addTag(HTMLPurifierPass::PURIFIER_TAG, ['profile' => $name])
;

if (isset($config['Cache.SerializerPath'])) {
$paths[] = $config['Cache.SerializerPath'];
$serializerPaths[] = $config['Cache.SerializerPath'];
}
}

$container->setParameter('exercise_html_purifier.cache_warmer.serializer.paths', array_unique($paths));
$container->register('exercise_html_purifier.purifiers_registry', HTMLPurifiersRegistry::class)
->setPublic(false)
;
$container->setAlias(HTMLPurifiersRegistryInterface::class, 'exercise_html_purifier.purifiers_registry')
->setPublic(false)
;
$container->setAlias(\HTMLPurifier::class, 'exercise_html_purifier.default')
->setPublic(false)
;
$container->setParameter('exercise_html_purifier.cache_warmer.serializer.paths', array_unique($serializerPaths));
}

/**
* {@inheritdoc}
*/
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;
}
}
9 changes: 9 additions & 0 deletions ExerciseHTMLPurifierBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@

namespace Exercise\HTMLPurifierBundle;

use Exercise\HTMLPurifierBundle\DependencyInjection\Compiler\HTMLPurifierPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class ExerciseHTMLPurifierBundle extends Bundle
{
/**
* {@inheritdoc}
*/
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new HTMLPurifierPass());
}
}
36 changes: 0 additions & 36 deletions Form/HTMLPurifierTransformer.php

This file was deleted.

56 changes: 56 additions & 0 deletions Form/Listener/HTMLPurifierListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Exercise\HTMLPurifierBundle\Form\Listener;

use Exercise\HTMLPurifierBundle\HTMLPurifiersRegistryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

class HTMLPurifierListener implements EventSubscriberInterface
{
private $registry;
private $profile;

/**
* @param HTMLPurifiersRegistryInterface $registry
* @param string $profile
*/
public function __construct(HTMLPurifiersRegistryInterface $registry, $profile)
{
$this->registry = $registry;
$this->profile = $profile;
}

public function purifySubmittedData(FormEvent $event)
{
if (!is_scalar($data = $event->getData())) {
// Hope there is a view transformer, otherwise an error might happen
return; // because we don't want to handle it here
}

if (0 === strlen($submittedData = trim($data))) {
return;
}

$event->setData($this->getPurifier()->purify($submittedData));
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SUBMIT => ['purifySubmittedData', /* as soon as possible */ 1000000],
];
}

/**
* @return \HTMLPurifier
*/
private function getPurifier()
{
return $this->registry->get($this->profile);
}
}
Loading

0 comments on commit 5d96bbc

Please sign in to comment.