Refactor the HTML Corrector.

From: damz <damz@dev.local.local>


---

 filter/filter.module  |   77 +++++++------------------------------------------
 filter/filter.test    |   59 +++++++++++++++++++-------------------
 system/system.install |   11 +++++++
 3 files changed, 52 insertions(+), 95 deletions(-)

diff --git modules/filter/filter.module modules/filter/filter.module
index 54ed78a..8dc08c5 100644
--- modules/filter/filter.module
+++ modules/filter/filter.module
@@ -746,74 +746,19 @@ function _filter_url($text, $format) {
  * Scan input and make sure that all HTML tags are properly closed and nested.
  */
 function _filter_htmlcorrector($text) {
-  // Prepare tag lists.
-  static $no_nesting, $single_use;
-  if (!isset($no_nesting)) {
-    // Tags which cannot be nested but are typically left unclosed.
-    $no_nesting = drupal_map_assoc(array('li', 'p'));
-
-    // Single use tags in HTML4
-    $single_use = drupal_map_assoc(array('base', 'meta', 'link', 'hr', 'br', 'param', 'img', 'area', 'input', 'col', 'frame'));
+  // DOM can load HTML soup. But, HTML soup can throw warnings, suppress them.
+  @$htmlDom = DOMDocument::loadHTML('<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>' . $text . '</body></html>');
+
+  // The result of DOMDocument->saveXML($bodyNode) is a partial (X)HTML document.
+  // We only need what is inside the body tag.
+  // The "s" modifier makes "." match newlines too.
+  $bodyNode = $htmlDom->getElementsByTagName('body')->item(0);
+  if (preg_match("|^<body[^>]*>(.*)</body>$|s", $htmlDom->saveXML($bodyNode), $matches)) {
+    return $matches[1];
   }
-
-  // Properly entify angles.
-  $text = preg_replace('!<([^a-zA-Z/])!', '&lt;\1', $text);
-
-  // Split tags from text.
-  $split = preg_split('/<([^>]+?)>/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
-  // Note: PHP ensures the array consists of alternating delimiters and literals
-  // and begins and ends with a literal (inserting $null as required).
-
-  $tag = FALSE; // Odd/even counter. Tag or no tag.
-  $stack = array();
-  $output = '';
-  foreach ($split as $value) {
-    // Process HTML tags.
-    if ($tag) {
-      list($tagname) = explode(' ', strtolower($value), 2);
-      // Closing tag
-      if ($tagname{0} == '/') {
-        $tagname = substr($tagname, 1);
-        // Discard XHTML closing tags for single use tags.
-        if (!isset($single_use[$tagname])) {
-          // See if we possibly have a matching opening tag on the stack.
-          if (in_array($tagname, $stack)) {
-            // Close other tags lingering first.
-            do {
-              $output .= '</' . $stack[0] . '>';
-            } while (array_shift($stack) != $tagname);
-          }
-          // Otherwise, discard it.
-        }
-      }
-      // Opening tag
-      else {
-        // See if we have an identical 'no nesting' tag already open and close it if found.
-        if (count($stack) && ($stack[0] == $tagname) && isset($no_nesting[$stack[0]])) {
-          $output .= '</' . array_shift($stack) . '>';
-        }
-        // Push non-single-use tags onto the stack
-        if (!isset($single_use[$tagname])) {
-          array_unshift($stack, $tagname);
-        }
-        // Add trailing slash to single-use tags as per X(HT)ML.
-        else {
-          $value = rtrim($value, ' /') . ' /';
-        }
-        $output .= '<' . $value . '>';
-      }
-    }
-    else {
-      // Passthrough all text.
-      $output .= $value;
-    }
-    $tag = !$tag;
-  }
-  // Close remaining tags.
-  while (count($stack) > 0) {
-    $output .= '</' . array_shift($stack) . '>';
+  else {
+    return "";
   }
-  return $output;
 }
 
 /**
diff --git modules/filter/filter.test modules/filter/filter.test
index 0a37b95..01e031a 100644
--- modules/filter/filter.test
+++ modules/filter/filter.test
@@ -178,24 +178,18 @@ class FilterAdminTestCase extends DrupalWebTestCase {
   }
 }
 
-class FilterTestCase extends DrupalWebTestCase {
-  protected $format;
-
+/**
+ * Unit tests for core filters.
+ */
+class FilterUnitTest extends DrupalWebTestCase {
   function getInfo() {
     return array(
       'name' => t('Core filters'),
-      'description' => t('Filter each filter individually: Convert URLs into links, Convert line breaks, Correct broken HTML, Escape all HTML, Limit allowed HTML tags.'),
+      'description' => t('Filter each filter individually: Convert line breaks, Correct broken HTML.'),
       'group' => t('Filter'),
     );
   }
 
-  function setUp() {
-    parent::setUp();
-
-    $admin_user = $this->drupalCreateUser(array('administer filters', 'create page content'));
-    $this->drupalLogin($admin_user);
-  }
-
   /**
    * Test the line break filter
    */
@@ -211,26 +205,33 @@ class FilterTestCase extends DrupalWebTestCase {
   }
 
   /**
-   * Test the HTML filter
+   * Test the HTML Corrector filter.
    */
-  function testHtmlFilter() {
-
-  }
-
-  function createFormat($filter) {
-    $edit = array(
-      'name' => $this->randomName(),
-      'roles[2]' => TRUE,
-      'filters[filter/' . $filter . ']' => TRUE,
+  function testHTMLCorrectorFilter() {
+    $test_case = array(
+      // Convert uppercased tag to proper lowercased ones.
+      '<P>test</P>' => '<p>test</p>',
+      '<P>test</p>' => '<p>test</p>',
+      // Automatically close single tags.
+      'test1<br class="test">test2' => 'test1<br class="test"/>test2',
+      'line1<hr>line2' => 'line1<hr/>line2',
+      '<img src="http://example.com/test.jpg">test</img>' => '<img src="http://example.com/test.jpg"/>test',
+      // Auto-close improperly nested tags.
+      '<p>test<p>test</p>\n' => '<p>test</p><p>test</p>\n',
+      // Properly close unclosed tags, and remove useless closing tags.
+      '<p>Line1<br><STRONG>bold stuff</b>' => '<p>Line1<br/><strong>bold stuff</strong></p>',
+      // Do not touch HTML comments.
+      '<!-- this is a comment -->' => '<!-- this is a comment -->',
+      'test <!-- comment -->' => 'test <!-- comment -->',
+      // New-lines are accepted and kept as-is.
+      '<p>test\n</p>\n' => '<p>test\n</p>\n',
+      // Encoding is correctly kept.
+      '<p>دروبال' => '<p>دروبال</p>'
     );
-    $this->drupalPost('admin/settings/filter/add', $edit, t('Save configuration'));
-    return db_fetch_object(db_query("SELECT * FROM {filter_format} WHERE name = '%s'", $edit['name']));
-  }
-
-  function deleteFormat($format) {
-    if ($format !== NULL) {
-      // Delete new filter.
-      $this->drupalPost('admin/settings/filter/delete/' . $format->format, array(), t('Delete'));
+    foreach ($test_case as $input => $expected_output) {
+      $output = _filter_htmlcorrector($input);
+      $this->assertIdentical($output, $expected_output, t('HTMLCorrector: %input is converted to %output (expected %expected)', array('%input' => $input, '%output' => $output, '%expected' => $expected_output)));
     }
   }
+
 }
diff --git modules/system/system.install modules/system/system.install
index aaf8ed6..7003270 100644
--- modules/system/system.install
+++ modules/system/system.install
@@ -256,6 +256,17 @@ function system_requirements($phase) {
   include_once DRUPAL_ROOT . '/includes/unicode.inc';
   $requirements = array_merge($requirements, unicode_requirements());
 
+  // Verify if the DOM PHP5 extension is available.
+  $has_dom = class_exists('DOMDocument');
+  if (!$has_dom) {
+    $requirements['php_dom'] = array(
+      'title' => $t('PHP DOM Extension'),
+      'value' => $t('Not found'),
+      'severity' => REQUIREMENT_ERROR,
+      'description' => $t("The DOM extension is part of PHP5 core, but doesn't seem to be enabled on your system. You need to enable the DOM extension on your PHP installation."),
+    );
+  }
+
   if ($phase == 'runtime') {
     // Check for update status module.
     if (!module_exists('update')) {
