- PVSM.RU - https://www.pvsm.ru -
Вашему вниманию представляется небольшая история появления расширения Link Properties Plus [1] и описание того, как работает его основная часть.
Расширение позволяет узнать размер, дату последнего изменения и некоторые другие свойства файла по ссылке (в том числе прямую ссылку после всех перенаправлений) без скачивания всего файла целиком. Если, конечно, все это сообщает сервер.
Когда-то давно было расширение Extended Link Properties [2], и оно работало.
Потом была обновленная версия на уже закрывшемся forum.addonsmirror.net [3] (копия на web.archive.org [4]).
Но в июне 2009-го [5] мне захотелось странного. Так уж получается, что, хорошо это (с точки зрения результата) или плохо (лучше бы я выспался), а мне иногда хочется странного.
В результате расширение научилось работать с окном выбора действия с файлом. Ну, и мелкие улучшения в качестве бонуса.
К сожалению, не обошлось и без сложностей: встроенный загрузчик файлов начинает скачивать файл, не дожидаясь, пока пользователь выберет, что следует делать с этим файлом. Это, конечно, отдельная «проблема» (к счастью, лишний трафик уже не столь критичен), но она порождала куда более серьезную: пока шло скачивание файла, нельзя было сделать запрос по той же самой ссылке – то есть запрос-то, вроде бы, посылался, но ответ приходит уже после скачивания. А без запроса, разумеется, ничего не узнать – только удаленному серверу известно, что за файл у него хранится.
Так появилась идея добавить к ссылке «?случайные_цифры». Да, есть вероятность, что сервер на измененный запрос вернет другую информацию, но это все равно куда лучше, чем ничего.
Хак, к слову, в новых версиях оказался уже не нужен, хотя у одного пользователя то ли падал, то ли зависал Thunderbird [6] при попытке открытия PDF-файлов – помогло включение скрытой настройки для принудительного включения хака.
А вот выкладывать на AMO [7] было как-то лень – и хак для окна загрузок тот еще, и было интереснее продолжить улучшения. К тому же, надо было менять идентификатор [8] расширения и как-то оповещать пользователей, уже установивших версию с оригинальным идентификатором.
Потом в Firefox 3.7a1pre удалили из контекстного меню пункт «Свойства», и оригинальное расширение перестало работать, теперь уже навсегда. Пришлось добавлять [9] поддержку Element Properties [10], расширения-заменителя.
Как бы там ни было, в мае 2010-го [11] появилась новая версия, пока еще тестовая. Уже со своим окошком и не зависящая от удаленного диалога свойств. Ну, и без разных полезных косметических мелочей не обошлось.
Тогда же был обнаружен недофорк: Extended Link Properties + [12], код которого полностью соответствовал моей версии 1.3.5 – изменения были, но коснулись они только локализации.
Я, конечно, обиделся – меня-то не спросили (и не пытались уговорить выложить на AMO), но тратить время и нервы на разборки категорически не хотелось. :) У меня была недоделанная новая версия с кучей недотестированных изменений – куда интереснее (и полезнее – да, это своеобразный эгоизм) было заниматься ей. Так что вместо разборок появилась поддержка FTP-ссылок.
Тем временем исправили Bug 455913 — nsIHelperAppLauncher should provide info about content length [13], так что размер в диалоге загрузки стало возможно узнать сразу же.
Это было неторопливо и с перерывами на несколько месяцев: 1.4.1pre1 в апреле 2011-го, релиз – спустя почти два года, в январе 2013-го.
Зато был сделан полноценный форк с новым идентификатором, добавлена поддержка новых версий Firefox с рыжей кнопкой вместо по умолчанию скрытого меню и возможность задавать вручную произвольный HTTP referer [14], открытие и сохранение ссылок прямо из окошка со свойствами. А еще поддержка Thunderbird, обработка практически всех протоколов и отображение прямой ссылки на файл. И даже когда-то давно обещанное автоматическое закрытие окошка.
Это была, так сказать, историческая справка.
А теперь сделаем простейшую реализацию получения свойств файла по ссылке.
Для неторопливых в самом конце [15] приведена ссылка на полученный код.
Для начала наиболее простой способ тестирования кода.
Надо включить devtools.chrome.enabled = true в about:config и запустить
Веб-разработка – Простой редактор JavaScript aka Scratchpad (Shift+F4)
Далее выбрать
Окружение – Браузер
Все, можно тестировать код, который будет запускаться с правами как у расширений (так что не следует запускать что попало).
Для отправки произвольных запросов существует nsIChannel [16], там же можно прочитать, что нам нужен или nsIIOService.newChannel() [17], или nsIIOService.newChannelFromURI() [18].
А у нас есть текстовая ссылка. То есть логично использовать newChannel(), да только вот практика показывает, что URI все же понадобится – можно наткнуться на самописный протокол (Custom Buttons [19]), который ничего не возвращает, а можно на протокол about: (nsIAboutModule [20]) – в общем-то, развлекательство, но и про такие ссылки можно кое-что узнать, так что почему бы и нет.
Таки образом, детали еще не ясны, но понятно, что надо создать экземпляр nsIChannel и вызвать у него asyncOpen() [21]. А этот asyncOpen() принимает первым аргументом реализацию nsIStreamListener [22]'а, которая и позволит узнать результат отправленного запроса.
Пожалуй, пора показывать пример:
// Некие исходные данные для примера:
var uriString = "https://addons.mozilla.org/firefox/downloads/latest/413716/addon-413716-latest.xpi";
var referer = "https://addons.mozilla.org/";
var ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var uri = ios.newURI(uriString, null, null);
var scheme = uri.scheme && uri.scheme.toLowerCase();
var channel = scheme == "about" && "nsIAboutModule" in Components.interfaces
// Небольшое колдунство для about: ссылок
? Components.classes["@mozilla.org/network/protocol/about;1?what=" + uri.path.replace(/[?&#].*$/, "")]
.getService(Components.interfaces.nsIAboutModule)
.newChannel(uri)
: ios.newChannelFromURI(uri);
Теперь у нас есть экземпляр nsIChannel, и с этим надо что-то делать. :)
Во-первых, следует реализовать nsIStreamListener. А во-вторых, пригодится nsIHttpChannel.visitRequestHeaders() [23]/nsIHttpChannel.visitResponseHeaders() [24] (на случай если получившийся nsIChannel еще и nsIHttpChannel [25]). Ну, а у nsIFTPChannel [26] есть свойство lastModifiedTime [27].
Так что получаем вот такое продолжение:
var observer = { ... }; // Тут надо реализовать интерфейс nsIStreamListener и nsIHttpHeaderVisitor
var data = []; // Для примера будем просто собирать результаты в массив
var headers = []; // Еще один массив, для заголовков
if(channel instanceof Components.interfaces.nsIHttpChannel) {
// Проверка на instanceof неявно делает
// channel.QueryInterface(Components.interfaces.nsIHttpChannel),
// но не генерирует ошибок в случае отсутствия поддержки запрашиваемого интерфейса
channel.requestMethod = "HEAD"; // HEAD-запрос
channel.setRequestHeader("Referer", referer, false);
channel.visitRequestHeaders(observer);
headers.push(""); // Отделим заголовки запроса от заголовков ответа
}
// Следующая строка выглядит странно, но nsIFTPChannel нам еще пригодится
channel instanceof Components.interfaces.nsIFTPChannel;
channel.asyncOpen(observer, null);
Вообще, конечно, размножать глобальные переменные без необходимости нехорошо, но так нагляднее. А если возникают какие-то сложности с нарезкой примеров на модули и прочей инкапсуляцией, у меня для вас плохие новости. :)
Теперь попробуем сделать объект observer, реализующий необходимые интерфейсы:
var observer = {
// nsIRequestObserver (nsIStreamListener наследует этот интерфейс)
onStartRequest: function(aRequest, aContext) {
if(aRequest instanceof Components.interfaces.nsIHttpChannel)
aRequest.visitResponseHeaders(this);
else {
if("contentType" in channel)
data.push("Тип содержимого: " + channel.contentType);
if("contentLength" in channel)
data.push("Размер файла: " + channel.contentLength);
if("responseStatus" in channel && "responseStatusText" in channel)
data.push("Статус: " + channel.responseStatus + " " + channel.responseStatusText);
if("lastModifiedTime" in aRequest && aRequest.lastModifiedTime) { // Firefox 4
var t = aRequest.lastModifiedTime;
data.push("Последнее изменение: " + new Date(t > 1e14 ? t/1000 : t).toLocaleString());
}
}
},
onStopRequest: function(aRequest, aContext, aStatusCode) {
if(aRequest instanceof Components.interfaces.nsIChannel && aRequest.URI)
data.push("Прямая ссылка: " + aRequest.URI.spec);
this.done();
},
// nsIStreamListener
onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
// Кажется, что-то пошло не так, не нужно нам данные получать, отменяем
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
},
// nsIHttpHeaderVisitor
visitHeader: function(aHeader, aValue) {
headers.push(aHeader + ": " + aValue);
switch(aHeader) {
// Тут можно как-то красиво форматировать данные
case "Content-Length": data.push("Размер файла: " + aValue); break;
case "Content-Type": data.push("Тип содержимого: " + aValue); break;
case "Last-Modified": data.push("Последнее изменение: " + new Date(aValue).toLocaleString());
}
},
done: function() {
alert(
data.join("n")
+ "nnЗаголовки:n" + headers.join("n")
);
}
};
В результате получим сообщение:
Тип содержимого: application/x-xpinstall
Последнее изменение: 26 Февраль 2013 г. 0:46:30
Размер файла: 46897
Прямая ссылка: https://addons.cdn.mozilla.net/storage/public-staging/413716/link_properties_plus-1.5.1-fx+sm+tb.xpi
Заголовки:
Host: addons.mozilla.org
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:20.0) Gecko/20100101 Firefox/20.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: https://addons.mozilla.org/
Server: nginx
X-Backend-Server: web13.addons.phx1.mozilla.com
Content-Type: application/x-xpinstall
Accept-Ranges: bytes
Last-Modified: Tue, 26 Feb 2013 00:46:30 GMT
X-Cache-Info: caching
Content-Length: 46897
Cache-Control: max-age=79492
Expires: Sun, 07 Apr 2013 17:32:01 GMT
Date: Sat, 06 Apr 2013 19:27:09 GMT
Для отслеживания перенаправлений есть nsIChannel.notificationCallbacks [28]. То есть нужно добавить
channel.notificationCallbacks = observer;
после создания channel и объекта observer, а в сам объект observer добавить реализацию nsIInterfaceRequestor [29]. При этом nsIInterfaceRequestor.getInterface() [30] должен уметь возвращать реализацию nsIChannelEventSink [31] для обработки перенаправлений.
Так что добавляем «приемник» информации о перенаправлениях рядом с двумя уже имеющимися:
var redirects = []; // Массив для данных о перенаправлениях
И обновляем функцию вывода результатов
done: function() {
alert(
data.join("n")
+ "nnПеренаправления:n" + redirects.join("n")
+ "nnЗаголовки:n" + headers.join("n")
);
}
А в наш observer надо добавить
var observer = {
...
// nsIInterfaceRequestor
getInterface: function(iid) {
if(iid.equals(Components.interfaces.nsIChannelEventSink))
return this;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
// nsIChannelEventSink
onChannelRedirect: function(oldChannel, newChannel, flags) { // Gecko < 2
this.onRedirect.apply(this, arguments);
},
asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
// Надо обязательно разрешить перенаправление, иначе запрос будет прерван!
callback.onRedirectVerifyCallback(Components.results.NS_OK);
this.onRedirect.apply(this, arguments);
},
onRedirect: function(oldChannel, newChannel, flags) {
if(!redirects.length) // Это самое первое перенаправление
redirects.push(oldChannel.URI.spec);
// https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIChannelEventSink#Constants
var ces = Components.interfaces.nsIChannelEventSink;
var types = [];
if(flags & ces.REDIRECT_TEMPORARY)
types.push("временное");
if(flags & ces.REDIRECT_PERMANENT)
types.push("постоянное");
if(flags & ces.REDIRECT_INTERNAL)
types.push("внутреннее");
redirects.push("=> (" + types.join(", ") + ") " + newChannel.URI.spec);
},
...
В результате к выводу добавится
Перенаправления:
https://addons.mozilla.org/firefox/downloads/latest/413716/addon-413716-latest.xpi
=> (постоянное) https://addons.mozilla.org/firefox/downloads/latest/link-properties-plus/addon-link-properties-plus-latest.xpi
=> (временное) https://addons.mozilla.org/firefox/downloads/file/185918/link_properties_plus-1.5.1-fx+sm+tb.xpi
=> (временное) https://addons.cdn.mozilla.net/storage/public-staging/413716/link_properties_plus-1.5.1-fx+sm+tb.xpi
Теперь еще можно добавить поддержку приватного режима. В статье Supporting per-window private browsing [32] как раз есть подходящий пример [33]:
var channel = Services.io.newChannel("http://example.org", null, null);
channel.QueryInterface(Components.interfaces.nsIPrivateBrowsingChannel);
channel.setPrivate(true); // force the channel to be loaded in private mode
А мы можем еще дополнительно убедиться, что приватный режим уже поддерживается:
if(
private // Флаг-настройка
&& "nsIPrivateBrowsingChannel" in Components.interfaces
&& channel instanceof Components.interfaces.nsIPrivateBrowsingChannel
&& "setPrivate" in channel
)
channel.setPrivate(true);
В реальном коде, конечно же, надо определять приватность источника ссылки. Но про это я уже писал [34] – с помощью resource://gre/modules/PrivateBrowsingUtils.jsm [35] можно узнать приватность любого объекта window [36].
Результат одним файлом:
https://gist.github.com/Infocatcher/5327631 [37]
Там же в ревизиях [38] можно отследить наращивание функциональности: добавление обработки перенаправлений и поддержки приватного режима.
Вот и все. Остается только добавить обработку ошибок, преобразовать в удобный для использования вид, заменить alert() на что-нибудь более удобное и прицепить вызов функции для получения свойств ссылки к интерфейсу.
P.S. Новая версия [39] расширения с отслеживанием перенаправлений и поддержкой приватного режима пока еще ожидает проверки.
Автор:
Источник [40]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/31407
Ссылки в тексте:
[1] Link Properties Plus: https://addons.mozilla.org/addon/link-properties-plus/
[2] Extended Link Properties: https://addons.mozilla.org/addon/extended-link-properties/
[3] forum.addonsmirror.net: http://forum.addonsmirror.net/index.php?showtopic=6594
[4] копия на web.archive.org: http://web.archive.org/web/20110811234800/http://forum.addonsmirror.net/index.php?showtopic=6594
[5] июне 2009-го: https://forum.mozilla-russia.org/viewtopic.php?pid=333576#p333576
[6] то ли падал, то ли зависал Thunderbird: https://addons.mozilla.org/addon/link-properties-plus/reviews/user:6909804
[7] AMO: https://addons.mozilla.org/
[8] идентификатор: https://developer.mozilla.org/en-US/docs/Install_Manifests#id
[9] добавлять: https://forum.mozilla-russia.org/viewtopic.php?pid=376740#p376740
[10] Element Properties: https://addons.mozilla.org/addon/element-properties/
[11] мае 2010-го: https://forum.mozilla-russia.org/viewtopic.php?pid=425319#p425319
[12] Extended Link Properties +: https://addons.mozilla.org/addon/extended-link-propertie-124503/
[13] Bug 455913 — nsIHelperAppLauncher should provide info about content length: https://bugzilla.mozilla.org/show_bug.cgi?id=455913
[14] HTTP referer: https://ru.wikipedia.org/wiki/HTTP_referer
[15] в самом конце: #result
[16] nsIChannel: https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIChannel
[17] nsIIOService.newChannel(): https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIIOService#newChannel%28%29
[18] nsIIOService.newChannelFromURI(): http://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIIOService#newChannelFromURI%28%29
[19] Custom Buttons: https://addons.mozilla.org/addon/custom-buttons/
[20] nsIAboutModule: https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIAboutModule
[21] asyncOpen(): https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIChannel#asyncOpen%28%29
[22] nsIStreamListener: http://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIStreamListener
[23] nsIHttpChannel.visitRequestHeaders(): https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIHttpChannel#visitRequestHeaders%28%29
[24] nsIHttpChannel.visitResponseHeaders(): https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIHttpChannel#visitResponseHeaders%28%29
[25] nsIHttpChannel: https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIHttpChannel
[26] nsIFTPChannel: https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIFTPChannel
[27] lastModifiedTime: https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIFTPChannel#Attributes
[28] nsIChannel.notificationCallbacks: https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIChannel#Attributes
[29] nsIInterfaceRequestor: http://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIInterfaceRequestor
[30] nsIInterfaceRequestor.getInterface(): https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIInterfaceRequestor#getInterface%28%29
[31] nsIChannelEventSink: http://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIChannelEventSink
[32] Supporting per-window private browsing: https://developer.mozilla.org/en-US/docs/Supporting_per-window_private_browsing
[33] пример: https://developer.mozilla.org/en-US/docs/Supporting_per-window_private_browsing#Forcing_a_channel_into_private_mode
[34] писал: http://habrahabr.ru/post/175469/
[35] resource://gre/modules/PrivateBrowsingUtils.jsm: http://mxr.mozilla.org/mozilla-central/source/toolkit/content/PrivateBrowsingUtils.jsm
[36] window: https://developer.mozilla.org/en-US/docs/DOM/window
[37] https://gist.github.com/Infocatcher/5327631: https://gist.github.com/Infocatcher/5327631
[38] в ревизиях: https://gist.github.com/Infocatcher/5327631/revisions
[39] Новая версия: https://addons.mozilla.org/addon/link-properties-plus/versions/1.5.2
[40] Источник: http://habrahabr.ru/post/175745/
Нажмите здесь для печати.