Как стать автором
Поиск
Написать публикацию
Обновить

Решение проблемы кодировок для AJAX и PHP без iconv

Вступление:


Многие, не исключая автора, сталкивались с проблемой, когда нужно использовать одновременно php и ajax в web проекте, написанном с использованием однобайтовой кодовой страницы (cp1251, koi-8, cp866, cp1252, latin-1 и т.д.). Все находили известный тезис что «AJAX работает только с UNICODE».
Попробуем разобрать проблему и найти решение.

Для нетерпеливых читателей скажу, что решение есть в виде написанной функции “encodeFormField”, которая работает с ЛЮБОЙ однобайтовой кодировкой. Эту функцию можно (нужно) использовать вместо escape или encodeURIComponent, после чего строковые данные доставляются до php в исходной кодировке (нет необходимости в использовании iconv в php скриптах).
Исподники (BSD License) можно загрузить по адресу:
www.viasoft.com.ua/js/encode_form_field.zip

Итак, задача:


1. Есть сайт, написанный на php
2. Кодировка html страниц сайта cp1251 (koi-8, cp866, cp1252, latin-1 и т.д.).
3. Нужно добавить AJAX функциональность, без необходимости переписывать php код

Лирика:


Как пишутся php скрипты обычно:

<body>
<form>
 <input type="text" name="my_field" value="Привет!" />
 <input type="submit" value="show me" />
</form>
<div>
<?
if (isset($_REQUEST['my_field']))
 {
  if ($_REQUEST['my_field'] == 'Привет!')
   {
    echo 'Напишите что-то поинтереснее!';
   }
  else
   {
    echo 'Получено:'.htmlspecialchars($_REQUEST['my_field']);
   }
 }
?>
</div>
</body>


Все работает, как и ожидалось:

Мы просто читаем данные запроса из массива $_REQUEST['my_field'] и получаем их в том виде, который ввел пользователь (если мы не забыли про «Content-Type: text/html; charset=windows-1251»).

Как это работает на самом деле:

1. Браузер передавая данные на сервер, кодирует символы текущей кодировки с кодами > 127 как последовательность байт в виде %{2 hex символа}. Например, в примере выше (для cp1251) мы увидим «test.php?my_field=%CF%F0%E8%E2%E5%F2%21». В этом случае CF — это код буквы «П» F0 — код буквы «р» и т.д.
2. PHP обрабатывает строковые данные «как есть», т.е. манипулирует строковыми данными как последовательностями однобайтовых символов, не пытаясь преобразовывать их в unicode (это скорее хорошо, так как это быстро). Поэтому последовательность байт %CF%F0%E8%E2%E5%F2%21 превратится в строку из 7-ми байт, что соответствует 7-ми символам слова «Привет!» в cp1251
3. Мы можем эту строку записать в БД, сравнить с другой строкой из таких же 7-ми символов и т.д.
Все это очень удобно, просто, так и хочется писать код дальше.
Собственно на php так это и делается в большинстве случаев.

А теперь JavaScript:


Допустим, мы хотим отправить на сервер HTTP request, сформированный в JavaScript (например, AJAX или даже простой location.replace переход) в котором поле query запроса будет кодироваться из JavaScript функцией.

Попробуем для кодирования поля из нашей формы применить функцию escape:


<body>
<form>
 <input type="text" name="my_field" id="my_field" value="Привет!" />
 <input type="button" value="show me" onclick="sendform()"/>
</form>
<div>
<script>
function sendform()
 {
  location.replace(location.pathname+'?my_field='+encodeURIComponent(document.getElementById('my_field').value));
 }
</script>
<?
if (isset($_REQUEST['my_field']))
 {
  if ($_REQUEST['my_field'] == 'Привет!')
   {
    echo 'Напишите что-то поинтереснее!';
   }
  else
   {
    echo 'Получено:'.htmlspecialchars($_REQUEST['my_field']);
   }
 }
?>
</div>
</body>


Как видим, «escape» не подходит:

Вместо:
test.php?my_field=%CF%F0%E8%E2%E5%F2%21
На сервер передается:
test.php?my_field=%u041F%u0440%u0438%u0432%u0435%u0442%21

В чем же дело?

«escape» кодирует символы с кодами > 127 из исходной кодовой страницы как %u{4 hex символа(unicode)} и php не распознает такую кодировку (что логично, вспомним, что строки в php это просто последовательности байт).

Попробуем применить вместо escape функцию encodeURIComponent:

Заменим:
location.replace(location.pathname+'?my_field='+escape(document.getElementById('my_field').value));
на:
location.replace(location.pathname+'?my_field='+encodeURIComponent(document.getElementById('my_field').value));

Как видим, «encodeURIComponent» тоже не подходит:

Вместо:
test.php?my_field=%CF%F0%E8%E2%E5%F2%21
На сервер передается:
test.php?my_field=%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD!

В чем же дело?

«encodeURIComponent» кодирует символы с кодами > 127 из исходной кодовой страницы путем перевода их в UTF-8 и затем передает их как последовательность %{2 hex символа}.
Примечание: Аналогично работает с символами > 127 функция encodeURI, впрочем она не подходит для url компонент.

Причина проблемы:


1. В JavaScript все строки представляются в unicode (именно так: При любой загрузке JavaScript кода, все строковые константы в нем автоматически переводятся в unicode).
2. В JavaScript нет других функций, которые кодируют строки параметров url, кроме escape и encodeURIComponent

Собственно вот мы и видим причину, по которой так много пишется что «AJAX работает только с UNICODE».

Существующие варианты решения:


1. Конечно, это не проблема, если весь сайт в UTF-8 или если его можно перенести в UTF-8, но это большой кусок работы и это не всегда возможно.
2. Можно использовать на PHP стороне функцию iconv для того чтобы перекодировать UTF-8 в нужную кодировку, но это не удобно (в частности, нужно просмотреть все обращения к аргументам) — хотя это наиболее часто рекомендуемый способ.

Как решить проблему проще:


Нужна JavaScript функция, которая кодирует данные также, как и браузер, тогда можно писать на php «как обычно».
Фактически, нужна функция которая использует таблицу перекодировки UNICODE -> текущая кодовая страница.
Существуют решения, которые реализуют такое преобразование для некоторых кодировок, но они не универсальны.

Решение:


После нескольких дней экспериментов, было найдено универсальное решение — функция encodeFormField, которая работает в любой однобайтовой кодировке (cp1251, koi-8, cp866, cp1252, latin-1 и т.д.).
Это и было опробовано в нескольких проектах.
Загрузить скрипт можно отсюда:
www.viasoft.com.ua/js/encode_form_field.zip

Основная идея:

0. Используем браузер для построения таблицы перекодировки unicode -> текущая кодовая страница:
1. javascript файл содержит строковую константу, которая содержит символы с кодами от 128 до 255 включительно.
2. Броузер загружает эту строку и преобразовывает ее в unicode в ТЕКУЩЕЙ кодировке.
3. Когда нужно преобразовать символ с кодом > 127 из ТЕКУЩЕЙ кодировки, ищется его позиция в этой строке.
4. Найденная позиция+128 и будет искомым hex кодом символа и она используется для кодировки url (или post) компонента запроса.

Как пользоваться:

1. Подключите encode_form_field.js
2. Используйте функцию encodeFormField(text) вместо escape(text) или encodeURIComponent(text) и получите правильную кодирование символов для однобайтовых кодировок.

Комменты приветствуются.

Успехов!

PS:
НЕ РЕКОМЕНДУЕТСЯ использовать copy-n-paste для того, чтобы вставить этот код к вам на страницу, так как большинство редакторов «сломают» строку преобразования. По этой же причине редактировать исходный файл нужно с осторожностью. Лучше загрузить файл из архива и использовать его на вашем сайте.

PPS:
Скрипит публикуется под BSD License, так что если есть идеи как все это улучшить — welcome!
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.