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

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


    Как это было

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

    1.3.x

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

    1.4.x

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

    1.5.x

    Это было неторопливо и с перерывами на несколько месяцев: 1.4.1pre1 в апреле 2011-го, релиз – спустя почти два года, в январе 2013-го.
    Зато был сделан полноценный форк с новым идентификатором, добавлена поддержка новых версий Firefox с рыжей кнопкой вместо по умолчанию скрытого меню и возможность задавать вручную произвольный HTTP referer, открытие и сохранение ссылок прямо из окошка со свойствами. А еще поддержка Thunderbird, обработка практически всех протоколов и отображение прямой ссылки на файл. И даже когда-то давно обещанное автоматическое закрытие окошка.

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

    Как это сейчас, реализация

    Для начала наиболее простой способ тестирования кода.
    Надо включить devtools.chrome.enabled = true в about:config и запустить
    Веб-разработка – Простой редактор JavaScript aka Scratchpad (Shift+F4)
    Далее выбрать
    Окружение – Браузер
    Все, можно тестировать код, который будет запускаться с правами как у расширений (так что не следует запускать что попало).

    Минимальный вариант для получения свойств ссылки

    Для отправки произвольных запросов существует nsIChannel, там же можно прочитать, что нам нужен или nsIIOService.newChannel(), или nsIIOService.newChannelFromURI().
    А у нас есть текстовая ссылка. То есть логично использовать newChannel(), да только вот практика показывает, что URI все же понадобится – можно наткнуться на самописный протокол (Custom Buttons), который ничего не возвращает, а можно на протокол about: (nsIAboutModule) – в общем-то, развлекательство, но и про такие ссылки можно кое-что узнать, так что почему бы и нет.
    Таки образом, детали еще не ясны, но понятно, что надо создать экземпляр nsIChannel и вызвать у него asyncOpen(). А этот asyncOpen() принимает первым аргументом реализацию nsIStreamListener'а, которая и позволит узнать результат отправленного запроса.
    Пожалуй, пора показывать пример:
    // Некие исходные данные для примера:
    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()/nsIHttpChannel.visitResponseHeaders() (на случай если получившийся nsIChannel еще и nsIHttpChannel). Ну, а у nsIFTPChannel есть свойство lastModifiedTime.
    Так что получаем вот такое продолжение:
    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")
    			+ "\n\nЗаголовки:\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. То есть нужно добавить
    channel.notificationCallbacks = observer;
    

    после создания channel и объекта observer, а в сам объект observer добавить реализацию nsIInterfaceRequestor. При этом nsIInterfaceRequestor.getInterface() должен уметь возвращать реализацию nsIChannelEventSink для обработки перенаправлений.
    Так что добавляем «приемник» информации о перенаправлениях рядом с двумя уже имеющимися:
    var redirects = []; // Массив для данных о перенаправлениях
    

    И обновляем функцию вывода результатов
    	done: function() {
    		alert(
    			data.join("\n")
    			+ "\n\nПеренаправления:\n" + redirects.join("\n")
    			+ "\n\nЗаголовки:\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 как раз есть подходящий пример:
    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);
    

    В реальном коде, конечно же, надо определять приватность источника ссылки. Но про это я уже писал – с помощью resource://gre/modules/PrivateBrowsingUtils.jsm можно узнать приватность любого объекта window.

    Итого

    Результат одним файлом:
    https://gist.github.com/Infocatcher/5327631
    Там же в ревизиях можно отследить наращивание функциональности: добавление обработки перенаправлений и поддержки приватного режима.

    Вот и все. Остается только добавить обработку ошибок, преобразовать в удобный для использования вид, заменить alert() на что-нибудь более удобное и прицепить вызов функции для получения свойств ссылки к интерфейсу.

    P.S. Новая версия расширения с отслеживанием перенаправлений и поддержкой приватного режима пока еще ожидает проверки.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 15

      –3
      Можно настроить блокировку фотографий с мелким весом с определенного сайта?
      Я хочу заблокировать все аватарки вконтакте, потому что они отвлекают сильно=) раньше я это делал с помощью adblock, но потом урлы аватарок и лстальных фотографий перестали иметь различия в формировании урл, теперь они отличаются только по весу.
        +1
        Честно говоря, это лучше у автора Adblock Plus и спросить.
        Лично мне кажется (я специально не разбирался), что решение о блокировке запроса надо принимать синхронно. Выглядит, во всяком случае, оно именно так, но Adblock Plus все-таки штука сложная, могу и ошибаться.
        А тут надо послать серверу запрос и дождаться ответа.
        Да еще и синхронный nsIChannel.open() мало того, что не рекомендуют, так еще и пишут, что он может быть не реализован вообще. Конечно, можно сделать синхронную обертку вокруг асинхронного кода, но это будет еще медленнее…
        Блокироваться (по идее, опять же) должно еще до начала каких бы то ни было запросов, так что никакого ответа от сервера у нас, скорее всего, еще не будет – то есть без запроса не обойтись.

        Ну, и надо понимать, что это дополнительный трафик будет, так что насколько это получится оправдано в конечном счете – еще вопрос.
        0
        По весу png на весь экран может оказаться в разы мельче такой аватарки
          0
          *.jpg тоже элемент адреса ссылки, соответственно тоже может использоваться как состовляющая для фильтра
            0
            Однако и в этом случае злую шутку может сыграть качество сжатия, а тут к аватаркам добавляются ещё и кнопочки на сайте и прочая аватаркообразная дребедень. В случае если нужно выпилить что-то из вконтакта, проще выпиливать весь вконтакт и разрешать только там где именно он и нужен. Соцплагины прекрасно отключаются теми же адблоками и ghostery
              0
              картинки для кнопок и дизайна затронуты не будут, у них источник совсем другой, их легко исключить из фильтра.
            0
            Да, это тоже проблема.
            Обычно-то просят подобное для экономии трафика – то есть большое отрезать.
            А тут если только еще и начало данных получать. Если, конечно, информация о размере в начале идет.
            Так вообще бессмысленно затея выглядит.
            0
            > отвлекают сильно=)
            От чего отвлекают??? Или это шутка?
              0
              не)) аватарки реально отвлекают) особенно женские) так и хочется открыть страницу и потерять свое время листая альбомы) и не нада говорит чтоб я удалялся из соц сети, все было круто когда я заблокировал все аватарки.
            0
            Пользовался им несколько лет назад, но когда фф ударился вскачь с версиями и изменениями, пришлось удалить все расширения, которые медленно или плохо обновлялись, в том числе и это, а потом уже был анлим и стало неважно, сколько весит файл.
              +1
              Ну вот, живое подтверждение неправильного маркетинга (или как там это правильно назвать?).
              Дело ведь не в циферках версий (строгую проверку уже давно отключили – большинство расширений теперь считаются совместимыми, пока не доказано обратное), а в изменении различных API.
              А ведь всего-то надо реже вносить критические правки, которые могут сломать расширения – в большинстве случаев это не должно мешать добавлению очередных фишек HTML/CSS.
              Да и реклама нужна, причем рекламировать надо уникальные особенности, а это как раз расширяемость вдоль и поперек.
                0
                Ну с расширениями более-менее они исправились, а вот полноценных тем, авторы которых выдержали марафон, осталось очень мало (Charamel делают маньяки).
                Пользуюсь лисой с самых первых версий 0.х, и к версии 4.0 окончательно надоело следить за профилем, бэкапить, восстанавливать, настраивать, перенастраивать, править rdf и тд. поэтому и произвел зачистку.
              0
              Тогда же был обнаружен недофорк: Extended Link Properties +

              пользуюсь как раз этим сейчас, попробую заменить:)
                0
                а для chrome есть что-то подобное?

                Only users with full accounts can post comments. Log in, please.