- PVSM.RU - https://www.pvsm.ru -

Firefox: размер файла по ссылке, или через тернии к форку

Скриншот расширения Link Properties Plus
Вашему вниманию представляется небольшая история появления расширения Link Properties Plus [1] и описание того, как работает его основная часть.
Расширение позволяет узнать размер, дату последнего изменения и некоторые другие свойства файла по ссылке (в том числе прямую ссылку после всех перенаправлений) без скачивания всего файла целиком. Если, конечно, все это сообщает сервер.

Как это было

Когда-то давно было расширение Extended Link Properties [2], и оно работало.
Потом была обновленная версия на уже закрывшемся forum.addonsmirror.net [3] (копия на web.archive.org [4]).

1.3.x

Но в июне 2009-го [5] мне захотелось странного. Так уж получается, что, хорошо это (с точки зрения результата) или плохо (лучше бы я выспался), а мне иногда хочется странного.
В результате расширение научилось работать с окном выбора действия с файлом. Ну, и мелкие улучшения в качестве бонуса.
К сожалению, не обошлось и без сложностей: встроенный загрузчик файлов начинает скачивать файл, не дожидаясь, пока пользователь выберет, что следует делать с этим файлом. Это, конечно, отдельная «проблема» (к счастью, лишний трафик уже не столь критичен), но она порождала куда более серьезную: пока шло скачивание файла, нельзя было сделать запрос по той же самой ссылке – то есть запрос-то, вроде бы, посылался, но ответ приходит уже после скачивания. А без запроса, разумеется, ничего не узнать – только удаленному серверу известно, что за файл у него хранится.
Так появилась идея добавить к ссылке «?случайные_цифры». Да, есть вероятность, что сервер на измененный запрос вернет другую информацию, но это все равно куда лучше, чем ничего.
Хак, к слову, в новых версиях оказался уже не нужен, хотя у одного пользователя то ли падал, то ли зависал Thunderbird [6] при попытке открытия PDF-файлов – помогло включение скрытой настройки для принудительного включения хака.
А вот выкладывать на AMO [7] было как-то лень – и хак для окна загрузок тот еще, и было интереснее продолжить улучшения. К тому же, надо было менять идентификатор [8] расширения и как-то оповещать пользователей, уже установивших версию с оригинальным идентификатором.
Потом в Firefox 3.7a1pre удалили из контекстного меню пункт «Свойства», и оригинальное расширение перестало работать, теперь уже навсегда. Пришлось добавлять [9] поддержку Element Properties [10], расширения-заменителя.

1.4.x

Как бы там ни было, в мае 2010-го [11] появилась новая версия, пока еще тестовая. Уже со своим окошком и не зависящая от удаленного диалога свойств. Ну, и без разных полезных косметических мелочей не обошлось.
Тогда же был обнаружен недофорк: Extended Link Properties + [12], код которого полностью соответствовал моей версии 1.3.5 – изменения были, но коснулись они только локализации.
Я, конечно, обиделся – меня-то не спросили (и не пытались уговорить выложить на AMO), но тратить время и нервы на разборки категорически не хотелось. :) У меня была недоделанная новая версия с кучей недотестированных изменений – куда интереснее (и полезнее – да, это своеобразный эгоизм) было заниматься ей. Так что вместо разборок появилась поддержка FTP-ссылок.
Тем временем исправили Bug 455913 — nsIHelperAppLauncher should provide info about content length [13], так что размер в диалоге загрузки стало возможно узнать сразу же.

1.5.x

Это было неторопливо и с перерывами на несколько месяцев: 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/