2012-01-19 07:01:42 -08:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* PageThumbsProtocol.js
|
|
|
|
*
|
|
|
|
* This file implements the moz-page-thumb:// protocol and the corresponding
|
|
|
|
* channel delivering cached thumbnails.
|
|
|
|
*
|
|
|
|
* URL structure:
|
|
|
|
*
|
|
|
|
* moz-page-thumb://thumbnail?url=http%3A%2F%2Fwww.mozilla.org%2F
|
|
|
|
*
|
|
|
|
* This URL requests an image for 'http://www.mozilla.org/'.
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const Cu = Components.utils;
|
|
|
|
const Cc = Components.classes;
|
|
|
|
const Cr = Components.results;
|
|
|
|
const Ci = Components.interfaces;
|
|
|
|
|
|
|
|
Cu.import("resource:///modules/PageThumbs.jsm");
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
|
|
|
"resource://gre/modules/Services.jsm");
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements the thumbnail protocol handler responsible for moz-page-thumb: URIs.
|
|
|
|
*/
|
|
|
|
function Protocol() {
|
|
|
|
}
|
|
|
|
|
|
|
|
Protocol.prototype = {
|
|
|
|
/**
|
|
|
|
* The scheme used by this protocol.
|
|
|
|
*/
|
|
|
|
get scheme() PageThumbs.scheme,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The default port for this protocol (we don't support ports).
|
|
|
|
*/
|
|
|
|
get defaultPort() -1,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The flags specific to this protocol implementation.
|
|
|
|
*/
|
|
|
|
get protocolFlags() {
|
2012-01-26 09:28:41 -08:00
|
|
|
return Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
|
2012-01-19 07:01:42 -08:00
|
|
|
Ci.nsIProtocolHandler.URI_NORELATIVE |
|
|
|
|
Ci.nsIProtocolHandler.URI_NOAUTH;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new URI object that is suitable for loading by this protocol.
|
|
|
|
* @param aSpec The URI string in UTF8 encoding.
|
|
|
|
* @param aOriginCharset The charset of the document from which the URI originated.
|
|
|
|
* @return The newly created URI.
|
|
|
|
*/
|
|
|
|
newURI: function Proto_newURI(aSpec, aOriginCharset) {
|
|
|
|
let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI);
|
|
|
|
uri.spec = aSpec;
|
|
|
|
return uri;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructs a new channel from the given URI for this protocol handler.
|
|
|
|
* @param aURI The URI for which to construct a channel.
|
|
|
|
* @return The newly created channel.
|
|
|
|
*/
|
|
|
|
newChannel: function Proto_newChannel(aURI) {
|
2012-05-02 13:11:19 -07:00
|
|
|
let {url} = parseURI(aURI);
|
|
|
|
let file = PageThumbsStorage.getFileForURL(url);
|
|
|
|
|
|
|
|
if (file.exists()) {
|
|
|
|
let fileuri = Services.io.newFileURI(file);
|
|
|
|
return Services.io.newChannelFromURI(fileuri);
|
|
|
|
}
|
|
|
|
|
2012-01-19 07:01:42 -08:00
|
|
|
return new Channel(aURI);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Decides whether to allow a blacklisted port.
|
|
|
|
* @return Always false, we'll never allow ports.
|
|
|
|
*/
|
|
|
|
allowPort: function () false,
|
|
|
|
|
|
|
|
classID: Components.ID("{5a4ae9b5-f475-48ae-9dce-0b4c1d347884}"),
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler])
|
|
|
|
};
|
|
|
|
|
|
|
|
let NSGetFactory = XPCOMUtils.generateNSGetFactory([Protocol]);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A channel implementation responsible for delivering cached thumbnails.
|
|
|
|
*/
|
|
|
|
function Channel(aURI) {
|
|
|
|
this._uri = aURI;
|
|
|
|
|
|
|
|
// nsIChannel
|
|
|
|
this.originalURI = aURI;
|
|
|
|
|
|
|
|
// nsIHttpChannel
|
|
|
|
this._responseHeaders = {"content-type": PageThumbs.contentType};
|
|
|
|
}
|
|
|
|
|
|
|
|
Channel.prototype = {
|
2012-03-27 05:14:00 -07:00
|
|
|
_uri: null,
|
|
|
|
_referrer: null,
|
|
|
|
_canceled: false,
|
|
|
|
_status: Cr.NS_OK,
|
|
|
|
_isPending: false,
|
2012-01-19 07:01:42 -08:00
|
|
|
_wasOpened: false,
|
2012-03-27 05:14:00 -07:00
|
|
|
_responseText: "OK",
|
|
|
|
_responseStatus: 200,
|
|
|
|
_responseHeaders: null,
|
|
|
|
_requestMethod: "GET",
|
|
|
|
_requestStarted: false,
|
|
|
|
_allowPipelining: true,
|
|
|
|
_requestSucceeded: true,
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
/* :::::::: nsIChannel ::::::::::::::: */
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
get URI() this._uri,
|
|
|
|
owner: null,
|
|
|
|
notificationCallbacks: null,
|
|
|
|
get securityInfo() null,
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
contentType: PageThumbs.contentType,
|
|
|
|
contentCharset: null,
|
|
|
|
contentLength: -1,
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
get contentDisposition() {
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_NOT_AVAILABLE);
|
|
|
|
},
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
get contentDispositionFilename() {
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_NOT_AVAILABLE);
|
|
|
|
},
|
2012-02-02 06:50:53 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
get contentDispositionHeader() {
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_NOT_AVAILABLE);
|
|
|
|
},
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
open: function Channel_open() {
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
|
|
},
|
2012-02-02 06:50:53 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
asyncOpen: function Channel_asyncOpen(aListener, aContext) {
|
|
|
|
if (this._isPending)
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_IN_PROGRESS);
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
if (this._wasOpened)
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_ALREADY_OPENED);
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
if (this._canceled)
|
|
|
|
return (Components.returnCode = this._status);
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
this._isPending = true;
|
|
|
|
this._wasOpened = true;
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
this._listener = aListener;
|
|
|
|
this._context = aContext;
|
|
|
|
|
|
|
|
if (this.loadGroup)
|
|
|
|
this.loadGroup.addRequest(this, null);
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
if (this._canceled)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let {url} = parseURI(this._uri);
|
2012-01-19 07:01:42 -08:00
|
|
|
if (!url) {
|
2012-03-27 05:14:00 -07:00
|
|
|
this._serveThumbnailNotFound();
|
2012-01-19 07:01:42 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
PageThumbsCache.getReadEntry(url, function (aEntry) {
|
|
|
|
let inputStream = aEntry && aEntry.openInputStream(0);
|
|
|
|
if (!inputStream || !inputStream.available()) {
|
2012-03-27 05:14:00 -07:00
|
|
|
if (aEntry)
|
|
|
|
aEntry.close();
|
|
|
|
this._serveThumbnailNotFound();
|
2012-01-19 07:01:42 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
this._entry = aEntry;
|
|
|
|
this._pump = Cc["@mozilla.org/network/input-stream-pump;1"].
|
|
|
|
createInstance(Ci.nsIInputStreamPump);
|
|
|
|
|
|
|
|
this._pump.init(inputStream, -1, -1, 0, 0, true);
|
|
|
|
this._pump.asyncRead(this, null);
|
|
|
|
|
|
|
|
this._trackThumbnailHitOrMiss(true);
|
2012-01-19 07:01:42 -08:00
|
|
|
}.bind(this));
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2012-03-27 05:14:00 -07:00
|
|
|
* Serves a "404 Not Found" if we didn't find the requested thumbnail.
|
2012-01-19 07:01:42 -08:00
|
|
|
*/
|
2012-03-27 05:14:00 -07:00
|
|
|
_serveThumbnailNotFound: function Channel_serveThumbnailNotFound() {
|
|
|
|
this._responseStatus = 404;
|
|
|
|
this._responseText = "Not Found";
|
|
|
|
this._requestSucceeded = false;
|
|
|
|
|
|
|
|
this.onStartRequest(this, null);
|
|
|
|
this.onStopRequest(this, null, Cr.NS_OK);
|
|
|
|
|
|
|
|
this._trackThumbnailHitOrMiss(false);
|
2012-01-19 07:01:42 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2012-03-27 05:14:00 -07:00
|
|
|
* Implements telemetry tracking for thumbnail cache hits and misses.
|
|
|
|
* @param aFound Whether the thumbnail was found.
|
2012-01-19 07:01:42 -08:00
|
|
|
*/
|
2012-03-27 05:14:00 -07:00
|
|
|
_trackThumbnailHitOrMiss: function Channel_trackThumbnailHitOrMiss(aFound) {
|
|
|
|
Services.telemetry.getHistogramById("FX_THUMBNAILS_HIT_OR_MISS")
|
|
|
|
.add(aFound);
|
2012-01-19 07:01:42 -08:00
|
|
|
},
|
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
/* :::::::: nsIStreamListener ::::::::::::::: */
|
|
|
|
|
|
|
|
onStartRequest: function Channel_onStartRequest(aRequest, aContext) {
|
|
|
|
if (!this.canceled && Components.isSuccessCode(this._status))
|
|
|
|
this._status = aRequest.status;
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
this._requestStarted = true;
|
|
|
|
this._listener.onStartRequest(this, this._context);
|
2012-01-19 07:01:42 -08:00
|
|
|
},
|
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
onDataAvailable: function Channel_onDataAvailable(aRequest, aContext,
|
|
|
|
aInStream, aOffset, aCount) {
|
|
|
|
this._listener.onDataAvailable(this, this._context, aInStream, aOffset, aCount);
|
|
|
|
},
|
|
|
|
|
|
|
|
onStopRequest: function Channel_onStopRequest(aRequest, aContext, aStatus) {
|
|
|
|
this._isPending = false;
|
|
|
|
this._status = aStatus;
|
|
|
|
|
|
|
|
this._listener.onStopRequest(this, this._context, aStatus);
|
|
|
|
this._listener = null;
|
|
|
|
this._context = null;
|
|
|
|
|
|
|
|
if (this._entry)
|
|
|
|
this._entry.close();
|
|
|
|
|
2012-01-19 07:01:42 -08:00
|
|
|
if (this.loadGroup)
|
2012-03-27 05:14:00 -07:00
|
|
|
this.loadGroup.removeRequest(this, null, aStatus);
|
2012-01-19 07:01:42 -08:00
|
|
|
},
|
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
/* :::::::: nsIRequest ::::::::::::::: */
|
|
|
|
|
|
|
|
get status() this._status,
|
|
|
|
get name() this._uri.spec,
|
|
|
|
isPending: function Channel_isPending() this._isPending,
|
|
|
|
|
|
|
|
loadFlags: Ci.nsIRequest.LOAD_NORMAL,
|
|
|
|
loadGroup: null,
|
|
|
|
|
|
|
|
cancel: function Channel_cancel(aStatus) {
|
|
|
|
if (this._canceled)
|
2012-01-19 07:01:42 -08:00
|
|
|
return;
|
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
this._canceled = true;
|
|
|
|
this._status = aStatus;
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
if (this._pump)
|
|
|
|
this._pump.cancel(aStatus);
|
|
|
|
},
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
suspend: function Channel_suspend() {
|
|
|
|
if (this._pump)
|
|
|
|
this._pump.suspend();
|
|
|
|
},
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
resume: function Channel_resume() {
|
|
|
|
if (this._pump)
|
|
|
|
this._pump.resume();
|
2012-01-19 07:01:42 -08:00
|
|
|
},
|
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
/* :::::::: nsIHttpChannel ::::::::::::::: */
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
get referrer() this._referrer,
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
set referrer(aReferrer) {
|
|
|
|
if (this._wasOpened)
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_IN_PROGRESS);
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
this._referrer = aReferrer;
|
2012-01-19 07:01:42 -08:00
|
|
|
},
|
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
get requestMethod() this._requestMethod,
|
|
|
|
|
|
|
|
set requestMethod(aMethod) {
|
|
|
|
if (this._wasOpened)
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_IN_PROGRESS);
|
|
|
|
|
|
|
|
this._requestMethod = aMethod.toUpperCase();
|
|
|
|
},
|
|
|
|
|
|
|
|
get allowPipelining() this._allowPipelining,
|
|
|
|
|
|
|
|
set allowPipelining(aAllow) {
|
|
|
|
if (this._wasOpened)
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_FAILURE);
|
|
|
|
|
|
|
|
this._allowPipelining = aAllow;
|
|
|
|
},
|
2012-01-19 07:01:42 -08:00
|
|
|
|
|
|
|
redirectionLimit: 10,
|
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
get responseStatus() {
|
|
|
|
if (this._requestStarted)
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_NOT_AVAILABLE);
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
return this._responseStatus;
|
|
|
|
},
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
get responseStatusText() {
|
|
|
|
if (this._requestStarted)
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_NOT_AVAILABLE);
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
return this._responseText;
|
|
|
|
},
|
2012-01-19 07:01:42 -08:00
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
get requestSucceeded() {
|
|
|
|
if (this._requestStarted)
|
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_NOT_AVAILABLE);
|
|
|
|
|
|
|
|
return this._requestSucceeded;
|
|
|
|
},
|
|
|
|
|
|
|
|
isNoCacheResponse: function Channel_isNoCacheResponse() false,
|
|
|
|
isNoStoreResponse: function Channel_isNoStoreResponse() false,
|
2012-01-19 07:01:42 -08:00
|
|
|
|
|
|
|
getRequestHeader: function Channel_getRequestHeader() {
|
2012-03-27 05:14:00 -07:00
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_NOT_AVAILABLE);
|
2012-01-19 07:01:42 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
setRequestHeader: function Channel_setRequestHeader() {
|
|
|
|
if (this._wasOpened)
|
2012-03-27 05:14:00 -07:00
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_IN_PROGRESS);
|
2012-01-19 07:01:42 -08:00
|
|
|
},
|
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
visitRequestHeaders: function Channel_visitRequestHeaders() {},
|
2012-01-19 07:01:42 -08:00
|
|
|
|
|
|
|
getResponseHeader: function Channel_getResponseHeader(aHeader) {
|
|
|
|
let name = aHeader.toLowerCase();
|
|
|
|
if (name in this._responseHeaders)
|
|
|
|
return this._responseHeaders[name];
|
|
|
|
|
2012-03-27 05:14:00 -07:00
|
|
|
throw (Components.returnCode = Cr.NS_ERROR_NOT_AVAILABLE);
|
2012-01-19 07:01:42 -08:00
|
|
|
},
|
|
|
|
|
|
|
|
setResponseHeader: function Channel_setResponseHeader(aHeader, aValue, aMerge) {
|
|
|
|
let name = aHeader.toLowerCase();
|
|
|
|
if (!aValue && !aMerge)
|
|
|
|
delete this._responseHeaders[name];
|
|
|
|
else
|
|
|
|
this._responseHeaders[name] = aValue;
|
|
|
|
},
|
|
|
|
|
|
|
|
visitResponseHeaders: function Channel_visitResponseHeaders(aVisitor) {
|
|
|
|
for (let name in this._responseHeaders) {
|
|
|
|
let value = this._responseHeaders[name];
|
|
|
|
|
|
|
|
try {
|
|
|
|
aVisitor.visitHeader(name, value);
|
|
|
|
} catch (e) {
|
|
|
|
// The visitor can throw to stop the iteration.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/* :::::::: nsIHttpChannelInternal ::::::::::::::: */
|
|
|
|
|
|
|
|
documentURI: null,
|
2012-03-27 05:14:00 -07:00
|
|
|
get canceled() this._canceled,
|
|
|
|
allowSpdy: false,
|
2012-01-19 07:01:42 -08:00
|
|
|
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel,
|
|
|
|
Ci.nsIHttpChannel,
|
|
|
|
Ci.nsIHttpChannelInternal,
|
|
|
|
Ci.nsIRequest])
|
2012-03-27 05:14:00 -07:00
|
|
|
}
|
2012-01-19 07:01:42 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Parses a given URI and extracts all parameters relevant to this protocol.
|
|
|
|
* @param aURI The URI to parse.
|
|
|
|
* @return The parsed parameters.
|
|
|
|
*/
|
|
|
|
function parseURI(aURI) {
|
|
|
|
let {scheme, staticHost} = PageThumbs;
|
|
|
|
let re = new RegExp("^" + scheme + "://" + staticHost + ".*?\\?");
|
|
|
|
let query = aURI.spec.replace(re, "");
|
|
|
|
let params = {};
|
|
|
|
|
|
|
|
query.split("&").forEach(function (aParam) {
|
|
|
|
let [key, value] = aParam.split("=").map(decodeURIComponent);
|
|
|
|
params[key.toLowerCase()] = value;
|
|
|
|
});
|
|
|
|
|
|
|
|
return params;
|
|
|
|
}
|