diff --git a/NEWS b/NEWS index df7895e4..06f6721b 100644 --- a/NEWS +++ b/NEWS @@ -21,7 +21,8 @@ NEWS ( CHANGELOG and HISTORY ) HTMLPurifier # New compact syntax for AttrDef objects that can be used to instantiate new objects via make() # Definitions (esp. HTMLDefinition) are now cached for a significant - performance boost + performance boost. You can disable caching by setting %Core.DefinitionCache + to null. ! HTML Purifier now works in PHP 4.3.2. ! Configuration form-editing API makes tweaking HTMLPurifier_Config a breeze! diff --git a/library/HTMLPurifier/Config.php b/library/HTMLPurifier/Config.php index ab2e98d4..5f862ab6 100644 --- a/library/HTMLPurifier/Config.php +++ b/library/HTMLPurifier/Config.php @@ -6,7 +6,7 @@ require_once 'HTMLPurifier/ConfigSchema.php'; require_once 'HTMLPurifier/HTMLDefinition.php'; require_once 'HTMLPurifier/CSSDefinition.php'; require_once 'HTMLPurifier/Doctype.php'; -require_once 'HTMLPurifier/DefinitionCache.php'; +require_once 'HTMLPurifier/DefinitionCacheFactory.php'; // accomodations for versions earlier than 4.3.10 and 5.0.2 // borrowed from PHP_Compat, LGPL licensed, by Aidan Lister @@ -76,6 +76,12 @@ class HTMLPurifier_Config */ var $autoFinalize = true; + /** + * Namespace indexed array of serials for specific namespaces (see + * getSerial for more info). + */ + var $serials = array(); + /** * @param $definition HTMLPurifier_ConfigSchema that defines what directives * are allowed. @@ -149,6 +155,18 @@ class HTMLPurifier_Config return $this->conf[$namespace]; } + /** + * Returns a md5 signature of a segment of the configuration object + * that uniquely identifies that particular configuration + * @param $namespace Namespace to get serial for + */ + function getBatchSerial($namespace) { + if (empty($this->serials[$namespace])) { + $this->serials[$namespace] = md5(serialize($this->getBatch($namespace))); + } + return $this->serials[$namespace]; + } + /** * Retrieves all directives, organized by namespace */ @@ -210,6 +228,8 @@ class HTMLPurifier_Config if ($namespace == 'HTML' || $namespace == 'CSS') { $this->definitions[$namespace] = null; } + + $this->serials[$namespace] = false; } /** @@ -235,7 +255,8 @@ class HTMLPurifier_Config */ function &getDefinition($type, $raw = false) { if (!$this->finalized && $this->autoFinalize) $this->finalize(); - $cache = HTMLPurifier_DefinitionCache::create($type, $this); + $factory = HTMLPurifier_DefinitionCacheFactory::instance(); + $cache = $factory->create($type, $this); if (!$raw) { // see if we can quickly supply a definition if (!empty($this->definitions[$type])) { diff --git a/library/HTMLPurifier/DefinitionCache.php b/library/HTMLPurifier/DefinitionCache.php index b2212256..da84682e 100644 --- a/library/HTMLPurifier/DefinitionCache.php +++ b/library/HTMLPurifier/DefinitionCache.php @@ -1,11 +1,13 @@ version; - $revision = $config->revision; - return $version . '-' . $revision . '-' . md5(serialize($config->getBatch($this->type))); + return $config->version . '-' . // possibly replace with function calls + $config->revision . '-' . + $config->getBatchSerial($this->type); } /** @@ -52,17 +54,6 @@ class HTMLPurifier_DefinitionCache return true; } - /** - * Factory method that creates a cache object based on configuration - * @param $name Name of definitions handled by cache - * @param $config Instance of HTMLPurifier_Config - */ - function create($name, $config) { - // only one implementation as for right now, $config will - // be used to determine implementation - return new HTMLPurifier_DefinitionCache_Serializer($name); - } - /** * Checks if a definition's type jives with the cache's type * @note Throws an error on failure diff --git a/library/HTMLPurifier/DefinitionCache/Decorator.php b/library/HTMLPurifier/DefinitionCache/Decorator.php new file mode 100644 index 00000000..ca2e53aa --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Decorator.php @@ -0,0 +1,62 @@ +copy(); + // reference is necessary for mocks in PHP 4 + $decorator->cache =& $cache; + $decorator->type = $cache->type; + return $decorator; + } + + /** + * Cross-compatible clone substitute + */ + function copy() { + return new HTMLPurifier_DefinitionCache_Decorator(); + } + + function add($def, $config) { + return $this->cache->add($def, $config); + } + + function set($def, $config) { + return $this->cache->set($def, $config); + } + + function replace($def, $config) { + return $this->cache->replace($def, $config); + } + + function get($config) { + return $this->cache->get($config); + } + + function flush() { + return $this->cache->flush(); + } + + function cleanup($config) { + return $this->cache->cleanup($config); + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php b/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php new file mode 100644 index 00000000..44908293 --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Decorator/Memory.php @@ -0,0 +1,47 @@ +definitions[$this->generateKey($config)] = $def; + return $status; + } + + function set($def, $config) { + $status = parent::set($def, $config); + if ($status) $this->definitions[$this->generateKey($config)] = $def; + return $status; + } + + function replace($def, $config) { + $status = parent::replace($def, $config); + if ($status) $this->definitions[$this->generateKey($config)] = $def; + return $status; + } + + function get($config) { + $key = $this->generateKey($config); + if (isset($this->definitions[$key])) return $this->definitions[$key]; + $this->definitions[$key] = parent::get($config); + return $this->definitions[$key]; + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in b/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in new file mode 100644 index 00000000..6bf4a63d --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Decorator/Template.php.in @@ -0,0 +1,46 @@ + \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache/Null.php b/library/HTMLPurifier/DefinitionCache/Null.php new file mode 100644 index 00000000..dd505cc3 --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Null.php @@ -0,0 +1,37 @@ + \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCacheFactory.php b/library/HTMLPurifier/DefinitionCacheFactory.php new file mode 100644 index 00000000..c68f3135 --- /dev/null +++ b/library/HTMLPurifier/DefinitionCacheFactory.php @@ -0,0 +1,82 @@ + array()); + var $decorators = array(); + + /** + * Retrieves an instance of global definition cache factory. + * @static + */ + function &instance($prototype = null) { + static $instance; + if ($prototype !== null) { + $instance = $prototype; + } elseif ($instance === null || $prototype === true) { + $instance = new HTMLPurifier_DefinitionCacheFactory(); + } + return $instance; + } + + /** + * Factory method that creates a cache object based on configuration + * @param $name Name of definitions handled by cache + * @param $config Instance of HTMLPurifier_Config + */ + function &create($type, $config) { + // only one implementation as for right now, $config will + // be used to determine implementation + $method = $config->get('Core', 'DefinitionCache'); + if ($method === null) { + $null = new HTMLPurifier_DefinitionCache_Null($type); + return $null; + } + if (!empty($this->caches[$method][$type])) { + return $this->caches[$method][$type]; + } + $cache = new HTMLPurifier_DefinitionCache_Serializer($type); + foreach ($this->decorators as $decorator) { + $new_cache = $decorator->decorate($cache); + // prevent infinite recursion in PHP 4 + unset($cache); + $cache = $new_cache; + } + $this->caches[$method][$type] = $cache; + return $this->caches[$method][$type]; + } + + /** + * Registers a decorator to add to all new cache objects + * @param + */ + function addDecorator($decorator) { + if (is_string($decorator)) { + $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; + $decorator = new $class; + } + $this->decorators[] = $decorator; + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCache/Decorator/MemoryTest.php b/tests/HTMLPurifier/DefinitionCache/Decorator/MemoryTest.php new file mode 100644 index 00000000..e715df4e --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCache/Decorator/MemoryTest.php @@ -0,0 +1,79 @@ +mock); + unset($this->cache); + $this->mock =& new HTMLPurifier_DefinitionCacheMock($this); + $this->mock->type = 'Test'; + $this->cache = new HTMLPurifier_DefinitionCache_Decorator_Memory(); + $this->cache = $this->cache->decorate($this->mock); + $this->def = $this->generateDefinition(); + $this->config = $this->generateConfigMock(); + } + + function test_get() { + $this->mock->expectOnce('get', array($this->config)); // only ONE call! + $this->mock->setReturnValue('get', $this->def, array($this->config)); + $this->assertEqual($this->cache->get($this->config), $this->def); + $this->assertEqual($this->cache->get($this->config), $this->def); + } + + function setupMockForSuccess($op) { + $this->mock->expectOnce($op, array($this->def, $this->config)); + $this->mock->setReturnValue($op, true, array($this->def, $this->config)); + $this->mock->expectNever('get'); + } + + function setupMockForFailure($op) { + $this->mock->expectOnce($op, array($this->def, $this->config)); + $this->mock->setReturnValue($op, false, array($this->def, $this->config)); + $this->mock->expectOnce('get', array($this->config)); + } + + function test_set() { + $this->setupMockForSuccess('set'); + $this->assertEqual($this->cache->set($this->def, $this->config), true); + $this->assertEqual($this->cache->get($this->config), $this->def); + } + + function test_set_failure() { + $this->setupMockForFailure('set'); + $this->assertEqual($this->cache->set($this->def, $this->config), false); + $this->cache->get($this->config); + } + + function test_replace() { + $this->setupMockForSuccess('replace'); + $this->assertEqual($this->cache->replace($this->def, $this->config), true); + $this->assertEqual($this->cache->get($this->config), $this->def); + } + + function test_replace_failure() { + $this->setupMockForFailure('replace'); + $this->assertEqual($this->cache->replace($this->def, $this->config), false); + $this->cache->get($this->config); + } + + function test_add() { + $this->setupMockForSuccess('add'); + $this->assertEqual($this->cache->add($this->def, $this->config), true); + $this->assertEqual($this->cache->get($this->config), $this->def); + } + + function test_add_failure() { + $this->setupMockForFailure('add'); + $this->assertEqual($this->cache->add($this->def, $this->config), false); + $this->cache->get($this->config); + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCache/DecoratorTest.php b/tests/HTMLPurifier/DefinitionCache/DecoratorTest.php new file mode 100644 index 00000000..9c4be611 --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCache/DecoratorTest.php @@ -0,0 +1,45 @@ +type = 'Test'; + + $cache = new HTMLPurifier_DefinitionCache_Decorator(); + $cache = $cache->decorate($mock); + + $this->assertIdentical($cache->type, $mock->type); + + $def = $this->generateDefinition(); + $config = $this->generateConfigMock(); + + $mock->expectOnce('add', array($def, $config)); + $cache->add($def, $config); + + $mock->expectOnce('set', array($def, $config)); + $cache->set($def, $config); + + $mock->expectOnce('replace', array($def, $config)); + $cache->replace($def, $config); + + $mock->expectOnce('get', array($config)); + $cache->get($config); + + $mock->expectOnce('flush', array()); + $cache->flush(); + + $mock->expectOnce('cleanup', array($config)); + $cache->cleanup($config); + + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCache/SerializerTest.php b/tests/HTMLPurifier/DefinitionCache/SerializerTest.php index 2da20ef1..9b9f385b 100644 --- a/tests/HTMLPurifier/DefinitionCache/SerializerTest.php +++ b/tests/HTMLPurifier/DefinitionCache/SerializerTest.php @@ -3,62 +3,18 @@ require_once 'HTMLPurifier/DefinitionCacheHarness.php'; require_once 'HTMLPurifier/DefinitionCache/Serializer.php'; -class HTMLPurifier_Definition_SerializerMock extends HTMLPurifier_Definition -{ - - var $_test; - var $_expect = false; - - function HTMLPurifier_Definition_SerializerMock(&$test_case) { - $this->_test =& $test_case; - } - - function expectDoSetupOnce() {$this->_expect = true;} - - function doSetup($config) { - if ($this->_expect) { - $this->_test->pass(); - } else { - $this->_test->fail('Unexpected call to doSetup'); - } - unset($this->_test, $this->_expect); - } - -} - class HTMLPurifier_DefinitionCache_SerializerTest extends HTMLPurifier_DefinitionCacheHarness { - function test__SerializerMock_pass() { - $config = 'config'; - generate_mock_once('UnitTestCase'); - $test =& new UnitTestCaseMock($this); - $test->expectOnce('pass'); - $mock = new HTMLPurifier_Definition_SerializerMock($test); - $mock->expectDoSetupOnce(); - $mock->doSetup($config); - } - - function test__SerializerMock_fail() { - $config = 'config'; - generate_mock_once('UnitTestCase'); - $test =& new UnitTestCaseMock($this); - $test->expectOnce('fail'); - $mock = new HTMLPurifier_Definition_SerializerMock($test); - $mock->doSetup($config); - } - function test() { $cache = new HTMLPurifier_DefinitionCache_Serializer('Test'); - $config_array = array('Foo' => 'Bar'); - - $config = $this->generateConfigMock($config_array); + $config = $this->generateConfigMock('serial'); $config->version = '1.0.0'; $config->revision = 2; - $config_md5 = '1.0.0-' . $config->revision . '-' . md5(serialize($config_array)); + $config_md5 = '1.0.0-' . $config->revision . '-serial'; $file = realpath( $rel_file = dirname(__FILE__) . @@ -114,7 +70,7 @@ class HTMLPurifier_DefinitionCache_SerializerTest extends HTMLPurifier_Definitio $def = new HTMLPurifier_Definition(); $def->setup = true; $def->type = 'NotTest'; - $config = $this->generateConfigMock(array('Test' => 'foo')); + $config = $this->generateConfigMock('testfoo'); $this->expectError('Cannot use definition of type NotTest in cache for Test'); $cache->add($def, $config); @@ -130,9 +86,9 @@ class HTMLPurifier_DefinitionCache_SerializerTest extends HTMLPurifier_Definitio $cache = new HTMLPurifier_DefinitionCache_Serializer('Test'); - $config1 = $this->generateConfigMock(array('Test' => 1)); - $config2 = $this->generateConfigMock(array('Test' => 2)); - $config3 = $this->generateConfigMock(array('Test' => 3)); + $config1 = $this->generateConfigMock('test1'); + $config2 = $this->generateConfigMock('test2'); + $config3 = $this->generateConfigMock('test3'); $def1 = $this->generateDefinition(array('info_candles' => 1)); $def2 = $this->generateDefinition(array('info_candles' => 2)); diff --git a/tests/HTMLPurifier/DefinitionCacheFactoryTest.php b/tests/HTMLPurifier/DefinitionCacheFactoryTest.php new file mode 100644 index 00000000..892f8805 --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCacheFactoryTest.php @@ -0,0 +1,65 @@ +oldFactory = HTMLPurifier_DefinitionCacheFactory::instance(); + HTMLPurifier_DefinitionCacheFactory::instance($new); + } + + function teardown() { + HTMLPurifier_DefinitionCacheFactory::instance($this->oldFactory); + } + + function test_create() { + $config = HTMLPurifier_Config::createDefault(); + $factory = HTMLPurifier_DefinitionCacheFactory::instance(); + $cache = $factory->create('Test', $config); + $this->assertEqual($cache, new HTMLPurifier_DefinitionCache_Serializer('Test')); + } + + function test_create_withDecorator() { + $config = HTMLPurifier_Config::createDefault(); + $factory =& HTMLPurifier_DefinitionCacheFactory::instance(); + $factory->addDecorator('Memory'); + $cache =& $factory->create('Test', $config); + $cache_real = new HTMLPurifier_DefinitionCache_Decorator_Memory(); + $cache_real = $cache_real->decorate(new HTMLPurifier_DefinitionCache_Serializer('Test')); + $this->assertEqual($cache, $cache_real); + } + + function test_create_withDecoratorObject() { + $config = HTMLPurifier_Config::createDefault(); + $factory =& HTMLPurifier_DefinitionCacheFactory::instance(); + $factory->addDecorator(new HTMLPurifier_DefinitionCache_Decorator_Memory()); + $cache =& $factory->create('Test', $config); + $cache_real = new HTMLPurifier_DefinitionCache_Decorator_Memory(); + $cache_real = $cache_real->decorate(new HTMLPurifier_DefinitionCache_Serializer('Test')); + $this->assertEqual($cache, $cache_real); + } + + function test_create_recycling() { + $config = HTMLPurifier_Config::createDefault(); + $factory =& HTMLPurifier_DefinitionCacheFactory::instance(); + $cache =& $factory->create('Test', $config); + $cache2 =& $factory->create('Test', $config); + $this->assertReference($cache, $cache2); + } + + function test_null() { + $config = HTMLPurifier_Config::create(array('Core.DefinitionCache' => null)); + $factory =& HTMLPurifier_DefinitionCacheFactory::instance(); + $cache =& $factory->create('Test', $config); + $this->assertEqual($cache, new HTMLPurifier_DefinitionCache_Null('Test')); + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCacheHarness.php b/tests/HTMLPurifier/DefinitionCacheHarness.php index d43df6bc..ebc15d68 100644 --- a/tests/HTMLPurifier/DefinitionCacheHarness.php +++ b/tests/HTMLPurifier/DefinitionCacheHarness.php @@ -8,10 +8,10 @@ class HTMLPurifier_DefinitionCacheHarness extends UnitTestCase * to a getBatch() call * @param $values Values to return when getBatch is invoked */ - function generateConfigMock($values = array()) { + function generateConfigMock($serial = 'defaultserial') { generate_mock_once('HTMLPurifier_Config'); $config = new HTMLPurifier_ConfigMock($this); - $config->setReturnValue('getBatch', $values, array('Test')); + $config->setReturnValue('getBatchSerial', $serial, array('Test')); $config->version = '1.0.0'; $config->revision = 1; return $config; diff --git a/tests/HTMLPurifier/DefinitionCacheTest.php b/tests/HTMLPurifier/DefinitionCacheTest.php index b117a282..eb78a11c 100644 --- a/tests/HTMLPurifier/DefinitionCacheTest.php +++ b/tests/HTMLPurifier/DefinitionCacheTest.php @@ -4,11 +4,6 @@ require_once 'HTMLPurifier/DefinitionCache.php'; class HTMLPurifier_DefinitionCacheTest extends UnitTestCase { - function test_create() { - $config = HTMLPurifier_Config::createDefault(); - $cache = HTMLPurifier_DefinitionCache::create('Test', $config); - $this->assertEqual($cache, new HTMLPurifier_DefinitionCache_Serializer('Test')); - } function test_isOld() { $cache = new HTMLPurifier_DefinitionCache('Test'); // non-functional diff --git a/tests/index.php b/tests/index.php index 543930a4..c10e128c 100644 --- a/tests/index.php +++ b/tests/index.php @@ -40,6 +40,10 @@ if ( is_string($GLOBALS['HTMLPurifierTest']['PEAR']) ) { // initialize and load HTML Purifier require_once '../library/HTMLPurifier.auto.php'; +// setup special DefinitionCacheFactory decorator +$factory =& HTMLPurifier_DefinitionCacheFactory::instance(); +$factory->addDecorator('Memory'); // since we deal with a lot of config objects + // load tests $test_files = array(); require 'test_files.php'; // populates $test_files array diff --git a/tests/test_files.php b/tests/test_files.php index 069b4aca..6ecdb3c7 100644 --- a/tests/test_files.php +++ b/tests/test_files.php @@ -61,7 +61,10 @@ $test_files[] = 'HTMLPurifier/ChildDef/TableTest.php'; $test_files[] = 'HTMLPurifier/ConfigSchemaTest.php'; $test_files[] = 'HTMLPurifier/ConfigTest.php'; $test_files[] = 'HTMLPurifier/ContextTest.php'; +$test_files[] = 'HTMLPurifier/DefinitionCacheFactoryTest.php'; $test_files[] = 'HTMLPurifier/DefinitionCacheTest.php'; +$test_files[] = 'HTMLPurifier/DefinitionCache/Decorator/MemoryTest.php'; +$test_files[] = 'HTMLPurifier/DefinitionCache/DecoratorTest.php'; $test_files[] = 'HTMLPurifier/DefinitionCache/SerializerTest.php'; $test_files[] = 'HTMLPurifier/DefinitionTest.php'; $test_files[] = 'HTMLPurifier/DoctypeRegistryTest.php';