Как обрабатывать ввод пользователем недопустимых символов UTF-8?
Я ищу общую стратегию/совет о том, как обрабатывать недопустимый ввод UTF-8 от пользователей.
Несмотря на то, что мое веб-приложение использует UTF-8, некоторые пользователи каким-то образом вводят недопустимые символы. Это приводит к ошибкам в PHPjson_encode() и в целом кажется плохой идеей.
Часто задаваемые вопросы W3C I18N: Многоязычные формы гласит: "Если получены данные, отличные от UTF-8, должно быть отправлено сообщение об ошибке".
- Как именно это должно быть практически сделано, по всему сайту с десятками различных мест, где можно вводить данные?
- Как вы представляете ошибку в полезной форме для пользователя?
- Как вы временно сохраняете и отображаете данные плохой формы, чтобы пользователь не потерял весь свой текст? Раздевать плохих персонажей? Используйте символ замены, и как?
- Для существующих данных в базе данных при обнаружении недопустимых данных UTF-8 следует ли мне попытаться преобразовать их и сохранить обратно (как?utf8_encode()? mb_конвертируемое кодирование()?), или оставить как есть в базе данных, но сделать что-то (что?) перед json_encode()?
РЕДАКТИРОВАТЬ: Я очень хорошо знаком с расширением mbstring и не спрашиваю "как UTF-8 работает в PHP". Я хотел бы получить совет от людей, имеющих опыт работы в реальных ситуациях, как они справились с этим.
EDIT2: В качестве части решения я бы очень хотел увидеть быстрый метод преобразования недопустимых символов в U+FFFD
9 answers
Атрибут 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 в кодовые точки Юникода:
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);
}
Это то, что вы искали?
Получение недопустимых символов из вашего веб-приложения может быть связано с наборами символов, принятыми для HTML-форм. Вы можете указать, какой набор символов использовать для форм с accept-charset
атрибут:
<form action="..." accept-charset="UTF-8">
Вы также можете взглянуть на аналогичные вопросы в StackOverflow для указателей на то, как обрабатывать недопустимые символы, например, те, что в столбце справа, но я думаю, что сигнализация об ошибке пользователю лучше, чем пытаться очистить эти недопустимые символы. символы, которые приводят к неожиданной потере важных данных или неожиданному изменению вводимых пользователем данных.
Я собрал довольно простой класс, чтобы проверить, находится ли ввод в 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);
Существует многобайтовое расширение для PHP, проверьте его: http://www.php.net/manual/en/book.mbstring.php
Вам следует попробовать функцию mb_check_encoding().
Удачи!
Для полноты ответа на этот вопрос (не обязательно лучший ответ)...
function as_utf8($s) {
return mb_convert_encoding($s, "UTF-8", mb_detect_encoding($s));
}
Я рекомендую просто не допускать попадания мусора внутрь. Не полагайтесь на пользовательские функции, которые могут вывести вашу систему из строя. Просто сопоставьте представленные данные с разработанным вами алфавитом. Создайте приемлемую строку алфавита и просмотрите отправленные данные, байт за байтом, как если бы это был массив. Вставьте допустимые символы в новую строку и опустите недопустимые символы. Данные, которые вы храните в своей базе данных, являются данными, инициированными пользователем, но фактически не предоставленными пользователем.
РЕДАКТИРОВАТЬ #4: Замена плохого персонажа на entiy: �
ПРАВКА #3: Обновлено: 22 сентября 2010 года в 13:32 Причина: Теперь возвращаемая строка - 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 .= '�';
}
}
}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 = '�';
}
}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.')';
}
Как насчет удаления всех символов за пределами вашего данного подмножества. По крайней мере, в некоторых частях моего приложения я бы не разрешал использовать символы за пределами [a-Z] [0-9 наборов], например имена пользователей. Вы можете создать функцию фильтра, которая автоматически удаляет все символы за пределами этого диапазона или возвращает ошибку, если обнаруживает их и передает решение пользователю.
Попробуйте сделать то, что делает 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="✓" />
</div>
<!-- form fields -->
</form>
См. railssnowman.info или начальный патч для объяснения.
- Чтобы браузер отправлял данные для отправки формы в кодировке UTF-8, просто отобразите страницу с заголовком типа содержимого "текст/html; кодировка=utf-8" (или используйте тег
meta http-equiv
). - Чтобы браузер отправлял данные для отправки формы в кодировке UTF-8, даже если пользователь возится со страницей кодирование (браузеры позволяют пользователям это делать), используйте
accept-charset="UTF-8"
в форме. - Чтобы браузер отправлял данные для отправки формы в кодировке UTF-8, даже если пользователь изменяет кодировку страницы (браузеры позволяют пользователям это делать), и даже если браузер IE, и пользователь переключил кодировку страницы на корейский и ввел корейские символы в поля формы, добавьте скрытый ввод в форму со значением, таким как
✓
, которое может быть только из кодировки Юникода (и, в данном примере, не корейская кодировка). кодировка).
Установите UTF-8 в качестве набора символов для всех заголовков, выводимых вашим PHP-кодом
В каждом выходном заголовке PHP укажите UTF-8 в качестве кодировки:
header('Content-Type: text/html; charset=utf-8');