Подтвердить что ты не робот

HTML5 History.pushState управляет URL-адресом, содержащим символы с кодировкой процента, не являющейся Ascii (Unicode)

В веб-приложении OSS у нас есть JS-код, который выполняет некоторое обновление Ajax (использует jQuery, не имеет значения). После обновления страницы вызов выполняется в интерфейсе истории html5 History.pushState в следующем коде:

var updateHistory = function(url) {
    var context = { state:1, rand:Math.random() };
    /* -----> bedfore the problem call <------- */
    History.pushState( context, "Questions", url );
    /* -----> after the problem call <------- */
    setTimeout(function (){
        /* HACK: For some weird reson, sometimes something overrides the above pushState so we re-aplly it
                 This might be caused by some other JS plugin.
                 The delay of 10msec allows the other plugin to override the URL.
        */
        History.replaceState( context, "Questions", url );
    }, 10);
};

[ Обратите внимание:: полный контекст кода предоставляется для контекста, часть HACK не является проблемой этого вопроса]

Приложение i18n'ed и использует URL-кодированные Unicode-сегменты в URL-адресах, поэтому непосредственно перед помеченным вызовом проблемы в приведенном выше коде аргумент URL содержит (как проверено в Firebug):

"/%D8%A7%D9%84%D8%A3%D8%B3%D8%A6%D9%84%D8%A9/scope:all/sort:activity-desc/page:1/"

Закодированный сегмент является utf-8 в процентном кодировании. URL-адрес в окне браузера: (просто для полноты, не имеет значения)

http://<base-url>/%D8%A7%D9%84%D8%A3%D8%B3%D8%A6%D9%84%D8%A9/

После вызова URL, отображаемый в окне браузера, изменится на:

http://<base-url>/%C3%98%C2%A7%C3%99%C2%84%C3%98%C2%A3%C3%98%C2%B3%C3%98%C2%A6%C3%99%C2%84%C3%98%C2%A9/scope:all/sort:activity-desc/page:1/

Сегмент, кодированный URL-адресом, - это просто mojibake, результат использования неправильной кодировки на некотором уровне. Правильный URL-адрес:

http://<base-url>/%D8%A7%D9%84%D8%A3%D8%B3%D8%A6%D9%84%D8%A9/scope:all/sort:activity-desc/page:1/

Это поведение было протестировано как для FF, так и для Chrome.

Интерфейс истории specs ничего не упоминает о кодированных URL-адресах, но я предполагаю стандарт по умолчанию для формирования URL-адреса (utf-8 и процентное кодирование и т.д.) будут применяться при использовании URL-адреса в вызовах функций для интерфейса.

Любая идея о том, что происходит здесь.

Изменить:

Я не обращал внимания на верхний регистр H в истории - этот код фактически использует обертку History.js для интерфейса истории. Я заменил прямой вызов History.pushState (обратите внимание на нижний регистр h), не пройдя через обертку, и код работает как ожидалось, насколько я могу судить. Проблема с исходным кодом все еще стоит - так что проблема с библиотекой History.js кажется.

4b9b3361

Ответ 1

Update

Как Doug S объясняет в комментариях ниже, последняя версия History.js включает исправление для этого поведения. Он также нашел что мое решение вызвало двойное кодирование при использовании в браузерах (например, IE 9 и ниже), для которых требуется хеш-резерв, поэтому Я рекомендую вместо использования исправления, подробно описанного ниже, просто загрузить последнюю версию.

Я сохранил свой первоначальный ответ ниже, так как он объясняет, что происходит более подробно.


Базель нашел решение своего рода, но есть еще некоторое замешательство в том, что происходит под капотом. Этот ответ подробно рассказывает о проблеме и предлагает лучшее исправление. (Если хотите, вы можете перейти к исправлению.)

Проблема

Сначала откройте консоль JS вашего браузера и запустите это:

window.encodeURI(window.unescape('%D8%A7%D9%84%D8%A3%D8%B3%D8%A6%D9%84%D8%A9'))

Это выглядит знакомо? Должно быть, что ваш URL-адрес искалечен. Проблема заключается в реализации History.unescapeString, а именно в этой строке:

tmp = window.unescape(result);

window.unescape - это функция DOM Level 0, то есть нестандартная реликвия из седых дней Netscape 2. Это использует правила экранирования, определенные в RFC 2396, в соответствии с которыми кодируются символы за пределами безоговорочного диапазона (буквенно-цифровые символы и небольшой набор символов пунктуации) как октеты.

Это отлично подходит для диапазона US-ASCII, но не все (действительно, подавляющее большинство) символов в UTF-8 могут быть представлены в одном байте. Поскольку URI не имеют встроенного способа представления используемого набора символов, window.unescape просто предполагает, что каждый символ сопоставляется с одним октетом и blithely искажает любые, которые этого не делают.

В этом примере первая буква вашего URL-адреса - арабская буква alef (ا), представленная двумя байтами: 0xD8 0xA7, window.unescape интерпретирует их как два отдельных символа: 0x00 0xD8 (Ø-capital O с инсультом) и 0x00 0xA7 (знак §-секции).

Это известная проблема с History.js.

Исправление

Как уже отмечалось выше, вопрос может быть обойден, используя собственную реализацию API истории, а не обложку History.js, т.е. history.pushState вместо history.pushState.

Это работает для браузеров, поддерживающих API истории, но теряет преимущество polyfill для тех, кто этого не делает. К счастью, есть лучшее исправление. Откройте источник History.js, на который вы ссылаетесь, и найдите эту строку (~ 1059 в моей копии):

tmp = window.unescape(result);

Замените его:

tmp = window.unescape(encodeURIComponent(result));

Или, если вы используете сжатый источник, замените a.unescape(c) на a.unescape(encodeURIComponent(c)).

Чтобы протестировать это изменение, я запустил тестовый набор test.js HTML5 jQuery на локальном веб-сервере внутри каталога с арабским именем. Перед тем, как внести изменения, тест 14 терпит неудачу; после изменения все тесты прошли.

Кредит

Хотя я нашел проблему и решение самостоятельно, Damien Antipa заслуживает признания за то, что нашел его первым и сделал запрос на удаление с исправлением.

Ответ 2

Я все еще могу воспроизвести это в следующем случае:

History.pushState(null, null, "?" + some_Unicode_String_Or_A_String_With_Whitespace);
document.location.hash += "&someStuff";

В этом случае параметр _suid удаляется и также некоторый элемент. Если строка не является юникодом или не имеет пробелов (так что нет символов%) - этого не происходит.

Этот обходной путь работал у меня:

History.pushState(null, null, "?" + some_Unicode_String_Or_A_String_With_Whitespace + "&someStuff");