Cuidados com a geração de XML

Artigo que explica alguns cuidados relacionados à geração de arquivos XML, especialmente relacionados à codificação do arquivo e utilização de caracteres impróprios.

Introdução

Ao gerar documentos XML com conteúdo dinâmico é preciso tomar cuidado com a sintaxe XML para que o documento não seja "mal formado". Muita gente conhece a sintaxe básica do XML, que especifica coisas como: é preciso um elemento raiz único, todo elemento que é aberto precisa ser fechado (ou ter o fechamento simplificado com "/>"), todos atributos precisam ter nome e valor, mesmo que o valor seja vazio.

Porém, um detalhe que nem todos sabem é que os elementos que contém texto não podem incluir qualquer tipo de caractere. Neste artigo veremos detalhes sobre isso.

Caracteres Proibidos no XML

Quando escrevi sobre Unicode, comentei que a codificação UTF-8 definia que um símbolo poderia ter de 1 a 4 bytes e, quando possui 1 byte, ele é idêntico à forma ASCII. Também falei que a tabela ASCII possui letras, números, alguns símbolos e alguns caracteres de controle. É justamente com estes caracteres de controle que devemos nos preocupar na hora de montar um XML 100% válido.

Acontece que, pela definição do XML, não podemos utilizar os caracteres de controle, com exceção de: tabulação ("\t" que possui código 9), quebra de linha (o famoso "\n", que possui código 10) e o retorno de carro (o famoso "\r", que possui código 13).

Normalmente, não é possível "digitar" os caracteres de controle (exceto as exceções), mas, então, como é que eles podem "surgir"? Acontece que alguns programas de editor de texto mais sofisticados (como o "MS-Word") utilizam estes caracteres reservados então, ao copiar um trecho de um texto feito nestes editores e colar num formulário web, é possível que estes dados vão para o Banco de Dados e sua base de dados fica "suja" com estes caracteres.

Bom, sabendo que estes caracteres podem existir, como podemos solucionar? Você poderia pensar: usa um bloco <![CDATA[ e ]]>. Mas nem este bloco é capaz de resolver. Você então poderia pensar: usa a função htmlentities ou htmlspecialchars, mas também não resolveria o problema e o documento não seria válido.

A solução é muito simples: não exiba estes caracteres no documento XML. Para isso, é preciso fazer um filtro para sanitizar o conteúdo a ser incluído no XML.

Se o seu documento XML tem codificação ISO-8859-1, você pode usar esta função abaixo para obter apenas os caracteres válidos:

/**
 * Limpa caracteres nao pertencentes a codificacao ISO-8859-1
 * @param string $texto
 * @return string
 */
function sanitizarISO88591($texto) {
    return preg_replace('/([\x00-\x08]|[\x0B-\x0C]|[\x0E-\x1F]|[\x7F-\x9F])/', '', $texto);
}

Se o seu documento XML tem codificação UTF-8, então precisa usar uma função um pouco mais sofisticada:

/**
 * Limpa caracteres nao pertencentes a codificacao UTF-8
 * @param string $texto
 * @return string
 */
function sanitizarUTF8($texto) {
    $saida = '';

    $i = 0;
    $len = strlen($texto);
    while ($i < $len) {
        $char = $texto[$i++];
        $ord = ord($char);

        // Primeiro byte 0xxxxxxx: simbolo possui 1 byte (ascii)
        if (($ord & 0b10000000) == 0b00000000) {

            // Caracteres de controle
            if (($ord >= 0 && $ord <= 31) || $ord == 127) {

                // Excecoes: tab, retorno de carro e quebra de linha
                if ($ord == 9 || $ord == 10 || $ord == 13) {
                    $saida .= $char;
                }
            } else {
                $saida .= $char;
            }

        // Primeiro byte 110xxxxx: simbolo possui 2 bytes
        } elseif (($ord & 0b11100000) == 0b11000000) {
            $char2 = $texto[$i++];
            $ord2 = ord($char2);

            // Segundo byte valido (10xxxxxx)
            if (($ord2 & 0b11000000) == 0b10000000) {
                $saida .= $char . $char2;
            }

        // Primeiro byte 1110xxxx: simbolo possui 3 bytes
        } elseif (($ord & 0b11110000) == 0b11100000) {
            $char2 = $texto[$i++];
            $ord2 = ord($char2);

            // Segundo byte valido (10xxxxxx)
            if (($ord2 & 0b11000000) == 0b10000000) {
                $char3 = $texto[$i++];
                $ord3 = ord($char3);

                // Terceiro byte valido (10xxxxxx)
                if (($ord3 & 0b11000000) == 0b10000000) {
                    $saida .= $char . $char2 . $char3;
                }
            }

        // Primeiro byte 11110xxx: simbolo possui 4 bytes
        } elseif (($ord & 0b11111000) == 0b11110000) {
            $char2 = $texto[$i++];
            $ord2 = ord($char2);

            // Segundo byte valido (10xxxxxx)
            if (($ord2 & 0b11000000) == 0b10000000) {
                $char3 = $texto[$i++];
                $ord3 = ord($char3);

                // Terceiro byte valido (10xxxxxx)
                if (($ord3 & 0b11000000) == 0b10000000) {
                    $char4 = $texto[$i++];
                    $ord4 = ord($char4);

                    // Quarto bytte valido (10xxxxxx)
                    if (($ord4 & 0b11000000) == 0b10000000) {
                        $saida .= $char . $char2 . $char3 . $char4;
                    }
                }
            }
        }
    }
    return $saida;
}

Observação: este código usa recursos do PHP 5.4. Se você ainda não usa esta versão, avalie a possibilidade de usá-lo, pois é a versão estável do PHP.

2 comentários

Anônimo disse...

Parabéns,

Bem Bacana. Usei algo semelhante lá no meu trabalho.

Parabéns Rubinho