diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index db60365eb4ba..33fb9467d38f 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -442,6 +442,7 @@ @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 diff --git a/toolkit/components/places/FaviconHelpers.cpp b/toolkit/components/places/FaviconHelpers.cpp index 4862c8898cab..fd66c5b4512e 100644 --- a/toolkit/components/places/FaviconHelpers.cpp +++ b/toolkit/components/places/FaviconHelpers.cpp @@ -613,7 +613,7 @@ AsyncFetchAndSetIconForPage::OnStopRequest(nsIRequest* aRequest, // If over the maximum size allowed, don't save data to the database to // avoid bloating it. - if (mIcon.data.Length() > MAX_FAVICON_SIZE) { + if (mIcon.data.Length() > nsIFaviconService::MAX_FAVICON_SIZE) { return NS_OK; } diff --git a/toolkit/components/places/PageIconProtocolHandler.js b/toolkit/components/places/PageIconProtocolHandler.js new file mode 100644 index 000000000000..003c1abaa8c2 --- /dev/null +++ b/toolkit/components/places/PageIconProtocolHandler.js @@ -0,0 +1,128 @@ +/* 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); + pipe.init(true, true, 0, Ci.nsIFaviconService.MAX_FAVICON_SIZE); + + // 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]); diff --git a/toolkit/components/places/moz.build b/toolkit/components/places/moz.build index 4a2184567db5..492e5b7f7cda 100644 --- a/toolkit/components/places/moz.build +++ b/toolkit/components/places/moz.build @@ -78,6 +78,7 @@ if CONFIG['MOZ_PLACES']: 'nsLivemarkService.js', 'nsPlacesExpiration.js', 'nsTaggingService.js', + 'PageIconProtocolHandler.js', 'PlacesCategoriesStarter.js', 'toolkitplaces.manifest', 'UnifiedComplete.js', diff --git a/toolkit/components/places/nsAnnoProtocolHandler.cpp b/toolkit/components/places/nsAnnoProtocolHandler.cpp index 30a0d75713ae..a2ace7c55cd0 100644 --- a/toolkit/components/places/nsAnnoProtocolHandler.cpp +++ b/toolkit/components/places/nsAnnoProtocolHandler.cpp @@ -336,8 +336,7 @@ nsAnnoProtocolHandler::NewFaviconChannel(nsIURI *aURI, nsIURI *aAnnotationURI, nsCOMPtr outputStream; nsresult rv = NS_NewPipe(getter_AddRefs(inputStream), getter_AddRefs(outputStream), - MAX_FAVICON_SIZE, MAX_FAVICON_SIZE, true, - true); + 0, nsIFaviconService::MAX_FAVICON_SIZE, true, true); NS_ENSURE_SUCCESS(rv, GetDefaultIcon(aLoadInfo, _channel)); // Create our channel. We'll call SetContentType with the right type when diff --git a/toolkit/components/places/nsFaviconService.cpp b/toolkit/components/places/nsFaviconService.cpp index bfe955691f4a..ddb825d1b024 100644 --- a/toolkit/components/places/nsFaviconService.cpp +++ b/toolkit/components/places/nsFaviconService.cpp @@ -347,7 +347,7 @@ nsFaviconService::ReplaceFaviconData(nsIURI* aFaviconURI, rv = OptimizeFaviconImage(aData, aDataLen, aMimeType, iconData->data, iconData->mimeType); NS_ENSURE_SUCCESS(rv, rv); - if (iconData->data.Length() > MAX_FAVICON_SIZE) { + if (iconData->data.Length() > nsIFaviconService::MAX_FAVICON_SIZE) { // We cannot optimize this favicon size and we are over the maximum size // allowed, so we will not save data to the db to avoid bloating it. mUnassociatedIcons.RemoveEntry(aFaviconURI); diff --git a/toolkit/components/places/nsFaviconService.h b/toolkit/components/places/nsFaviconService.h index 11e596c724ae..11c530af478d 100644 --- a/toolkit/components/places/nsFaviconService.h +++ b/toolkit/components/places/nsFaviconService.h @@ -23,11 +23,6 @@ #include "FaviconHelpers.h" -// Favicons bigger than this size should not be saved to the db to avoid -// bloating it with large image blobs. -// This still allows us to accept a favicon even if we cannot optimize it. -#define MAX_FAVICON_SIZE 10240 - // Most icons will be smaller than this rough estimate of the size of an // uncompressed 16x16 RGBA image of the same dimensions. #define MAX_ICON_FILESIZE(s) ((uint32_t) s*s*4) diff --git a/toolkit/components/places/nsIFaviconService.idl b/toolkit/components/places/nsIFaviconService.idl index b70067ed0f6e..4c9fcb2947a9 100644 --- a/toolkit/components/places/nsIFaviconService.idl +++ b/toolkit/components/places/nsIFaviconService.idl @@ -15,6 +15,12 @@ interface nsIFaviconService : nsISupports // The favicon is being loaded from a non-private browsing window const unsigned long FAVICON_LOAD_NON_PRIVATE = 2; + /** + * Favicons bigger than this size in bytes, won't be saved to the database to + * avoid bloating it with large image blobs. + */ + const unsigned long MAX_FAVICON_SIZE = 10240; + /** * For a given icon URI, this will return a URI that will result in the image. * In most cases, this is an annotation URI. For chrome URIs, this will do diff --git a/toolkit/components/places/nsNavHistory.cpp b/toolkit/components/places/nsNavHistory.cpp index 4117c58ec1d9..7e39b58afb11 100644 --- a/toolkit/components/places/nsNavHistory.cpp +++ b/toolkit/components/places/nsNavHistory.cpp @@ -1145,17 +1145,18 @@ nsNavHistory::CanAddURI(nsIURI* aURI, bool* canAdd) // 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; diff --git a/toolkit/components/places/tests/favicons/test_page-icon_protocol.js b/toolkit/components/places/tests/favicons/test_page-icon_protocol.js new file mode 100644 index 000000000000..17987f31ff8f --- /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"); +}); diff --git a/toolkit/components/places/tests/favicons/xpcshell.ini b/toolkit/components/places/tests/favicons/xpcshell.ini index 3675a9dd31a8..a876d8a28bf0 100644 --- a/toolkit/components/places/tests/favicons/xpcshell.ini +++ b/toolkit/components/places/tests/favicons/xpcshell.ini @@ -27,6 +27,7 @@ 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] diff --git a/toolkit/components/places/toolkitplaces.manifest b/toolkit/components/places/toolkitplaces.manifest index 09988538557d..cd9665200e97 100644 --- a/toolkit/components/places/toolkitplaces.manifest +++ b/toolkit/components/places/toolkitplaces.manifest @@ -26,3 +26,7 @@ contract @mozilla.org/places/colorAnalyzer;1 {d056186c-28a0-494e-aacc-9e433772b1 # 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}