Эмуляция CORS на стороне клиента: кроссбраузерное решение некоторых пользовательских задач без расширений

  • Tutorial

I. В чём проблема



Расширения для браузеров — мощный инструмент продвинутого веб-сёрфинга, самая доступная, развитая и распространённая часть целого ряда инструментов. Однако расширения имеют и слабые стороны: каждый браузер требует знания и применения своих правил и форматов, а это дополнительная сложность для создателя. Расширения не кроссбраузерны, что сразу ограничивает их адресат. Есть попытки обобщить создание расширений, но они могут добавлять уже свою дополнительную прослойку форматов и правил.

Когда расширение улучшает специфические стороны браузерного интерфейса, без него не обойтись. Но некоторые задачи универсальны, не связаны с частными средствами браузера и, тем не менее, без расширения их тоже не выполнить. Одна из таких задач — кроссдоменные XMLHttpRequest запросы, нарушающие политику одного источника.

II. Частные попытки решения



Есть тип расширений, который помогает в этом случае и даже более того — призван упразднить необходимость многих других расширений. Это известные Greasemonkey и Tampermonkey: они позволяют обойтись простым JavaScript кодом для решения многих задач и при этом создают целый ряд вспомогательных способов для быстрой установки таких скриптов из общих источников независимо от подготовки пользователей. Однако и они не решают до конца проблему кроссбраузерности и простоты создания и использования. К тому же описание возможностей одного из самых мощных их инструментов — GM_xmlhttpRequest — говорит о том, что не все новые стандарты в нём реализованы (трудно понять, можно ли получить какой-то тип ответа кроме responseText, в частности — доступен ли документ как DOM-дерево).

В идеале хотелось бы чистого JavaScript, который бы работал во всех браузерах. Как это делается в букмарклетах или локальных HTML-страничках со скриптами. Но и букмарклеты, работающие в контексте текущей страницы, и локальные файлы не могут совершать кроссдоменные XMLHttpRequest-запросы.

Тут нам на помощь приходит технология CORS. Не сама по себе: сам её принцип не даёт пользователю влиять на её механизмы, полностью отдавая обмен данными во власть сервера и браузера. Но есть механизмы, позволяющие вмешиваться в общение этих двух элементов и подстраивать его под нужды пользователя.

Некоторые из них реализованы опять-таки в виде браузерных расширений (хм).

Firefox имеет замечательное средство редактирования HTTP-заголовков на лету: moz-rewrite. Его самый функциональный подвид — Rewrite HTTP Headers (JS). Расширение предоставляет хороший контроль над редактированием заголовков в зависимости от разных условий, позволяет писать правила на самом JavaScript с использованием переменных и доступом к полученным уже заголовкам.

Google Chrome имеет целый ряд менее функциональных расширений вроде Requestly: их возможности куда более скромны и плохо подойдут для наших нужд.

IE, кажется, не обзавёлся тут ничем подходящим.

III. Универсальное решение.



В общем, мы опять подошли к той же проблеме: страдает кроссбраузерность и простота. Необходим инструмент, который был бы независим от частностей браузеров, и тут на ум приходит локальный прокси-сервер. К счастью, есть очень простое и легковесное решение: быстро устанавливается, почти не требует настройки, реализовано уже для всех основных операционных систем. Это Fiddler.

Чаще всего он используется для изучения HTTP-трафика на стороне пользователя. Но программа умеет также и редактировать HTTP-заголовки на лету, и даже изменять тела ответов, что, в принципе, позволяет ей заменить расширения для пользовательских скриптов и стилей, вроде Greasemonkey или Stylish (пользователи же IE могут с нею впервые попробовать мощь подобных инструментов) — однако мы пока не будем писать об этой стороне приложения и остановимся лишь на перехвате и редактировании заголовков.

Скачать программу можно здесь. Советую сразу же добавить к ней расширение (хм...) для более удобного редактирования правил перехвата и изменения трафика. У него как минимум два названия и два места обитания на сайте:

FiddlerScript Editor

Syntax-Highlighting Add-Ons

Помимо прочего, оно добавит прямо в программу неплохой справочник всех встроенных свойств и методов Fiddler-а, ведь даже вполне объёмная документация не упоминает всего богатства.

При этом не нужно пугаться: Fiddler использует вполне стандартный JScript.NET, по сути, тот же JavaScript, и знакомство с дополнительными объектами, свойствами и методами приложения равны изучению одного небольшого интерфейса, вроде тех же XMLHttpRequest или свойств элементов DOM. Есть краткое введение от автора.

IV. Быстрый старт



Желающие получить полный контроль над инструментом могут потратить несколько часов на чтение документации и изучение встроенного справочника (или даже прочитать написанную автором программы книгу), я же попробую набросать сейчас вариант ответа для такого случая: вы написали букмарклет или приложение в виде локальной (или даже размещённой в сети) HTML-странички и должны разъяснить обычному пользователю, как заставить всё это работать в его браузере (есть шанс, что некоторые пользователи Firefox или Chrome, разбалованные расширениями, вас не поймут, но пользователи IE обретают тут чуть ли не единственный выход на волю).

1. Скачиваем и устанавливаем Fiddler и расширение к нему (благо, оба файла весят всего 1,7 MB, их можно даже выслать в почтовом вложении).

2. По умолчанию в программе уже почти всё настроено для наших нужд. Единственная дополнительная настройка нужна будет, если ваше приложение использует HTTPS-запросы. Просто поставьте галочки на этой вкладке:



3. IE и Google Chrome не нужно дополнительно настраивать: они по умолчанию используют системный прокси, если такой появляется, а Fiddler на время запуска как раз регистрирует себя таким прокси, очень просто и удобно. Настройки Firefox по умолчанию немного другие, но тут тоже ничего сложного. Всё кратко описано здесь:

docs.telerik.com/fiddler/Configure-Fiddler/Tasks/ConfigureBrowsers
docs.telerik.com/fiddler/Configure-Fiddler/Tasks/FirefoxHTTPS

4.После этих трёх шагов при каждом запуске Fiddler-а трафик будет автоматически идти через него, а после закрытия программы опять возвращаться на обычный путь. Если вы не хотите, чтобы Fiddler был постоянным посредником, и планируете запускать его только во время работы букмарклета или локального приложения, тогда удобнее будет добавить опцию в ярлык запуска программы:



и в настройках Fiddler-а выбрать сворачивание в значок. Тогда программа просто будет на время появляться в трее (закрывать её можно из меню тамошнего значка). Сначала это кажется слишком сложным: запускать целое приложение для работы букмарклета или странички, но быстро привыкаешь. Можно воспринимать Fiddler как кроссбраузерное расширение или как платформу для кроссбраузерных расширений, если это психологически удобнее. А если оставлять его работающим постоянно и настроить автоматический запуск при входе в систему, можно будет вообще о нём забыть: работает оно совершенно прозрачно, памяти потребляет мало (особенно если настроить его на сохранение и отображение минимального количества сессий, что нам вообще никак не вредит, — спокойно выбирайте «Keep 100 sessions», после этого на экране всегда будет лишь 100 строчек последних запросов, и память не будет существенно заполняться); процессор тоже не грузится, и никаких функциональных проблем или сколько-нибудь ощутимых задержек в работу сайтов не привносится.

5. Остался один последний шаг — не совсем тривиальный для обычного пользователя, но при внимательном выполнении в нём тоже нет ничего страшного. Нужно добавить небольшой код, который будет исполняться Fiddler-ом при каждом подходящем HTTP-соединении и изменять/добавлять нужные нам заголовки. Код является универсальным для большинства нужд кроссдоменных запросов.

Запускаем Fiddler и в меню выбираем эту позицию меню (или просто жмём Ctrl+R):



Если вы установили упомянутое расширение, в ответ открывается небольшой встроенный редактор (справа от него как раз будет колонка с полным справочником всего, что Fiddler добавляет к обычному JavaScript; при выборе элемента списка в верхней части колонки появляется краткое описание объекта, свойства или метода):



В редакторе уже будет загружен особый скрипт, похожий на пользовательские скрипты Greasemonkey: только выполняется он не при каждой загрузке подходящей странички в браузере, а при каждом подходящем событии в HTTP-трафике, проходящем через Fiddler. В скрипте уже есть предварительный код от самих разработчиков, но нам нет необходимости в нём разбираться. Нам просто нужно вставить свой маленький фрагмент в уже присутствующую функцию. Чтобы облегчить нам задачу, в редакторе есть особая позиция меню, сразу переводящая к этой функции:



Туда нам и нужно вставить следующий код (он будет объяснён позже):

	if (oSession.oRequest.headers.ExistsAndContains('Accept-Language', ',qya;q=0.001') && oSession.oRequest.headers.Exists('Origin')) {
		oSession.oResponse.headers['Access-Control-Allow-Origin'] = oSession.oRequest.headers['Origin'];
		oSession.oResponse.headers['Access-Control-Allow-Credentials'] = 'true';
		if (oSession.oRequest.headers.Exists('Access-Control-Request-Method')) {
			oSession.oResponse.headers['Access-Control-Allow-Methods'] = oSession.oRequest.headers['Access-Control-Request-Method'];
		}
		if (oSession.oRequest.headers.Exists('Access-Control-Request-Headers')) {
			oSession.oResponse.headers['Access-Control-Allow-Headers']  = oSession.oRequest.headers['Access-Control-Request-Headers'];
		}
		if (oSession.oResponse.headers.Exists('Vary')) {
			oSession.oResponse.headers['Vary'] += ', Origin';
		}
		else {
			oSession.oResponse.headers['Vary']  = 'Origin';
		}
	}


После вставки код функции должен выглядеть так (до вставки там уже был один небольшой блок, вставлять нужно после него):



После сохранения файла Fiddler автоматически перезагрузит и проанализирует скрипт и, если всё в порядке, издаст одобрительный звук и сообщит в строке статуса о принятии новой версии. Если вставка что-то нарушила, будет выдано сообщение об ошибке. Если что-то пошло не так и вы не можете вернуть файл в исходное положение, просто удалите его, и при следующем запуске программа восстановит его в первоначальном виде (адрес файла в Win 7 — c:\Users\[имя пользователя]\Documents\Fiddler2\Scripts\CustomRules.js, см. об аварийном восстановлении в справке или в комментариях в самом начале файла). В конце концов, если вам нужно передать своё приложение человеку, который побоится редактировать файл правил сам, передайте ему готовый, уже со вставкой, и пусть его подменит по нужному адресу.

Вот и всё. После этих шагов связка «Fiddler + букмарклет / локальная страница» может считаться кроссбраузерным расширением или приложением (в конце статьи я представлю несколько примитивных примеров).

V. Некоторые особенности механизма



1. Прежде всего я задался целью как-то выделить запросы от моих приложений в общем потоке, чтобы Fiddler-у не приходилось изменять все заголовки и совершать лишние проверки внутри неподходящих сессий. Пришлось последовательно опробовать и отбросить несколько вариантов.

а. Прибавка к адресам запросов особого хеша (#fiddler-измени-заголовки). Не будет работать, поскольку хеш не отправляется по HTTP, он имеет смысл только для браузера (следовательно, такой способ пригодился бы, имей мы дело с развитым расширением HTTP-редактирования для браузера: оно могло бы перехватывать хеши и изменять заголовки на основании этого условия (упомянутый Rewrite HTTP Headers (JS) может); если когда-нибудь будет разработано такое расширение с реализацией для всех основных браузеров включая Edge, можно будет подумать и о таком варианте).

б. Явное прибавление порта по умолчанию — будет отсекаться браузером. Добавлять же нестандартный порт, чтобы Fiddler перехватывал его и менял на обычный, я посчитал рискованным делом.

в. Прибавление перед доменом авторизационной части URL вида user:password@ с произвольными строками в надежде, что сервер их проигнорирует. Оказалось, что браузеры почему-то не пересылают эту часть по HTTP, до Fiddler-а она не доходит. Вдобавок эта часть, как и номер порта, всё равно бы пропадала при переадресациях, которые для XMLHttpRequest проходят прозрачно, без перехвата, — и обмен заголовков с новым адресом уже проходил бы без участия Fiddler-а.

г. Добавление маркера в часть query/search (&fiddler=1). Обнаружилось, что некоторые сервера, обнаружив незнакомую опцию в этой части, совершают переадресацию на URL уже без этой опции. Для таких случаев можно было бы добавлять маркер, скажем, &&&, который для сервера не был бы виден (при парсинге превращался бы в три пустые опции и не вызывал бы серверных опасений), — но, опять-таки, если будет происходить обычная переадресация, он из адреса пропадёт.

д. Куки. Пропадут при переадресации на другой домен.

е. Добавление особого пользовательского заголовка (какого-нибудь X-Hello-Fiddler). Оказалось, что наличие такого заголовка вызывает так называемый Preflighted request, который можно перехватить и отредактировать, но, если за ним последует переадресация, браузер вообще прервёт запрос — Preflighted request с переадресацией несовместим.

ё. Добавление стандартного заголовка. Оказалось, что и тут не всё так просто. Часть заголовков браузер не даёт добавлять по соображениям безопасности, он может создавать их только сам. Часть заголовков, даже стандартных, всё равно вызывает Preflighted request. Чтобы этого не происходило, нужно ограничиться очень малым числом заголовков, которые считаются «простыми». См. о них: www.w3.org/TR/cors/#terminology («simple header»).

Так я пришёл к оставшемуся способу: внесение безопасных изменений в обычные заголовки, которые позволяет изменять браузер. Единственным таким заголовком оказался Accept-Language. Он формируется браузером согласно настройкам предпочитаемых языков в опциях браузера. Представление о них в JavaScript коде можно получить при помощи свойства navigator.languages, но оно не поддерживается в IE11 (поддерживаемое всеми браузерами свойство navigator.language выдаёт лишь язык интерфейса самого браузера). Например, у меня на трёх браузерах код JSON.stringify([navigator.language, navigator.languages]) в консоли выдаёт следующее:

Chrome:  "[ "en-US", [ "en-US", "en", "ru", "uk" ] ]"
Firefox: "[ "en-US", [ "en-US", "en", "ru", "uk" ] ]"
IE11:    "[ "ru-RU",             null              ]"


Удобнее всего подсмотреть в веб-консоли, какой заголовок отправляет сам браузер, и использовать этот код в своих скриптах, добавляя к нему маркер. У меня браузеры выдают такие заголовки Accept-Language:

Chrome:		en-US,en;q=0.8,ru;q=0.6,uk;q=0.4
Firefox:	en-US,en;q=0.8,ru;q=0.5,uk;q=0.3
IE11:		en-US,en;q=0.8,ru;q=0.5,uk;q=0.3


Остаётся добавить к ним какой-нибудь редкий язык с самым маленьким коэффициентом предпочтения. Я выбрал квенья (Quenya), благо он уже включён в стандарт под кодом qya. Значит, нам нужно добавить в заголовок что-то вроде qya;q=0.001 (минимально допустимое стандартом число), что мы и видим в самом первом проверяемом условии для Fiddler-а чуть выше (вместе с проверкой заголовка Origin, являющегося знаком кроссдоменного запроса, — без него вообще нет смысла вмешиваться в заголовки, всё и так будет работать).

2. Что делают директивы вставки после проверки двух условий:

а. зеркалят отосланный Origin запроса — без этого браузер не отдаст ответ скрипту (при запросах от локальных страниц там будет null, но и его должен вернуть сервер);

б. добавляют поддержку авторизации при запросах (Access-Control-Allow-Credentials — без этого браузер не даст доступа к полученной информации, связанной с куками, то есть вы не получите ту страничку, какую получаете при авторизации на сайте);

в. для подстраховки зеркалят запросы браузера на особые методы и заголовки, если вдруг их удастся добавить скрипту или их захочет добавить сам браузер (я пока с такими случаями не сталкивался);

г. добавляют заголовок Vary, без которого браузер просто закэширует ответ и будет получать его из кэша в обход Fiddler-а, но при этом станет натыкаться на другой источник в Access-Control-Allow-Origin и обрывать запрос — заголовок Vary как раз поставит кэширование таких ответов в зависимость от источника запроса.

3. Некоторые возможные проблемы (в основном, у IE) и попытки их решения.

а. IE11 всё ещё пользуется так и не получившей распространение технологией P3P. Если сервер её не реализует (а так бывает в большинстве случаев), IE не станет отсылать куки такому серверу, если они не основные (что как раз и является правилом для кроссдоменных XMLHttpRequest запросов). В Firefox есть старый баг, не позволяющий даже расширениям отсылать куки, если в настройках браузера сторонние куки не разрешены. В IE имеем что-то похожее. Правила P3P очень сложны, пользователю неподвластны и вмешиваться в них при помощи Fiddler-а я не решился (к тому же куки устанавливаются вместе с правилами P3P заранее, и Fiddler-у пришлось бы за этим следить всё время и во всех сессиях, независимо от нужд приложений). Однако есть несложный выход из ситуации. В настройках IE нужно открыть эти окна и отметить такие опции:



После этого вы получите поддержку авторизации в XMLHttpRequest-запросах для IE. Кстати, я наткнулся на странный глюк: после однократного применения этой опции и последующей отмены её, IE всё равно продолжал слать сторонние куки. Возможно, это было уникальное стечение обстоятельств.

Ещё можно попробовать внести каждый нужный сайт в исключения в том же диалоге или в диалоге политики конфиденциальности, но это путь долгий и не универсальный, хоть и немного успокаивающий полезную или излишнюю паранойю.

В Chrome и Firefox сторонние куки разрешены по умолчанию (в Firefox есть полезная опция принятия только кук посещённых сайтов, чего хватает в большинстве случаев), так что здесь можно дополнительно ничего не настраивать.

б. В IE11 есть очень странный баг, природу которого я так и не понял. Только на некоторых сайтах и только при переадресации появляются два непонятных сообщения об ошибке:

SEC7120: Источник null не найден в заголовке Access-Control-Allow-Origin.

XMLHttpRequest: Сетевая ошибка 0x80070005, Отказано в доступе.

При этом в заголовках всё в норме: и в запросе, и в ответе присутствует правильный источник запроса, а никакой не null (если запрос отправляется не с локальной страницы). С чего браузер берёт, что источником запроса в переадресации стал null, совершенно непонятно. Как это исправить, я тоже не придумал.

в. Если вдруг случится, что браузер закэшировал в прошлом ответ на ваш запрос, не перехваченный Fiddler-ом (то есть в нём не было заголовка Vary с включённым в него Origin), это станет препятствием для повторного получения этого ответа при уже кроссдоменном запросе от скрипта. В таком случае, если это действительно необходимо, можно или очистить кэш (слишком радикальный выход), или настроить работу браузера без кэша во время открытой консоли разработчика — благо, консоли всех трёх основных браузеров это позволяют: в Chrome и Firefox есть опция в настройках консоли, а в IE11 есть кнопка на вкладке сети. После этого можно будет просто открывать консоль, если работа скрипта наткнётся на такое стечение обстоятельств.

г. На некоторых страницах букмарклеты отказываются работать по причине ещё одной технологии: Content Security Policy. В Firefox никак не исправят баг, который не даёт запустить вообще никаких букмарклетов, даже без XMLHttpRequest, на страницах со строгой CSP (например, Twitter или GitHab). В других браузерах CSP может не дать запустить кроссдоменный запрос. Эта проблема решается в два шага (если вы не хотите отключать CSP в тех браузерах, которые это позволяют, — а это слишком радикальный выход):

— создаём ещё одно условие Fiddlera-у для временного удаления заголовка Content-Security-Policy (вставку нужно делать после вышеупомянутого блока):

	if (/[?&]tempnocsp=1\b/i.test(oSession.PathAndQuery)) {
		oSession.oResponse.headers.Remove('Content-Security-Policy');
		oSession.oResponse.headers.Remove('X-Content-Security-Policy');
	}


— прежде чем запустить скрипт, открываем нужную страницу с добавлением query-маркера tempnocsp=1 в URL (тут уже нам не грозит непредвиденная переадресация, поскольку мы заранее знаем нужный адрес; если же сервер испугается, съест эту часть адреса и переадресует страницу, можно повторить финт с заголовком языка — добавить ещё один редкий и переписать условие для Fiddler-а). Быстро перезагрузить текущую страницу с добавлением маркера можно при помощи букмарклета:

javascript:(function(l) {
	if (!/[?&]tempnocsp=1\b/i.test(l.href)) {
		l.href += (/\?/.test(l.href) ? "&" : "?") + "tempnocsp=1";
	}
})(location);


Это пока все проблемы, которые я обнаружил. При перечислении и объяснении деталей всё звучит довольно страшно и запутано, но в большинстве случаев это совершенно не критично. Я уже какое-то время пользуюсь связкой Fiddler-а с простыми скриптами и страничками, упростил под это дело несколько своих старых расширений для Firefox и Chrome — и нисколько об этом не жалею. Лёгкость использования спровоцировала даже написание нескольких новых инструментов, которые раньше я бы обленился реализовывать в виде расширений. Единожды настроив Fiddler, потом в подавляющем большинстве случаев остаётся лишь добавлять выбранный маркер в скриптовые запросы — и всё.

VI. Некоторые примеры программ



Следующие примеры не имеют большой самостоятельной ценности, они могут показаться неуклюжими, для их случаев наверняка есть лучшие решения. Мне просто захотелось продемонстрировать несколько инструментов, для которых, не будь описанной связки, пришлось бы или писать расширения, или вообще искать другие пути (в случае с IE). Привожу четыре примера — два букмарклета и две локальные странички.

1. Запрос имени, типа, времени создания/изменения и размера файла по ссылке или заданному адресу.

Для этого нам достаточно использовать метод HEAD, чтобы получить заголовки Content-Type, Last-Modified и Content-Length. Не обо всех файлах сервер сообщает такую информацию, в таком случае мы просто получим в выводе скрипта знак вопроса. Что касается имени файла, эта информация может быть полезна при переадресации. Однако используемое свойство responseURL пока не поддерживается IE11 и в нём на этом месте всегда будет знак вопроса (если можно, проверьте, пожалуйста, как с этим обстоит дело в Edge).

Одно предупреждение: заголовки Content-Type и Last-Modified браузер отдаёт скрипту без проблем, а вот заголовок Content-Length считает небезопасным и просто так не даст к нему доступ. Например, Chrome выдаёт сообщение:

VM117:1 Refused to get unsafe header "Content-Length"

Тут самое время познакомиться с последним добавлением условия Fiddler-у, завершающим необходимый контроль над CORS: вставкой заголовка, разрешающего открытие скриптам нужных заголовков из серверного ответа. Вставку нужно делать внутри первого блока, ставя её в зависимость от наших главных условий:

		if (oSession.RequestMethod == 'HEAD') {
			if (oSession.oResponse.headers.Exists('Access-Control-Expose-Headers')) {
				oSession.oResponse.headers['Access-Control-Expose-Headers'] += ', Content-Length';
			}
			else {
				oSession.oResponse.headers['Access-Control-Expose-Headers'] = 'Content-Length';
			}
		}


В итоге, полная вставка, которая у меня внесена в пользовательский скрипт Fiddler-а и которая охватывает все текущие нужды, выглядит так:

Окончательный код вставки в функцию OnBeforeResponse
	if (oSession.oRequest.headers.ExistsAndContains('Accept-Language', ',qya;q=0.001') && oSession.oRequest.headers.Exists('Origin')) {
		oSession.oResponse.headers['Access-Control-Allow-Origin'] = oSession.oRequest.headers['Origin'];
		oSession.oResponse.headers['Access-Control-Allow-Credentials'] = 'true';
		if (oSession.oRequest.headers.Exists('Access-Control-Request-Method')) {
			oSession.oResponse.headers['Access-Control-Allow-Methods'] = oSession.oRequest.headers['Access-Control-Request-Method'];
		}
		if (oSession.oRequest.headers.Exists('Access-Control-Request-Headers')) {
			oSession.oResponse.headers['Access-Control-Allow-Headers']  = oSession.oRequest.headers['Access-Control-Request-Headers'];
		}
		if (oSession.oResponse.headers.Exists('Vary')) {
			oSession.oResponse.headers['Vary'] += ', Origin';
		}
		else {
			oSession.oResponse.headers['Vary']  = 'Origin';
		}
		if (oSession.RequestMethod == 'HEAD') {
			if (oSession.oResponse.headers.Exists('Access-Control-Expose-Headers')) {
				oSession.oResponse.headers['Access-Control-Expose-Headers'] += ', Content-Length';
			}
			else {
				oSession.oResponse.headers['Access-Control-Expose-Headers'] = 'Content-Length';
			}
		}
	}
	if (/[?&]tempnocsp=1\b/i.test(oSession.PathAndQuery)) {
		oSession.oResponse.headers.Remove('Content-Security-Policy');
		oSession.oResponse.headers.Remove('X-Content-Security-Policy');
	}


Таперь мы можем использовать следующий букмарклет, например, когда нам предлагают скачать по ссылке файл неизвестного размера и типа, — если, конечно, сервер вообще сообщит нужные данные и если не спрячет файл за каскадом редиректов:

Код букмарклета
javascript:(function(url, xhr) {
	if(!url) {return;}
	xhr = new XMLHttpRequest();
	try {xhr.open('HEAD', url, true);}
	catch (e) {
		alert(e.name + ': ' + e.message);
		return;
	}
	xhr.setRequestHeader('Accept-Language', 'en-US,en;q=0.8,ru;q=0.6,uk;q=0.4,qya;q=0.001');
	xhr.responseType = 'document';
	xhr.timeout = 10000;
	xhr.withCredentials = true;
	xhr.onload = function(evt, l) {
		l = this.getResponseHeader('Content-Length');
		alert([
				this.responseURL || '?',
				this.getResponseHeader('Content-Type')  || '?',
				this.getResponseHeader('Last-Modified') || '?',
				l ?
					[l                          + ' B',
					(l / 1024)      .toFixed(3) + ' kB',
					(l / 1048576)   .toFixed(3) + ' MB',
					(l / 1073741824).toFixed(3) + ' GB'].join(' \u2248 ')
				:
					'?'
		].join('\n\n') + '\n\n');
	};
	xhr.ontimeout = xhr.onerror = function(evt) {alert(evt.type.charAt(0).toUpperCase() + evt.type.slice(1) + '.');};
	try {xhr.send(null);}
	catch (e) {alert(e.name + ': ' + e.message); return;}
})(document.activeElement.href || prompt('URL:'))


Достаточно или установить фокус на нужной ссылке, или скопировать в буфер необходимый URL для будущей вставки, а затем нажать на букмарклет. Скажем, по поводу адреса картинки чуть выше (с окошками настроек IE11 — habrastorage.org/files/5c2/1e7/e48/5c21e7e4838b43b0b4f275d041a4e234.png) скрипт в Chrome выдаст следующее:



А ссылка на последнюю ночную сборку Firefox в репозитории выдаёт в Nightly это:



Напоследок стоит отметить, что с локальными адресами скрипт работать не будет — для них принципиально не разрешены такие запросы. Chrome поймает исключение в xhr.onerror и выдаст сообщение:

XMLHttpRequest cannot load file:///... Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https, chrome-extension-resource.

Firefox сгенерирует исключение уже на вызове xhr.send(), и, если его не поймать, скрипт зависнет. Сообщение будет такое:

NS_ERROR_DOM_BAD_URI: Access to restricted URI denied

IE11 выдаёт исключение ещё раньше, на вызове xhr.open(). Мы его тоже поймаем и завершим скрипт. Сообщение IE об ошибке самое короткое:

Error: Отказано в доступе.

2. Навигация по «хлебным крошкам»

Кроссбраузерная реализация так называемой навигационной цепочки или «Breadcrumbs», которая хорошо знакома пользователям Total Commander, последних версий Проводника или файловых менеджеров в Линуксе.

В Firefox есть замечательное расширение Advanced Locationbar. В Chrome есть чуть менее удобное и немного глючное расширение Breadcrumb Navigator. В IE11, кажется, нет ничего (хотя традиционная близость интерфейсов IE и обычного Explorer-а могла бы наконец натолкнуть разработчиков на мысль).

Ну, а у нас будет не такая удобная, но зато немного расширенная версия. При нажатии на букмарклет вверху странички будет всплывать блок с адресом, поделенным на сегменты, превращённые в самостоятельные ссылки. Разбору на подсекции будут подлежать: доменное имя (если есть поддомены), путь (если есть подпапки), поисковая строка (если есть несколько опций). При наведении на ссылку чуть ниже будет отображаться адрес перехода, а ещё ниже — заголовок документа по ссылке, для чего мы при наведении будем отправлять XMLHttpRequest и кэшировать его результат. Закрывается блок при двойном щелчке на любом месте внутри него (хотя можно настроить и закрытие при выведении мышки за границы блока — тогда кнопка букмарклета будет работать как меню).

Код букмарклета
javascript:(function(d, l, throbber, div, url, ar, arl, i, str, el, info_url, info_title){
	if (d.activeElement.href) {l = d.activeElement;}
	else {l = location;}
	throbber = 'data:image/gif;base64,R0lGODlhEAAQAOMIAAAAABoaGjMzM0xMTGZmZoCAgJmZmbKysv///////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgAIACwAAAAAEAAQAAAESBDJiQCgmFqbZwjVhhwH9n3hSJbeSa1sm5GUIHSTYSC2jeu63q0D3PlwCB1lMMgUChgmk/J8LqUIAgFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UKgmFqbpxDV9gAA9n3hSJbeSa1sm5HUMHTTcTy2jeu63q0D3PlwDx2FQMgYDBgmk/J8LqWPQuFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YSgmFqb5xjV9gQB9n3hSJbeSa1sm5EUQXQTADy2jeu63q0D3PlwDx2lUMgcDhgmk/J8LqUPg+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+cagmFqbJyHV9ggC9n3hSJbeSa1sm5FUUXRTEDy2jeu63q0D3PlwDx3FYMgAABgmk/J8LqWPw+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+QihmFqbZynV9gwD9n3hSJbeSa1sm5GUYXSTIDy2jeu63q0D3PlwDx3lcMgEAhgmk/J8LqUPAOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UqhmFqbpzHV9hAE9n3hSJbeSa1sm5HUcXTTMDy2jeu63q0D3PlwDx0FAMgIBBgmk/J8LqWPQOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YyhmFqb5znV9hQF9n3hSJbeSa1sm5EUAHQTQTy2jeu63q0D3PlwDx0lEMgMBhgmk/J8LqUPgeBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+c6hmFqbJwDV9hgG9n3hSJbeSa1sm5FUEHRTUTy2jeu63q0D3PlwDx1FIMgQCBgmk/J8LqWPweBRhV6z2q0VF94iJ9pOBAA7';

	div = d.createElement('div');
	div.setAttribute('data-breadcrumbs-main', '');
	div.appendChild(d.createElement('style')).textContent=[
		'div[data-breadcrumbs-main] {position:fixed; top:0px; left:0; z-index:999999; width:100%; padding:10px !important; background-color:white !important; outline:1px solid black !important; font-size: 12pt !important; text-align: left !important;}',
		'div[data-breadcrumbs-main] a {font-size: 12pt !important; text-decoration: underline !important;}',
		'div[data-breadcrumbs-main] div {margin-top: 20px!important; color: silver !important;}',
		'div[data-breadcrumbs-info-title] {background-repeat: no-repeat !important; background-position: left center !important; padding-left: 20px !important;}',
	].join('\n');

	url = l.protocol + (/:[/][/]/.test(l.href) ? '//' : '');
	div.appendChild(d.createElement('span')).textContent = url;

	ar = l.hostname.split('.');
	arl = ar.length;
	if(arl > 1) {
		ar.splice(arl - 2, 2, ar.slice(arl - 2).join('.'));
		arl--;
	}
	for (i = 0; i < arl; i++) {
		str = ar[i];
		if (i > 0) {div.appendChild(d.createElement('span')).textContent = '.';}
		el = div.appendChild(d.createElement('a'));
		el.textContent = str;
		el.href = url + ar.slice(i).join('.');
	}
	url += l.hostname;

	if(l.port && /:\d+$/.test(l.host)) {
		div.appendChild(d.createElement('span')).textContent = ':';
		el = div.appendChild(d.createElement('a'));
		el.textContent = l.port;
		url += ':' + l.port;
		el.href = url;
	}

	div.appendChild(d.createElement('span')).textContent = '/';
	url += '/';
	if(l.pathname.length > 1){
		ar = l.pathname.split('/');
		ar.shift();
		arl = ar.length;
		for (i = 0; i < arl; i++) {
			str = ar[i];
			if (i < arl-1) {
				el = div.appendChild(d.createElement('a'));
				el.textContent = str;
				url += str;
				div.appendChild(d.createElement('span')).textContent = '/';
				url += '/';
				el.href = url;
			}
			else if (str !== '') {
				el = div.appendChild(d.createElement('a'));
				el.textContent = str;
				url += str;
				el.href = url;
			}
		}
	}

	if(l.search){
		ar = l.search.split('&');
		arl = ar.length;
		ar[0] = ar[0].substring(1);
		div.appendChild(d.createElement('span')).textContent = '?';
		url += '?';
		for (i = 0; i < arl; i++) {
			str = ar[i];
			if (i > 0) {
				div.appendChild(d.createElement('span')).textContent = '&';
				url += '&';
			}
			el = div.appendChild(d.createElement('a'));
			el.textContent = str;
			url += str;
			el.href = url;
		}
	}

	if(l.hash){
		div.appendChild(d.createElement('span')).textContent = '#';
		el = div.appendChild(d.createElement('a'));
		el.textContent = l.hash.substring(1);
		url += l.hash;
		el.href = url;
	}

	if (div.querySelector('a:last-of-type').href == location.href) {
		div.querySelector('a:last-of-type').setAttribute('data-breadcrumbs-doc-title', d.title);
	}

	info_url = div.appendChild(d.createElement('div'));
	info_url.setAttribute('data-breadcrumbs-info-url', '');
	info_url.textContent = 'URL';

	info_title = div.appendChild(d.createElement('div'));
	info_title.setAttribute('data-breadcrumbs-info-title', '');
	info_title.textContent = 'Title';

	div.addEventListener('mouseover', function(evt, h, t){
		if (h = evt.target.href) {
			info_url.textContent = h;
			if (t = evt.target.getAttribute('data-breadcrumbs-doc-title')) {
				info_title.textContent = t;
			}
			else{getTitle(evt.target);}
		}
	});

	div.addEventListener('dblclick' /*mouseleave*/, function(){div.parentNode.removeChild(div);});

	d.body.appendChild(div);

	function getTitle(lnk, xhr) {
		info_title.style.backgroundImage = 'url('+ throbber + ')';
		xhr = new XMLHttpRequest();
		try {xhr.open('GET', lnk.href, true);}
		catch (e) {
			info_title.style.backgroundImage = 'none';
			info_title.textContent = e.name + ': ' + e.message;
			return;
		}
		xhr.setRequestHeader('Accept-Language', 'en-US,en;q=0.8,ru;q=0.6,uk;q=0.4,qya;q=0.001');
		xhr.responseType = 'document';
		xhr.timeout = 10000;
		xhr.withCredentials = true;
		xhr.onload = function() {
			info_title.style.backgroundImage = 'none';
			if (this.response && this.response.title) {
				info_title.textContent = this.response.title;
				lnk.setAttribute('data-breadcrumbs-doc-title', this.response.title);
			}
			else {
				info_title.textContent = '?';
				lnk.setAttribute('data-breadcrumbs-doc-title', '?');
			}
		};
		xhr.ontimeout = xhr.onerror = function(evt){
			info_title.style.backgroundImage = 'none';
			info_title.textContent = evt.type.charAt(0).toUpperCase() + evt.type.slice(1) + '.';
		};
		try {xhr.send(null);}
		catch (e) {
			info_title.style.backgroundImage = 'none';
			info_title.textContent = e.name + ': ' + e.message;
		}
	}
})(document)


Если не выходить за границы источника (в частности, поддомена) и если какие-то части адреса не предусматривают переадресацию на другой поддомен, такой скрипт мог бы обходиться и без CORS. Но теперь мы можем обойти все части адреса, в том числе и основной домен. Скажем, на страничке поиска по MDN при наведении на основной домен мы получаем не ошибку, а название документа:



Прежние замечания о доступе к локальным файлам остаются в силе.

Поскольку размер этого букмарклета в читабельном виде превышает ограничения IE11 (см. ссылки в конце статьи), вот его версия, минифицированная при помощи UglifyJS:

Минифицированный код букмарклета
javascript:(function(e,t,n,a,A,i,r,o,l,p,d,s){function h(e,t){s.style.backgroundImage='url('+n+')',t=new XMLHttpRequest;try{t.open('GET',e.href,!0)}catch(a){return s.style.backgroundImage='none',void(s.textContent=a.name+': '+a.message)}t.setRequestHeader('Accept-Language','en-US,en;q=0.8,ru;q=0.6,uk;q=0.4,qya;q=0.001'),t.responseType='document',t.timeout=1e4,t.withCredentials=!0,t.onload=function(){s.style.backgroundImage='none',this.response&&this.response.title?(s.textContent=this.response.title,e.setAttribute('data-breadcrumbs-doc-title',this.response.title)):(s.textContent='?',e.setAttribute('data-breadcrumbs-doc-title','?'))},t.ontimeout=t.onerror=function(e){s.style.backgroundImage='none',s.textContent=e.type.charAt(0).toUpperCase()+e.type.slice(1)+'.'};try{t.send(null)}catch(a){s.style.backgroundImage='none',s.textContent=a.name+': '+a.message}}for(t=e.activeElement.href?e.activeElement:location,n='data:image/gif;base64,R0lGODlhEAAQAOMIAAAAABoaGjMzM0xMTGZmZoCAgJmZmbKysv///////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgAIACwAAAAAEAAQAAAESBDJiQCgmFqbZwjVhhwH9n3hSJbeSa1sm5GUIHSTYSC2jeu63q0D3PlwCB1lMMgUChgmk/J8LqUIAgFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UKgmFqbpxDV9gAA9n3hSJbeSa1sm5HUMHTTcTy2jeu63q0D3PlwDx2FQMgYDBgmk/J8LqWPQuFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YSgmFqb5xjV9gQB9n3hSJbeSa1sm5EUQXQTADy2jeu63q0D3PlwDx2lUMgcDhgmk/J8LqUPg+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+cagmFqbJyHV9ggC9n3hSJbeSa1sm5FUUXRTEDy2jeu63q0D3PlwDx3FYMgAABgmk/J8LqWPw+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+QihmFqbZynV9gwD9n3hSJbeSa1sm5GUYXSTIDy2jeu63q0D3PlwDx3lcMgEAhgmk/J8LqUPAOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UqhmFqbpzHV9hAE9n3hSJbeSa1sm5HUcXTTMDy2jeu63q0D3PlwDx0FAMgIBBgmk/J8LqWPQOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YyhmFqb5znV9hQF9n3hSJbeSa1sm5EUAHQTQTy2jeu63q0D3PlwDx0lEMgMBhgmk/J8LqUPgeBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+c6hmFqbJwDV9hgG9n3hSJbeSa1sm5FUEHRTUTy2jeu63q0D3PlwDx1FIMgQCBgmk/J8LqWPweBRhV6z2q0VF94iJ9pOBAA7',a=e.createElement('div'),a.setAttribute('data-breadcrumbs-main',''),a.appendChild(e.createElement('style')).textContent='div[data-breadcrumbs-main] {position:fixed; top:0px; left:0; z-index:999999; width:100%; padding:10px !important; background-color:white !important; outline:1px solid black !important; font-size: 12pt !important; text-align: left !important;}\ndiv[data-breadcrumbs-main] a {font-size: 12pt !important; text-decoration: underline !important;}\ndiv[data-breadcrumbs-main] div {margin-top: 20px!important; color: silver !important;}\ndiv[data-breadcrumbs-info-title] {background-repeat: no-repeat !important; background-position: left center !important; padding-left: 20px !important;}',A=t.protocol+(/:[\/][\/]/.test(t.href)?'//':''),a.appendChild(e.createElement('span')).textContent=A,i=t.hostname.split('.'),r=i.length,r>1&&(i.splice(r-2,2,i.slice(r-2).join('.')),r--),o=0;r>o;o++)l=i[o],o>0&&(a.appendChild(e.createElement('span')).textContent='.'),p=a.appendChild(e.createElement('a')),p.textContent=l,p.href=A+i.slice(o).join('.');if(A+=t.hostname,t.port&&/:\d+$/.test(t.host)&&(a.appendChild(e.createElement('span')).textContent=':',p=a.appendChild(e.createElement('a')),p.textContent=t.port,A+=':'+t.port,p.href=A),a.appendChild(e.createElement('span')).textContent='/',A+='/',t.pathname.length>1)for(i=t.pathname.split('/'),i.shift(),r=i.length,o=0;r>o;o++)l=i[o],r-1>o?(p=a.appendChild(e.createElement('a')),p.textContent=l,A+=l,a.appendChild(e.createElement('span')).textContent='/',A+='/',p.href=A):''!==l&&(p=a.appendChild(e.createElement('a')),p.textContent=l,A+=l,p.href=A);if(t.search)for(i=t.search.split('&'),r=i.length,i[0]=i[0].substring(1),a.appendChild(e.createElement('span')).textContent='?',A+='?',o=0;r>o;o++)l=i[o],o>0&&(a.appendChild(e.createElement('span')).textContent='&',A+='&'),p=a.appendChild(e.createElement('a')),p.textContent=l,A+=l,p.href=A;t.hash&&(a.appendChild(e.createElement('span')).textContent='#',p=a.appendChild(e.createElement('a')),p.textContent=t.hash.substring(1),A+=t.hash,p.href=A),a.querySelector('a:last-of-type').href==location.href&&a.querySelector('a:last-of-type').setAttribute('data-breadcrumbs-doc-title',e.title),d=a.appendChild(e.createElement('div')),d.setAttribute('data-breadcrumbs-info-url',''),d.textContent='URL',s=a.appendChild(e.createElement('div')),s.setAttribute('data-breadcrumbs-info-title',''),s.textContent='Title',a.addEventListener('mouseover',function(e,t,n){(t=e.target.href)&&(d.textContent=t,(n=e.target.getAttribute('data-breadcrumbs-doc-title'))?s.textContent=n:h(e.target))}),a.addEventListener('dblclick',function(){a.parentNode.removeChild(a)}),e.body.appendChild(a)})(document)


3. Агрегация данных в локальной странице.

Следующий пример можно использовать как шаблон для собирания разной информации на одной локальной страничке. В начале скрипта находится отправной объект sites, содержащий минимум данных для добавления в агрегатор новых заданий. Каждый вложенный в него объект содержит два обязательных ключа: адрес сайта и CSS-селекторы элементов, из которых нужно извлечь текст; и может содержать два факультативных ключа: страницу перехода по ссылке с целью полного ознакомления (если она не совпадает со страницей запроса информации) и регулярное выражение, которым извлекаемый текст нужно обработать (в виде массива из двух элементов — шаблона и строки замены).

Селекторы пользователь может копировать и вставлять при помощи меню элементов в консоли разработчика (в IE11 можно только подсмотреть в строке селекторного варианта уже знакомых нам хлебных крошек, обновляющейся под кодом страницы при выборе разных элементов DOM-дерева).

Для простоты примера в документ включены страницы профиля одного и того же пользователя в четырёх разделах Хабрахабра, селекторы выбирают минимум информации о пользователе, а однотипные регулярные выражения минимально обрабатывают один из фрагментов для лучшей читабельности.

Пользуясь этими настройками, скрипт создаёт по разделу на каждый вложенный объект и загружает туда информацию сразу после открытия локальной страницы-агрегатора. Почти никакой интерактивности не предусмотрено (разве что хеш в адресе самой странички предотвращает исполнение запросов, но сделано это просто для удобства отладки). Поскольку добавлять мы будем проверенные адреса, отлавливать особенные исключения, как в предыдущих случаях, мы в этот раз не станем.

После сохранения кода в HTML-файл и открытия его в браузере мы получаем вот такую выборку:



Сам код агрегатора:

Код локальной странички
<!doctype html>
<html>
	<head>
		<meta charset='UTF-8'><title>User Data Aggregation</title>
		<style>
			body > div > a {display: inline-block; background-repeat: no-repeat;}
			body > div > pre, body > div > a {padding: 5px 5px 5px 20px; margin: 0px;}
		</style>
		<script>
/******************************************************************************/
'use strict';
/******************************************************************************/
var sites = {
	'habrahabr.ru': {
		checkPageURL: 'http://habrahabr.ru/users/vmb/',
		keyElementSelectors: '#layout > div.inner > div.user_header > div.karma, #layout > div.inner > div.user_header > div.rating, #layout > div.inner > div.content_left > div.user_profile > div.rating-place, #layout > div.inner > div.sidebar_right > div.block.user_info > div.info',
		re: [ /(\d+) (\d+ .+)/, '$1 ($2)' ]
	},
	'geektimes.ru': {
		checkPageURL: 'http://geektimes.ru/users/vmb/',
		keyElementSelectors: '#layout > div.inner > div.user_header > div.karma, #layout > div.inner > div.user_header > div.rating, #layout > div.inner > div.content_left > div.user_profile > div.rating-place, #layout > div.inner > div.sidebar_right > div.block.user_info > div.info',
		re: [ /(\d+) (\d+ .+)/, '$1 ($2)' ]
	},
	'megamozg.ru': {
		checkPageURL: 'http://megamozg.ru/users/vmb/',
		keyElementSelectors: '#layout > div.inner > div.user_header > div.karma, #layout > div.inner > div.user_header > div.rating, #layout > div.inner > div.content_left > div.user_profile > div.rating-place, #layout > div.inner > div.sidebar_right > div.block.user_info > div.info',
		re: [ /(\d+) (\d+ .+)/, '$1 ($2)' ]
	},
	'toster.ru': {
		checkPageURL: 'https://toster.ru/user/vmb',
		keyElementSelectors: '#js-canvas > div.layout__body > section > div.column_main > main > header > div.page-header__stats > ul > li',
	},
};
/******************************************************************************/
var throbber = 'data:image/gif;base64,R0lGODlhEAAQAOMIAAAAABoaGjMzM0xMTGZmZoCAgJmZmbKysv///////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgAIACwAAAAAEAAQAAAESBDJiQCgmFqbZwjVhhwH9n3hSJbeSa1sm5GUIHSTYSC2jeu63q0D3PlwCB1lMMgUChgmk/J8LqUIAgFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UKgmFqbpxDV9gAA9n3hSJbeSa1sm5HUMHTTcTy2jeu63q0D3PlwDx2FQMgYDBgmk/J8LqWPQuFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YSgmFqb5xjV9gQB9n3hSJbeSa1sm5EUQXQTADy2jeu63q0D3PlwDx2lUMgcDhgmk/J8LqUPg+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+cagmFqbJyHV9ggC9n3hSJbeSa1sm5FUUXRTEDy2jeu63q0D3PlwDx3FYMgAABgmk/J8LqWPw+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+QihmFqbZynV9gwD9n3hSJbeSa1sm5GUYXSTIDy2jeu63q0D3PlwDx3lcMgEAhgmk/J8LqUPAOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UqhmFqbpzHV9hAE9n3hSJbeSa1sm5HUcXTTMDy2jeu63q0D3PlwDx0FAMgIBBgmk/J8LqWPQOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YyhmFqb5znV9hQF9n3hSJbeSa1sm5EUAHQTQTy2jeu63q0D3PlwDx0lEMgMBhgmk/J8LqUPgeBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+c6hmFqbJwDV9hgG9n3hSJbeSa1sm5FUEHRTUTy2jeu63q0D3PlwDx1FIMgQCBgmk/J8LqWPweBRhV6z2q0VF94iJ9pOBAA7';
/******************************************************************************/
document.addEventListener('DOMContentLoaded', init);
/******************************************************************************/
function init() {
	for (var site in sites) {
		var div = document.body.appendChild(document.createElement('div'));
		var querier = document.createElement('a');
		querier.textContent = site;
		querier.target = '_blank';
		querier.href = sites[site].openPageURL || sites[site].checkPageURL;
		div.appendChild(querier);
	}
	if(!location.hash){getAll();}
}
/******************************************************************************/
function getAll() {
	var queriers = document.querySelectorAll('a');
	for (var i = 0, querier; querier = queriers[i]; i++) {
		window.setTimeout(getDoc, i*1000, querier);
	}
}
/******************************************************************************/
function getDoc(querier) {
	querier.style.backgroundImage = 'url('+ throbber + ')';
	var site = querier.textContent;
	var xhr = new XMLHttpRequest();
	xhr.open('GET', sites[site].checkPageURL, true);
	xhr.setRequestHeader('Accept-Language','en-US,en;q=0.8,ru;q=0.6,uk;q=0.4,qya;q=0.001');
	xhr.responseType = 'document';
	xhr.timeout = 10000;
	xhr.withCredentials = true;
	xhr.onload = function() {
		processDoc(this.response, querier);
	};
	xhr.ontimeout = xhr.onerror = function(evt) {
		querier.parentNode.appendChild(document.createElement('pre')).textContent =
					evt.type.charAt(0).toUpperCase() + evt.type.slice(1) + '.';
		querier.style.backgroundImage = 'none';
	};
	xhr.send(null);
}
/******************************************************************************/
function processDoc(doc, querier) {
	var site = querier.textContent;
	var keyElements = doc.querySelectorAll(sites[site].keyElementSelectors);
	if (keyElements.length) {
		querier.parentNode.appendChild(document.createElement('pre')).textContent =
			[].map.call(keyElements, function(el) {
				el = el.textContent.trim().replace(/\s+/g, ' ');
				if (sites[site].re) {el = el.replace(sites[site].re[0], sites[site].re[1]);}
				return el;
			}).join('\n');
	}
	else {
		querier.parentNode.appendChild(document.createElement('pre')).textContent = 'Parsing error.';
	}
	querier.style.backgroundImage = 'none';
}
/******************************************************************************/
		</script>
	</head>
	<body></body>
</html>


4. Запрос информации и сравнение её с предыдущим состоянием

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

Для примера взяты два чарта IMDb (поэтому данный случай можно рассматривать как шаблон для сравнения других рейтингов): 250 лучших фильмов и 250 лучших телевизионных сериалов. Каждый запрос сравнивает данные с предыдущим запросом и выводит отличия: что добавилось, что исчезло, что поднялось или опустилось в списке.

В начале скрипта мы встречаем уже знакомый нам объект с настройками, но тут он проще: только адрес страницы и селекторы ключевых элементов (как правило, это будут ссылки на подробную информацию об элементах рейтинга).

Для хранения результатов скрипт использует localStorage, но, если вы планируете хранить долгую историю объёмных запросов, лучше будет реализовать хранение и получение данных при помощи indexedDB.

Для каждого чарта скрипт создаёт блок текущих новостей и блок истории прошлых сравнений (с заголовком, включающим кнопку очистки истории и опцию её полного отключения; в случае с IMDb, я бы отключил историю, потому что сами списки большие, а изменения происходят всё время — особенно в порядке элементов рейтинга; с такой историей страница будет быстро разрастаться).

Объект с сохраняемыми данными содержит массив ссылок на фильмы в рейтинге, массив их названий, историю различий (если она не очищена и не была отключена), опцию ведения истории и дату последней проверки.

Результаты сравнения (при отключённой истории) в Chrome выглядят так (плюс означает вхождение нового фильма в топ, минус — удаление фильма из топа (и то, и другое бывает иногда на очень короткое время — особенно неустойчивы последние элементы списка), стрелки обозначают движение позиций вниз или вверх).

а. Для фильмов:



б. Для сериалов:



Тут мы должны сделать последнее важное замечание. Chrome и Firefox предоставляют localStorage для локальных страниц, а IE11 — нет (проверьте, пожалуйста, кто может, как с этим обстоит дело в Edge). Однако Fiddler нам и тут сослужит хорошую службу. Он может работать и как небольшой сервер статических страничек. Раз он уже всё равно запущен, можем заодно попросить его отдавать нашу страничку как бы с сетевого адреса при оговоренных запросах, чтобы IE11 разрешал для неё localStorage. Делается это очень просто в одной из вкладок Fiddler-а, в «AutoResponder» (обратите внимание на вторую галочку, без неё Fiddler перестанет пропускать запросы, не подходящие под заданный вами URL):



Внизу вкладки вам нужно ввести два адреса: любой сетевой путь относительно localhost (можно относительно 127.0.0.1; в принципе, можно задать любой адрес, но так будет логически и психологически проще), его вы сохраните в закладках IE11; и локальный адрес странички на диске. После этого всё будет работать и в IE11. Вот, например, страничка первого открытия, когда сравнивать ещё нечего, просто сохраняется первое status quo:



Наконец, код документа:

Код локальной странички
<!doctype html>
<html>
	<head>
		<meta charset='UTF-8'><title>IMDb Charts History</title>
		<style>
			body > div > * {padding: 5px 20px 5px 20px; margin: 2px;}
			body > div > a {display: inline-block; background-repeat: no-repeat; background-position: left center;}
			body > div > pre {border: 1px solid silver;}
			body > div > pre:not(:first-of-type) {background-color: gainsboro;}
			body > div > pre > button {margin: 0px 0px 0px 20px;}
		</style>
		<script>
/******************************************************************************/
'use strict';
/******************************************************************************/
var sites = {
	'IMDb Top 250': {
		checkPageURL: 'http://www.imdb.com/chart/top',
		keyElementsSelector: '#main > div > span > div > div > div.lister > table > tbody > tr > td.titleColumn > a',
	},
	'IMDb Top 250 TV': {
		checkPageURL: 'http://www.imdb.com/chart/toptv',
		keyElementsSelector: '#main > div > span > div > div > div.lister > table > tbody > tr > td.titleColumn > a',
	},
};
/******************************************************************************/
var netData = JSON.parse(localStorage['IMDbChartsHistory.netData'] || '{}');
var throbber = 'data:image/gif;base64,R0lGODlhEAAQAOMIAAAAABoaGjMzM0xMTGZmZoCAgJmZmbKysv///////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgAIACwAAAAAEAAQAAAESBDJiQCgmFqbZwjVhhwH9n3hSJbeSa1sm5GUIHSTYSC2jeu63q0D3PlwCB1lMMgUChgmk/J8LqUIAgFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UKgmFqbpxDV9gAA9n3hSJbeSa1sm5HUMHTTcTy2jeu63q0D3PlwDx2FQMgYDBgmk/J8LqWPQuFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YSgmFqb5xjV9gQB9n3hSJbeSa1sm5EUQXQTADy2jeu63q0D3PlwDx2lUMgcDhgmk/J8LqUPg+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+cagmFqbJyHV9ggC9n3hSJbeSa1sm5FUUXRTEDy2jeu63q0D3PlwDx3FYMgAABgmk/J8LqWPw+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+QihmFqbZynV9gwD9n3hSJbeSa1sm5GUYXSTIDy2jeu63q0D3PlwDx3lcMgEAhgmk/J8LqUPAOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UqhmFqbpzHV9hAE9n3hSJbeSa1sm5HUcXTTMDy2jeu63q0D3PlwDx0FAMgIBBgmk/J8LqWPQOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YyhmFqb5znV9hQF9n3hSJbeSa1sm5EUAHQTQTy2jeu63q0D3PlwDx0lEMgMBhgmk/J8LqUPgeBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+c6hmFqbJwDV9hgG9n3hSJbeSa1sm5FUEHRTUTy2jeu63q0D3PlwDx1FIMgQCBgmk/J8LqWPweBRhV6z2q0VF94iJ9pOBAA7';
/******************************************************************************/
document.addEventListener('DOMContentLoaded', init);
/******************************************************************************/
function init() {
	for (var site in sites) {
		if(!netData[site]) {
			netData[site] = {lastEntries: [], lastEntriesTitles: [], history: [], saveHistory: true, lastCheck: ''};
		}
		var div = document.body.appendChild(document.createElement('div'));
		var watcher = div.appendChild(document.createElement('a'));
		watcher.textContent = site;
		watcher.target = '_blank';
		watcher.href = sites[site].openPageURL || sites[site].checkPageURL;
	}
	localStorage['IMDbChartsHistory.netData'] = JSON.stringify(netData);
	if(!location.hash){getAll();}
}
/******************************************************************************/
function getAll() {
	var watchers = document.querySelectorAll('a');
	for (var i = 0, watcher; watcher = watchers[i]; i++) {
		window.setTimeout(getDoc, i*2000, watcher);
	}
}
/******************************************************************************/
function getDoc(watcher) {
	watcher.style.backgroundImage = 'url('+ throbber + ')';
	var site = watcher.textContent;
	var xhr = new XMLHttpRequest();
	xhr.open('GET', sites[site].checkPageURL, true);
	xhr.setRequestHeader('Accept-Language','en-US,en;q=0.8,ru;q=0.6,uk;q=0.4,qya;q=0.001');
	xhr.responseType = 'document';
	xhr.timeout = 10000;
	xhr.withCredentials = true;
	xhr.onload = function() {
		processDoc(this.response, watcher);
	};
	xhr.ontimeout = xhr.onerror = function(evt) {
		watcher.parentNode.appendChild(document.createElement('pre')).textContent =
					evt.type.charAt(0).toUpperCase() + evt.type.slice(1) + '.';
		watcher.style.backgroundImage = 'none';
	};
	xhr.send(null);
}
/******************************************************************************/
function processDoc(doc, watcher) {
	var site = watcher.textContent;
	var siteData = netData[site];
	var keyElements = doc.querySelectorAll(sites[site].keyElementsSelector);
	var info, news;
	if (keyElements.length) {
		var now = new Date(Date.now()).toLocaleString();
		var curEntries = [].map.call(keyElements, function(el) {
			el.search = '';
			return el.href;
		});
		var curEntriesTitles = [].map.call(keyElements, function(el) {
			return el.textContent;
		});
		if (siteData.lastEntries.length) {
			var added = curEntries.filter(function(el) {
					return siteData.lastEntries.indexOf(el) == -1;
				}).map(function(el) {
					var ci = curEntries.indexOf(el);
					return '+ ' + (ci + 1) + '\t<a href="' + el + '">' + curEntriesTitles[ci] + '</a>';
			});
			var removed = siteData.lastEntries.filter(function(el) {
					return curEntries.indexOf(el) == -1;
				}).map(function(el) {
					var li = siteData.lastEntries.indexOf(el);
					return '- (' + (li + 1) + ')\t<a href="' + el + '">' + siteData.lastEntriesTitles[li] + '</a>';
			});
			var risen = siteData.lastEntries.filter(function(el, li) {
					var ci = curEntries.indexOf(el);
					return ci > -1 && ci < li;
				}).map(function(el) {
					var ci = curEntries.indexOf(el);
					return '↑ ' + (siteData.lastEntries.indexOf(el) + 1) + '\t→\t' + (ci + 1) + '\t<a href="' + el + '">' + curEntriesTitles[ci] + '</a>';
			});
			var fallen = siteData.lastEntries.filter(function(el, li) {
					return curEntries.indexOf(el) > li;
				}).map(function(el) {
					var ci = curEntries.indexOf(el);
					return '↓ ' + (siteData.lastEntries.indexOf(el) + 1) + '\t→\t' + (ci + 1) + '\t<a href="' + el + '">' + curEntriesTitles[ci] + '</a>';
			});
			news = added.length || removed.length || risen.length || fallen.length;
			info = now + ' (previous check: ' + siteData.lastCheck + ')<hr>' +
				(news?
					[added.join('\n'), removed.join('\n'), risen.join('\n'), fallen.join('\n')]
						.filter(function(el){return el.length;}).join('\n\n')
					:
					'No news.'
				);
		}
		else {
			info = now + '<hr>First launch. Data saved (' + curEntries.length + ' entries).';
		}
		watcher.parentNode.appendChild(document.createElement('pre')).innerHTML = info;
		var historyHeader = watcher.parentNode.appendChild(document.createElement('pre'));
		var historyOptionLabel = historyHeader.appendChild(document.createElement('label'));
		var historyOption = historyOptionLabel.appendChild(document.createElement('input'));
		historyOption.type = 'checkbox';
		historyOption.checked = siteData.saveHistory;
		historyOption.addEventListener('click', toggleHistory);
		historyOptionLabel.appendChild(document.createTextNode('Save history'));
		var clearer = historyHeader.appendChild(document.createElement('button'));
		clearer.type = 'button';
		clearer.textContent = 'Clear history';
		clearer.disabled = !siteData.history.length;
		clearer.addEventListener('click', clearHistory);
		watcher.parentNode.appendChild(document.createElement('pre')).innerHTML =
			siteData.history.length? siteData.history.join('\n<hr>\n') : 'No history.';
		siteData.lastEntries = curEntries;
		siteData.lastEntriesTitles = curEntriesTitles;
		if (news && siteData.saveHistory) {siteData.history.unshift(info);}
		siteData.lastCheck = now;
		localStorage['IMDbChartsHistory.netData'] = JSON.stringify(netData);
	}
	else {
		watcher.parentNode.appendChild(document.createElement('pre')).textContent = 'Parsing error.';
	}
	watcher.style.backgroundImage = 'none';
}
/******************************************************************************/
function toggleHistory(evt) {
	var site = evt.target.parentNode.parentNode.parentNode.querySelector('a').textContent;
	var siteData = netData[site];
	siteData.saveHistory = evt.target.checked;
	localStorage['IMDbChartsHistory.netData'] = JSON.stringify(netData);
}
/******************************************************************************/
function clearHistory(evt) {
	var site = evt.target.parentNode.parentNode.querySelector('a').textContent;
	var siteData = netData[site];
	siteData.history = [];
	evt.target.parentNode.parentNode.querySelector('pre:last-of-type').innerHTML = 'History cleared.';
	localStorage['IMDbChartsHistory.netData'] = JSON.stringify(netData);
	evt.target.disabled = true;
}
/******************************************************************************/
		</script>
	</head>
	<body></body>
</html>


VII. Приложение



1. Поскольку в IE добавлять букмарклеты с нуля — занятие муторное, вот код первого и второго примера в виде ссылок внутри HTML-странички. Сохраните код в файл, откройте в IE и перетащите ссылки в закладки, если нужно:

Страничка с ссылками-букмарклетами
<!doctype html>
<html>
	<head><meta charset="UTF-8"><title>Bookmarklets</title></head>
	<body style="text-align: center; font-family: monospace; font-size: 12pt;">
		<p><a href="javascript:(function(url, xhr) {if(!url) {return;}xhr = new XMLHttpRequest();try {xhr.open('HEAD', url, true);}catch (e) {alert(e.name + ': ' + e.message);return;}xhr.setRequestHeader('Accept-Language', 'en-US,en;q=0.8,ru;q=0.6,uk;q=0.4,qya;q=0.001');xhr.responseType = 'document';xhr.timeout = 10000;xhr.withCredentials = true;xhr.onload = function(evt, l) {l = this.getResponseHeader('Content-Length');alert([this.responseURL || '?',this.getResponseHeader('Content-Type')  || '?',this.getResponseHeader('Last-Modified') || '?',l ?[l                          + ' B',(l / 1024)      .toFixed(3) + ' kB',(l / 1048576)   .toFixed(3) + ' MB',(l / 1073741824).toFixed(3) + ' GB'].join(' \u2248 '):'?'].join('\n\n') + '\n\n');};xhr.ontimeout = xhr.onerror = function(evt) {alert(evt.type.charAt(0).toUpperCase() + evt.type.slice(1) + '.');};try {xhr.send(null);}catch (e) {alert(e.name + ': ' + e.message); return;}})(document.activeElement.href || prompt('URL:'))">О файле</a></p>
		<p><a href="javascript:(function(e,t,n,a,A,i,r,o,l,p,d,s){function h(e,t){s.style.backgroundImage='url('+n+')',t=new XMLHttpRequest;try{t.open('GET',e.href,!0)}catch(a){return s.style.backgroundImage='none',void(s.textContent=a.name+': '+a.message)}t.setRequestHeader('Accept-Language','en-US,en;q=0.8,ru;q=0.6,uk;q=0.4,qya;q=0.001'),t.responseType='document',t.timeout=1e4,t.withCredentials=!0,t.onload=function(){s.style.backgroundImage='none',this.response&&this.response.title?(s.textContent=this.response.title,e.setAttribute('data-breadcrumbs-doc-title',this.response.title)):(s.textContent='?',e.setAttribute('data-breadcrumbs-doc-title','?'))},t.ontimeout=t.onerror=function(e){s.style.backgroundImage='none',s.textContent=e.type.charAt(0).toUpperCase()+e.type.slice(1)+'.'};try{t.send(null)}catch(a){s.style.backgroundImage='none',s.textContent=a.name+': '+a.message}}for(t=e.activeElement.href?e.activeElement:location,n='data:image/gif;base64,R0lGODlhEAAQAOMIAAAAABoaGjMzM0xMTGZmZoCAgJmZmbKysv///////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgAIACwAAAAAEAAQAAAESBDJiQCgmFqbZwjVhhwH9n3hSJbeSa1sm5GUIHSTYSC2jeu63q0D3PlwCB1lMMgUChgmk/J8LqUIAgFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UKgmFqbpxDV9gAA9n3hSJbeSa1sm5HUMHTTcTy2jeu63q0D3PlwDx2FQMgYDBgmk/J8LqWPQuFRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YSgmFqb5xjV9gQB9n3hSJbeSa1sm5EUQXQTADy2jeu63q0D3PlwDx2lUMgcDhgmk/J8LqUPg+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+cagmFqbJyHV9ggC9n3hSJbeSa1sm5FUUXRTEDy2jeu63q0D3PlwDx3FYMgAABgmk/J8LqWPw+FRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+QihmFqbZynV9gwD9n3hSJbeSa1sm5GUYXSTIDy2jeu63q0D3PlwDx3lcMgEAhgmk/J8LqUPAOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+UqhmFqbpzHV9hAE9n3hSJbeSa1sm5HUcXTTMDy2jeu63q0D3PlwDx0FAMgIBBgmk/J8LqWPQOBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+YyhmFqb5znV9hQF9n3hSJbeSa1sm5EUAHQTQTy2jeu63q0D3PlwDx0lEMgMBhgmk/J8LqUPgeBRhV6z2q0VF94iJ9pOBAAh+QQBCgAPACwAAAAAEAAQAAAESPDJ+c6hmFqbJwDV9hgG9n3hSJbeSa1sm5FUEHRTUTy2jeu63q0D3PlwDx1FIMgQCBgmk/J8LqWPweBRhV6z2q0VF94iJ9pOBAA7',a=e.createElement('div'),a.setAttribute('data-breadcrumbs-main',''),a.appendChild(e.createElement('style')).textContent='div[data-breadcrumbs-main] {position:fixed; top:0px; left:0; z-index:999999; width:100%; padding:10px !important; background-color:white !important; outline:1px solid black !important; font-size: 12pt !important; text-align: left !important;}\ndiv[data-breadcrumbs-main] a {font-size: 12pt !important; text-decoration: underline !important;}\ndiv[data-breadcrumbs-main] div {margin-top: 20px!important; color: silver !important;}\ndiv[data-breadcrumbs-info-title] {background-repeat: no-repeat !important; background-position: left center !important; padding-left: 20px !important;}',A=t.protocol+(/:[\/][\/]/.test(t.href)?'//':''),a.appendChild(e.createElement('span')).textContent=A,i=t.hostname.split('.'),r=i.length,r>1&&(i.splice(r-2,2,i.slice(r-2).join('.')),r--),o=0;r>o;o++)l=i[o],o>0&&(a.appendChild(e.createElement('span')).textContent='.'),p=a.appendChild(e.createElement('a')),p.textContent=l,p.href=A+i.slice(o).join('.');if(A+=t.hostname,t.port&&/:\d+$/.test(t.host)&&(a.appendChild(e.createElement('span')).textContent=':',p=a.appendChild(e.createElement('a')),p.textContent=t.port,A+=':'+t.port,p.href=A),a.appendChild(e.createElement('span')).textContent='/',A+='/',t.pathname.length>1)for(i=t.pathname.split('/'),i.shift(),r=i.length,o=0;r>o;o++)l=i[o],r-1>o?(p=a.appendChild(e.createElement('a')),p.textContent=l,A+=l,a.appendChild(e.createElement('span')).textContent='/',A+='/',p.href=A):''!==l&&(p=a.appendChild(e.createElement('a')),p.textContent=l,A+=l,p.href=A);if(t.search)for(i=t.search.split('&'),r=i.length,i[0]=i[0].substring(1),a.appendChild(e.createElement('span')).textContent='?',A+='?',o=0;r>o;o++)l=i[o],o>0&&(a.appendChild(e.createElement('span')).textContent='&',A+='&'),p=a.appendChild(e.createElement('a')),p.textContent=l,A+=l,p.href=A;t.hash&&(a.appendChild(e.createElement('span')).textContent='#',p=a.appendChild(e.createElement('a')),p.textContent=t.hash.substring(1),A+=t.hash,p.href=A),a.querySelector('a:last-of-type').href==location.href&&a.querySelector('a:last-of-type').setAttribute('data-breadcrumbs-doc-title',e.title),d=a.appendChild(e.createElement('div')),d.setAttribute('data-breadcrumbs-info-url',''),d.textContent='URL',s=a.appendChild(e.createElement('div')),s.setAttribute('data-breadcrumbs-info-title',''),s.textContent='Title',a.addEventListener('mouseover',function(e,t,n){(t=e.target.href)&&(d.textContent=t,(n=e.target.getAttribute('data-breadcrumbs-doc-title'))?s.textContent=n:h(e.target))}),a.addEventListener('dblclick',function(){a.parentNode.removeChild(a)}),e.body.appendChild(a)})(document)">Хлебные крошки</a></p>
	</body>
</html>


2. Если вы захотите что-то применить на практике, возможно, вам пригодятся другие мои заметки, так или иначе относящиеся к теме (в том числе к решению разных проблем в IE):

Букмарклеты в Internet Explorer 11: формат хранения, лимиты и негласные правила, коварный баг

Букмарклеты: если XPath недоступен, а селекторов и методов навигации по DOM не хватает

Надёжный localStorage для букмарклетов

Спасибо за внимание)




P.S. Проверка через виртуальную машину показывает, что в MS Edge версии 11.00.10240.16397 от 7.22.2015 (по версии файла), она же 20.10240.16384.0 (по информации в настройках):

а. основные и сторонние куки разрешены по умолчанию, поэтому никакой дополнительной настройки для XMLHttpRequest.withCredentials не требуется;

б. ошибка Источник null не найден в заголовке Access-Control-Allow-Origin при переадресациях больше не случается: после переадресации Origin отсылается как null и дальше всё работает, как и задумано.

в. свойство navigator.languages всё ещё не реализовано;

г. свойство XMLHttpRequest.responseURL всё ещё не реализовано;

д. хранилище localStorage для локальных страниц для локальных страниц всё ещё не реализовано.
  • +7
  • 14,5k
  • 5
Поделиться публикацией

Комментарии 5

    0
    В проблему кроссбраузерности заменили проблемой HTTPS сайтов. Поменять стиль странички в gmail или вставить свои скрипты уже не получится, правильно я понимаю?
      0
      Я не призываю полностью отказаться от пользовательских скриптов и стилей, когда на них уже и так всё готово и они поддерживаются браузером. Однако и при помощи Fiddlera, думаю, это возможно: он декодирует HTTPS, значит может и вставлять нужные части и в зашифрованные тела ответов. Просто для меня не в этом его главный смысл.
        0
        он декодирует https через свой сертификат, что уничижает всю безопасность.
          0
          Ну, он этого и не скрывает. Но ведь нет нужды через него банковские транзакции проводить. К тому же можно настроить декодирование только определённых сайтов.
      0
      P.S. Проверка через виртуальную машину показывает, что в MS Edge версии 11.00.10240.16397 от 7.22.2015 (по версии файла), она же 20.10240.16384.0 (по информации в настройках):

      а. основные и сторонние куки разрешены по умолчанию, поэтому никакой дополнительной настройки для XMLHttpRequest.withCredentials не требуется;

      б. ошибка Источник null не найден в заголовке Access-Control-Allow-Origin при переадресациях больше не случается: после переадресации Origin отсылается как null и дальше всё работает, как и задумано.

      в. свойство navigator.languages всё ещё не реализовано;

      г. свойство XMLHttpRequest.responseURL всё ещё не реализовано;

      д. хранилище localStorage для локальных страниц для локальных страниц всё ещё не реализовано.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.