From 3a1d505b3dd639d453b7e8a5c4dd876d870609cf Mon Sep 17 00:00:00 2001
From: "Edward Z. Yang" <edwardzyang@thewritingpot.com>
Date: Wed, 27 Jun 2007 02:03:15 +0000
Subject: [PATCH] [2.0.1] Implement haphazard error collection for
 AttrValidator. - Error collector / Language can take arrays and listify them
 - AttrValidator takes token by reference - Formatted errors now have their
 severity <strong> - 100 test-cases! W00t!

git-svn-id: http://htmlpurifier.org/svnroot/htmlpurifier/trunk@1250 48356398-32a2-884e-a903-53898d9a118a
---
 NEWS                                          |   4 +-
 art/100cases.png                              | Bin 0 -> 2732 bytes
 library/HTMLPurifier/AttrValidator.php        |  65 +++++++++++++-----
 library/HTMLPurifier/ErrorCollector.php       |   7 +-
 library/HTMLPurifier/Language.php             |  62 +++++++++++++----
 library/HTMLPurifier/Language/messages/en.php |  12 +++-
 .../Strategy/RemoveForeignElements.php        |   2 +-
 .../Strategy/ValidateAttributes.php           |   6 +-
 .../HTMLPurifier/AttrValidator_ErrorsTest.php |  50 ++++++++++++++
 tests/HTMLPurifier/ErrorCollectorTest.php     |   8 +--
 tests/HTMLPurifier/LanguageTest.php           |  32 ++++++++-
 tests/test_files.php                          |   1 +
 12 files changed, 205 insertions(+), 44 deletions(-)
 create mode 100644 art/100cases.png
 create mode 100644 tests/HTMLPurifier/AttrValidator_ErrorsTest.php

diff --git a/NEWS b/NEWS
index c7788e04..7d89e7dc 100644
--- a/NEWS
+++ b/NEWS
@@ -18,8 +18,8 @@ NEWS ( CHANGELOG and HISTORY )                                     HTMLPurifier
 ! Newlines normalized internally, and then converted back to the
   value of PHP_EOL. If this is not desired, set your newline format
   using %Output.Newline.
-! Beta error collection, messages are implemented for Lexer and
-  RemoveForeignElements
+! Beta error collection, messages are implemented for the most generic
+  cases involving Lexing or Strategies
 - Clean up special case code for <script> tags
 - Reorder includes for DefinitionCache decorators, fixes a possible
   missing class error
diff --git a/art/100cases.png b/art/100cases.png
new file mode 100644
index 0000000000000000000000000000000000000000..03103a07e5b19dca4547b9637b10508479722b99
GIT binary patch
literal 2732
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
U|A63+=_lac9c#0ipWL7Q8<vzev;Y7A

literal 0
HcmV?d00001

diff --git a/library/HTMLPurifier/AttrValidator.php b/library/HTMLPurifier/AttrValidator.php
index 6b8ec243..b0e79adc 100644
--- a/library/HTMLPurifier/AttrValidator.php
+++ b/library/HTMLPurifier/AttrValidator.php
@@ -1,12 +1,31 @@
 <?php
 
+/**
+ * Validates the attributes of a token. Doesn't manage required attributes
+ * very well. The only reason we factored this out was because RemoveForeignElements
+ * also needed it besides ValidateAttributes.
+ */
 class HTMLPurifier_AttrValidator
 {
     
-    
-    function validateToken($token, &$config, &$context) {
+    /**
+     * Validates the attributes of a token, returning a modified token
+     * that has valid tokens
+     * @param $token Reference to token to validate. We require a reference
+     *     because the operation this class performs on the token are
+     *     not atomic, so the context CurrentToken to be updated
+     *     throughout
+     * @param $config Instance of HTMLPurifier_Config
+     * @param $context Instance of HTMLPurifier_Context
+     */
+    function validateToken(&$token, &$config, &$context) {
             
         $definition = $config->getHTMLDefinition();
+        $e =& $context->get('ErrorCollector', true);
+        
+        // initialize CurrentToken if necessary
+        $current_token =& $context->get('CurrentToken', true);
+        if (!$current_token) $context->register('CurrentToken', $token);
         
         if ($token->type !== 'start' && $token->type !== 'empty') return $token;
         
@@ -14,21 +33,21 @@ class HTMLPurifier_AttrValidator
         // DEFINITION CALL
         $d_defs = $definition->info_global_attr;
         
-        // copy out attributes for easy manipulation
-        $attr = $token->attr;
+        // reference attributes for easy manipulation
+        $attr =& $token->attr;
         
         // do global transformations (pre)
         // nothing currently utilizes this
         foreach ($definition->info_attr_transform_pre as $transform) {
-            $attr = $transform->transform($attr, $config, $context);
+            $attr = $transform->transform($o = $attr, $config, $context);
+            if ($e && ($attr != $o)) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
         }
         
         // do local transformations only applicable to this element (pre)
         // ex. <p align="right"> to <p style="text-align:right;">
-        foreach ($definition->info[$token->name]->attr_transform_pre
-            as $transform
-        ) {
-            $attr = $transform->transform($attr, $config, $context);
+        foreach ($definition->info[$token->name]->attr_transform_pre as $transform) {
+            $attr = $transform->transform($o = $attr, $config, $context);
+            if ($e && ($attr != $o)) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
         }
         
         // create alias to this element's attribute definition array, see
@@ -36,6 +55,9 @@ class HTMLPurifier_AttrValidator
         // DEFINITION CALL
         $defs = $definition->info[$token->name]->attr;
         
+        $attr_key = false;
+        $context->register('CurrentAttr', $attr_key);
+        
         // iterate through all the attribute keypairs
         // Watch out for name collisions: $key has previously been used
         foreach ($attr as $attr_key => $value) {
@@ -69,9 +91,17 @@ class HTMLPurifier_AttrValidator
             
             // put the results into effect
             if ($result === false || $result === null) {
+                // this is a generic error message that should replaced
+                // with more specific ones when possible
+                if ($e) $e->send(E_ERROR, 'AttrValidator: Attribute removed');
+                
                 // remove the attribute
                 unset($attr[$attr_key]);
             } elseif (is_string($result)) {
+                // generally, if a substitution is happening, there
+                // was some sort of implicit correction going on. We'll
+                // delegate it to the attribute classes to say exactly what.
+                
                 // simple substitution
                 $attr[$attr_key] = $result;
             }
@@ -83,21 +113,24 @@ class HTMLPurifier_AttrValidator
             // others would prepend themselves).
         }
         
+        $context->destroy('CurrentAttr');
+        
         // post transforms
         
-        // ex. <x lang="fr"> to <x lang="fr" xml:lang="fr">
+        // global (error reporting untested)
         foreach ($definition->info_attr_transform_post as $transform) {
-            $attr = $transform->transform($attr, $config, $context);
+            $attr = $transform->transform($o = $attr, $config, $context);
+            if ($e && ($attr != $o)) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
         }
         
-        // ex. <bdo> to <bdo dir="ltr">
+        // local (error reporting untested)
         foreach ($definition->info[$token->name]->attr_transform_post as $transform) {
-            $attr = $transform->transform($attr, $config, $context);
+            $attr = $transform->transform($o = $attr, $config, $context);
+            if ($e && ($attr != $o)) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
         }
         
-        // commit changes
-        $token->attr = $attr;
-        return $token;
+        // destroy CurrentToken if we made it ourselves
+        if (!$current_token) $context->destroy('CurrentToken');
         
     }
     
diff --git a/library/HTMLPurifier/ErrorCollector.php b/library/HTMLPurifier/ErrorCollector.php
index a66209c8..c0254ffc 100644
--- a/library/HTMLPurifier/ErrorCollector.php
+++ b/library/HTMLPurifier/ErrorCollector.php
@@ -26,9 +26,10 @@ class HTMLPurifier_ErrorCollector
      * @param $severity int Error severity, PHP error style (don't use E_USER_)
      * @param $msg string Error message text
      */
-    function send($severity, $msg, $args = array()) {
+    function send($severity, $msg) {
         
-        if (!is_array($args)) {
+        $args = array();
+        if (func_num_args() > 2) {
             $args = func_get_args();
             array_shift($args);
             unset($args[0]);
@@ -94,7 +95,7 @@ class HTMLPurifier_ErrorCollector
         foreach ($errors as $error) {
             list($line, $severity, $msg) = $error;
             $string = '';
-            $string .= $this->locale->getErrorName($severity) . ': ';
+            $string .= '<strong>' . $this->locale->getErrorName($severity) . '</strong>: ';
             $string .= $this->generator->escape($msg); 
             if ($line) {
                 // have javascript link generation that causes 
diff --git a/library/HTMLPurifier/Language.php b/library/HTMLPurifier/Language.php
index c89592ad..ea1a99c9 100644
--- a/library/HTMLPurifier/Language.php
+++ b/library/HTMLPurifier/Language.php
@@ -78,6 +78,25 @@ class HTMLPurifier_Language
         return $this->errorNames[$int];
     }
     
+    /**
+     * Converts an array list into a string readable representation
+     */
+    function listify($array) {
+        $sep      = $this->getMessage('Item separator');
+        $sep_last = $this->getMessage('Item separator last');
+        $ret = '';
+        for ($i = 0, $c = count($array); $i < $c; $i++) {
+            if ($i == 0) {
+            } elseif ($i + 1 < $c) {
+                $ret .= $sep;
+            } else {
+                $ret .= $sep_last;
+            }
+            $ret .= $array[$i];
+        }
+        return $ret;
+    }
+    
     /**
      * Formats a localised message with passed parameters
      * @param $key string identifier of message
@@ -94,22 +113,35 @@ class HTMLPurifier_Language
         $generator = false;
         foreach ($args as $i => $value) {
             if (is_object($value)) {
-                // complicated stuff
-                if (!$generator) $generator = $this->context->get('Generator');
-                // assuming it's a token
-                if (isset($value->name)) $subst['$'.$i.'.Name'] = $value->name;
-                if (isset($value->data)) $subst['$'.$i.'.Data'] = $value->data;
-                $subst['$'.$i.'.Compact'] = 
-                $subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value);
-                // a more complex algorithm for compact representation
-                // could be introduced for all types of tokens. This
-                // may need to be factored out into a dedicated class
-                if (!empty($value->attr)) {
-                    $stripped_token = $value->copy();
-                    $stripped_token->attr = array();
-                    $subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token);
+                if (is_a($value, 'HTMLPurifier_Token')) {
+                    // factor this out some time
+                    if (!$generator) $generator = $this->context->get('Generator');
+                    if (isset($value->name)) $subst['$'.$i.'.Name'] = $value->name;
+                    if (isset($value->data)) $subst['$'.$i.'.Data'] = $value->data;
+                    $subst['$'.$i.'.Compact'] = 
+                    $subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value);
+                    // a more complex algorithm for compact representation
+                    // could be introduced for all types of tokens. This
+                    // may need to be factored out into a dedicated class
+                    if (!empty($value->attr)) {
+                        $stripped_token = $value->copy();
+                        $stripped_token->attr = array();
+                        $subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token);
+                    }
+                    $subst['$'.$i.'.Line'] = $value->line ? $value->line : 'unknown';
+                }
+                continue;
+            } elseif (is_array($value)) {
+                $keys = array_keys($value);
+                if (array_keys($keys) === $keys) {
+                    // list
+                    $subst['$'.$i] = $this->listify($value);
+                } else {
+                    // associative array
+                    // no $i implementation yet, sorry
+                    $subst['$'.$i.'.Keys'] = $this->listify($keys);
+                    $subst['$'.$i.'.Values'] = $this->listify(array_values($value));
                 }
-                $subst['$'.$i.'.Line'] = $value->line ? $value->line : 'unknown';
                 continue;
             }
             $subst['$' . $i] = $value;
diff --git a/library/HTMLPurifier/Language/messages/en.php b/library/HTMLPurifier/Language/messages/en.php
index 00e6b5dc..56969cf0 100644
--- a/library/HTMLPurifier/Language/messages/en.php
+++ b/library/HTMLPurifier/Language/messages/en.php
@@ -5,7 +5,14 @@ $fallback = false;
 $messages = array(
 
 'HTMLPurifier' => 'HTML Purifier',
-'LanguageFactoryTest: Pizza' => 'Pizza', // for unit testing purposes
+
+// for unit testing purposes
+'LanguageFactoryTest: Pizza' => 'Pizza',
+'LanguageTest: List' => '$1',
+'LanguageTest: Hash' => '$1.Keys; $1.Values',
+
+'Item separator' => ', ',
+'Item separator last' => ' and ', // non-Harvard style
 
 'ErrorCollector: No errors' => 'No errors detected. However, because error reporting is still incomplete, there may have been errors that the error collector was not notified of; please inspect the output HTML carefully.',
 'ErrorCollector: At line' => ' at line $line',
@@ -37,6 +44,9 @@ $messages = array(
 'Strategy_FixNesting: Node reorganized'      => 'Contents of $CurrentToken.Compact node reorganized to enforce its content model',
 'Strategy_FixNesting: Node contents removed' => 'Contents of $CurrentToken.Compact node removed',
 
+'AttrValidator: Attributes transformed' => 'Attributes on $CurrentToken.Compact transformed from $1.Keys to $2.Keys',
+'AttrValidator: Attribute removed' => '$CurrentAttr.Name attribute on $CurrentToken.Compact removed',
+
 );
 
 $errorNames = array(
diff --git a/library/HTMLPurifier/Strategy/RemoveForeignElements.php b/library/HTMLPurifier/Strategy/RemoveForeignElements.php
index 591b3faa..993789d7 100644
--- a/library/HTMLPurifier/Strategy/RemoveForeignElements.php
+++ b/library/HTMLPurifier/Strategy/RemoveForeignElements.php
@@ -91,7 +91,7 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
                         $definition->info[$token->name]->required_attr &&
                         ($token->name != 'img' || $remove_invalid_img) // ensure config option still works
                     ) {
-                        $token = $attr_validator->validateToken($token, $config, $context);
+                        $attr_validator->validateToken($token, $config, $context);
                         $ok = true;
                         foreach ($definition->info[$token->name]->required_attr as $name) {
                             if (!isset($token->attr[$name])) {
diff --git a/library/HTMLPurifier/Strategy/ValidateAttributes.php b/library/HTMLPurifier/Strategy/ValidateAttributes.php
index 1c9e09b3..c9d0ef7c 100644
--- a/library/HTMLPurifier/Strategy/ValidateAttributes.php
+++ b/library/HTMLPurifier/Strategy/ValidateAttributes.php
@@ -27,6 +27,9 @@ class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy
         // setup validator
         $validator = new HTMLPurifier_AttrValidator();
         
+        $token = false;
+        $context->register('CurrentToken', $token);
+        
         foreach ($tokens as $key => $token) {
             
             // only process tokens that have attributes,
@@ -36,7 +39,8 @@ class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy
             // skip tokens that are armored
             if (!empty($token->armor['ValidateAttributes'])) continue;
             
-            $tokens[$key] = $validator->validateToken($token, $config, $context);
+            // note that we have no facilities here for removing tokens
+            $validator->validateToken($token, $config, $context);
         }
         
         $context->destroy('IDAccumulator');
diff --git a/tests/HTMLPurifier/AttrValidator_ErrorsTest.php b/tests/HTMLPurifier/AttrValidator_ErrorsTest.php
new file mode 100644
index 00000000..841d0523
--- /dev/null
+++ b/tests/HTMLPurifier/AttrValidator_ErrorsTest.php
@@ -0,0 +1,50 @@
+<?php
+
+require_once 'HTMLPurifier/ErrorsHarness.php';
+require_once 'HTMLPurifier/AttrValidator.php';
+
+class HTMLPurifier_AttrValidator_ErrorsTest extends HTMLPurifier_ErrorsHarness
+{
+    
+    function invoke($input) {
+        $validator = new HTMLPurifier_AttrValidator();
+        $validator->validateToken($input, $this->config, $this->context);
+    }
+    
+    function testAttributesTransformedGlobalPre() {
+        $this->config->set('HTML', 'DefinitionID',
+          'HTMLPurifier_AttrValidator_ErrorsTest::testAttributesTransformedGlobalPre');
+        $def =& $this->config->getHTMLDefinition(true);
+        generate_mock_once('HTMLPurifier_AttrTransform');
+        $transform = new HTMLPurifier_AttrTransformMock();
+        $input = array('original' => 'value');
+        $output = array('class' => 'value'); // must be valid
+        $transform->setReturnValue('transform', $output, array($input, new AnythingExpectation(), new AnythingExpectation()));
+        $def->info_attr_transform_pre[] = $transform;
+        $this->expectErrorCollection(E_NOTICE, 'AttrValidator: Attributes transformed', $input, $output);
+        $token = new HTMLPurifier_Token_Start('span', $input, 1);
+        $this->invoke($token);
+    }
+    
+    function testAttributesTransformedLocalPre() {
+        $this->config->set('HTML', 'TidyLevel', 'heavy');
+        $input = array('align' => 'right');
+        $output = array('style' => 'text-align:right;');
+        $this->expectErrorCollection(E_NOTICE, 'AttrValidator: Attributes transformed', $input, $output);
+        $token = new HTMLPurifier_Token_Start('p', $input, 1);
+        $this->invoke($token);
+    }
+    
+    // to lazy to check for global post and global pre
+    
+    function testAttributeRemoved() {
+        $this->expectErrorCollection(E_ERROR, 'AttrValidator: Attribute removed');
+        $this->expectContext('CurrentAttr', 'foobar');
+        $token = new HTMLPurifier_Token_Start('p', array('foobar' => 'right'), 1);
+        $this->expectContext('CurrentToken', $token);
+        $this->invoke($token);
+    }
+    
+}
+
+?>
\ No newline at end of file
diff --git a/tests/HTMLPurifier/ErrorCollectorTest.php b/tests/HTMLPurifier/ErrorCollectorTest.php
index 1338819b..cbc6e0ff 100644
--- a/tests/HTMLPurifier/ErrorCollectorTest.php
+++ b/tests/HTMLPurifier/ErrorCollectorTest.php
@@ -45,8 +45,8 @@ class HTMLPurifier_ErrorCollectorTest extends UnitTestCase
         $this->assertIdentical($collector->getRaw(), $result);
         
         $formatted_result = 
-            '<ul><li>Warning: Message 2 at line 3</li>'.
-            '<li>Error: Message 1 at line 23</li></ul>';
+            '<ul><li><strong>Warning</strong>: Message 2 at line 3</li>'.
+            '<li><strong>Error</strong>: Message 1 at line 23</li></ul>';
         
         $config = HTMLPurifier_Config::create(array('Core.MaintainLineNumbers' => true));
         
@@ -91,8 +91,8 @@ class HTMLPurifier_ErrorCollectorTest extends UnitTestCase
         $this->assertIdentical($collector->getRaw(), $result);
         
         $formatted_result = 
-            '<ul><li>Error: Message 1</li>'.
-            '<li>Error: Message 2</li></ul>';
+            '<ul><li><strong>Error</strong>: Message 1</li>'.
+            '<li><strong>Error</strong>: Message 2</li></ul>';
         $config = HTMLPurifier_Config::createDefault();
         $this->assertIdentical($collector->getHTMLFormatted($config), $formatted_result);
     }
diff --git a/tests/HTMLPurifier/LanguageTest.php b/tests/HTMLPurifier/LanguageTest.php
index 5bf04b05..dd56fe3c 100644
--- a/tests/HTMLPurifier/LanguageTest.php
+++ b/tests/HTMLPurifier/LanguageTest.php
@@ -7,6 +7,13 @@ class HTMLPurifier_LanguageTest extends UnitTestCase
     
     var $lang;
     
+    function generateEnLanguage() {
+        $factory = HTMLPurifier_LanguageFactory::instance();
+        $config = HTMLPurifier_Config::create(array('Core.Language' => 'en'));
+        $context = new HTMLPurifier_Context();
+        return $factory->create($config, $context);
+    }
+    
     function test_getMessage() {
         $config = HTMLPurifier_Config::createDefault();
         $context = new HTMLPurifier_Context();
@@ -26,7 +33,7 @@ class HTMLPurifier_LanguageTest extends UnitTestCase
         $this->assertIdentical($lang->formatMessage('LanguageTest: Error', array(1=>'fatal', 32)), 'Error is fatal on line 32');
     }
     
-    function test_formatMessage_complexParameter() {
+    function test_formatMessage_tokenParameter() {
         $config = HTMLPurifier_Config::createDefault();
         $context = new HTMLPurifier_Context();
         $generator = new HTMLPurifier_Generator(); // replace with mock if this gets icky
@@ -43,6 +50,29 @@ class HTMLPurifier_LanguageTest extends UnitTestCase
             'Data Token: data>, data&gt;, data&gt;, 23');
     }
     
+    function test_listify() {
+        $lang = $this->generateEnLanguage();
+        $this->assertEqual($lang->listify(array('Item')), 'Item');
+        $this->assertEqual($lang->listify(array('Item', 'Item2')), 'Item and Item2');
+        $this->assertEqual($lang->listify(array('Item', 'Item2', 'Item3')), 'Item, Item2 and Item3');
+    }
+    
+    function test_formatMessage_arrayParameter() {
+        $lang = $this->generateEnLanguage();
+        
+        $array = array('Item1', 'Item2', 'Item3');
+        $this->assertIdentical(
+            $lang->formatMessage('LanguageTest: List', array(1=>$array)),
+            'Item1, Item2 and Item3'
+        );
+        
+        $array = array('Key1' => 'Value1', 'Key2' => 'Value2');
+        $this->assertIdentical(
+            $lang->formatMessage('LanguageTest: Hash', array(1=>$array)),
+            'Key1 and Key2; Value1 and Value2'
+        );
+    }
+    
 }
 
 ?>
\ No newline at end of file
diff --git a/tests/test_files.php b/tests/test_files.php
index b0ee2389..0c503527 100644
--- a/tests/test_files.php
+++ b/tests/test_files.php
@@ -52,6 +52,7 @@ $test_files[] = 'HTMLPurifier/AttrTransform/LangTest.php';
 $test_files[] = 'HTMLPurifier/AttrTransform/LengthTest.php';
 $test_files[] = 'HTMLPurifier/AttrTransform/NameTest.php';
 $test_files[] = 'HTMLPurifier/AttrTypesTest.php';
+$test_files[] = 'HTMLPurifier/AttrValidator_ErrorsTest.php';
 $test_files[] = 'HTMLPurifier/ChildDef/ChameleonTest.php';
 $test_files[] = 'HTMLPurifier/ChildDef/CustomTest.php';
 $test_files[] = 'HTMLPurifier/ChildDef/OptionalTest.php';