Firefox: file size by link, or through thorns to fork

    Link Properties Plus Extension Screenshot
    A short history of the appearance of the Link Properties Plus extension and a description of how its main part works are presented to your attention .
    The extension allows you to find out the size, date of the last change and some other properties of the file by reference (including a direct link after all redirects) without downloading the entire file. Unless, of course, all this is reported by the server.


    How it was

    Once upon a time there was an extension of Extended Link Properties , and it worked.
    Then there was an updated version on the already closed forum.addonsmirror.net ( copy on web.archive.org ).

    1.3.x

    But in June 2009, I wanted something strange. It just so happens that whether it’s good (from the point of view of the result) or bad (it would be better if I had enough sleep), and sometimes I want something strange.
    As a result, the extension learned to work with the file action selection window. Well, and minor improvements as a bonus.
    Unfortunately, there were some difficulties: the built-in file loader starts downloading the file, without waiting for the user to choose what to do with this file. This, of course, is a separate “problem” (fortunately, the excess traffic is not so critical anymore), but it generated much more serious: while the file was downloading, it was impossible to make a request using the same link - that is, the request seems to be , was sent, but the answer comes after downloading. And without a request, of course, you don’t know anything - only the remote server knows what kind of file it has.
    So the idea came to add to the link "? random_digits . " Yes, there is a chance that the server will return other information to the changed request, but this is still much better than nothing.
    Hack, by the way, was no longer needed in the new versions, although one user either crashed or Thunderbird crashed when trying to open PDF files from attachments - the inclusion of a hidden setting to force hacking helped.
    But spreading it on AMO was somehow lazy - and the hack for the download window is the same, and it was more interesting to continue the improvements. In addition, it was necessary to change the extension identifier and somehow notify users who already installed the version with the original identifier.
    Then, in Firefox 3.7a1pre, the “Properties” item was removed from the context menu, and the original extension stopped working, now forever. I had to add support for Element Properties, extension-substitute.

    1.4.x

    Be that as it may, in May 2010 a new version appeared, still a test one. Already with its own window and independent of the remote properties dialog. Well, without various useful cosmetic trifles it could not do.
    At the same time, a misunderstanding was discovered: Extended Link Properties + , the code of which fully corresponded to my version 1.3.5 - there were changes, but they only affected localization.
    Of course, I was offended - they didn’t ask me (and did not try to persuade me to put it on AMO), but categorically I did not want to waste time and nerves. :) I had an unfinished new version with a bunch of untested changes - it was much more interesting (and more useful - yes, this is a kind of egoism) to do it. So instead of disassembly, support for FTP links appeared.
    In the meantime, they fixed Bug 455913 - nsIHelperAppLauncher should provide info about content length , so it became possible to find out the size in the download dialog right away.

    1.5.x

    It was leisurely and intermittently for several months: 1.4.1pre1 in April 2011, release - almost two years later, in January 2013.
    But a full fork was made with a new identifier, support for new versions of Firefox with a red button was added instead of the default hidden menu and the ability to manually set an arbitrary HTTP referer , open and save links directly from the properties window. And also support for Thunderbird, processing of almost all protocols and displaying a direct link to the file. And even the once-long-promised automatic closing of the window.

    It was, so to speak, a historical reference.
    And now we’ll make the simplest implementation of obtaining file properties by reference.
    For leisurely at the very end The link to the received code is given.

    Like it is now, implementation

    For starters, the easiest way to test code.
    You must enable devtools.chrome.enabled = true in about: config and start
    Web development - Simple JavaScript editor aka Scratchpad (Shift + F4)
    Next, select
    Environment - Browser
    Everything, you can test the code that will run with permissions like the extensions (so that you should not run anything).

    The minimum option to get link properties

    To send arbitrary requests, there is nsIChannel , you can also read that we need either nsIIOService.newChannel () or nsIIOService.newChannelFromURI () .
    And we have a text link. That is, it is logical to use newChannel (), but only practice shows that you still need a URI - you can come across a custom protocol ( Custom Buttons ) that returns nothing, or you can about:: nsIAboutModule - in general, entertainment, but you can find out something about such links, so why not.
    Thus, the details are not yet clear, but it is clear that you need to create an instance of nsIChannel and call asyncOpen () on it. And this asyncOpen () takes the implementation of nsIStreamListener as the first argument , which will let you know the result of the sent request.
    Perhaps it's time to show an example:
    // Некие исходные данные для примера:
    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);
    

    Now we have an instance of nsIChannel, and something needs to be done. :)
    Firstly, you should implement nsIStreamListener. And secondly, nsIHttpChannel.visitRequestHeaders () / nsIHttpChannel.visitResponseHeaders () is useful (in case the resulting nsIChannel is also an nsI Http Channel ). Well, nsI FTP Channel has the lastModifiedTime property .
    So we get the following continuation:
    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);
    

    In general, of course, to propagate global variables without the need is not good, but it is more obvious. And if there are any difficulties with cutting examples into modules and other encapsulation, I have bad news for you. :)

    Now let's try to make an observer object that implements the necessary interfaces:
    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")
    		);
    	}
    };
    


    As a result, we get the message:
    Тип содержимого: 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
    


    Redirect Tracking

    For tracking redirects, there is nsIChannel.notificationCallbacks . That is, you need to add
    channel.notificationCallbacks = observer;
    

    after creating the channel and the observer object, and add the nsIInterfaceRequestor implementation to the observer object itself . At the same time, nsIInterfaceRequestor.getInterface () should be able to return an implementation of nsIChannelEventSink to handle redirects.
    So we add a “receiver” of redirection information next to two existing ones:
    var redirects = []; // Массив для данных о перенаправлениях
    

    And update the output function
    	done: function() {
    		alert(
    			data.join("\n")
    			+ "\n\nПеренаправления:\n" + redirects.join("\n")
    			+ "\n\nЗаголовки:\n" + headers.join("\n")
    		);
    	}
    

    And in our observer must be added
    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);
    	},
    	...
    


    As a result, the conclusion will be added
    Перенаправления:
    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
    


    Private mode

    Now you can still add support for private mode. In the article Supporting per-window private browsing there is just a suitable example :
    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
    

    And we can still make sure that the private mode is already supported:
    if(
    	private // Флаг-настройка
    	&& "nsIPrivateBrowsingChannel" in Components.interfaces
    	&& channel instanceof Components.interfaces.nsIPrivateBrowsingChannel
    	&& "setPrivate" in channel
    )
    	channel.setPrivate(true);
    

    In real code, of course, you need to determine the privacy of the link source. But I already wrote about this - with the help of resource: //gre/modules/PrivateBrowsingUtils.jsm you can find out the privacy of any window object .

    Total

    The result is a single file:
    https://gist.github.com/Infocatcher/5327631
    There, in the revisions, you can track the build-up of functionality: adding redirect handling and support for private mode.

    That's all. All that remains is to add error handling, convert it to a user-friendly view, replace alert () with something more convenient, and attach a function call to get the link properties to the interface.

    PS The new version of the extension with redirect tracking and support for private mode is still awaiting verification.

    Also popular now: