Index: invisimail.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/invisimail/invisimail.module,v
retrieving revision 1.3.4.7
diff -u -p -r1.3.4.7 invisimail.module
--- invisimail.module	18 Jun 2009 21:25:01 -0000	1.3.4.7
+++ invisimail.module	29 Jun 2009 21:48:47 -0000
@@ -1,6 +1,8 @@
 <?php
 // $Id: invisimail.module,v 1.3.4.7 2009/06/18 21:25:01 crell Exp $
 
+define('INVISIMAIL_MAILTO_ASCII', '&#109;&#97;&#105;&#108;&#116;&#111;&#58;');
+
 /**
  * @file
  * This module provides a filter that will search content for email addresses
@@ -98,28 +100,111 @@ function invisimail_filter($op, $delta =
 }
 
 function invisimail($string, $format) {
-  $pattern = '!(<p>|<li>|<br />|[\s\(])([A-Za-z0-9._-]+@[A-Za-z0-9._-]+\.[A-Za-z]{2,4})([.,]?)(?=(</p>|</li>|<br />|[\s/)]))!i';
-  // the callback needs to know what filter we're using
-  // however there's no way to hand off that variable
-  // so we'll set a global variable
-  $GLOBALS['invisimail_format'] = $format;
+  // The check for the user/name portion of the email address. This is
+  // encapsulable regex that looks for at least one valid character (in most
+  // cases, a space), preceded by one invalid character, followed by at least
+  // one valid character.
+  $valid_user_chars = 'a-zA-Z0-9_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\'';
+  $user = "(?<![$valid_user_chars])[$valid_user_chars]+";
+  // For the domain portion of an email addy, you can have a string domain,
+  // an ipv4 address, or an ipv6 address. These three regex capture each of
+  // those possibilities, respectively.
+  $domain = '(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.?)+';
+  $ipv4 = '[0-9]{1,3}(?:\.[0-9]{1,3}){3}';
+  $ipv6 = '[0-9a-fA-F]{1,4}(?:[0-9a-fA-F]{1,4}){7}';
+  // Now we stick it all together into a generalized, encapsulated, portable,
+  // and non-subitem-capturing (hence all the '(?:', which mark subpatterns as
+  // non-capturing) regex for grabbing email addresses.
+  $mail = "(?:$user)+\@(?:$domain|(?:\[(?:$ipv4|$ipv6)\]))";
+  // PCRE pattern modifiers; 'i' enables case-insensitivity, and 'S' enables
+  // the additional pattern analysis, as our regex is one that can benefit
+  // (it is a non-anchored pattern without a single fixed starting character.
+  // see http://us2.php.net/manual/en/reference.pcre.pattern.modifiers.php).
+  // Global case insensitivity is a little sloppy to use, but selectively
+  // toggling it within only some of the subpatterns isn't really worth the
+  // effort.
+  $modifiers = 'iS';
+  // The final pattern. We deal with these as an entire group because invisimail
+  // allows options that require us to deal with both an href and its text
+  // in relation to one another.
+  $pattern = "@(<a .*href=['\"](mailto:$mail)['\"].*>)?((?>(?<!mailto:))$mail)@$modifiers";
+
+  // The callback needs to know about the filter we're using, but there's no way
+  // to hand off that variable through preg_replace_callback. So we set it as a
+  // global variable.
+  $GLOBALS['invisimail_format'] = array(
+    'js' => variable_get('invisimail_js_'.$format, FALSE),
+    'link' => variable_get('invisimail_link_'.$format, FALSE),
+  );
 
   return preg_replace_callback($pattern, 'invisimail_callback', $string);
 }
 
+/**
+ * Callback function used by preg_replace_callback.
+ *
+ * Expects the regex pattern defined in invisimail() to be used.
+ *
+ * @param array $matches
+ *  An array of matched patterns from our regex. Any match will have four keys,
+ *  numbered 0-3, which correspond to the capturing groups in our regex. For all
+ *  these examples, assume the following string:
+ *  <p><a href="mailto:user@domain.com">user@domain.com</a></p>
+ *    - 0: The entire matched string. This will include the email address, as
+ *      well as that email address' corresponding href mailto iff it exists. In
+ *      the example, it would be
+ *        <a href="mailto:user@domain.com">user@domain.com
+ *    - 1: The full string of the <a> tag portion of the match, iff that portion
+ *      was matched; otherwise an empty string. In the example, it would be
+ *        <a href="mailto:user@domain.com">
+ *    - 2: The mailto + email address string from the href portion of the match,
+ *      iff an href was found. In the example, it would be:
+ *        mailto:user@domain.com
+ *    - 3: The email address itself. In the example, just:
+ *      user@domain.com
+ *
+ * @return string
+ *   The full ASCII-encoded string that will replace the entire match.
+ */
 function invisimail_callback($matches) {
   $format = $GLOBALS['invisimail_format'];
-  $js = variable_get('invisimail_js_'.$format, FALSE);
-  $link = variable_get('invisimail_link_'.$format, FALSE);
 
-  return $matches[1] . invisimail_ascii_encode($matches[2], $js, $link) . $matches[3];
+  // If $matches[1] is empty, no link portion was matched for this item, so
+  // it's a simple replace operation.
+  if (empty($matches[1])) {
+    return invisimail_ascii_encode($matches[3], $format['js'], $format['link']);
+  }
+  // We DO have an existing link portion. Do our best to preserve it; that means
+  // ignoring the js setting, since it makes for heinous string transformations.
+  return str_replace(array($matches[2], $matches[3]),
+    array(_invisimail_encode_string($matches[2], FALSE), _invisimail_encode_string($matches[3], FALSE)),
+    $matches[0]);
+}
+
+function invisimail_encode_add_link($string, $js, $text) {
+  $encode = _invisimail_encode_string($string, $js);
+
+  $text = is_null($text) ? $encode : $text;
+
+  if ($js) {
+    $output = "<script type='text/javascript'><!--
+    document.write('";
+    $output .= '<a href="' . INVISIMAIL_MAILTO_ASCII . "'+'$encode'+'\">'+'$text'+'</a>";
+    $output .= "');
+    //-->
+    </script>";
+  }
+  else {
+    $output = '<a href="' . INVISIMAIL_MAILTO_ASCII . "$encode\">$text</a>";
+  }
+  return $output;
 }
 
 /**
- * This function does the actual encoding.
+ * ASCII-encode an email address.
  *
  * @param $string
- *   A string which contains only an email addres to be encoded.
+ *   A string containing _only_ an email address to be encoded.
  * @param $js
  *   Optional: A boolean which sets whether Javascript is used for encoding.
  * @param $link
@@ -127,20 +212,22 @@ function invisimail_callback($matches) {
  * @param $text
  *   Optional: The text to be used for the link.
  * @return
- *   An ascii encoded email address.
+ *   An ascii encoded email address, optionally with link included.
  */
 function invisimail_ascii_encode($string, $js = FALSE, $link = FALSE, $text = NULL) {
+  return $link ? invisimail_encode_add_link($string, $js, $text) : _invisimail_encode_string($string, $js);
+}
 
-  if ($text == NULL) {
-    $text = $string;
-  }
-
-  if ($js) {
-    $output = "<script type='text/javascript'><!--
-    document.write('";
-  }
-
-  for ($i=0; $i < strlen($string); $i++)	 {
+/**
+ * Performs ASCII encoding on the provided string.
+ *
+ * @param string $string
+ * @param bool $js
+ * @return string
+ */
+function _invisimail_encode_string($string, $js) {
+  $encode = '';
+  for ($i=0; $i < strlen($string); $i++)   {
     $char = substr($string, $i, 1);
     $encode .= '&#'.ord($char).';';
     if ($js) {
@@ -150,25 +237,7 @@ function invisimail_ascii_encode($string
       }
     }
   }
-
-  if ($link && !$js) {
-    // ascii in this next line is "mailto:"
-    $output .= "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;$encode\">$encode</a>";
-  }
-  elseif ($link && $js) {
-    $output .= "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;'+'$encode'+'\">'+'$encode'+'</a>";
-  }
-  else {
-    $output .= $encode;
-  }
-
-  if ($js) {
-    $output .= "');
-    //-->
-    </script>";
-  }
-
-  return $output;
+  return $encode;
 }
 
 
@@ -217,7 +286,7 @@ function theme_invisimail_formatter_invi
  * Settings form for the invisimail CCK formatter.
  *
  */
-function invismail_formatter_settings() {
+function invisimail_formatter_settings() {
   return system_settings_form(_invisimail_settings('formatter'));
 }
 
