Bug 1283825 - Add a page-icon protocol to fetch the best icon for a url. r=adw draft
authorMarco Bonardo <mbonardo@mozilla.com>
Thu, 30 Jun 2016 18:17:44 +0200
changeset 383150 061f1687dfd9efb3c90398f130b12bce3e35a678
parent 383149 003411f82097d960183f2ade75a81f16498ffe12
child 524405 d7debc0ea09a0304b8d680d7d43830db1c7657e8
push id21946
push usermak77@bonardo.net
push dateFri, 01 Jul 2016 11:31:06 +0000
reviewersadw
bugs1283825
milestone50.0a1
Bug 1283825 - Add a page-icon protocol to fetch the best icon for a url. r=adw MozReview-Commit-ID: 3exDniH8Hkm
browser/installer/package-manifest.in
toolkit/components/places/PageIconProtocolHandler.js
toolkit/components/places/moz.build
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/tests/favicons/test_page-icon_protocol.js
toolkit/components/places/tests/favicons/xpcshell.ini
toolkit/components/places/toolkitplaces.manifest
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -433,16 +433,17 @@
 @RESPATH@/browser/components/@DLL_PREFIX@browsercomps@DLL_SUFFIX@
 @RESPATH@/components/txEXSLTRegExFunctions.manifest
 @RESPATH@/components/txEXSLTRegExFunctions.js
 @RESPATH@/components/toolkitplaces.manifest
 @RESPATH@/components/nsLivemarkService.js
 @RESPATH@/components/nsTaggingService.js
 @RESPATH@/components/UnifiedComplete.js
 @RESPATH@/components/nsPlacesExpiration.js
+@RESPATH@/components/PageIconProtocolHandler.js
 @RESPATH@/components/PlacesCategoriesStarter.js
 @RESPATH@/components/ColorAnalyzer.js
 @RESPATH@/components/PageThumbsProtocol.js
 @RESPATH@/components/nsDefaultCLH.manifest
 @RESPATH@/components/nsDefaultCLH.js
 @RESPATH@/components/nsContentPrefService.manifest
 @RESPATH@/components/nsContentPrefService.js
 @RESPATH@/components/nsContentDispatchChooser.manifest
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/PageIconProtocolHandler.js
@@ -0,0 +1,129 @@
+/* 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/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+
+function makeDefaultFaviconChannel(uri, loadInfo) {
+  let channel = Services.io.newChannelFromURIWithLoadInfo(
+    PlacesUtils.favicons.defaultFavicon, loadInfo);
+  channel.originalURI = uri;
+  return channel;
+}
+
+function streamDefaultFavicon(uri, loadInfo, outputStream) {
+  try {
+    // Open up a new channel to get that data, and push it to our output stream.
+    // Create a listener to hand data to the pipe's output stream.
+    let listener = Cc["@mozilla.org/network/simple-stream-listener;1"]
+                     .createInstance(Ci.nsISimpleStreamListener);
+    listener.init(outputStream, {
+      onStartRequest(request, context) {},
+      onStopRequest(request, context, statusCode) {
+        // We must close the outputStream regardless.
+        outputStream.close();
+      }
+    });
+    let defaultIconChannel = makeDefaultFaviconChannel(uri, loadInfo);
+    defaultIconChannel.asyncOpen2(listener);
+  } catch (ex) {
+    Cu.reportError(ex);
+    outputStream.close();
+  }
+}
+
+function PageIconProtocolHandler() {
+}
+
+PageIconProtocolHandler.prototype = {
+  get scheme() {
+    return "page-icon";
+  },
+
+  get defaultPort() {
+    return -1;
+  },
+
+  get protocolFlags() {
+    return Ci.nsIProtocolHandler.URI_NORELATIVE |
+           Ci.nsIProtocolHandler.URI_NOAUTH |
+           Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
+           Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE;
+  },
+
+  newURI(spec, originCharset, baseURI) {
+    let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI);
+    uri.spec = spec;
+    return uri;
+  },
+
+  newChannel2(uri, loadInfo) {
+    try {
+      // Create a pipe that will give us an output stream that we can use once
+      // we got all the favicon data.
+      let pipe = Cc["@mozilla.org/pipe;1"]
+                   .createInstance(Ci.nsIPipe);
+      // TODO: use MAX_FAVICON_SIZE!!!
+      pipe.init(true, true, 0, 0xffffffff, null);
+
+      // Create our channel.
+      let channel = Cc['@mozilla.org/network/input-stream-channel;1']
+                      .createInstance(Ci.nsIInputStreamChannel);
+      channel.QueryInterface(Ci.nsIChannel);
+      channel.setURI(uri);
+      channel.contentStream = pipe.inputStream;
+      channel.loadInfo = loadInfo;
+
+      let pageURI = NetUtil.newURI(uri.path);
+      PlacesUtils.favicons.getFaviconDataForPage(pageURI, (iconuri, len, data, mime) => {
+        if (len == 0) {
+          channel.contentType = "image/png";
+          streamDefaultFavicon(uri, loadInfo, pipe.outputStream);
+          return;
+        }
+
+        try {
+          channel.contentType = mime;
+          // Pass the icon data to the output stream.
+          let stream = Cc["@mozilla.org/binaryoutputstream;1"]
+                         .createInstance(Ci.nsIBinaryOutputStream);
+          stream.setOutputStream(pipe.outputStream);
+          stream.writeByteArray(data, len);
+          stream.close();
+          pipe.outputStream.close();
+        } catch (ex) {
+          channel.contentType = "image/png";
+          streamDefaultFavicon(uri, loadInfo, pipe.outputStream);
+        }
+      });
+
+      return channel;
+    } catch (ex) {
+      return makeDefaultFaviconChannel(uri, loadInfo);
+    }
+  },
+
+  newChannel(uri) {
+    return this.newChannel2(uri, null);
+  },
+
+  allowPort(port, scheme) {
+    return false;
+  },
+
+  classID: Components.ID("{60a1f7c6-4ff9-4a42-84d3-5a185faa6f32}"),
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIProtocolHandler
+  ])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PageIconProtocolHandler]);
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -73,16 +73,17 @@ if CONFIG['MOZ_PLACES']:
         'PlacesUtils.jsm',
     ]
 
     EXTRA_COMPONENTS += [
         'ColorAnalyzer.js',
         'nsLivemarkService.js',
         'nsPlacesExpiration.js',
         'nsTaggingService.js',
+        'PageIconProtocolHandler.js',
         'PlacesCategoriesStarter.js',
         'toolkitplaces.manifest',
         'UnifiedComplete.js',
     ]
 
     if CONFIG['MOZ_SUITE']:
         EXTRA_COMPONENTS += [
             'nsPlacesAutoComplete.js',
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -1140,27 +1140,28 @@ nsNavHistory::CanAddURI(nsIURI* aURI, bo
   }
   if (scheme.EqualsLiteral("https")) {
     *canAdd = true;
     return NS_OK;
   }
 
   // now check for all bad things
   if (scheme.EqualsLiteral("about") ||
+      scheme.EqualsLiteral("blob") ||
+      scheme.EqualsLiteral("chrome") ||
+      scheme.EqualsLiteral("data") ||
       scheme.EqualsLiteral("imap") ||
-      scheme.EqualsLiteral("news") ||
+      scheme.EqualsLiteral("javascript") ||
       scheme.EqualsLiteral("mailbox") ||
       scheme.EqualsLiteral("moz-anno") ||
-      scheme.EqualsLiteral("view-source") ||
-      scheme.EqualsLiteral("chrome") ||
+      scheme.EqualsLiteral("news") ||
+      scheme.EqualsLiteral("page-icon") ||
       scheme.EqualsLiteral("resource") ||
-      scheme.EqualsLiteral("data") ||
-      scheme.EqualsLiteral("wyciwyg") ||
-      scheme.EqualsLiteral("javascript") ||
-      scheme.EqualsLiteral("blob")) {
+      scheme.EqualsLiteral("view-source") ||
+      scheme.EqualsLiteral("wyciwyg")) {
     return NS_OK;
   }
   *canAdd = true;
   return NS_OK;
 }
 
 // nsNavHistory::GetNewQuery
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/favicons/test_page-icon_protocol.js
@@ -0,0 +1,66 @@
+const ICON_DATA = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+const TEST_URI = NetUtil.newURI("http://mozilla.org/");
+const ICON_URI = NetUtil.newURI("http://mozilla.org/favicon.ico");
+
+function fetchIconForSpec(spec) {
+ return new Promise((resolve, reject) => {
+    NetUtil.asyncFetch({
+      uri: NetUtil.newURI("page-icon:" + TEST_URI.spec),
+      loadUsingSystemPrincipal: true,
+      contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
+    }, (input, status, request) => {
+       if (!Components.isSuccessCode(status)) {
+        reject(new Error("unable to load icon"));
+        return;
+      }
+
+      try {
+        let data = NetUtil.readInputStreamToString(input, input.available());
+        let contentType = request.QueryInterface(Ci.nsIChannel).contentType;
+        input.close();
+        resolve({ data, contentType });
+      } catch (ex) {
+        reject(ex);
+      }
+    });
+  });
+}
+
+var gDefaultFavicon;
+var gFavicon;
+
+add_task(function* setup() {
+  PlacesTestUtils.addVisits({ uri: TEST_URI });
+
+  PlacesUtils.favicons.replaceFaviconDataFromDataURL(
+    ICON_URI, ICON_DATA, (Date.now() + 8640000) * 1000,
+    Services.scriptSecurityManager.getSystemPrincipal());
+
+  yield new Promise(resolve => {
+    PlacesUtils.favicons.setAndFetchFaviconForPage(
+      TEST_URI, ICON_URI, false,
+      PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+      resolve, Services.scriptSecurityManager.getSystemPrincipal());
+  });
+
+  gDefaultFavicon = yield fetchIconForSpec(PlacesUtils.favicons.defaultFavicon);
+  gFavicon = yield fetchIconForSpec(ICON_DATA);
+});
+
+add_task(function* known_url() {
+  let {data, contentType} = yield fetchIconForSpec(TEST_URI.spec);
+  Assert.equal(contentType, gFavicon.contentType);
+  Assert.ok(data == gFavicon.data, "Got the favicon data");
+});
+
+add_task(function* unknown_url() {
+  let {data, contentType} = yield fetchIconForSpec("http://www.moz.org/");
+  Assert.equal(contentType, gDefaultFavicon.contentType);
+  Assert.ok(data == gDefaultFavicon.data, "Got the default favicon data");
+});
+
+add_task(function* invalid_url() {
+  let {data, contentType} = yield fetchIconForSpec("test");
+  Assert.equal(contentType, gDefaultFavicon.contentType);
+  Assert.ok(data == gDefaultFavicon.data, "Got the default favicon data");
+});
--- a/toolkit/components/places/tests/favicons/xpcshell.ini
+++ b/toolkit/components/places/tests/favicons/xpcshell.ini
@@ -22,11 +22,12 @@ support-files =
 
 [test_expireAllFavicons.js]
 [test_favicons_conversions.js]
 # Bug 676989: test fails consistently on Android
 fail-if = os == "android"
 [test_getFaviconDataForPage.js]
 [test_getFaviconURLForPage.js]
 [test_moz-anno_favicon_mime_type.js]
+[test_page-icon_protocol.js]
 [test_query_result_favicon_changed_on_child.js]
 [test_replaceFaviconData.js]
 [test_replaceFaviconDataFromDataURL.js]
--- a/toolkit/components/places/toolkitplaces.manifest
+++ b/toolkit/components/places/toolkitplaces.manifest
@@ -21,8 +21,12 @@ category bookmark-observers PlacesCatego
 
 # ColorAnalyzer.js
 component {d056186c-28a0-494e-aacc-9e433772b143} ColorAnalyzer.js
 contract @mozilla.org/places/colorAnalyzer;1 {d056186c-28a0-494e-aacc-9e433772b143}
 
 # UnifiedComplete.js
 component {f964a319-397a-4d21-8be6-5cdd1ee3e3ae} UnifiedComplete.js
 contract @mozilla.org/autocomplete/search;1?name=unifiedcomplete {f964a319-397a-4d21-8be6-5cdd1ee3e3ae}
+
+# PageIconProtocolHandler.js
+component {60a1f7c6-4ff9-4a42-84d3-5a185faa6f32} PageIconProtocolHandler.js
+contract @mozilla.org/network/protocol;1?name=page-icon {60a1f7c6-4ff9-4a42-84d3-5a185faa6f32}