Permitir que os usuários utilizem códigos HTML nas suas colaborações não apenas deixa o nosso site mais bonito, mas também demonstra respeito pelos usuários, ao permitir que se expressem utilizando seus conhecimentos.
Porém, deve haver limite. Algumas marcações mal escritas podem estragar o layout da página ou até mesmo conter programas perigosos para os visitantes. Como filtrar o que pode e o que não pode ser utilizado?
Vamos criar uma função para esta tarefa. Tudo o que não for permitido ou o que estiver com algum erro de sintaxe será convertido de modo a ser apresentado na tela como parte do conteúdo.
O PHP já possui uma função específica para converter tudo aquilo que não queremos que o navegador interprete como código HTML. Trata-se da função htmlspecialchars(). Experimente alguma coisa como:
print htmlspecialchars ('<html><body><h1>Eu não sou um documento html</h1></body></html>');
O resultado desta operação será de que tudo o que foi enviado para a função será impresso no navegador literalmente como texto. Isto é útil. Mas como ela não distingue marcações benignas de marcações malignas, vamos criar uma função capaz desta distinção.
Nossa função terá um conjunto de marcações e parâmetros de marcações que podem ser aceitos por padrão. Mas um segundo argumento indica se desejamos ou não filtrar o conteúdo. Embora pareça patético enviar um conteúdo para uma função e, ao mesmo tempo, indicar que este não deva ser filtrado, isto nos será útil posteriormente.
Se o segundo parâmetro da função for 1, a filtragem deverá ser feita normalmente; se for 2, todo o HTML é permitido – o conteúdo não será filtrado; mas, se 0, então todo o início de marcação HTML (“<“) será convertido para a entidade “<”.
Não utilizaremos htmlspecialchars(), pois o conteúdo poderá conter entidades HTML que não desejaremos escapar.
Vamos À função:
<?php
function filter_html ($string, $filter_html_mode=1)
{ // filter html
// A lista de tags permitidas por padrão
static $valid_tags = array(
'b'=>true, // Texto em negrito
'br'=>true, // Quebra de linha forçada
'caption'=>true, // Legenda de tabela
'col'=>true, // Coluna de tabela
'colgroup'=>true, // Grupo de colunas de tabela
'div'=>true, // Contentor genérico
'em'=>true, // ênfase
'hr'=>true, // Barra horizontal
'i'=>true, // Texto em itálico
'mark'=>true, // Destaque
's'=>true, // Texto em negrito
'small'=>true, // Texto pequeno
'span'=>true, // Contentor genérico
'strong'=>true, // Texto grande
'sub'=>true, // Texto Subescrito
'sup'=>true, // Texto superescrito
'table'=>true, // Tabela
'tbody'=>true, // Corpo de tabela
'td'=>true, // Célula de tabela
'tfoot'=>true, // Rodapé de tabela
'th'=>true, // Cabeçalho de tabela
'thead'=>true, // Cabeçalho de tabela
'tr'=>true, // Linha de tabela
'u'=>true, // Texto sublinhado
'wbr'=>true // Local propício para quebra de linha
);
// As tags que não trabalham em pares.
static $single_tags=array(
'br'=>true,
'hr'=>true,
'wbr'=>true
);
// A lista de argumentos permitidos por padrão
static $valid_arguments = array (
'/'=>true, // Argumento padrão para marcações singulares
'abbr'=>true, // Cabeçalho - legenda
'alt'=>true, // Descrição
'axis'=>true, // Lista de cabeçalhos relacionados
'cellpadding'=>true, // Preenchimento da célula
'cellspacing'=>true, // Espaçamento entre células
'cite'=>true, // Endereço da citação
'colspan'=>true, // Número de colunas ocupadas pela célula
'frame'=>true, // Parte do quadro a ser renderizado
'header'=>true, // Cabeçalho
'height'=>true, // Altura
'href'=>true, // Endereço
'lang'=>true, // Idioma
'name'=>true, // Nome
'rowspan'=>true, // Número de linhas ocupadas pela célula
'rules'=>true,
'title'=>true, // Título - legenda
'scope'=>true, // Campo coberto pelas células do cabeçalho
'span'=>true,
'src'=>true, // Endereço do recurso
'style'=>true, // Regras de folha de estilo
'summary'=>true, // Descrição de estrutura para saída em fala
'width'=>true // Largura
);
// Se nenhum HTML for permitido
if ($filter_html_mode == 0)
return str_replace ('<', '<', $string);
// Se todo o HTML for permitido
elseif ($filter_html_mode == 2)
return $string;
// O resultado
$result = '';
// O ponteiro que utilizaremos para percorrer a string
$pointer = 0;
// Vamos iniciar um loop infinito para percorrer a string
while (true)
{ // infinity loop
// Vamos procurar o início de uma tag ("<")
$tag_start = strpos ($string, '<', $pointer);
// Se não for encontrado, a tarefa terminou e sairemos do loop
if ($tag_start === false)
{ // tag not found
$result .= substr ($string, $pointer);
return $result;
} // tag not found
// Vamos procurar o fim da tag (">")
$tag_start ++;
$tag_end = strpos ($string, '>', $tag_start);
/// Se não for encontrado, a tarefa terminou e sairemos do loop
if ($tag_end === false)
{ // tag end not found
$result .= str_replace ('<', '<', substr ($string, $pointer));
return $result;
} // tag end not found
// Vamos recortar o conteúdo da tag e fazer uma análize
$tag = substr ($string, $tag_start, $tag_end - $tag_start);
$tag_end ++;
// Vamos recortar cada elemento da tag
preg_match_all ('/([a-zA-Z]+)([=]["][^"]*["])?\ ?\/?/', $tag, $preg);
$tag_array = $preg[1];
// O primeiro elemento corresponde à própria tag
$tag_original = array_shift ($tag_array);
// A tag poderá estar em maiúsculas,
// vamos converter para minúsculas a fim de poder comparar com a lista de tags válidas
$tag_type = strtolower ($tag_original);
// Vamos comparar o primeiro elemento com a lista de tags válidas
$accept = false;
if (isset ($valid_tags[$tag_type]))
{ // accept tag
$accept = true;
// Vamos comparar os demais elementos com a lista de argumentos válidos
foreach ($tag_array as $argument)
{ // each argument
$argument = strtolower ($argument);
if (!isset ($valid_arguments[$argument]))
{ // invalid argument
$accept = false;
break;
} // invalid argument
} // each argument
} // accept tag
// Se toda a tag foi aceita
if ($accept)
{ // allow tag
// Se a tag trabalha sozinha
if (isset ($single_tags[$tag_type]))
{ // allow single tag
$result .= substr ($string, $pointer, $tag_end - $pointer);
} // allow single tag
// Outro caso, iremos procurar uma tag de fechamento correspondente
else
{ // allow double tag
$close_tag = '</' . $tag_original . '>';
$close_tag_start = strpos ($string, $close_tag, $tag_end);
// Se a tag de fechamento não foi encontrada
if ($close_tag_start === false)
{ // deny no closed tag
$result .= str_replace ('<', '<', substr ($string, $pointer, $tag_end - $pointer));
} // deny no closed tag
// Se encontramos a tag de fechamento
else
{ // close tag found
$result .= substr ($string, $pointer, $tag_end - $pointer);
// IMPORTANTE: O conteúdo encontrado entre as tags
// será analizado em busca de outras tags
$result .= filter_html (substr ($string, $tag_end, $close_tag_start - $tag_end));
$result .= $close_tag;
$tag_end = $close_tag_start + strlen ($close_tag);
} // close tag found
} // allow double tag
} // allow tag
// Caso a tag ou algum argumento não foi permitido
else
{ // deny - escape html special chars
$result .= str_replace ('<', '<', substr ($string, $pointer, $tag_end - $pointer));
} // deny - escape html special chars
// Avançaremos o ponteiro para continuar a análize
$pointer = $tag_end;
} // infinity loop
} // filter html
?>
Da forma como está configurada, esta função é bastante liberal, permitindo marcações para estilizar o texto e construir tabelas. Argumentos como style (para folhas de estilo) e para estruturar as tabelas são permitidas. Por outro lado, marcações ativas ou argumentos que poderiam acionar scripts não são permitidos.
Marcações que estiverem “cruzadas” como em
<i><u>texto</i></u>
, ou que não tenham a tag de fechamento encontradas serão escapadas para o conteúdo. Isto ajudará o usuário e evitará mal intencionados que porventura queiram estragar o layout da página com algo como
<table style="display:none">
– isto poderia fazer o navegador não exibir o restante do documento, esperando o fechamento da tabela.