如何处理无效UTF-8字符的用户输入?

时间:2010-09-15 06:42:38

标签: php encoding utf-8

我正在寻找关于如何处理来自用户的无效UTF-8输入的一般策略/建议。

即使我的webapp使用UTF-8,某些用户也会输入无效字符。这会导致PHP的json_encode()出错,总体来说这似乎是一个坏主意。

W3C I18N FAQ: Multilingual Forms说“如果收到非UTF-8数据,则应该发回错误信息。”。

  • 在几个可以输入数据的不同地方的网站中,究竟应该如何实际完成这项工作?
  • 如何以有用的方式向用户呈现错误?
  • 如何临时存储和显示不良表单数据,以便用户不会丢失所有文本?剥掉坏人物?使用替换字符,以及如何使用?
  • 对于数据库中的现有数据,当检测到无效的UTF-8数据时,我应该尝试将其转换并保存回来(如何?utf8_encode()?mb_convert_encoding()?),或者离开数据库中的as-is,但在json_encode()之前做了什么(什么?)?

编辑:我对mbstring扩展非常熟悉,并没有问“UTF-8如何在PHP中工作”。我希望那些在实际情况下有经验的人提供建议。

EDIT2:作为解决方案的一部分,我真的希望看到 fast 方法将无效字符转换为U + FFFD

9 个答案:

答案 0 :(得分:58)

accept-charset="UTF-8"属性只是浏览器要遵循的准则,他们不会被迫以这种方式提交,蹩脚的表单提交机器人就是一个很好的例子......

我通常做的是忽略不良字符,无论是通过iconv()还是使用不太可靠的utf8_encode() / utf8_decode()函数,如果您使用iconv,您也可以选择译成坏词。

以下是使用iconv()的示例:

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

如果您想向用户显示错误消息,我可能会以全局方式执行此操作,而不是基于每个值接收,这样的事情可能会很好:

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!

您可能还需要规范化新行和剥离(非)可见控制字符,如下所示:

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);
}

从UTF-8转换为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

可能比其他任何替代方案都快,但没有广泛测试过。


示例:

$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);
}

这是你在找什么?

答案 1 :(得分:4)

从Web应用程序接收无效字符可能与为HTML表单假定的字符集有关。您可以使用accept-charset attribute指定要用于表单的字符集:

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

您还可以查看StackOverflow中的类似问题,以获取有关如何处理无效字符的指示,例如:右侧列中的那些,但我认为向用户发出错误信号比尝试清除那些导致意外丢失重要数据或意外更改用户输入的无效字符更好。

答案 2 :(得分:2)

我整理了一个相当简单的类来检查输入是否为UTF-8,并根据需要运行utf8_encode()

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);

答案 3 :(得分:1)

PHP有一个多字节扩展名,请查看:http://www.php.net/manual/en/book.mbstring.php

您应该尝试mb_check_encoding()功能。

祝你好运!

答案 4 :(得分:1)

我建议不要让垃圾进入。不要依赖自定义功能,这会使你的系统陷入困境。只需将提交的数据与您设计的字母表对齐即可。创建一个可接受的字母表字符串并逐字节地处理提交的数据,就好像它是一个数组一样。将可接受的字符推送到新字符串,并省略不可接受的字符。然后,您存储在数据库中的数据是由用户触发的数据,但实际上不是用户提供的数据。

编辑#4:编辑#4: 用entiy替换坏人:

编辑#3:编辑#3: 更新时间:2010年9月22日@ 1:32 pm 原因:现在返回的字符串是UTF-8,另外我使用了您提供的测试文件作为证据。

<?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.')';
}

答案 5 :(得分:1)

完整性问题(不一定是最佳答案)...

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

答案 6 :(得分:0)

如何剥离给定子集之外的所有字符。至少在我的应用程序的某些部分,我不允许在[a-Z] [0-9集]之外使用字符,例如用户名。您可以构建一个过滤器函数,该函数静默地剥离此范围之外的所有字符,或者如果它检测到它们则返回错误并将决定推送给用户。

答案 7 :(得分:0)

尝试执行Rails所做的操作,强制所有浏览器始终发布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>

有关说明,请参阅railssnowman.infothe initial patch

  1. 要让浏览器以UTF-8编码发送表单提交数据,只需使用Content-Type标题“text / html; charset = utf-8”呈现页面(或使用meta http-equiv标记)。
  2. 要让浏览器以UTF-8编码发送表单提交数据,即使用户使用页面编码(浏览器允许用户这样做),也请在表单中使用accept-charset="UTF-8"
  3. 让浏览器以UTF-8编码发送表单提交数据,即使用户摆弄页面编码(浏览器允许用户这样做),即使浏览器是IE并且用户切换了页面编码在表单字段中使用朝鲜语并输入韩语字符,使用&#x2713;等值添加隐藏的输入到表单,该值只能来自Unicode字符集(在此示例中,不是韩语字符集)。 / LI>

答案 8 :(得分:0)

将UTF-8设置为PHP代码输出的所有标头的字符集

在每个PHP输出标头中,指定UTF-8作为编码:

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