diff --git a/library/HTMLPurifier/CSSDefinition.php b/library/HTMLPurifier/CSSDefinition.php index 4db04694..e5d963f3 100644 --- a/library/HTMLPurifier/CSSDefinition.php +++ b/library/HTMLPurifier/CSSDefinition.php @@ -24,6 +24,8 @@ require_once 'HTMLPurifier/AttrDef/Enum.php'; class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition { + var $type = 'CSS'; + /** * Assoc array of attribute name to definition object. */ diff --git a/library/HTMLPurifier/Config.php b/library/HTMLPurifier/Config.php index 38011a53..25522d58 100644 --- a/library/HTMLPurifier/Config.php +++ b/library/HTMLPurifier/Config.php @@ -6,6 +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'; /** * Configuration object that triggers customizable behavior. @@ -176,11 +177,9 @@ class HTMLPurifier_Config // reset definitions if the directives they depend on changed // this is a very costly process, so it's discouraged // with finalization - if ($namespace == 'HTML' || $namespace == 'Attr') { + if ($namespace == 'HTML') { $this->html_definition = null; - $this->doctype = null; - } - if ($namespace == 'CSS') { + } elseif ($namespace == 'CSS') { $this->css_definition = null; } } @@ -192,34 +191,59 @@ class HTMLPurifier_Config */ function &getHTMLDefinition($raw = false) { if (!$this->finalized && $this->autoFinalize) $this->finalize(); - if ( - empty($this->html_definition) || // hasn't ever been setup - ($raw && $this->html_definition->setup) // requesting new one - ) { - if (!$raw) { - $this->html_definition = HTMLPurifier_HTMLDefinition::getCache($this); - if ($this->html_definition) return $this->html_definition; - } - $this->html_definition = new HTMLPurifier_HTMLDefinition(); - if ($raw) return $this->html_definition; // no setup! + $cache = HTMLPurifier_DefinitionCache::create('HTML', $this); + if($this->checkDefinition($this->html_definition, $cache, $raw)) { + return $this->html_definition; } - if (!$this->html_definition->setup) { - $this->html_definition->setup($this); - $this->html_definition->saveCache($this); - } - return $this->html_definition; + return $this->createDefinition( + $this->html_definition, + $cache, + $raw, + new HTMLPurifier_HTMLDefinition() + ); } /** * Retrieves reference to the CSS definition */ - function &getCSSDefinition() { + function &getCSSDefinition($raw = false) { if (!$this->finalized && $this->autoFinalize) $this->finalize(); - if ($this->css_definition === null) { - $this->css_definition = new HTMLPurifier_CSSDefinition(); - $this->css_definition->setup($this); + $cache = HTMLPurifier_DefinitionCache::create('CSS', $this); + if($this->checkDefinition($this->css_definition, $cache, $raw)) { + return $this->css_definition; } - return $this->css_definition; + return $this->createDefinition( + $this->css_definition, + $cache, + $raw, + new HTMLPurifier_CSSDefinition() + ); + } + + /** + * Checks the variable and cache for an easy-access definition, + * sets def to variable and returns true if available + */ + function checkDefinition(&$var, $cache, $raw) { + if ($raw) return false; + if (!empty($var)) { + if (!$var->setup) $var->setup($this); + return true; + } + $var = $cache->get($this); + return (bool) $var; + } + + /** + * Generates a new definition, possibly returning it raw, returns + * reference to variable. + */ + function &createDefinition(&$var, $cache, $raw, $obj) { + $var = $obj; + if ($raw) return $var; + $var->setup($this); + $cache->set($var, $this); + return $var; } /** @@ -266,6 +290,14 @@ class HTMLPurifier_Config return $this->finalized; } + /** + * Finalizes configuration only if auto finalize is on and not + * already finalized + */ + function autoFinalize() { + if (!$this->finalized && $this->autoFinalize) $this->finalize(); + } + /** * Finalizes a configuration object, prohibiting further change */ diff --git a/library/HTMLPurifier/Definition.php b/library/HTMLPurifier/Definition.php index 2a430bde..f45951bb 100644 --- a/library/HTMLPurifier/Definition.php +++ b/library/HTMLPurifier/Definition.php @@ -12,6 +12,11 @@ class HTMLPurifier_Definition */ var $setup = false; + /** + * What type of definition is it? + */ + var $type; + /** * Sets up the definition object into the final form, something * not done by the constructor diff --git a/library/HTMLPurifier/DefinitionCache.php b/library/HTMLPurifier/DefinitionCache.php new file mode 100644 index 00000000..7e9942f3 --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache.php @@ -0,0 +1,83 @@ +type = $type; + } + + /** + * Generates a unique identifier for a particular configuration + * @param Instance of HTMLPurifier_Config + */ + function generateKey($config) { + return md5(serialize($config->getBatch($this->type))); + } + + /** + * 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); + } + + /** + * Adds a definition object to the cache + */ + function add($def, $config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + + /** + * Unconditionally saves a definition object to the cache + */ + function set($def, $config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + + /** + * Retrieves a definition object from the cache + */ + function get($config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + + /** + * Removes a definition object to the cache + */ + function remove($config) { + trigger_error('Cannot call abstract method', E_USER_ERROR); + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/DefinitionCache/Serializer.php b/library/HTMLPurifier/DefinitionCache/Serializer.php new file mode 100644 index 00000000..ab5398bf --- /dev/null +++ b/library/HTMLPurifier/DefinitionCache/Serializer.php @@ -0,0 +1,64 @@ +generateFilePath($config); + if (file_exists($file)) return false; + return $this->_write($file, serialize($def)); + } + + function set($def, $config) { + $file = $this->generateFilePath($config); + return $this->_write($file, serialize($def)); + } + + function get($config) { + $file = $this->generateFilePath($config); + if (!file_exists($file)) return false; + return unserialize(file_get_contents($file)); + } + + function remove($config) { + $file = $this->generateFilePath($config); + if (!file_exists($file)) return false; + return unlink($file); + } + + /** + * Generates the file path to the serial file corresponding to + * the configuration and definition name + */ + function generateFilePath($config) { + $key = $this->generateKey($config); + return dirname(__FILE__) . '/Serializer/' . $this->type . '/' . $key . '.ser'; + } + + /** + * Convenience wrapper function for file_put_contents + * @param $file File name to write to + * @param $data Data to write into file + * @return Number of bytes written if success, or false if failure. + */ + function _write($file, $data) { + static $file_put_contents; + if ($file_put_contents === null) { + $file_put_contents = function_exists('file_put_contents'); + } + if ($file_put_contents) { + return file_put_contents($file, $data); + } + $fh = fopen($file, 'w'); + if (!$fh) return false; + $status = fwrite($fh, $contents); + fclose($fh); + return $status; + } + +} + +?> \ No newline at end of file diff --git a/library/HTMLPurifier/HTMLDefinition.php b/library/HTMLPurifier/HTMLDefinition.php index 5b66a871..a3505877 100644 --- a/library/HTMLPurifier/HTMLDefinition.php +++ b/library/HTMLPurifier/HTMLDefinition.php @@ -155,6 +155,7 @@ class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition /** PUBLIC BUT INTERNAL VARIABLES */ + var $type = 'HTML'; var $manager; /**< Instance of HTMLPurifier_HTMLModuleManager */ /** @@ -164,44 +165,6 @@ class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition $this->manager = new HTMLPurifier_HTMLModuleManager(); } - /** - * Retrieve definition object from cache - */ - function getCache($config) { - static $cache = array(); - $file = HTMLPurifier_HTMLDefinition::getCacheFile($config); - if (isset($cache[$file])) return $cache[$file]; // unit test optimization - if (!file_exists($file)) return false; - $cache[$file] = unserialize(file_get_contents($file)); - return $cache[$file]; - } - - /** - * Determines a cache key identifier for a particular configuration - */ - function getCacheKey($config) { - return md5(serialize(array($config->getBatch('HTML'), $config->getBatch('Attr')))); - } - - /** - * Determines file a particular configuration's definition is stored in - */ - function getCacheFile($config) { - $key = HTMLPurifier_HTMLDefinition::getCacheKey($config); - return dirname(__FILE__) . '/HTMLDefinition/' . $key . '.ser'; - } - - /** - * Saves HTMLDefinition to cache - */ - function saveCache($config) { - $file = $this->getCacheFile($config); - $contents = serialize($this); - $fh = fopen($file, 'w'); - fwrite($fh, $contents); - fclose($fh); - } - function doSetup($config) { $this->processModules($config); $this->setupConfigStuff($config); diff --git a/tests/HTMLPurifier/ConfigTest.php b/tests/HTMLPurifier/ConfigTest.php index 5952d2f4..8a5aeeb9 100644 --- a/tests/HTMLPurifier/ConfigTest.php +++ b/tests/HTMLPurifier/ConfigTest.php @@ -262,7 +262,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase } function test_getCSSDefinition() { - $this->old_copy = CS::instance($this->old_copy); + $this->old_copy = HTMLPurifier_ConfigSchema::instance($this->old_copy); $config = HTMLPurifier_Config::createDefault(); $config->autoFinalize = false; diff --git a/tests/HTMLPurifier/DefinitionCache/SerializerTest.php b/tests/HTMLPurifier/DefinitionCache/SerializerTest.php new file mode 100644 index 00000000..16ada892 --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCache/SerializerTest.php @@ -0,0 +1,177 @@ +_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 UnitTestCase +{ + + 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_md5 = md5(serialize($config_array)); + + $file = realpath( + $rel_file = dirname(__FILE__) . + '/../../../library/HTMLPurifier/DefinitionCache/Serializer/Test/' . + $config_md5 . '.ser' + ); + if($file) unlink($file); // prevent previous failures from causing problems + + $config = $this->generateConfigMock($config_array); + $this->assertIdentical($config_md5, $cache->generateKey($config)); + + $def_original = $this->generateDefinition(); + + $cache->add($def_original, $config); + $this->assertFileExist($rel_file); + + $file_generated = $cache->generateFilePath($config); + $this->assertIdentical(realpath($rel_file), realpath($file_generated)); + + $def_1 = $cache->get($config); + $this->assertIdentical($def_original, $def_1); + + $def_original->info_random = 'changed'; + + $cache->set($def_original, $config); + $def_2 = $cache->get($config); + + $this->assertIdentical($def_original, $def_2); + $this->assertNotEqual ($def_original, $def_1); + + $def_original->info_random = 'did it change?'; + + $this->assertFalse($cache->add($def_original, $config)); + $def_3 = $cache->get($config); + + $this->assertNotEqual ($def_original, $def_3); // did not change! + $this->assertIdentical($def_3, $def_2); + + $cache->remove($config); + $this->assertFileNotExist($file); + + $def_4 = $cache->get($config); + $this->assertFalse($def_4); + + } + + function test_errors() { + /*$cache = new HTMLPurifier_DefinitionCache_Serializer('Test'); + $def = new HTMLPurifier_Definition(); + $def->setup = true; + $def->type = 'NotTest'; + + $this->expectError('Cannot add definition of type NotTest to cache for Test');*/ + } + + function test_flush() { + /* + $cache = new HTMLPurifier_DefinitionCache_Serializer(); + + $config1 = $this->generateConfigMock(array('Candles' => 1)); + $config2 = $this->generateConfigMock(array('Candles' => 2)); + $config3 = $this->generateConfigMock(array('Candles' => 3)); + + $def1 = $this->generateDefinition(array('info_candles' => 1)); + $def2 = $this->generateDefinition(array('info_candles' => 2)); + $def3 = $this->generateDefinition(array('info_candles' => 3)); + + $cache->add($def1, $config1); + $cache->add($def2, $config2); + $cache->add($def3, $config3); + + $this->assertTrue($cache->get('Test', $config1)); + $this->assertTrue($cache->get('Test', $config2)); + $this->assertTrue($cache->get('Test', $config3)); + + $cache->flush('Test'); + */ + } + + /** + * Generate a configuration mock object that returns $values + * to a getBatch() call + * @param $values Values to return when getBatch is invoked + */ + function generateConfigMock($values) { + generate_mock_once('HTMLPurifier_Config'); + $config = new HTMLPurifier_ConfigMock($this); + $config->setReturnValue('getBatch', $values, array('Test')); + return $config; + } + + /** + * Returns an anonymous def that has been setup and named Test + */ + function generateDefinition($member_vars = array()) { + $def = new HTMLPurifier_Definition(); + $def->setup = true; + $def->type = 'Test'; + foreach ($member_vars as $key => $val) { + $def->$key = $val; + } + return $def; + } + + /** + * Asserts that a file exists, ignoring the stat cache + */ + function assertFileExist($file) { + clearstatcache(); + $this->assertTrue(file_exists($file), 'Expected ' . $file . ' exists'); + } + + /** + * Asserts that a file does not exist, ignoring the stat cache + */ + function assertFileNotExist($file) { + $this->assertFalse(file_exists($file), 'Expected ' . $file . ' does not exist'); + } + +} + +?> \ No newline at end of file diff --git a/tests/HTMLPurifier/DefinitionCacheTest.php b/tests/HTMLPurifier/DefinitionCacheTest.php new file mode 100644 index 00000000..0d1f512e --- /dev/null +++ b/tests/HTMLPurifier/DefinitionCacheTest.php @@ -0,0 +1,14 @@ +assertEqual($cache, new HTMLPurifier_DefinitionCache_Serializer('Test')); + } +} + +?> \ No newline at end of file diff --git a/tests/test_files.php b/tests/test_files.php index cb184154..03761564 100644 --- a/tests/test_files.php +++ b/tests/test_files.php @@ -58,6 +58,8 @@ $test_files[] = 'ChildDef/TableTest.php'; $test_files[] = 'ConfigSchemaTest.php'; $test_files[] = 'ConfigTest.php'; $test_files[] = 'ContextTest.php'; +$test_files[] = 'DefinitionCacheTest.php'; +$test_files[] = 'DefinitionCache/SerializerTest.php'; $test_files[] = 'DefinitionTest.php'; $test_files[] = 'DoctypeRegistryTest.php'; $test_files[] = 'ElementDefTest.php';