Мне нужно сохранить пользовательский агент посетителя.
Это мой фрагмент:
// 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: quote вместо подготовленных операторов только по соображениям производительности (это быстрее, и этот скрипт выполняется тысячи раз в секунду).
Бывает, что у некоторых пользователей есть этот пользовательский агент:
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 … »для столбца« user_agent »в строке 1» при чтении заголовка ответа из восходящего потока
В чем причина и как это можно исправить?
Изменить: больше отладки выяснил $ua
значение:
Mozilla / 5.0 (Linux; Android 5.0; ÖWN S1 Build / LRX21M) AppleWebKit / 537.36 (KHTML, как Gecko) Версия / 4.0 Chrome / 37.0.0.0 Mobile Safari / 537.36
Прежде всего, когда речь идет о проблемах кодирования, всегда необходимо проверять необработанные байты и никогда не полагаться на выходные данные какого-либо процесса, который обрабатывает или интерпретирует входные данные. В случае с PHP, var_dump () всегда хорошая отправная точка, но вам также нужно сбросить в гекс или даже прибегнуть к шестнадцатеричный редактор:
<?php
var_dump(bin2hex($_SERVER['HTTP_USER_AGENT']));
Мое обоснованное предположение (и я не верю, что это слишком далеко от истины) таково:
Некоторый браузер Android отправляет заголовок HTTP, который включает Öwn Smart Build
закодировано в ISO-8859-1, где Ö
ака «ПИСЬМО ЛАТИНСКОГО КАПИТАЛА O С ДИАРЕЗОМ» (U + 00D6) кодируется как D6
,
Стек вашего приложения настроен для UTF-8 (разумный выбор), где Ö
будет закодирован как C396
,
PHP не знает / заботится, потому что строки PHP не поддерживают кодирование (они просто потоки байтов).
MySQL обрабатывается D6
Говорят, что это UTF-8 (но это не так).
Имел D6
Если бы действительный символ UTF-8 (другой) или часть (другой) многобайтовой последовательности, вставка была бы завершена с незначительной потерей данных (оригинал Öwn
текст был бы потерян и заменен чем-то другим). Хорошо это или плохо, это недопустимый UTF-8, поэтому MySQL прерывает вставку с сообщением об ошибке, которое вы описываете.
Почему MySQL не может справиться с этим? Давайте проверим UTF-8 определение:
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
Оригинальная латиница-1 Öw
текст закодирован как D6 77
который в переводе на двоичный:
11010110 01001101
^^^ ^^
В UTF-8, 110…
означает «Начало двухбайтового символа». Второй байт должен начинаться с 10…
но у нас есть 01…
вместо. К сожалению!
Как вы можете решить это? Это сложнее, чем кажется. Если бы вы могли точно знать, что ввод ISO-8859-1, это просто прямое преобразование:
<?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"
Но как ты можешь знать? Я не уверен, позволяет ли заголовок User-Agent MIME закодированные слова и даже там, браузер может просто отправлять неверные данные в любом случае. Возможно, вы можете поймать ошибку (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)
Других решений пока нет …