--- customerror.module	2010-08-09 16:33:37.000000000 +0100
+++ customerror.module	2010-08-10 15:35:02.000000000 +0100
@@ -54,6 +54,9 @@ function customerror_help($path, $arg) {
   return $output;
 }
 
+/**
+ * Form builder function for admin settings.
+ */
 function customerror_admin_settings() {
   $form = array(
     'customerror_form_description' => array(
@@ -100,23 +103,97 @@ function customerror_admin_settings() {
     );
   }
 
-  $form['redirects'] = array(
+  $redirects = variable_get('customerror_redirects', array());
+
+  $form['customerror_redirects'] = array(
     '#type'            => 'fieldset',
     '#title'           => t('Redirects'),
-    '#collapsed'       => TRUE,
+    '#collapsed'       => empty($redirects) ? TRUE : FALSE, // If there is atleast one redirect value then keep the fieldset open.
     '#collapsible'     => TRUE,
+    '#tree'            => TRUE,
+    '#theme'           => 'customerror_redirects_table',
+    '#description'     => t('Each custom redirect is stored as a pair - the original "old" source page and a "new" destination. When a visitor tries to visit the old page they will automatically be taken to the new destination page thus avoiding a "404 not found". The old page can be a regular expression to match with any part of the path. If you are unfamilar with regular expressions, a simple search string will work, but will match any part of the URL, for example <em>index.html</em> will match both <em>http://example.com/index.html &amp; http://example.com/archive/index.html</em>. The keyword <em>&lt;front></em> is allowed as a destination, to take the visitor to the front page.'),
   );
-  $form['redirects']['customerror_redirect'] = array(
-    '#type'            => 'textarea',
-    '#title'           => t('Redirect list'),
-    '#default_value'   => variable_get('customerror_redirect', ''),
-    '#rows'            => 10,
-    '#description'     => t('These are custom redirect pairs, one per line. Each pair requires a path to match (which is a regular expression) and a destination separated by a space. The keyword <em>&lt;front></em> is allowed as a destination. If you are unfamilar with regular expressions, a simple search string will work, but will match any part of the URl. For example <em>index.html &lt;front></em> will match both <em>http://example.com/index.html &amp; http://example.com/archive/index.html</em>.'),
+
+  // Define a template, to make it easy to create/reset the text field form items.
+  $textfield_template = array(
+    '#type'          => 'textfield',
+    '#default_value' => '',
+    '#size'          => 30,
+    '#maxlength'     => 100,
   );
 
+  $key = 0;
+  foreach ($redirects as $item) {
+    $key ++;
+    $form['customerror_redirects'][$key]['src'] = $textfield_template;
+    $form['customerror_redirects'][$key]['src']['#default_value'] = $item['src'];
+    $form['customerror_redirects'][$key]['dst'] = $textfield_template;
+    $form['customerror_redirects'][$key]['dst']['#default_value'] = $item['dst'];
+  }
+
+  // Add some empty rows for new entries.
+  for ($i = $key + 1; $i <= ($key + 5); $i++) {
+    $form['customerror_redirects'][$i]['src'] = $textfield_template;
+    $form['customerror_redirects'][$i]['dst'] = $textfield_template;
+  }
+
+  // On submit, make changes to the settings before passing them off to system_settings_form_submit().
+  $form['#submit'][] = 'customerror_admin_settings_submit';
+
   return system_settings_form($form);
 }
 
+/**
+ * Theme the 'customer_redirects' fieldset.
+ */
+function theme_customerror_redirects_table($form) {
+  // Display the redirect pairs in a table.
+  $header = array(t('Number'), t('Old page or path'), t('New destination'));
+  foreach (element_children($form) as $key) {
+    $row = array();
+    $row[] = $key;
+    $row[] = drupal_render($form[$key]['src']);
+    $row[] = drupal_render($form[$key]['dst']);
+    $rows[] = $row;
+  }
+  $output = theme('table', $header, $rows);
+  $output .= drupal_render($form);
+  return $output;
+}
+
+/**
+ * Form validation handler for admin settings form.
+ */
+function customerror_admin_settings_validate($form, &$form_state) {
+  // Check that both parts of the pair have values.
+  $redirects = $form_state['values']['customerror_redirects'];
+  foreach ($redirects as $key => $item) {
+    // If there is a source but no destination (or vice versa) then mark it as an error.
+    if (empty($item['src']) && !empty($item['dst'])) {
+      form_set_error("customerror_redirects][$key][src",
+        t('Please enter an old page value for the new destination %dst.', array('%dst' => $item['dst'])));
+    }
+    if (empty($item['dst']) && !empty($item['src'])) {
+      form_set_error("customerror_redirects][$key][dst",
+        t('Please enter a new destination value for the old page %src.', array('%src' => $item['src'])));
+    }
+  }
+}
+
+/**
+ * Form submit handler for admin settings form.
+ */
+function customerror_admin_settings_submit($form, &$form_state) {
+  // Remove all the unused field pairs.
+  foreach ($form_state['values']['customerror_redirects'] as $key => $item) {
+    if (empty($item['src'])) {
+      unset($form_state['values']['customerror_redirects'][$key]);
+    }
+  }
+  return;
+}
+
 function customerror_menu() {
   $items = array();
 
@@ -197,11 +274,14 @@ function customerror_theme() {
     'customerror' => array(
       'arguments' => array('code', 'content')
     ),
+    'customerror_redirects_table' => array(
+      'arguments' => array('form' => NULL),
+    ),
   );
 }
 
 /**
- * Themeable function 
+ * Themeable function
  */
 function theme_customerror($code, $content) {
   return $content;
@@ -221,28 +301,22 @@ function customerror_user($op, $edit, $u
   }
 }
 
+/**
+ * Check the requested page against the list of custom redircts.
+ * If there is a match then change the destination and goto that page.
+ */
 function customerror_check_redirect() {
   $destination = $_REQUEST['destination'];
   if (empty($destination)) {
     return;
   }
+  $destination = str_replace(" ", "%20", $destination); // In case there are spaces in the requested URL, we escape them.
 
-  $list = explode("\n", variable_get('customerror_redirect', ''));
-  if (count($list) <= 1) {
-    return;
-  }
-
-  foreach($list as $item) {
-    list($src, $dst) = explode(' ', $item);
-    if (isset($src) && isset($dst)) {
-      $src = str_replace("/", "\\/", $src);
-      $dst = str_replace("\r", "", $dst);
-      // In case there are spaces in the URL, we escape them
-      $orig_dst = str_replace(" ", "%20", $destination);
-      if (preg_match('/'. $src .'/', $orig_dst)) {
-        $_REQUEST['destination'] = $dst;
-        drupal_goto($dst);
-      }
+  $redirects = variable_get('customerror_redirects', array());
+  foreach ($redirects as $item) {
+    if (preg_match('/'. str_replace("/", "\\/", $item['src']) .'/', $destination)) {
+      $_REQUEST['destination'] = $item['dst'];
+      drupal_goto($item['dst']);
     }
   }
 }
--- customerror.install	2008-09-13 03:56:30.000000000 +0100
+++ customerror.install	2010-08-10 14:41:46.000000000 +0100
@@ -10,3 +10,31 @@ function customerror_uninstall() {
     }
   }
 }
+
+/**
+ * Implementation of hook_update_N().
+ * Convert from plain text variable (customerror_redirect) to a structured array (customerror_redirect)
+ */
+function customerror_update_6101() {
+  $ret = array();
+
+  if ($text_redirects = variable_get('customerror_redirect', NULL)) {
+    // A value exists in the old plain-text variable, so convert it into a structured array.
+    $customerror_redirects = array();
+    $list = explode("\n", $text_redirects);
+    foreach ($list as $item) {
+      list($src, $dst) = explode(' ', $item);
+      if (isset($src) || isset($dst)) {
+        $customerror_redirects[] = array('src' => trim(strip_tags($src)), 'dst' => trim(strip_tags($dst, '<front>')));
+      }
+    }
+    variable_set('customerror_redirects', $customerror_redirects);
+    $ret[] = array('success' => TRUE, 'query' => "variable_set('customerror_redirects' ...) with value created from plain text variable 'customerror_redirect'");
+  }
+
+  // Remove the plain-text variable.
+  variable_del('customerror_redirect');
+  $ret[] = array('success' => TRUE, 'query' => "variable_del('customerror_redirect') - remove old variable");
+
+  return $ret;
+}
