diff --git a/NEWS b/NEWS index 9375910b..34b892b3 100644 --- a/NEWS +++ b/NEWS @@ -14,6 +14,10 @@ NEWS ( CHANGELOG and HISTORY ) HTMLPurifier information # Transform modules changed to Tidy modules, which offer more flexibility and better modularization +# Configuration object now finalizes itself when a read operation is + performed on it, ensuring that its internal state stays consistent. + To revert this behavior, you can set the $autoFinalize member variable + off, but it's not recommended. . Unit test for ElementDef created, ElementDef behavior modified to be more flexible . Added convenience functions for HTMLModule constructors diff --git a/library/HTMLPurifier/Config.php b/library/HTMLPurifier/Config.php index c94e01f6..3184b8c9 100644 --- a/library/HTMLPurifier/Config.php +++ b/library/HTMLPurifier/Config.php @@ -35,6 +35,17 @@ class HTMLPurifier_Config */ var $css_definition; + /** + * Bool indicator whether or not config is finalized + */ + var $finalized = false; + + /** + * Bool indicator whether or not to automatically finalize + * the object if a read operation is done + */ + var $autoFinalize = true; + /** * @param $definition HTMLPurifier_ConfigSchema that defines what directives * are allowed. @@ -78,6 +89,7 @@ class HTMLPurifier_Config * @param $key String key */ function get($namespace, $key, $from_alias = false) { + if (!$this->finalized && $this->autoFinalize) $this->finalize(); if (!isset($this->def->info[$namespace][$key])) { trigger_error('Cannot retrieve value of undefined directive', E_USER_WARNING); @@ -96,6 +108,7 @@ class HTMLPurifier_Config * @param $namespace String namespace */ function getBatch($namespace) { + if (!$this->finalized && $this->autoFinalize) $this->finalize(); if (!isset($this->def->info[$namespace])) { trigger_error('Cannot retrieve undefined namespace', E_USER_WARNING); @@ -111,6 +124,7 @@ class HTMLPurifier_Config * @param $value Mixed value */ function set($namespace, $key, $value, $from_alias = false) { + if ($this->isFinalized('Cannot set directive after finalization')) return; if (!isset($this->def->info[$namespace][$key])) { trigger_error('Cannot set undefined directive to value', E_USER_WARNING); @@ -164,6 +178,7 @@ class HTMLPurifier_Config * called before it's been setup, otherwise won't work. */ 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 @@ -179,6 +194,7 @@ class HTMLPurifier_Config * Retrieves reference to the CSS definition */ function &getCSSDefinition() { + if (!$this->finalized && $this->autoFinalize) $this->finalize(); if ($this->css_definition === null) { $this->css_definition = new HTMLPurifier_CSSDefinition(); $this->css_definition->setup($this); @@ -192,6 +208,7 @@ class HTMLPurifier_Config * @param $config_array Configuration associative array */ function loadArray($config_array) { + if ($this->isFinalized('Cannot load directives after finalization')) return; foreach ($config_array as $key => $value) { $key = str_replace('_', '.', $key); if (strpos($key, '.') !== false) { @@ -213,10 +230,29 @@ class HTMLPurifier_Config * @param $filename Name of ini file */ function loadIni($filename) { + if ($this->isFinalized('Cannot load directives after finalization')) return; $array = parse_ini_file($filename, true); $this->loadArray($array); } + /** + * Checks whether or not the configuration object is finalized. + * @param $error String error message, or false for no error + */ + function isFinalized($error = false) { + if ($this->finalized && $error) { + trigger_error($error, E_USER_ERROR); + } + return $this->finalized; + } + + /** + * Finalizes a configuration object, prohibiting further change + */ + function finalize() { + $this->finalized = true; + } + } ?> diff --git a/tests/HTMLPurifier/AttrDef/HTML/IDTest.php b/tests/HTMLPurifier/AttrDef/HTML/IDTest.php index 98fffbba..add764fd 100644 --- a/tests/HTMLPurifier/AttrDef/HTML/IDTest.php +++ b/tests/HTMLPurifier/AttrDef/HTML/IDTest.php @@ -66,9 +66,12 @@ class HTMLPurifier_AttrDef_HTML_IDTest extends HTMLPurifier_AttrDefHarness $this->assertDef('user_story95_alas'); $this->assertDef('user_alas', 'user_story95_user_alas'); // ! - + } + + function testLocalPrefixWithoutMainPrefix() { // no effect when IDPrefix isn't set $this->config->set('Attr', 'IDPrefix', ''); + $this->config->set('Attr', 'IDPrefixLocal', 'story95_'); $this->expectError('%Attr.IDPrefixLocal cannot be used unless '. '%Attr.IDPrefix is set'); $this->assertDef('amherst'); diff --git a/tests/HTMLPurifier/AttrDef/URITest.php b/tests/HTMLPurifier/AttrDef/URITest.php index f9a9ab41..8f035e32 100644 --- a/tests/HTMLPurifier/AttrDef/URITest.php +++ b/tests/HTMLPurifier/AttrDef/URITest.php @@ -247,13 +247,10 @@ class HTMLPurifier_AttrDef_URITest extends HTMLPurifier_AttrDefHarness $this->def = new HTMLPurifier_AttrDef_URI(); $this->config->set('URI', 'DisableExternal', true); + $this->config->set('URI', 'Host', 'sub.example.com'); $this->assertDef('/foobar.txt'); $this->assertDef('http://google.com/', false); - $this->assertDef('http://sub.example.com/alas?foo=asd', false); - - $this->config->set('URI', 'Host', 'sub.example.com'); - $this->assertDef('http://sub.example.com/alas?foo=asd'); $this->assertDef('http://example.com/teehee', false); $this->assertDef('http://www.example.com/#man', false); diff --git a/tests/HTMLPurifier/ConfigTest-finalize.ini b/tests/HTMLPurifier/ConfigTest-finalize.ini new file mode 100644 index 00000000..81720463 --- /dev/null +++ b/tests/HTMLPurifier/ConfigTest-finalize.ini @@ -0,0 +1,2 @@ +[Poem] +Meter = alexandrine \ No newline at end of file diff --git a/tests/HTMLPurifier/ConfigTest.php b/tests/HTMLPurifier/ConfigTest.php index 69bbf81b..61381731 100644 --- a/tests/HTMLPurifier/ConfigTest.php +++ b/tests/HTMLPurifier/ConfigTest.php @@ -42,6 +42,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase CS::define('Element', 'Object', new stdClass(), 'mixed', 'Model representation.'); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; // test default value retrieval $this->assertIdentical($config->get('Element', 'Abbr'), 'H'); @@ -65,6 +66,12 @@ class HTMLPurifier_ConfigTest extends UnitTestCase $config->set('Element', 'IsotopeNames', array(238 => 'Plutonium-238', 239 => 'Plutonium-239')); $config->set('Element', 'Object', false); // unmodeled + $this->expectError('Cannot set undefined directive to value'); + $config->set('Element', 'Metal', true); + + $this->expectError('Value is of invalid type'); + $config->set('Element', 'Radioactive', 'very'); + // test value retrieval $this->assertIdentical($config->get('Element', 'Abbr'), 'Pu'); $this->assertIdentical($config->get('Element', 'Name'), 'plutonium'); @@ -76,17 +83,9 @@ class HTMLPurifier_ConfigTest extends UnitTestCase $this->assertIdentical($config->get('Element', 'IsotopeNames'), array(238 => 'Plutonium-238', 239 => 'Plutonium-239')); $this->assertIdentical($config->get('Element', 'Object'), false); - // errors - $this->expectError('Cannot retrieve value of undefined directive'); $config->get('Element', 'Metal'); - $this->expectError('Cannot set undefined directive to value'); - $config->set('Element', 'Metal', true); - - $this->expectError('Value is of invalid type'); - $config->set('Element', 'Radioactive', 'very'); - } function testEnumerated() { @@ -108,6 +107,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase 'synth' => 'electronic')); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; // case sensitive @@ -143,6 +143,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase CS::define('ReportCard', 'Absences', 0, 'int', 'How many times missing from school?'); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; $config->set('ReportCard', 'English', 'B-'); $this->assertIdentical($config->get('ReportCard', 'English'), 'B-'); @@ -158,11 +159,12 @@ class HTMLPurifier_ConfigTest extends UnitTestCase function testAliases() { - HTMLPurifier_ConfigSchema::defineNamespace('Home', 'Sweet home.'); - HTMLPurifier_ConfigSchema::define('Home', 'Rug', 3, 'int', 'ID.'); - HTMLPurifier_ConfigSchema::defineAlias('Home', 'Carpet', 'Home', 'Rug'); + CS::defineNamespace('Home', 'Sweet home.'); + CS::define('Home', 'Rug', 3, 'int', 'ID.'); + CS::defineAlias('Home', 'Carpet', 'Home', 'Rug'); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; $this->assertIdentical($config->get('Home', 'Rug'), 3); @@ -183,6 +185,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase CS::define('Variables', 'AngularAcceleration', 'alpha', 'string', 'In rad/s^2'); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; // grab a namespace $this->assertIdentical( @@ -207,6 +210,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase CS::define('Shortcut', 'Cut', 'x', 'istring', 'Cut text'); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; $config->loadIni(dirname(__FILE__) . '/ConfigTest-loadIni.ini'); @@ -224,6 +228,7 @@ class HTMLPurifier_ConfigTest extends UnitTestCase $this->old_copy = HTMLPurifier_ConfigSchema::instance($this->old_copy); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; $def = $config->getCSSDefinition(); $this->assertIsA($def, 'HTMLPurifier_CSSDefinition'); @@ -253,9 +258,10 @@ class HTMLPurifier_ConfigTest extends UnitTestCase } function test_getCSSDefinition() { - $this->old_copy = HTMLPurifier_ConfigSchema::instance($this->old_copy); + $this->old_copy = CS::instance($this->old_copy); $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; $def = $config->getCSSDefinition(); $this->assertIsA($def, 'HTMLPurifier_CSSDefinition'); @@ -263,11 +269,11 @@ class HTMLPurifier_ConfigTest extends UnitTestCase function test_loadArray() { // setup a few dummy namespaces/directives for our testing - HTMLPurifier_ConfigSchema::defineNamespace('Zoo', 'Animals we have.'); - HTMLPurifier_ConfigSchema::define('Zoo', 'Aadvark', 0, 'int', 'Have?'); - HTMLPurifier_ConfigSchema::define('Zoo', 'Boar', 0, 'int', 'Have?'); - HTMLPurifier_ConfigSchema::define('Zoo', 'Camel', 0, 'int', 'Have?'); - HTMLPurifier_ConfigSchema::define( + CS::defineNamespace('Zoo', 'Animals we have.'); + CS::define('Zoo', 'Aadvark', 0, 'int', 'Have?'); + CS::define('Zoo', 'Boar', 0, 'int', 'Have?'); + CS::define('Zoo', 'Camel', 0, 'int', 'Have?'); + CS::define( 'Zoo', 'Others', array(), 'list', 'Other animals we have one of.' ); @@ -305,9 +311,9 @@ class HTMLPurifier_ConfigTest extends UnitTestCase function test_create() { - HTMLPurifier_ConfigSchema::defineNamespace('Cake', 'Properties of it.'); - HTMLPurifier_ConfigSchema::define('Cake', 'Sprinkles', 666, 'int', 'Number of.'); - HTMLPurifier_ConfigSchema::define('Cake', 'Flavor', 'vanilla', 'string', 'Flavor of the batter.'); + CS::defineNamespace('Cake', 'Properties of it.'); + CS::define('Cake', 'Sprinkles', 666, 'int', 'Number of.'); + CS::define('Cake', 'Flavor', 'vanilla', 'string', 'Flavor of the batter.'); $config = HTMLPurifier_Config::createDefault(); $config->set('Cake', 'Sprinkles', 42); @@ -326,6 +332,31 @@ class HTMLPurifier_ConfigTest extends UnitTestCase } + function test_finalize() { + + // test finalization + + CS::defineNamespace('Poem', 'Violets are red, roses are blue...'); + CS::define('Poem', 'Meter', 'iambic', 'string', 'Rhythm of poem.'); + + $config = HTMLPurifier_Config::createDefault(); + $config->autoFinalize = false; + + $config->set('Poem', 'Meter', 'irregular'); + + $config->finalize(); + + $this->expectError('Cannot set directive after finalization'); + $config->set('Poem', 'Meter', 'vedic'); + + $this->expectError('Cannot load directives after finalization'); + $config->loadArray(array('Poem.Meter' => 'octosyllable')); + + $this->expectError('Cannot load directives after finalization'); + $config->loadIni(dirname(__FILE__) . '/ConfigTest-finalize.ini'); + + } + } ?> \ No newline at end of file diff --git a/tests/HTMLPurifier/EncoderTest.php b/tests/HTMLPurifier/EncoderTest.php index ef14b139..0bab873e 100644 --- a/tests/HTMLPurifier/EncoderTest.php +++ b/tests/HTMLPurifier/EncoderTest.php @@ -39,7 +39,9 @@ class HTMLPurifier_EncoderTest extends UnitTestCase ); $this->assertNoErrors(); - $config->set('Core', 'Encoding', 'ISO-8859-1'); + $config = HTMLPurifier_Config::create(array( + 'Core.Encoding' => 'ISO-8859-1' + )); // Now it gets converted $this->assertIdentical( @@ -47,8 +49,10 @@ class HTMLPurifier_EncoderTest extends UnitTestCase "\xC3\xB6" ); - $config->set('Test', 'ForceNoIconv', true); - + $config = HTMLPurifier_Config::create(array( + 'Core.Encoding' => 'ISO-8859-1', + 'Test.ForceNoIconv' => true + )); $this->assertIdentical( HTMLPurifier_Encoder::convertToUTF8("\xF6", $config, $context), "\xC3\xB6" @@ -69,7 +73,9 @@ class HTMLPurifier_EncoderTest extends UnitTestCase "\xC3\xB6" ); - $config->set('Core', 'Encoding', 'ISO-8859-1'); + $config = HTMLPurifier_Config::create(array( + 'Core.Encoding' => 'ISO-8859-1' + )); // Now it gets converted $this->assertIdentical( @@ -86,7 +92,10 @@ class HTMLPurifier_EncoderTest extends UnitTestCase } // Plain PHP implementation has slightly different behavior - $config->set('Test', 'ForceNoIconv', true); + $config = HTMLPurifier_Config::create(array( + 'Core.Encoding' => 'ISO-8859-1', + 'Test.ForceNoIconv' => true + )); $this->assertIdentical( HTMLPurifier_Encoder::convertFromUTF8("\xC3\xB6", $config, $context), "\xF6" @@ -98,8 +107,10 @@ class HTMLPurifier_EncoderTest extends UnitTestCase ); // Preserve the characters! - - $config->set('Core', 'EscapeNonASCIICharacters', true); + $config = HTMLPurifier_Config::create(array( + 'Core.Encoding' => 'ISO-8859-1', + 'Core.EscapeNonASCIICharacters' => true + )); $this->assertIdentical( HTMLPurifier_Encoder::convertFromUTF8($chinese, $config, $context), "中文 (Chinese)" diff --git a/tests/HTMLPurifier/URISchemeRegistryTest.php b/tests/HTMLPurifier/URISchemeRegistryTest.php index 21a24348..8edc3bdc 100644 --- a/tests/HTMLPurifier/URISchemeRegistryTest.php +++ b/tests/HTMLPurifier/URISchemeRegistryTest.php @@ -9,9 +9,10 @@ class HTMLPurifier_URISchemeRegistryTest extends UnitTestCase generate_mock_once('HTMLPurifier_URIScheme'); - $config = HTMLPurifier_Config::createDefault(); - $config->set('URI', 'AllowedSchemes', array('http' => true, 'telnet' => true)); - $config->set('URI', 'OverrideAllowedSchemes', true); + $config = HTMLPurifier_Config::create(array( + 'URI.AllowedSchemes' => 'http, telnet', + 'URI.OverrideAllowedSchemes' => true + )); $context = new HTMLPurifier_Context(); $registry = new HTMLPurifier_URISchemeRegistry(); @@ -34,7 +35,10 @@ class HTMLPurifier_URISchemeRegistryTest extends UnitTestCase $this->assertIdentical($registry->getScheme('foobar', $config, $context), $scheme_foobar); // now, test when overriding is not allowed - $config->set('URI', 'OverrideAllowedSchemes', false); + $config = HTMLPurifier_Config::create(array( + 'URI.AllowedSchemes' => 'http, telnet', + 'URI.OverrideAllowedSchemes' => false + )); $this->assertNull($registry->getScheme('foobar', $config, $context)); // scheme not allowed and never registered