📜 ⬆️ ⬇️

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

Screenshot of the Link Properties Plus extension
A short history of the appearance of the Link Properties Plus extension and a description of how its main part works is presented to your attention.
The extension allows you to find out the size, the date of the last change, and some other properties of the file by reference (including a direct link after all redirections) 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 of the already closed forum.addonsmirror.net ( copy on web.archive.org ).

1.3.x

But in June of 2009, I wanted a strange one. So it turns out that it’s good (in terms of results) or bad (I wish I had slept), and sometimes I want something strange.
As a result, the extension learned to work with the file selection window. Well, minor improvements as a bonus.
Unfortunately, it was not without its difficulties: the built-in file downloader 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 extra traffic is no longer so critical), but it gave rise to a much more serious one: while the file was being downloaded, it was impossible to make a request using the same link - that is, the request seemed to be , was sent, but the answer comes after the download. And without a request, of course, I don’t know anything - only the remote server knows what file it has.
So the idea was to add to the link "? random_number . Yes, there is a possibility that the server will return other information to the modified request, but this is still much better than nothing.
Hack, by the way, turned out to be no longer needed in new versions, although one user either fell or Thunderbird hung up trying to open PDF files from attachments — it helped to enable hidden settings to force the hack to turn on.
But it was somehow lazy to lay out on the AMO - and the hack for the download window was still, and it was more interesting to continue the improvements. In addition, it was necessary to change the extension identifier and somehow notify users who have 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 , the replacement extensions.
')

1.4.x

Anyway, in May of 2010 a new version appeared, for the time being a test version. Already with its window and not dependent on the remote properties dialog. Well, and without various useful cosmetic trifles not done.
At the same time, a shortfall was discovered: Extended Link Properties + , the code of which fully corresponded to my version 1.3.5 — there were changes, but they affected only the localization.
Of course, I was offended - they didn’t ask me (and didn’t try to persuade to put it on AMO), but I really didn’t want to waste time and nerves on disassembling. :) I had an unfinished new version with a bunch of untested changes - much more interesting (and more useful - yes, this is a kind of selfishness) was to deal with it. So instead of fights there is support for FTP links.
In the meantime, Bug 455913 - nsIHelperAppLauncher should provide info about content length , so that the size in the download dialog could be recognized right away.

1.5.x

It was leisurely and with breaks for several months: 1.4.1pre1 in April of 2011, release - after almost two years, in January of 2013.
But a full-fledged fork was made with a new identifier, support was added for new versions of Firefox with a red button 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 support Thunderbird, processing almost all protocols and displaying a direct link to the file. And even once long ago promised automatic closing of the window.

It was, so to speak, a historical reference.
And now we will make the simplest implementation of getting the file properties by reference.
For the leisurely at the very end, there is a link to the received code.

How is it now, implementation

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

The minimum option for obtaining reference properties

For sending arbitrary requests, there is nsIChannel , in the same place you can 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 the URI - you can stumble upon a custom protocol ( Custom Buttons ) that does not return anything, but it is possible for the about: ( nsIAboutModule ) protocol - in general, entertainment, but also about such links you can learn something, 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 it asyncOpen () . And this asyncOpen () takes as its first argument the implementation of nsIStreamListener 'a, which will let us know the result of the sent query.
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 we need to do something about it. :)
First, you should implement nsIStreamListener. And secondly, nsIHttpChannel.visitRequestHeaders () / nsIHttpChannel.visitResponseHeaders () is useful (in case the resulting nsIChannel is also an nsI Http Channel ). Well, the nsI FTP Channel has a 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 unnecessarily 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 


Track redirects

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

after creating the channel and observer object, and in the observer object itself add the implementation nsIInterfaceRequestor . At the same time, nsIInterfaceRequestor.getInterface () should be able to return an implementation of nsIChannelEventSink to handle redirects.
So add a “receiver” of information about redirections next to two already 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 you need to add
 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 output 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 add support for private mode. The Supporting per-window article private browsing just has 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 additionally 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, it is necessary to determine the privacy of the source of the link. But I already wrote about this - using resource: //gre/modules/PrivateBrowsingUtils.jsm, you can find out the privacy of any window object.

Total

Result in one file:
https://gist.github.com/Infocatcher/5327631
In the same revision, you can track the build-up functionality: adding processing redirects and support for private mode.

That's all. It remains only to add error handling, convert to a convenient form for use, replace alert () with something more convenient, and attach a function call to get the properties of the link to the interface.

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

Source: https://habr.com/ru/post/175745/


All Articles