Bug 497543 - Part 2 - moz-page-thumb:// Protocol and Channel; r=mak
☠☠ backed out by 97973b8c04be ☠ ☠
authorTim Taubert <tim.taubert@gmx.de>
Thu, 19 Jan 2012 16:01:42 +0100
changeset 86124 cc55d047eff997e7ad704036ec24e0f66d5ab37d
parent 86123 9883a9f66cf04ebe9b2c1b885954d02f97612278
child 86125 13e27937167b8693fa4fe1b0f6d61746b5c12d4e
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs497543
milestone12.0a1
Bug 497543 - Part 2 - moz-page-thumb:// Protocol and Channel; r=mak
browser/components/Makefile.in
browser/components/thumbnails/BrowserPageThumbs.manifest
browser/components/thumbnails/Makefile.in
browser/components/thumbnails/PageThumbsProtocol.js
browser/installer/package-manifest.in
--- a/browser/components/Makefile.in
+++ b/browser/components/Makefile.in
@@ -66,16 +66,17 @@ PARALLEL_DIRS = \
   places \
   preferences \
   privatebrowsing \
   search \
   sessionstore \
   shell \
   sidebar \
   tabview \
+  thumbnails \
   migration \
   $(NULL)
 
 ifdef MOZ_SAFE_BROWSING
 PARALLEL_DIRS += safebrowsing
 endif
 
 TEST_DIRS += test
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/BrowserPageThumbs.manifest
@@ -0,0 +1,2 @@
+component {5a4ae9b5-f475-48ae-9dce-0b4c1d347884} PageThumbsProtocol.js
+contract @mozilla.org/network/protocol;1?name=moz-page-thumb {5a4ae9b5-f475-48ae-9dce-0b4c1d347884}
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/Makefile.in
@@ -0,0 +1,23 @@
+# 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/.
+
+DEPTH		= ../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+EXTRA_COMPONENTS = \
+  BrowserPageThumbs.manifest \
+  PageThumbsProtocol.js \
+  $(NULL)
+
+ifdef ENABLE_TESTS
+	DIRS += test
+endif
+
+include $(topsrcdir)/config/rules.mk
+
+XPIDL_FLAGS += -I$(topsrcdir)/browser/components/
new file mode 100644
--- /dev/null
+++ b/browser/components/thumbnails/PageThumbsProtocol.js
@@ -0,0 +1,448 @@
+/* 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, "NetUtil",
+  "resource://gre/modules/NetUtil.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() {
+    return Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE |
+           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) {
+    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 = {
+  /**
+   * Tracks if the channel has been opened, yet.
+   */
+  _wasOpened: false,
+
+  /**
+   * Opens this channel asynchronously.
+   * @param aListener The listener that receives the channel data when available.
+   * @param aContext A custom context passed to the listener's methods.
+   */
+  asyncOpen: function Channel_asyncOpen(aListener, aContext) {
+    if (this._wasOpened)
+      throw Cr.NS_ERROR_ALREADY_OPENED;
+
+    if (this.canceled)
+      return;
+
+    this._listener = aListener;
+    this._context = aContext;
+
+    this._isPending = true;
+    this._wasOpened = true;
+
+    // Try to read the data from the thumbnail cache.
+    this._readCache(function (aData) {
+      // Update response if there's no data.
+      if (!aData) {
+        this._responseStatus = 404;
+        this._responseText = "Not Found";
+      }
+
+      this._startRequest();
+
+      if (!this.canceled) {
+        this._addToLoadGroup();
+
+        if (aData)
+          this._serveData(aData);
+
+        if (!this.canceled)
+          this._stopRequest();
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Reads a data stream from the cache entry.
+   * @param aCallback The callback the data is passed to.
+   */
+  _readCache: function Channel_readCache(aCallback) {
+    let {url} = parseURI(this._uri);
+
+    // Return early if there's no valid URL given.
+    if (!url) {
+      aCallback(null);
+      return;
+    }
+
+    // Try to get a cache entry.
+    PageThumbsCache.getReadEntry(url, function (aEntry) {
+      let inputStream = aEntry && aEntry.openInputStream(0);
+
+      function closeEntryAndFinish(aData) {
+        if (aEntry) {
+          aEntry.close();
+        }
+        aCallback(aData);
+      }
+
+      // Check if we have a valid entry and if it has any data.
+      if (!inputStream || !inputStream.available()) {
+        closeEntryAndFinish();
+        return;
+      }
+
+      try {
+        // Read the cache entry's data.
+        NetUtil.asyncFetch(inputStream, function (aData, aStatus) {
+          // We might have been canceled while waiting.
+          if (this.canceled)
+            return;
+
+          // Check if we have a valid data stream.
+          if (!Components.isSuccessCode(aStatus) || !aData.available())
+            aData = null;
+
+          closeEntryAndFinish(aData);
+        }.bind(this));
+      } catch (e) {
+        closeEntryAndFinish();
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Calls onStartRequest on the channel listener.
+   */
+  _startRequest: function Channel_startRequest() {
+    try {
+      this._listener.onStartRequest(this, this._context);
+    } catch (e) {
+      // The listener might throw if the request has been canceled.
+      this.cancel(Cr.NS_BINDING_ABORTED);
+    }
+  },
+
+  /**
+   * Calls onDataAvailable on the channel listener and passes the data stream.
+   * @param aData The data to be delivered.
+   */
+  _serveData: function Channel_serveData(aData) {
+    try {
+      let available = aData.available();
+      this._listener.onDataAvailable(this, this._context, aData, 0, available);
+    } catch (e) {
+      // The listener might throw if the request has been canceled.
+      this.cancel(Cr.NS_BINDING_ABORTED);
+    }
+  },
+
+  /**
+   * Calls onStopRequest on the channel listener.
+   */
+  _stopRequest: function Channel_stopRequest() {
+    try {
+      this._listener.onStopRequest(this, this._context, this.status);
+    } catch (e) {
+      // This might throw but is generally ignored.
+    }
+
+    // The request has finished, clean up after ourselves.
+    this._cleanup();
+  },
+
+  /**
+   * Adds this request to the load group, if any.
+   */
+  _addToLoadGroup: function Channel_addToLoadGroup() {
+    if (this.loadGroup)
+      this.loadGroup.addRequest(this, this._context);
+  },
+
+  /**
+   * Removes this request from its load group, if any.
+   */
+  _removeFromLoadGroup: function Channel_removeFromLoadGroup() {
+    if (!this.loadGroup)
+      return;
+
+    try {
+      this.loadGroup.removeRequest(this, this._context, this.status);
+    } catch (e) {
+      // This might throw but is ignored.
+    }
+  },
+
+  /**
+   * Cleans up the channel when the request has finished.
+   */
+  _cleanup: function Channel_cleanup() {
+    this._removeFromLoadGroup();
+    this.loadGroup = null;
+
+    this._isPending = false;
+
+    delete this._listener;
+    delete this._context;
+  },
+
+  /* :::::::: nsIChannel ::::::::::::::: */
+
+  contentType: PageThumbs.contentType,
+  contentLength: -1,
+  owner: null,
+  contentCharset: null,
+  notificationCallbacks: null,
+
+  get URI() this._uri,
+  get securityInfo() null,
+
+  /**
+   * Opens this channel synchronously. Not supported.
+   */
+  open: function Channel_open() {
+    // Synchronous data delivery is not implemented.
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  /* :::::::: nsIHttpChannel ::::::::::::::: */
+
+  redirectionLimit: 10,
+  requestMethod: "GET",
+  allowPipelining: true,
+  referrer: null,
+
+  get requestSucceeded() true,
+
+  _responseStatus: 200,
+  get responseStatus() this._responseStatus,
+
+  _responseText: "OK",
+  get responseStatusText() this._responseText,
+
+  /**
+   * Checks if the server sent the equivalent of a "Cache-control: no-cache"
+   * response header.
+   * @return Always false.
+   */
+  isNoCacheResponse: function () false,
+
+  /**
+   * Checks if the server sent the equivalent of a "Cache-control: no-cache"
+   * response header.
+   * @return Always false.
+   */
+  isNoStoreResponse: function () false,
+
+  /**
+   * Returns the value of a particular request header. Not implemented.
+   */
+  getRequestHeader: function Channel_getRequestHeader() {
+    throw Cr.NS_ERROR_NOT_AVAILABLE;
+  },
+
+  /**
+   * This method is called to set the value of a particular request header.
+   * Not implemented.
+   */
+  setRequestHeader: function Channel_setRequestHeader() {
+    if (this._wasOpened)
+      throw Cr.NS_ERROR_IN_PROGRESS;
+  },
+
+  /**
+   * Call this method to visit all request headers. Not implemented.
+   */
+  visitRequestHeaders: function () {},
+
+  /**
+   * Gets the value of a particular response header.
+   * @param aHeader The case-insensitive name of the response header to query.
+   * @return The header value.
+   */
+  getResponseHeader: function Channel_getResponseHeader(aHeader) {
+    let name = aHeader.toLowerCase();
+    if (name in this._responseHeaders)
+      return this._responseHeaders[name];
+
+    throw Cr.NS_ERROR_NOT_AVAILABLE;
+  },
+
+  /**
+   * This method is called to set the value of a particular response header.
+   * @param aHeader The case-insensitive name of the response header to query.
+   * @param aValue The response header value to set.
+   */
+  setResponseHeader: function Channel_setResponseHeader(aHeader, aValue, aMerge) {
+    let name = aHeader.toLowerCase();
+    if (!aValue && !aMerge)
+      delete this._responseHeaders[name];
+    else
+      this._responseHeaders[name] = aValue;
+  },
+
+  /**
+   * Call this method to visit all response headers.
+   * @param aVisitor The header visitor.
+   */
+  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;
+      }
+    }
+  },
+
+  /* :::::::: nsIRequest ::::::::::::::: */
+
+  loadFlags: Ci.nsIRequest.LOAD_NORMAL,
+  loadGroup: null,
+
+  get name() this._uri.spec,
+
+  _status: Cr.NS_OK,
+  get status() this._status,
+
+  _isPending: false,
+  isPending: function () this._isPending,
+
+  resume: function () {},
+  suspend: function () {},
+
+  /**
+   * Cancels this request.
+   * @param aStatus The reason for cancelling.
+   */
+  cancel: function Channel_cancel(aStatus) {
+    if (this.canceled)
+      return;
+
+    this._isCanceled = true;
+    this._status = aStatus;
+
+    this._cleanup();
+  },
+
+  /* :::::::: nsIHttpChannelInternal ::::::::::::::: */
+
+  documentURI: null,
+
+  _isCanceled: false,
+  get canceled() this._isCanceled,
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel,
+                                         Ci.nsIHttpChannel,
+                                         Ci.nsIHttpChannelInternal,
+                                         Ci.nsIRequest])
+};
+
+/**
+ * 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;
+}
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -279,16 +279,17 @@
 @BINPATH@/components/fuelApplication.js
 @BINPATH@/components/WebContentConverter.js
 @BINPATH@/components/BrowserComponents.manifest
 @BINPATH@/components/nsBrowserContentHandler.js
 @BINPATH@/components/nsBrowserGlue.js
 @BINPATH@/components/nsSetDefaultBrowser.manifest
 @BINPATH@/components/nsSetDefaultBrowser.js
 @BINPATH@/components/BrowserPlaces.manifest
+@BINPATH@/components/BrowserPageThumbs.manifest
 @BINPATH@/components/nsPrivateBrowsingService.manifest
 @BINPATH@/components/nsPrivateBrowsingService.js
 @BINPATH@/components/toolkitsearch.manifest
 @BINPATH@/components/nsSearchService.js
 @BINPATH@/components/nsSearchSuggestions.js
 @BINPATH@/components/passwordmgr.manifest
 @BINPATH@/components/nsLoginInfo.js
 @BINPATH@/components/nsLoginManager.js
@@ -342,16 +343,17 @@
 @BINPATH@/components/toolkitplaces.manifest
 @BINPATH@/components/nsLivemarkService.js
 @BINPATH@/components/nsTaggingService.js
 @BINPATH@/components/nsPlacesAutoComplete.manifest
 @BINPATH@/components/nsPlacesAutoComplete.js
 @BINPATH@/components/nsPlacesExpiration.js
 @BINPATH@/components/PlacesProtocolHandler.js
 @BINPATH@/components/PlacesCategoriesStarter.js
+@BINPATH@/components/PageThumbsProtocol.js
 @BINPATH@/components/nsDefaultCLH.manifest
 @BINPATH@/components/nsDefaultCLH.js
 @BINPATH@/components/nsContentPrefService.manifest
 @BINPATH@/components/nsContentPrefService.js
 @BINPATH@/components/nsContentDispatchChooser.manifest
 @BINPATH@/components/nsContentDispatchChooser.js
 @BINPATH@/components/nsHandlerService.manifest
 @BINPATH@/components/nsHandlerService.js