Блог → Пишем функцию для фильтрации введенного HTML кода

Вступление

Рано или поздно перед человеком, занимающимся веб-программированием возникает задача в фильтрации введенного пользователем HTML кода. Вот такая задача, однажды, встала и передо мной. Готового решения на тот момент мне найти не удалось. Вся информация, которую я откопал по этому вопросу, как обычно, ограничивалась советами, надо сделать так, надо сделать эдак, но конкретного, работающего решения, почему-то никто не предложил. Особенно порадовал один человек, который сказал, что если он и возьмется написать подобный фильтр, то только за большие деньги. В отличие от этого человека, в этом посте я во всех деталях расскажу как сделать фильтрацию введенного пользователем HTML кода, и в конце, вы получите работающий пример, который сможете применять в своих проектах.

Постановка задачи

Для начала, давайте определимся с тем, что мы хотим получить в конечном итоге. Наш фильтр будет реализован в виде функции. На вход этой функции поступает исходный HTML код, а на выходе имеем профильтрованный HTML код. Выглядит это следующим образом:

sub htmlFilter(){
     my $in = shift() || "";
     my $out;
     return $out
}

В качестве языка программирования будем использовать Perl.

Теперь надо ответить на следующий вопрос. Что должна уметь наша функция? Она должна закрывать все незакрытые тэги, удалять недопустимые тэги, удалять недопустимые атрибуты тэгов, удалять атрибуты, которые имеют недопустимые значения, экранировать специальные символы HTML в тексте и в атрибутах тэгов, переводить все символы имен тэгов и атрибутов в нижний регистр, согласно спецификации XHTML. Как мы видим работу предстоит проделать довольно серьезную.

Шаг №1

Первым делом зададим тэги, которые будет пропускать фильтр.

my %tags = (
     
     "img"=>{
          "CLOSE_TAG"=>0, 
          "ATTR"=>{
               "src"=>{
                    "TYPE"=>"text"
               },
               "align"=>{
                    "TYPE"=>"fixed",
                    "VALUES"=>["left", "right", "center"]
               },
               "width"=>{
                    "TYPE"=>"number"
               },
               "height"=>{
                    "TYPE"=>"number"
               }
          }
     },

     "a"=>{
          "CLOSE_TAG"=>1,
          "ATTR"=>{
               "href"=>{
                    "TYPE"=>"text"
               }
          }
     },

     "span"=>{
          "CLOSE_TAG"=>1,
          "ATTR"=>{
               "class"=>{
                    "TYPE"=>"text"
               }
          }
     },

     "p"=>{
          "CLOSE_TAG"=>1
     },

     "blockquote"=>{
          "CLOSE_TAG"=>1
     },

     "code"=>{
          "CLOSE_TAG"=>1
     },

     "pre"=>{
          "CLOSE_TAG"=>1
     },

     "ul"=>{
          "CLOSE_TAG"=>1
     },

     "ol"=>{
          "CLOSE_TAG"=>1
     },

     "li"=>{
          "CLOSE_TAG"=>1
     }
);

Несмотря на кажущуюся громоздкость приведенного хэша, работать с ним довольно просто.

#Вывод всех допустимых имен тэгов
foreach(keys(%tags)) {
     print $_."\n";
}
	
#Вывод всех ключей конкретного тэга
foreach(keys(%{$tags{"img"}})){
     print $_."\n";
}
	
#Вывод всех допустимых имен аттрибутов конкретного тэга
foreach(keys(%{$tags{"img"}{"ATTR"}})){
     print $_."\n";
}
	
#Вывод типа аттрибута
print $tags{"img"}{"ATTR"}{"src"}{"TYPE"};
	
#Вывод допустимых значений конкретного атрибута
foreach(@{$tags{"img"}{"ATTR"}{"align"}{"VALUES"}}){
     print $_."\n";
}

К тому же этот хэш обеспечивает нам гибкость, в настройке фильтра.

Шаг №2

Следующим шагом зададим спец. символы, которые нужно экранировать.

my %specialChars = (
     "\""=>"&quot;", "<"=>"&lt",      ">"=>"&gt",      "‘"=>"&lsquo;", 
     "’"=>"&rsquo;", "‚"=>"&sbquo;",  "“"=>"&ldquo;",  "”"=>"&rdquo;", 
     "„"=>"&bdquo;", "‹"=>"&lsaquo;", "›"=>"&rsaquo;", "€"=>"&euro;", 
     "§"=>"&sect;",  "©"=>"&copy;",   "«"=>"&laquo;",  "»"=>"&raquo;", 
     "®"=>"&reg;",   "°"=>"&deg;"
);

Обращаю ваше внимание на то, что символ "&" указывать не надо. Почему? Это вы увидите ниже. Еще отмечу, что здесь я указал лишь некоторые символы для экранирования. На самом деле полный список спец. символов HTML намного больше.

Шаг №3

Зададим разрешенные мнемоники.

my @mnemonics = qw(nbsp lt gt);

Шаг №4

Теперь можно перейти непосредственно к написанию самого фильтра. Для начала разделим полученный код на тэги и участки простого текста.

my @segments;
my $lastSegment;
while($in =~ m/(.*?)<(\s+)?([^<>]*?)(\s+)?>/igs){
     push(@segments, {"TYPE"=>"text", "VALUE"=>$1}, {"TYPE"=>"tag", "VALUE"=>$3});
     $lastSegment = $';
}
if(defined($lastSegment)){push(@segments, {"TYPE"=>"text", "VALUE"=>$lastSegment})}
if(!@segments){push(@segments, {"TYPE"=>"text", "VALUE"=>$in})}

Шаг №5

Обрабатываем все полученные сегменты.

my @openTagsStack;
for(my $i=0; $i<@segments; $i++){
			
     #Если участок является простым текстом - экранируем в нем спец. символы HTML, но оставляем разрешенные мнемоники
     if($segments[$i]{"TYPE"} eq "text"){
          $segments[$i]{"VALUE"} =~ s/&/&amp;/g;
          while ((my $key, my $value) = each(%specialChars)) {
               $segments[$i]{"VALUE"} =~ s/$key/$value/g;
          }
          foreach(@mnemonics){
               $segments[$i]{"VALUE"} =~ s/&amp;$_;/&$_;/g;
          }
     }
		
     #Если участок является тэгом...
     if($segments[$i]{"TYPE"} eq "tag"){
		
          #находим тип тэга(открывающий/закрывающий), имя тэга, строку атрибутов
          my($tagType, $tagName, $attrString);
          if($segments[$i]{"VALUE"} =~ m/^(\s+)?(\/)?(\s+)?(\w+)((\s+)(.+))?$/){
               if($2 eq "\/") {$tagType = "close"}
               else {$tagType = "open"}
               $tagName = $4;
				
               #переводим имя тэга в нижний регистр
               $tagName = lc($tagName);
				
               $attrString = $7;
          }
			
          #проверяем является ли тэг допустимым
          my $tagOk = 0;
          foreach(keys(%tags)) {
               if($tagName eq $_){
                    if((($tagType eq "close")&&($tags{$_}{"CLOSE_TAG"})) || ($tagType eq "open")){
                         $tagOk = 1;
                    }
               }
          }
			
          #если тэг недопустимый удаляем его
          if(!$tagOk){$segments[$i]{"TYPE"} = "deleted_tag"}
			
          #если тэг допустимый...
          if($tagOk){
			
               #и открывающий
               if($tagType eq "open"){
				
                    #получаем его атрибуты
                    my %attr;
                    while($attrString =~ m/(\w+)(\s+)?=(\s+)?\"(\s+)?([^"]*?)(\s+)?\"/g){
						
                         my $attrName = $1;
						
                         #переводим имя атрибута в нижний регистр
                         $attrName = lc($attrName);
						
                         $attr{$attrName} = $5;
                    }
					
                    #проверяем найденные аттрибуты и оставляем только допустимые
                    $segments[$i]{"VALUE"} = $tagName;
                    while ((my $key, my $value) = each(%attr)) {
                         foreach(keys(%{$tags{$tagName}{"ATTR"}})){
                              if($key eq $_){

                                   if($tags{$tagName}{"ATTR"}{$_}{"TYPE"} eq "text"){
                                        $value =~ s/&/&amp;/g;
                                        while ((my $k, my $v) = each(%specialChars)) {
                                             $value =~ s/$k/$v/g;
                                        }
                                        $segments[$i]{"VALUE"} .= " $key=\"$value\""
                                   }

                                   if(($tags{$tagName}{"ATTR"}{$_}{"TYPE"} eq "number") && (($value =~ /^[+-]?\d+$/)||(!$value))){
                                        $segments[$i]{"VALUE"} .= " $key=\"$value\""
                                   }

                                   if($tags{$tagName}{"ATTR"}{$_}{"TYPE"} eq "fixed"){
                                        foreach(@{$tags{"$tagName"}{"ATTR"}{$_}{"VALUES"}}){
                                             if($value eq lc($_)){
                                                  $segments[$i]{"VALUE"} .= " $key=\"$value\""
                                             }
                                        }
                                   }
                              }
                         }
                    }
					
                    #если тэг не требует закрывающего, закрываем его по правилам XHTML
                    if(!($tags{$tagName}{"CLOSE_TAG"})){$segments[$i]{"VALUE"} .= " /"}
					
                    #в противном случае заносим его в стек открывающих тэгов
                    else {
                         push(@openTagsStack, $tagName);
                    }
               }

               #если тэг допустимый и закрывающий
               if($tagType eq "close"){	
					
                    #если в стеке что-нибудь есть...
                    if(@openTagsStack){
					
                         #и тэг соответствует последнему открывающему из стэка
                         if($tagName eq $openTagsStack[-1]){
                              pop(@openTagsStack);
                         }
					
                         #если не соответствует
                         else{
                              splice(@segments, $i, 0, {"TYPE"=>"tag", "VALUE"=>"/".$openTagsStack[-1]});
                              pop(@openTagsStack);
                         }
                    }
					
                    #если стэк пустой - удаляем текущий закрывающий тэг
                    else{
                         $segments[$i]{"TYPE"} = "deleted_tag";
                    }
               }
          }
     }		
}

Шаг №6

Закрываем все тэги, которые остались в стеке.

while(@openTagsStack){
     push (@segments, {"TYPE" => "tag", "VALUE" => "/".pop(@openTagsStack)})
}

Шаг №7

Собираем профильтрованный код и возвращаем его.

for(my $i=0; $i<@segments; $i++){
     if(($segments[$i]{'TYPE'} eq "tag")) {$out .= "<$segments[$i]{'VALUE'}>"}
     if(($segments[$i]{'TYPE'} eq "text")) {$out .= $segments[$i]{'VALUE'}}
}

return $out;

Исходный код фильтра


Комментарии (5)

Евгений
Оличная статья! Коротко, ясно и понятно.
Ответить
zen
Только несколько смущает этот участок кода: ... $lastSegment = $'; ...
Ответить
Savvateev
По-другому никак не получалось сделать. Кстати есть более новая версия фильтра переписанная на PHP. Вот ссылка http://savvateev.org/blog/36/
Ответить
zen
К сожалению (или счастью?) пишу только на PERL. Листинг исправить можно так: $lastSegment = substr( $in, $+[0] );
Ответить
Savvateev
Я тоже когда-то писал только на Perl, а сейчас полностью перешел на PHP, и не сколько не жалею. Спасибо, что подправили код. Так более грамотно.
Ответить


Оставить свой комментарий


Представтесь, пожалуйста *

Ваш комментарий

Число на картинке *

captcha

На хостинг