Вступление:
Многие, не исключая автора, сталкивались с проблемой, когда нужно использовать одновременно php и ajax в web проекте, написанном с использованием однобайтовой кодовой страницы (cp1251, koi-8, cp866, cp1252, latin-1 и т.д.). Все находили известный тезис что "AJAX работает только с UNICODE".
Попробуем разобрать проблему и найти решение.
Для нетерпеливых читателей скажу, что решение есть в виде написанной функции “encodeFormField”, которая работает с ЛЮБОЙ однобайтовой кодировкой. Эту функцию можно (нужно) использовать вместо escape или encodeURIComponent, после чего строковые данные доставляются до php в исходной кодировке (нет необходимости в использовании iconv в php скриптах).
Исподники (BSD License) можно загрузить по адресу:
http://www.viasoft.com.ua/js/encode_form_field.zip
Итак, задача:
1. Есть сайт, написанный на php
2. Кодировка html страниц сайта cp1251 (koi-8, cp866, cp1252, latin-1 и т.д.).
3. Нужно добавить AJAX функциональность, без необходимости переписывать php код
Лирика:
Как пишутся php скрипты обычно:
<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>
Все работает, как и ожидалось:
Мы просто читаем данные запроса из массива $_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:
<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='+escape(document.getElementById('my_field').value));
}
</script>
<?
if (isset($_REQUEST['my_field']))
{
if ($_REQUEST['my_field'] == 'Привет!')
{
echo 'Напишите что-то поинтереснее!';
}
else
{
echo 'Получено:'.htmlspecialchars($_REQUEST['my_field']);
}
}
?>
</div>
Как видим, "escape" не подходит:
Вместо:
test.php?my_field=%CF%F0%E8%E2%E5%F2%21
На сервер передается:
test.php?my_field=Привет%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%B F%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 и т.д.).
Это и было опробовано в нескольких проектах.
Загрузить скрипт можно отсюда:
http://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) и получите правильную кодирование символов для однобайтовых кодировок.
Как пользоваться (продвинутый режим):
Библиотека содержит функцию-заглушку работающую для случая UTF8 и детектор кодировок.
Поэтому, если сайт постепенно мигрирует с однобайтовой кодировки на UTF8, лучше искользовать так:
(Добавилось с версии 1.2)
<script src="encode_form_field_utf8_detect.js"></script>
<script>
if (encodeFormFieldIsPageOnUTF8())
{
document.write('<'+'script src="encode_form_field_utf8_stub.js"></'+'script>');
}
else
{
document.write('<'+'script src="encode_form_field.js"></'+'script>');
}
</script>
Комменты приветствуются.
Успехов!
PS:
НЕ РЕКОМЕНДУЕТСЯ использовать copy-n-paste для того, чтобы вставить этот код к вам на страницу, так как большинство редакторов "сломают" строку преобразования. По этой же причине редактировать исходный файл нужно с осторожностью. Лучше загрузить файл из архива и использовать его на вашем сайте.
PPS:
Скрипит публикуется под BSD License, так что если есть идеи как все это улучшить - welcome!
PPPS:
Некоторые однобайтовые кодировки (например windows-1257, ISO-8859-3) не поддерживают однозначного преобразования в юникод и обратно, по крайней мере браузер не может построить такую таблицу.
Для того, чтобы проверить такой случай, к библиотеке начиная с версии 1.2 прилагаются юнит-тесты