PX : code

Message Filter by Steve Edberg
Download this code


<?php
class MessageFilter {

##===============================================
#
#  Version 1.0beta (1 June 1999)
#
## PURPOSE:
#
#     This class is used to edit and/or censor HTML in user entries.
#
#     The three methods - check_length(), check_words(), and
#  check_html() - can be used separately or the message can be
#  validated completely via check_all().
#     Each function returns a two-element array
#        ((int) <StatusCode>, (str) <EditedMessage>),
#  where
#        StatusCode     (negative -> error; zero -> OK; postive -> informational message)
#        EditedMessage     (the result of the function)
#  The error message can be recovered via $ObjectHandle->Messages[$ReturnedErrorCode].
#
#     Also, if check_html() finds any mismatched tags (according to the definitions in
#  $AllowableHTML), a list is put into $ObjectHandle->ProblemTags.
#
## MODIFICATION HISTORY:
#
#  Steve Edberg <sbedberg@ucdavis.edu>, May 1999
#     Original
##===============================================

##===============================================
## Public Variables
##===============================================

#  Parameters for check_length (non-positive values mean 'no limit')

   
var $MaxTextLength   0;     # Maximum # of displayable characters, ignoring HTML
   
var $MaxTotalLength  0;     # Max # of characters total

#  Parameters for check_words

   
var $CensorList   = array(    # List of words to remove (case insignificant; may contain POSIX regular expression)
      
);
   var 
$CensorMode   2;        # 1: exact match
                                 # 2: match word beginnings
                                 # 3: match string anywhere in words
   
var $CensorReplace   =        # Text to replace censored word with
      
'[deleted]';

   var 
$CensorCount     0;     # (OUTPUT) Number of words censored

#  Parameters for check_html

   
var $AllowableHTML   = array( # List of tags to allow; see notes for check_html() for more info
      
);

   var 
$ProblemTags     '';    # (OUTPUT) List of mismatched or badly-nested tags found

#  Other parameters

   
var $Messages = array(        # (OUTPUT) List of error/info messages
         
0  => 'Success',
         -
=> 'Text length too long.',
         -
=> 'Text and HTML string length too long',
         -
=> 'Unmatched or badly nested tags',
         -
=> 'Badly formed tags (stray &lt;s or &gt;s)',
         
1  => 'Empty string',
         
2  => 'Some words were censored',
         
3  => 'Some HTML was edited out'
      
);

##===============================================
## Private Variables
##===============================================

##===============================================
## Public Methods
##===============================================

   
function init($VarList) {

#     Where $VarList = array('<varname>'=>'<value'>, ...);
#     Just a quick way to set all parameters at once; variables are not defined as
#     Public are ignored. This relies on the fact that all public vars are initialized above.

      
if (is_array($VarList)) {
         while (list(
$var$value) = each($VarList)) {
            if (  isset(
$this->{$var}) &&
               
substr($var01) != '_')    # vars starting with '_' are private
            
{                                # by my convention
               
$this->{$var} = $value;
            }
         }
      }
      return;

   }

##-----------------------------------------------

   
function check_all($Message) {

#     Returns only status code/message of last function executed

      
list($StatusCode$EditedMessage) = $this->check_length($Message);
      if (
$StatusCode >= 0) {
         list(
$StatusCode$EditedMessage) = $this->check_words($EditedMessage);
      }
      if (
$StatusCode >= 0) {
         list(
$StatusCode$EditedMessage) = $this->check_html($EditedMessage);
      }

      return array(
$StatusCode$EditedMessage);
   }

##-----------------------------------------------

   
function check_length($Message) {

#     The total length is checked first; if that test fails, text-only length test is not done.

      
$StatusCode    0;
      
$EditedMessage $Message;

      if (
strlen($EditedMessage) == 0) {

         
$StatusCode    1;

      } elseif (
$this->MaxTotalLength && strlen($Message) > $this->MaxTotalLength) {

         
$StatusCode    = -2;
         
$EditedMessage substr($Message0$this->MaxTotalLength);

      } elseif (  
$this->MaxTextLength &&
                  
strlen(ereg_replace('<[^>]*>'''$Message)) > $this->MaxTextLength) {

         
$StatusCode    = -1;
         
$EditedMessage '';

      }

      return array(
$StatusCode$EditedMessage);
   }

##-----------------------------------------------

   
function check_words($Message) {

#     The regexp for censoring the message was based on one found in an early
#     beta of Phorum version 2 (http://www.phorum.org/) by Brian Moon et. al.

      
$StatusCode          0;
      
$EditedMessage       $Message;
      
$this->CensorCount   0;

      if (
strlen($Message) == 0) {

         
$StatusCode 1;

      } elseif (
is_array($this->CensorList)) {

         
$Replacement $this->CensorReplace;

         if (
$this->CensorMode == 1) {       # Exact match
            
$RegExPrefix   '([^[:alpha:]]|^)';
            
$RegExSuffix   '([^[:alpha:]]|$)';
         } elseif (
$this->CensorMode == 2) {    # Word beginning
            
$RegExPrefix   '([^[:alpha:]]|^)';
            
$RegExSuffix   '[[:alpha:]]*([^[:alpha:]]|$)';
         } elseif (
$this->CensorMode == 3) {    # Word fragment
            
$RegExPrefix   '([^[:alpha:]]*)[[:alpha:]]*';
            
$RegExSuffix   '[[:alpha:]]*([^[:alpha:]]*)';
         }

         for (
$i 0$i count($this->CensorList) && $RegExPrefix != ''$i++) {
            
$EditedMessage eregi_replace(  $RegExPrefix.$this->CensorList[$i].$RegExSuffix,
                                             
"\\1$Replacement\\2",
                                             
$EditedMessage
                           
);
         }

         if (
$EditedMessage != $Message) { $StatusCode 2; }

         
$Bits explode($Replacement$EditedMessage);  # count number of replacements; you may want to reject
         
$this->CensorCount count($Bits) - 1;          # a message if it has more than <x> words censored.

      
}

      return array(
$StatusCode$EditedMessage);
   }

##-----------------------------------------------

   
function check_html($Message) {

#     Each element in the AllowableHTML array is the tag body, without the <> (eg; '/i' for the </I> tag), and case
#     is irrelevant. You may include Posix regexps in the tags. For example, you can use the '.*' (match 0 or more
#     of any character) construct so that
#              'a href=.*'
#     matches
#              <A HREF="it/does/not/matter/what" comes=next until-the=greater-than-sign>
#     Improper tag nesting (eg; '<i>...<b>...</i>...</b'>) is detected as well.
#
#     $AllowableHTML = array(array(<start tag>, <end tag, if any>, <tag name, if desired>), ...)

      
$StatusCode    0;
      
$EditedMessage $Message;

      
$BadTags       ereg_replace('[^<>]'''$Message);
      
$BadTags       ereg_replace('<>'''$BadTags);

      if (
strlen($Message) == 0) {

         
$StatusCode 1;

      } elseif (
strlen($BadTags) > 0) {

         
$StatusCode = -4;

      } elseif (
is_array($this->AllowableHTML)) {

#        This algorithm, relies on the fact that this regexp will match a leading null
#        (that is, when the first character in $Message is a < or >), so that $MessageParts[0]
#        will never be '<', and all the odd-numbered indices of $MessageParts will contain
#        the text within the HTML tags.

         
$MessageParts        split('<[[:space:]]*|[[:space:]]*>'$Message);
         
$EditedMessage       $MessageParts[0];
         
$this->ProblemTags   '';
         unset(
$TagStack);

         for (
$i=1$i count($MessageParts); $i++) {

            if (
$i%2) {    # Odd number - tag text!

               
reset($this->AllowableHTML);
               
$TagOk 0;
               while (list( , 
$stuff) = each($this->AllowableHTML)) {
                  list(
$start$end$tagname) = $stuff;

                  if(
$start != '' && eregi("^$start\$"$MessageParts[$i])) {

                     
$TagOk         1;
                     
$EditedMessage .= '<'.$MessageParts[$i].'>';
                     if (
$end != '') { $TagStack[] = $start; }

                  } elseif (
$end != '') {

                     if(
eregi("^$end\$"$MessageParts[$i])) {

                        
$TagOk         1;
                        
$EditedMessage .= '<'.$MessageParts[$i].'>';

                        
$x count($TagStack)-1;

                        if (
$TagStack[$x] != $start) {
                           
$TagStack[] = $end;
                        } else {
                           
$TagStack[$x] = '';
# I tried to use
#     unset($TagStack[$x]);
# here, but PHP (3.0.6 under WinNT4.0sp3) died ('Document contains no data' in
# Netscape, no error logged) on unset($TagStack[0]). This means I need an extra
# step below to clean out blank elements.

                        
}

                     }

                  }

                  if (!
$TagOk) { $StatusCode 3; }

               }

            } else {       
# Even - plain text

               
$EditedMessage .= $MessageParts[$i];

            }

         }

         if (
is_array($TagStack)) {    # Clean out all blank elements in stack
            
unset($Temp);
            
reset($TagStack);
            while (list(
$i$value) = each($TagStack)) {
               if (
$value != '') { $Temp[] = $value; }
            }
            if (
is_array($Temp)) {
               
$StatusCode          = -3;
               
$this->ProblemTags   implode(','$Temp);
            }
         }

      }

      return array(
$StatusCode$EditedMessage);

   }

##===============================================
## Private Methods
##===============================================

};
?>

Comments or questions?
PX is running PHP 5.2.17
Thanks to Miranda Productions for hosting and bandwidth.
Use of any code from PX is at your own risk.