it-swarm.dev

Como lidar com a entrada do usuário de caracteres UTF-8 inválidos?

Eu estou procurando geral uma estratégia/conselhos sobre como lidar com entrada UTF-8 inválida de usuários.

Embora meu webapp use UTF-8, alguns usuários inserem caracteres inválidos. Isso causa erros no PHP json_encode () e no geral parece uma péssima idéia.

W3C I18N FAQ: Formulários Multilíngües diz "Se dados não-UTF-8 forem recebidos, uma mensagem de erro deve ser enviada de volta.".

  • Como exatamente isso deve ser feito, em um site com dezenas de lugares diferentes onde os dados podem ser inseridos?
  • Como você apresenta o erro de uma maneira útil para o usuário?
  • Como você armazena e exibe temporariamente dados de formulários incorretos para que o usuário não perca todos os seus textos? Tira personagens ruins? Use um personagem substituto e como?
  • Para dados existentes no banco de dados, quando dados UTF-8 inválidos são detectados, devo tentar convertê-los e salvá-los novamente (como? utf8_encode ()? mb_convert_encoding () ?), Ou deixe como -é no banco de dados, mas fazendo alguma coisa (o que?) antes de json_encode ()?

EDIT: Eu estou muito familiarizado com a extensão mbstring e não estou perguntando "como o UTF-8 funciona em PHP". Eu gostaria de conselhos de pessoas com experiência em situações do mundo real como eles lidaram com isso.

EDIT2: Como parte da solução, eu realmente gostaria de ver um método fast para converter caracteres inválidos para U + FFFD

37
philfreo

O atributo accept-charset="UTF-8" é apenas uma diretriz para os navegadores seguirem, eles não são forçados a enviar isso dessa forma, os bots de submissão de formulários ruins são um bom exemplo ...

O que eu costumo fazer é ignorar caracteres ruins, seja por meio de iconv() ou com funções menos confiáveis ​​ utf8_encode() / utf8_decode() , se você usar iconv você também tem a opção de transliterar caracteres ruins.

Aqui está um exemplo usando iconv():

$str_ignore = iconv('UTF-8', 'UTF-8//IGNORE', $str);
$str_translit = iconv('UTF-8', 'UTF-8//TRANSLIT', $str);

Se você quiser exibir uma mensagem de erro para seus usuários, eu provavelmente faria isso de uma maneira global, em vez de uma base por valor recebido, algo como isso provavelmente ficaria bem:

function utf8_clean($str)
{
    return iconv('UTF-8', 'UTF-8//IGNORE', $str);
}

$clean_GET = array_map('utf8_clean', $_GET);

if (serialize($_GET) != serialize($clean_GET))
{
    $_GET = $clean_GET;
    $error_msg = 'Your data is not valid UTF-8 and has been stripped.';
}

// $_GET is clean!

Você também pode querer normalizar novas linhas e remover caracteres de controle (não) visíveis, como este:

function Clean($string, $control = true)
{
    $string = iconv('UTF-8', 'UTF-8//IGNORE', $string);

    if ($control === true)
    {
            return preg_replace('~\p{C}+~u', '', $string);
    }

    return preg_replace(array('~\r\n?~', '~[^\P{C}\t\n]+~u'), array("\n", ''), $string);
}

Código para converter de UTF-8 para codepoints Unicode:

function Codepoint($char)
{
    $result = null;
    $codepoint = unpack('N', iconv('UTF-8', 'UCS-4BE', $char));

    if (is_array($codepoint) && array_key_exists(1, $codepoint))
    {
        $result = sprintf('U+%04X', $codepoint[1]);
    }

    return $result;
}

echo Codepoint('à'); // U+00E0
echo Codepoint('ひ'); // U+3072

Provavelmente mais rápido que qualquer outra alternativa, ainda não o testei extensivamente.


Exemplo:

$string = 'hello world�';

// U+FFFEhello worldU+FFFD
echo preg_replace_callback('/[\p{So}\p{Cf}\p{Co}\p{Cs}\p{Cn}]/u', 'Bad_Codepoint', $string);

function Bad_Codepoint($string)
{
    $result = array();

    foreach ((array) $string as $char)
    {
        $codepoint = unpack('N', iconv('UTF-8', 'UCS-4BE', $char));

        if (is_array($codepoint) && array_key_exists(1, $codepoint))
        {
            $result[] = sprintf('U+%04X', $codepoint[1]);
        }
    }

    return implode('', $result);
}

É isso que você estava procurando?

58
Alix Axel

Receber caracteres inválidos do seu aplicativo da web pode ter a ver com os conjuntos de caracteres assumidos para formulários HTML. Você pode especificar qual conjunto de caracteres deve ser usado para formulários com o atributo accept-charset :

<form action="..." accept-charset="UTF-8">

Você também pode querer dar uma olhada em perguntas semelhantes no StackOverflow para obter dicas sobre como lidar com caracteres inválidos, por exemplo, aqueles na coluna à direita, mas acho que sinalizar um erro para o usuário é melhor do que tentar limpar os caracteres inválidos que causam perda inesperada de dados significativos ou alteração inesperada das entradas do usuário.

4
Archimedix

Eu coloquei uma classe bastante simples para verificar se a entrada está em UTF-8 e para executar através de utf8_encode(), conforme necessário:

class utf8
{

    /**
     * @param array $data
     * @param int $options
     * @return array
     */
    public static function encode(array $data)
    {
        foreach ($data as $key=>$val) {
            if (is_array($val)) {
                $data[$key] = self::encode($val, $options);
            } else {
                if (false === self::check($val)) {
                    $data[$key] = utf8_encode($val);
                }
            }
        }

        return $data;
    }

    /**
     * Regular expression to test a string is UTF8 encoded
     * 
     * RFC3629
     * 
     * @param string $string The string to be tested
     * @return bool
     * 
     * @link http://www.w3.org/International/questions/qa-forms-utf-8.en.php
     */
    public static function check($string)
    {
        return preg_match('%^(?:
            [\x09\x0A\x0D\x20-\x7E]              # ASCII
            | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
            |  \xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
            | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  # straight 3-byte
            |  \xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
            |  \xF0[\x90-\xBF][\x80-\xBF]{2}     # planes 1-3
            | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
            |  \xF4[\x80-\x8F][\x80-\xBF]{2}     # plane 16
            )*$%xs',
            $string);
    }
}

// For example
$data = utf8::encode($_POST);
2
Nev Stokes

Eu recomendo simplesmente não permitir que o lixo entre. Não confie em funções personalizadas, o que pode atrapalhar seu sistema. Basta percorrer os dados enviados em relação a um alfabeto criado por você. Crie uma string de alfabeto aceitável e passe os dados enviados, byte por byte, como se fosse uma matriz. Empurre caracteres aceitáveis ​​para uma nova string e omita caracteres inaceitáveis. Os dados que você armazena em seu banco de dados são dados acionados pelo usuário, mas não dados realmente fornecidos pelo usuário.

EDIT # 4: Substituindo o caractere ruim pelo recurso:

EDIT # 3: Atualizado: 22 de setembro de 2010 @ 1:32 pm Motivo: Agora a string retornada é UTF-8, mais eu usei o arquivo de teste que você forneceu como prova.

<?php
// build alphabet
// optionally you can remove characters from this array

$alpha[]= chr(0); // null
$alpha[]= chr(9); // tab
$alpha[]= chr(10); // new line
$alpha[]= chr(11); // tab
$alpha[]= chr(13); // carriage return

for ($i = 32; $i <= 126; $i++) {
$alpha[]= chr($i);
}

/* remove comment to check ascii ordinals */

// /*
// foreach ($alpha as $key=>$val){
//  print ord($val);
//  print '<br/>';
// }
// print '<hr/>';
//*/
// 
// //test case #1
// 
// $str = 'afsjdfhasjhdgljhasdlfy42we875y342q8957y2wkjrgSAHKDJgfcv kzXnxbnSXbcv   '.chr(160).chr(127).chr(126);
// 
// $string = teststr($alpha,$str);
// print $string;
// print '<hr/>';
// 
// //test case #2
// 
// $str = ''.'©?™???';
// $string = teststr($alpha,$str);
// print $string;
// print '<hr/>';
// 
// $str = '©';
// $string = teststr($alpha,$str);
// print $string;
// print '<hr/>';

$file = 'http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt';
$testfile = implode(chr(10),file($file));

$string = teststr($alpha,$testfile);
print $string;
print '<hr/>';


function teststr(&$alpha, &$str){
    $strlen = strlen($str);
    $newstr = chr(0); //null
    $x = 0;
    if($strlen >= 2){

        for ($i = 0; $i < $strlen; $i++) {
            $x++;
            if(in_array($str[$i],$alpha)){
                // passed
                $newstr .= $str[$i];
            }else{
                // failed
                print 'Found out of scope character. (ASCII: '.ord($str[$i]).')';
                print '<br/>';
                $newstr .= '&#65533;';
            }
        }
    }elseif($strlen <= 0){
        // failed to qualify for test
        print 'Non-existent.';

    }elseif($strlen === 1){
        $x++;
        if(in_array($str,$alpha)){
            // passed

            $newstr = $str;
        }else{
            // failed
            print 'Total character failed to qualify.';
            $newstr = '&#65533;';
        }
    }else{
        print 'Non-existent (scope).';
        }

if(mb_detect_encoding($newstr, "UTF-8") == "UTF-8"){
// skip
}else{
    $newstr = utf8_encode($newstr);
}


// test encoding:
if(mb_detect_encoding($newstr, "UTF-8")=="UTF-8"){
    print 'UTF-8 :D<br/>';
    }else{
        print 'ENCODED: '.mb_detect_encoding($newstr, "UTF-8").'<br/>';
        }




return $newstr.' (scope: '.$x.', '.$strlen.')';
}
1
Geekster

Para completar esta questão (não necessariamente a melhor resposta) ...

function as_utf8($s) {
    return mb_convert_encoding($s, "UTF-8", mb_detect_encoding($s));
}
1
philfreo

Existe uma extensão multibyte para PHP, confira: http://www.php.net/manual/en/book.mbstring.php

Você deve tentar mb_check_encoding () function.

Boa sorte!

1
Otar

Tente fazer o que o Rails faz para forçar todos os navegadores a postarem dados UTF-8:

<form accept-charset="UTF-8" action="#{action}" method="post"><div
    style="margin:0;padding:0;display:inline">
    <input name="utf8" type="hidden" value="&#x2713;" />
  </div>
  <!-- form fields -->
</form>

Veja railssnowman.info ou o patch inicial para uma explicação.

  1. Para que o navegador envie dados de envio de formulário na codificação UTF-8, basta renderizar a página com um cabeçalho Content-Type de "text/html; charset = utf-8" (ou use uma tag meta http-equiv).
  2. Para que o navegador envie dados de envio de formulário na codificação UTF-8, mesmo que o usuário mexa na codificação da página (os navegadores permitem que os usuários façam isso), use accept-charset="UTF-8" no formulário.
  3. Para que o navegador envie dados de envio de formulário na codificação UTF-8, mesmo que o usuário mexa na codificação da página (os navegadores permitem que os usuários façam isso) e até mesmo se o navegador for IE e o usuário alternou a codificação de página para coreano e digitou caracteres coreanos nos campos de formulário, adicione uma entrada oculta ao formulário com um valor como &#x2713; que só pode ser do conjunto de caracteres Unicode (e, neste exemplo, não o conjunto de caracteres coreano).
0
yfeldblum

Que tal descartar todos os caracteres fora de seu subconjunto. Pelo menos em algumas partes do meu aplicativo eu não permitiria usar caracteres fora do [a-Z] [0-9 conjuntos], por exemplo nomes de usuários. Você pode criar uma função de filtro que retenha silenciosamente todos os caracteres fora desse intervalo ou que retorne um erro se ele os detectar e enviar a decisão ao usuário.

0
Elzo Valugi

Defina UTF-8 como o conjunto de caracteres para todos os cabeçalhos enviados pelo seu código PHP

Em todo cabeçalho de saídaPHP, especifique UTF-8 como a codificação:

header('Content-Type: text/html; charset=utf-8');
0
Mr. Nobody