常规错误:1366字符串值不正确:'\ xD6wn Sm ...'列'user_agent'

时间:2017-12-05 16:01:19

标签: php pdo

我需要保存访问用户的用户代理。

这是我的代码:

// User Agent
$ua = $_SERVER['HTTP_USER_AGENT']??'';
$ua_md5 = md5($ua);
// Search if the UA already exists in the user_agents table
$ua_id = $db->query("SELECT id FROM user_agents WHERE md5='".$ua_md5."';")->fetchColumn();

if(!$ua_id) {
    // If it doesn't exist, insert it and get its id
    $db->query("INSERT INTO user_agents (md5, user_agent) VALUES ('$ua_md5', ".$db->quote($ua).")");
    $ua_id = $db->lastInsertId();
}

我使用PDO:仅出于性能原因引用而不是预处理语句(它更快,此脚本每秒运行数千次)。

有些用户有这个用户代理:

  

Mozilla / 5.0(Linux; Android 5.0; \ xD6wn Smart Build / LRX21M)AppleWebKit / 537.36(KHTML,与Gecko一样)版本/ 4.0 Chrome / 37.0.0.0 Mobile Safari / 537.36

此错误导致插入失败:

  

“PHP消息:SQLSTATE [HY000]:常规错误:1366字符串值不正确:'\ xD6wn Sm ...'用于第1行”user_agent“列,同时从上游读取响应头

是什么原因以及如何解决?

编辑:更多调试发现$ua值为:

  

Mozilla / 5.0(Linux; Android 5.0;ÖWN1SBuild / LRX21M)AppleWebKit / 537.36(KHTML,类似Gecko)版本/ 4.0 Chrome / 37.0.0.0 Mobile Safari / 537.36

1 个答案:

答案 0 :(得分:3)

首先,当涉及编码问题时,始终需要检查原始字节,而不是依赖于呈现或解释输入的某些进程的输出。对于PHP,var_dump()始终是一个很好的起点,但您还需要dump to hex或甚至诉诸hexadecimal editor

<?php
var_dump(bin2hex($_SERVER['HTTP_USER_AGENT']));

到目前为止我受过良好教育的猜测(而且我不相信它与真理相距甚远)是:

  1. 某些Android浏览器正在发送包含ISO-8859-1中编码的Öwn Smart Build的HTTP标头,其中Ö又称'LATIN CAPITAL LETTER O WITH DIAERESIS' (U+00D6)编码为D6

  2. 您的应用堆栈配置为UTF-8(明智的选择),其中Ö将被编码为C396

  3. PHP并不知道/关心,因为PHP字符串不能识别编码(它们只是字节流)。

  4. MySQL被处理D6,被告知它的UTF-8(但它不是)。

  5. 如果D6是一个有效的UTF-8(不同)字符,或者是(不同的)多字节序列的一部分,那么插入就会完成而导致较小的数据丢失(原始Öwn文本将丢失并被其他东西取代)。无论好坏,它都不是有效的UTF-8,因此MySQL会使用您描述的错误消息中止插入。

  6. 为什么MySQL无法应对它?我们来查看UTF-8 definition

    Nr of
    Bytes    Byte 1   Byte 2       Byte 3     Byte 4
    1      0xxxxxxx       
    2      110xxxxx   10xxxxxx     
    3      1110xxxx   10xxxxxx   10xxxxxx   
    4      11110xxx   10xxxxxx   10xxxxxx   10xxxxxx
    

    原始Latin-1 Öw文本编码为D6 77,翻译为二进制文件:

    11010110 01001101
    ^^^      ^^
    

    在UTF-8中,110…表示&#34;开始2字节字符&#34;。然后,第二个字节应以10…开头,但我们改为01…。的糟糕!

    你怎么解决这个问题?它比看起来更棘手。如果您确定输入是ISO-8859-1,那么它只是straightforward conversion

    <?php
    $input = "\xD6wn";
    $output = mb_convert_encoding($input, 'UTF-8', 'ISO-8859-1');
    var_dump(bin2hex($input), bin2hex($output));
    
    string(6) "d6776e"
    string(8) "c396776e"
    

    Online demo

    但是,你怎么知道?我不确定User-Agent标头是否允许MIME Encoded-Words,即使在那里,浏览器也可能只发送无效数据。也许您可以捕获错误(MySQL错误代码1366又名ER_TRUNCATED_WRONG_VALUE_FOR_FIELD看起来相当精确)并再次尝试假设ISO-8859-1。并且验证输入是有效的UTF-8可能也是一个好主意,尽管它可以使处理变得麻烦:

    <?php
    $latin1 = "\xD6wn";
    $utf8 = "\xc3\x96wn";
    var_dump(mb_check_encoding($latin1, 'UTF-8'));
    var_dump(mb_check_encoding($utf8, 'UTF-8'));
    
    bool(false)
    bool(true)
    

    Online demo