From 4492b5f8e774bf3b4f21e4e468fc052cbcbb468a Mon Sep 17 00:00:00 2001 From: Thomas Groman Date: Mon, 16 Dec 2019 19:48:42 -0800 Subject: initial commit --- components/feeds/BrowserFeeds.manifest | 28 + components/feeds/FeedConverter.js | 591 +++++++++ components/feeds/FeedWriter.js | 1397 ++++++++++++++++++++ components/feeds/WebContentConverter.js | 927 +++++++++++++ components/feeds/content/subscribe.css | 7 + components/feeds/content/subscribe.js | 23 + components/feeds/content/subscribe.xhtml | 65 + components/feeds/content/subscribe.xml | 40 + components/feeds/jar.mn | 9 + components/feeds/moz.build | 33 + components/feeds/nsFeedSniffer.cpp | 363 +++++ components/feeds/nsFeedSniffer.h | 37 + components/feeds/nsIFeedResultService.idl | 66 + .../feeds/nsIWebContentConverterRegistrar.idl | 117 ++ 14 files changed, 3703 insertions(+) create mode 100644 components/feeds/BrowserFeeds.manifest create mode 100644 components/feeds/FeedConverter.js create mode 100644 components/feeds/FeedWriter.js create mode 100644 components/feeds/WebContentConverter.js create mode 100644 components/feeds/content/subscribe.css create mode 100644 components/feeds/content/subscribe.js create mode 100644 components/feeds/content/subscribe.xhtml create mode 100644 components/feeds/content/subscribe.xml create mode 100644 components/feeds/jar.mn create mode 100644 components/feeds/moz.build create mode 100644 components/feeds/nsFeedSniffer.cpp create mode 100644 components/feeds/nsFeedSniffer.h create mode 100644 components/feeds/nsIFeedResultService.idl create mode 100644 components/feeds/nsIWebContentConverterRegistrar.idl (limited to 'components/feeds') diff --git a/components/feeds/BrowserFeeds.manifest b/components/feeds/BrowserFeeds.manifest new file mode 100644 index 0000000..a584323 --- /dev/null +++ b/components/feeds/BrowserFeeds.manifest @@ -0,0 +1,28 @@ +# WebappRT doesn't need these instructions, and they don't necessarily work +# with it, but it does use a GRE directory that the GRE shares with Firefox, +# so in order to prevent the instructions from being processed for WebappRT, +# we need to restrict them to the applications that depend on them, i.e.: +# +# b2g: {3c2e2abc-06d4-11e1-ac3b-374f68613e61} +# browser: {8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4} +# mobile/android: {aa3c5121-dab2-40e2-81ca-7ea25febc110} +# mobile/xul: {a23983c0-fd0e-11dc-95ff-0800200c9a66} +# +# In theory we should do this for all these instructions, but in practice it is +# sufficient to do it for the app-startup one, and the file is simpler that way. + +component {229fa115-9412-4d32-baf3-2fc407f76fb1} FeedConverter.js +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.feed&to=*/* {229fa115-9412-4d32-baf3-2fc407f76fb1} +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.video.feed&to=*/* {229fa115-9412-4d32-baf3-2fc407f76fb1} +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.audio.feed&to=*/* {229fa115-9412-4d32-baf3-2fc407f76fb1} +component {2376201c-bbc6-472f-9b62-7548040a61c6} FeedConverter.js +contract @mozilla.org/browser/feeds/result-service;1 {2376201c-bbc6-472f-9b62-7548040a61c6} +component {4f91ef2e-57ba-472e-ab7a-b4999e42d6c0} FeedConverter.js +contract @mozilla.org/network/protocol;1?name=feed {4f91ef2e-57ba-472e-ab7a-b4999e42d6c0} +component {1c31ed79-accd-4b94-b517-06e0c81999d5} FeedConverter.js +contract @mozilla.org/network/protocol;1?name=pcast {1c31ed79-accd-4b94-b517-06e0c81999d5} +component {49bb6593-3aff-4eb3-a068-2712c28bd58e} FeedWriter.js +contract @mozilla.org/browser/feeds/result-writer;1 {49bb6593-3aff-4eb3-a068-2712c28bd58e} +component {792a7e82-06a0-437c-af63-b2d12e808acc} WebContentConverter.js +contract @mozilla.org/embeddor.implemented/web-content-handler-registrar;1 {792a7e82-06a0-437c-af63-b2d12e808acc} +category app-startup WebContentConverter service,@mozilla.org/embeddor.implemented/web-content-handler-registrar;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} diff --git a/components/feeds/FeedConverter.js b/components/feeds/FeedConverter.js new file mode 100644 index 0000000..d0f5737 --- /dev/null +++ b/components/feeds/FeedConverter.js @@ -0,0 +1,591 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/debug.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +function LOG(str) { + dump("*** " + str + "\n"); +} + +const FS_CONTRACTID = "@mozilla.org/browser/feeds/result-service;1"; +const FPH_CONTRACTID = "@mozilla.org/network/protocol;1?name=feed"; +const PCPH_CONTRACTID = "@mozilla.org/network/protocol;1?name=pcast"; + +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; +const TYPE_ANY = "*/*"; + +const PREF_SELECTED_APP = "browser.feeds.handlers.application"; +const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_SELECTED_READER = "browser.feeds.handler.default"; + +const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application"; +const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; +const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler"; +const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default"; + +const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application"; +const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; +const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler"; +const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default"; + +function getPrefAppForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_APP; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_APP; + + default: + return PREF_SELECTED_APP; + } +} + +function getPrefWebForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_WEB; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_WEB; + + default: + return PREF_SELECTED_WEB; + } +} + +function getPrefActionForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_ACTION; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_ACTION; + + default: + return PREF_SELECTED_ACTION; + } +} + +function getPrefReaderForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_READER; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_READER; + + default: + return PREF_SELECTED_READER; + } +} + +function safeGetCharPref(pref, defaultValue) { + var prefs = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + try { + return prefs.getCharPref(pref); + } + catch (e) { + } + return defaultValue; +} + +function FeedConverter() { +} +FeedConverter.prototype = { + classID: Components.ID("{229fa115-9412-4d32-baf3-2fc407f76fb1}"), + + /** + * This is the downloaded text data for the feed. + */ + _data: null, + + /** + * This is the object listening to the conversion, which is ultimately the + * docshell for the load. + */ + _listener: null, + + /** + * Records if the feed was sniffed + */ + _sniffed: false, + + /** + * See nsIStreamConverter.idl + */ + convert: function FC_convert(sourceStream, sourceType, destinationType, + context) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * See nsIStreamConverter.idl + */ + asyncConvertData: function FC_asyncConvertData(sourceType, destinationType, + listener, context) { + this._listener = listener; + }, + + /** + * Whether or not the preview page is being forced. + */ + _forcePreviewPage: false, + + /** + * Release our references to various things once we're done using them. + */ + _releaseHandles: function FC__releaseHandles() { + this._listener = null; + this._request = null; + this._processor = null; + }, + + /** + * See nsIFeedResultListener.idl + */ + handleResult: function FC_handleResult(result) { + // Feeds come in various content types, which our feed sniffer coerces to + // the maybe.feed type. However, feeds are used as a transport for + // different data types, e.g. news/blogs (traditional feed), video/audio + // (podcasts) and photos (photocasts, photostreams). Each of these is + // different in that there's a different class of application suitable for + // handling feeds of that type, but without a content-type differentiation + // it is difficult for us to disambiguate. + // + // The other problem is that if the user specifies an auto-action handler + // for one feed application, the fact that the content type is shared means + // that all other applications will auto-load with that handler too, + // regardless of the content-type. + // + // This means that content-type alone is not enough to determine whether + // or not a feed should be auto-handled. This means that for feeds we need + // to always use this stream converter, even when an auto-action is + // specified, not the basic one provided by WebContentConverter. This + // converter needs to consume all of the data and parse it, and based on + // that determination make a judgment about type. + // + // Since there are no content types for this content, and I'm not going to + // invent any, the upshot is that while a user can set an auto-handler for + // generic feed content, the system will prevent them from setting an auto- + // handler for other stream types. In those cases, the user will always see + // the preview page and have to select a handler. We can guess and show + // a client handler, but will not be able to show web handlers for those + // types. + // + // If this is just a feed, not some kind of specialized application, then + // auto-handlers can be set and we should obey them. + try { + var feedService = + Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + if (!this._forcePreviewPage && result.doc) { + var feed = result.doc.QueryInterface(Ci.nsIFeed); + var handler = safeGetCharPref(getPrefActionForType(feed.type), "ask"); + + if (handler != "ask") { + if (handler == "reader") + handler = safeGetCharPref(getPrefReaderForType(feed.type), "bookmarks"); + switch (handler) { + case "web": + var wccr = + Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + if ((feed.type == Ci.nsIFeed.TYPE_FEED && + wccr.getAutoHandler(TYPE_MAYBE_FEED)) || + (feed.type == Ci.nsIFeed.TYPE_VIDEO && + wccr.getAutoHandler(TYPE_MAYBE_VIDEO_FEED)) || + (feed.type == Ci.nsIFeed.TYPE_AUDIO && + wccr.getAutoHandler(TYPE_MAYBE_AUDIO_FEED))) { + wccr.loadPreferredHandler(this._request); + return; + } + break; + + default: + LOG("unexpected handler: " + handler); + // fall through -- let feed service handle error + case "bookmarks": + case "client": + try { + var title = feed.title ? feed.title.plainText() : ""; + var desc = feed.subtitle ? feed.subtitle.plainText() : ""; + feedService.addToClientReader(result.uri.spec, title, desc, feed.type); + return; + } catch(ex) { /* fallback to preview mode */ } + } + } + } + + var ios = + Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var chromeChannel; + + // handling a redirect, hence forwarding the loadInfo from the old channel + // to the newchannel. + var oldChannel = this._request.QueryInterface(Ci.nsIChannel); + var loadInfo = oldChannel.loadInfo; + + // If there was no automatic handler, or this was a podcast, + // photostream or some other kind of application, show the preview page + // if the parser returned a document. + if (result.doc) { + + // Store the result in the result service so that the display + // page can access it. + feedService.addFeedResult(result); + + // Now load the actual XUL document. + var aboutFeedsURI = ios.newURI("about:feeds", null, null); + chromeChannel = ios.newChannelFromURIWithLoadInfo(aboutFeedsURI, loadInfo); + chromeChannel.originalURI = result.uri; + chromeChannel.owner = + Services.scriptSecurityManager.getNoAppCodebasePrincipal(aboutFeedsURI); + } else { + chromeChannel = ios.newChannelFromURIWithLoadInfo(result.uri, loadInfo); + } + + chromeChannel.loadGroup = this._request.loadGroup; + chromeChannel.asyncOpen2(this._listener); + } + finally { + this._releaseHandles(); + } + }, + + /** + * See nsIStreamListener.idl + */ + onDataAvailable: function FC_onDataAvailable(request, context, inputStream, + sourceOffset, count) { + if (this._processor) + this._processor.onDataAvailable(request, context, inputStream, + sourceOffset, count); + }, + + /** + * See nsIRequestObserver.idl + */ + onStartRequest: function FC_onStartRequest(request, context) { + var channel = request.QueryInterface(Ci.nsIChannel); + + // Check for a header that tells us there was no sniffing + // The value doesn't matter. + try { + var httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + // Make sure to check requestSucceeded before the potentially-throwing + // getResponseHeader. + if (!httpChannel.requestSucceeded) { + // Just give up, but don't forget to cancel the channel first! + request.cancel(Cr.NS_BINDING_ABORTED); + return; + } + var noSniff = httpChannel.getResponseHeader("X-Moz-Is-Feed"); + } + catch (ex) { + this._sniffed = true; + } + + this._request = request; + + // Save and reset the forced state bit early, in case there's some kind of + // error. + var feedService = + Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + this._forcePreviewPage = feedService.forcePreviewPage; + feedService.forcePreviewPage = false; + + // Parse feed data as it comes in + this._processor = + Cc["@mozilla.org/feed-processor;1"]. + createInstance(Ci.nsIFeedProcessor); + this._processor.listener = this; + this._processor.parseAsync(null, channel.URI); + + this._processor.onStartRequest(request, context); + }, + + /** + * See nsIRequestObserver.idl + */ + onStopRequest: function FC_onStopRequest(request, context, status) { + if (this._processor) + this._processor.onStopRequest(request, context, status); + }, + + /** + * See nsISupports.idl + */ + QueryInterface: function FC_QueryInterface(iid) { + if (iid.equals(Ci.nsIFeedResultListener) || + iid.equals(Ci.nsIStreamConverter) || + iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver)|| + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, +}; + +/** + * Keeps parsed FeedResults around for use elsewhere in the UI after the stream + * converter completes. + */ +function FeedResultService() { +} + +FeedResultService.prototype = { + classID: Components.ID("{2376201c-bbc6-472f-9b62-7548040a61c6}"), + + /** + * A URI spec -> [nsIFeedResult] hash. We have to keep a list as the + * value in case the same URI is requested concurrently. + */ + _results: { }, + + /** + * See nsIFeedResultService.idl + */ + forcePreviewPage: false, + + /** + * See nsIFeedResultService.idl + */ + addToClientReader: function FRS_addToClientReader(spec, title, subtitle, feedType) { + var prefs = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + + var handler = safeGetCharPref(getPrefActionForType(feedType), "bookmarks"); + if (handler == "ask" || handler == "reader") + handler = safeGetCharPref(getPrefReaderForType(feedType), "bookmarks"); + + switch (handler) { + case "client": + var clientApp = prefs.getComplexValue(getPrefAppForType(feedType), Ci.nsILocalFile); + + // For the benefit of applications that might know how to deal with more + // URLs than just feeds, send feed: URLs in the following format: + // + // http urls: replace scheme with feed, e.g. + // http://foo.com/index.rdf -> feed://foo.com/index.rdf + // other urls: prepend feed: scheme, e.g. + // https://foo.com/index.rdf -> feed:https://foo.com/index.rdf + var ios = + Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var feedURI = ios.newURI(spec, null, null); + if (feedURI.schemeIs("http")) { + feedURI.scheme = "feed"; + spec = feedURI.spec; + } + else + spec = "feed:" + spec; + + // Retrieving the shell service might fail on some systems, most + // notably systems where GNOME is not installed. + try { + var ss = + Cc["@mozilla.org/browser/shell-service;1"]. + getService(Ci.nsIShellService); + ss.openApplicationWithURI(clientApp, spec); + } catch(e) { + // If we couldn't use the shell service, fallback to using a + // nsIProcess instance + var p = + Cc["@mozilla.org/process/util;1"]. + createInstance(Ci.nsIProcess); + p.init(clientApp); + p.run(false, [spec], 1); + } + break; + + default: + // "web" should have been handled elsewhere + LOG("unexpected handler: " + handler); + // fall through + case "bookmarks": + var wm = + Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + var topWindow = wm.getMostRecentWindow("navigator:browser"); + topWindow.PlacesCommandHook.addLiveBookmark(spec, title, subtitle); + break; + } + }, + + /** + * See nsIFeedResultService.idl + */ + addFeedResult: function FRS_addFeedResult(feedResult) { + NS_ASSERT(feedResult.uri != null, "null URI!"); + NS_ASSERT(feedResult.uri != null, "null feedResult!"); + var spec = feedResult.uri.spec; + if(!this._results[spec]) + this._results[spec] = []; + this._results[spec].push(feedResult); + }, + + /** + * See nsIFeedResultService.idl + */ + getFeedResult: function RFS_getFeedResult(uri) { + NS_ASSERT(uri != null, "null URI!"); + var resultList = this._results[uri.spec]; + for (var i in resultList) { + if (resultList[i].uri == uri) + return resultList[i]; + } + return null; + }, + + /** + * See nsIFeedResultService.idl + */ + removeFeedResult: function FRS_removeFeedResult(uri) { + NS_ASSERT(uri != null, "null URI!"); + var resultList = this._results[uri.spec]; + if (!resultList) + return; + var deletions = 0; + for (var i = 0; i < resultList.length; ++i) { + if (resultList[i].uri == uri) { + delete resultList[i]; + ++deletions; + } + } + + // send the holes to the end + resultList.sort(); + // and trim the list + resultList.splice(resultList.length - deletions, deletions); + if (resultList.length == 0) + delete this._results[uri.spec]; + }, + + createInstance: function FRS_createInstance(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + + QueryInterface: function FRS_QueryInterface(iid) { + if (iid.equals(Ci.nsIFeedResultService) || + iid.equals(Ci.nsIFactory) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, +}; + +/** + * A protocol handler that attempts to deal with the variant forms of feed: + * URIs that are actually either http or https. + */ +function GenericProtocolHandler() { +} +GenericProtocolHandler.prototype = { + _init: function GPH_init(scheme) { + var ios = + Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + this._http = ios.getProtocolHandler("http"); + this._scheme = scheme; + }, + + get scheme() { + return this._scheme; + }, + + get protocolFlags() { + return this._http.protocolFlags; + }, + + get defaultPort() { + return this._http.defaultPort; + }, + + allowPort: function GPH_allowPort(port, scheme) { + return this._http.allowPort(port, scheme); + }, + + newURI: function GPH_newURI(spec, originalCharset, baseURI) { + // Feed URIs can be either nested URIs of the form feed:realURI (in which + // case we create a nested URI for the realURI) or feed://example.com, in + // which case we create a nested URI for the real protocol which is http. + + var scheme = this._scheme + ":"; + if (spec.substr(0, scheme.length) != scheme) + throw Cr.NS_ERROR_MALFORMED_URI; + + var prefix = spec.substr(scheme.length, 2) == "//" ? "http:" : ""; + var inner = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService).newURI(spec.replace(scheme, prefix), + originalCharset, baseURI); + var netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil); + const URI_INHERITS_SECURITY_CONTEXT = Ci.nsIProtocolHandler + .URI_INHERITS_SECURITY_CONTEXT; + if (netutil.URIChainHasFlags(inner, URI_INHERITS_SECURITY_CONTEXT)) + throw Cr.NS_ERROR_MALFORMED_URI; + + var uri = netutil.newSimpleNestedURI(inner); + uri.spec = inner.spec.replace(prefix, scheme); + return uri; + }, + + newChannel2: function GPH_newChannel(aUri, aLoadInfo) { + var inner = aUri.QueryInterface(Ci.nsINestedURI).innerURI; + var channel = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService). + newChannelFromURIWithLoadInfo(inner, aLoadInfo); + + if (channel instanceof Components.interfaces.nsIHttpChannel) + // Set this so we know this is supposed to be a feed + channel.setRequestHeader("X-Moz-Is-Feed", "1", false); + channel.originalURI = aUri; + return channel; + }, + + + QueryInterface: function GPH_QueryInterface(iid) { + if (iid.equals(Ci.nsIProtocolHandler) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +function FeedProtocolHandler() { + this._init('feed'); +} +FeedProtocolHandler.prototype = new GenericProtocolHandler(); +FeedProtocolHandler.prototype.classID = Components.ID("{4f91ef2e-57ba-472e-ab7a-b4999e42d6c0}"); + +function PodCastProtocolHandler() { + this._init('pcast'); +} +PodCastProtocolHandler.prototype = new GenericProtocolHandler(); +PodCastProtocolHandler.prototype.classID = Components.ID("{1c31ed79-accd-4b94-b517-06e0c81999d5}"); + +var components = [FeedConverter, + FeedResultService, + FeedProtocolHandler, + PodCastProtocolHandler]; + + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/components/feeds/FeedWriter.js b/components/feeds/FeedWriter.js new file mode 100644 index 0000000..facde58 --- /dev/null +++ b/components/feeds/FeedWriter.js @@ -0,0 +1,1397 @@ +# -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +# 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/. + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +const FEEDWRITER_CID = Components.ID("{49bb6593-3aff-4eb3-a068-2712c28bd58e}"); +const FEEDWRITER_CONTRACTID = "@mozilla.org/browser/feeds/result-writer;1"; + +function LOG(str) { + var prefB = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + + var shouldLog = prefB.getBoolPref("feeds.log", false); + + if (shouldLog) + dump("*** Feeds: " + str + "\n"); +} + +/** + * Wrapper function for nsIIOService::newURI. + * @param aURLSpec + * The URL string from which to create an nsIURI. + * @returns an nsIURI object, or null if the creation of the URI failed. + */ +function makeURI(aURLSpec, aCharset) { + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + try { + return ios.newURI(aURLSpec, aCharset, null); + } catch (ex) { } + + return null; +} + +const XML_NS = "http://www.w3.org/XML/1998/namespace"; +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; +const URI_BUNDLE = "chrome://browser/locale/feeds/subscribe.properties"; + +const PREF_SELECTED_APP = "browser.feeds.handlers.application"; +const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_SELECTED_READER = "browser.feeds.handler.default"; + +const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application"; +const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; +const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler"; +const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default"; + +const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application"; +const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; +const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler"; +const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default"; + +const PREF_SHOW_FIRST_RUN_UI = "browser.feeds.showFirstRunUI"; + +const TITLE_ID = "feedTitleText"; +const SUBTITLE_ID = "feedSubtitleText"; + +function getPrefAppForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_APP; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_APP; + + default: + return PREF_SELECTED_APP; + } +} + +function getPrefWebForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_WEB; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_WEB; + + default: + return PREF_SELECTED_WEB; + } +} + +function getPrefActionForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_ACTION; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_ACTION; + + default: + return PREF_SELECTED_ACTION; + } +} + +function getPrefReaderForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_READER; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_READER; + + default: + return PREF_SELECTED_READER; + } +} + +/** + * Converts a number of bytes to the appropriate unit that results in a + * number that needs fewer than 4 digits + * + * @return a pair: [new value with 3 sig. figs., its unit] + */ +function convertByteUnits(aBytes) { + var units = ["bytes", "kilobyte", "megabyte", "gigabyte"]; + let unitIndex = 0; + + // convert to next unit if it needs 4 digits (after rounding), but only if + // we know the name of the next unit + while ((aBytes >= 999.5) && (unitIndex < units.length - 1)) { + aBytes /= 1024; + unitIndex++; + } + + // Get rid of insignificant bits by truncating to 1 or 0 decimal points + // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235 + aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) ? 1 : 0); + + return [aBytes, units[unitIndex]]; +} + +function FeedWriter() {} +FeedWriter.prototype = { + _mimeSvc : Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService), + + _getPropertyAsBag: function FW__getPropertyAsBag(container, property) { + return container.fields.getProperty(property). + QueryInterface(Ci.nsIPropertyBag2); + }, + + _getPropertyAsString: function FW__getPropertyAsString(container, property) { + try { + return container.fields.getPropertyAsAString(property); + } + catch (e) { + } + return ""; + }, + + _setContentText: function FW__setContentText(id, text) { + this._contentSandbox.element = this._document.getElementById(id); + this._contentSandbox.textNode = text.createDocumentFragment(this._contentSandbox.element); + var codeStr = + "while (element.hasChildNodes()) " + + " element.removeChild(element.firstChild);" + + "element.appendChild(textNode);"; + if (text.base) { + this._contentSandbox.spec = text.base.spec; + codeStr += "element.setAttributeNS('" + XML_NS + "', 'base', spec);"; + } + Cu.evalInSandbox(codeStr, this._contentSandbox); + this._contentSandbox.element = null; + this._contentSandbox.textNode = null; + }, + + /** + * Safely sets the href attribute on an anchor tag, providing the URI + * specified can be loaded according to rules. + * @param element + * The element to set a URI attribute on + * @param attribute + * The attribute of the element to set the URI to, e.g. href or src + * @param uri + * The URI spec to set as the href + */ + _safeSetURIAttribute: + function FW__safeSetURIAttribute(element, attribute, uri) { + var secman = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + const flags = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; + try { + secman.checkLoadURIStrWithPrincipal(this._feedPrincipal, uri, flags); + // checkLoadURIStrWithPrincipal will throw if the link URI should not be + // loaded, either because our feedURI isn't allowed to load it or per + // the rules specified in |flags|, so we'll never "linkify" the link... + } + catch (e) { + // Not allowed to load this link because secman.checkLoadURIStr threw + return; + } + + this._contentSandbox.element = element; + this._contentSandbox.uri = uri; + var codeStr = "element.setAttribute('" + attribute + "', uri);"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + }, + + /** + * Use this sandbox to run any dom manipulation code on nodes which + * are already inserted into the content document. + */ + __contentSandbox: null, + get _contentSandbox() { + // This whole sandbox setup is totally archaic. It was introduced in bug + // 360529, presumably before the existence of a solid security membrane, + // since all of the manipulation of content here should be made safe by + // Xrays. + // Now that anonymous content is no longer content-accessible, manipulating + // the xml stylesheet content can't be done from content anymore. + // + // The right solution would be to rip out all of this sandbox junk and + // manipulate the DOM directly, but that would require a lot of rewriting. + // So, for now, we just give the sandbox an nsExpandedPrincipal with []. + // This has the effect of giving it Xrays, and making it same-origin with + // the XBL scope, thereby letting it manipulate anonymous content. + if (!this.__contentSandbox) + this.__contentSandbox = new Cu.Sandbox([this._window], + {sandboxName: 'FeedWriter'}); + + return this.__contentSandbox; + }, + + /** + * Calls doCommand for a given XUL element within the context of the + * content document. + * + * @param aElement + * the XUL element to call doCommand() on. + */ + _safeDoCommand: function FW___safeDoCommand(aElement) { + this._contentSandbox.element = aElement; + Cu.evalInSandbox("element.doCommand();", this._contentSandbox); + this._contentSandbox.element = null; + }, + + __faviconService: null, + get _faviconService() { + if (!this.__faviconService) + this.__faviconService = Cc["@mozilla.org/browser/favicon-service;1"]. + getService(Ci.nsIFaviconService); + + return this.__faviconService; + }, + + __bundle: null, + get _bundle() { + if (!this.__bundle) { + this.__bundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(URI_BUNDLE); + } + return this.__bundle; + }, + + _getFormattedString: function FW__getFormattedString(key, params) { + return this._bundle.formatStringFromName(key, params, params.length); + }, + + _getString: function FW__getString(key) { + return this._bundle.GetStringFromName(key); + }, + + /* Magic helper methods to be used instead of xbl properties */ + _getSelectedItemFromMenulist: function FW__getSelectedItemFromList(aList) { + var node = aList.firstChild.firstChild; + while (node) { + if (node.localName == "menuitem" && node.getAttribute("selected") == "true") + return node; + + node = node.nextSibling; + } + + return null; + }, + + _setCheckboxCheckedState: function FW__setCheckboxCheckedState(aCheckbox, aValue) { + // see checkbox.xml, xbl bindings are not applied within the sandbox! + this._contentSandbox.checkbox = aCheckbox; + var codeStr; + var change = (aValue != (aCheckbox.getAttribute('checked') == 'true')); + if (aValue) + codeStr = "checkbox.setAttribute('checked', 'true'); "; + else + codeStr = "checkbox.removeAttribute('checked'); "; + + if (change) { + this._contentSandbox.document = this._document; + codeStr += "var event = document.createEvent('Events'); " + + "event.initEvent('CheckboxStateChange', true, true);" + + "checkbox.dispatchEvent(event);" + } + + Cu.evalInSandbox(codeStr, this._contentSandbox); + }, + + /** + * Returns a date suitable for displaying in the feed preview. + * If the date cannot be parsed, the return value is "false". + * @param dateString + * A date as extracted from a feed entry. (entry.updated) + */ + _parseDate: function FW__parseDate(dateString) { + // Convert the date into the user's local time zone + dateObj = new Date(dateString); + + // Make sure the date we're given is valid. + if (!dateObj.getTime()) + return false; + + var dateService = Cc["@mozilla.org/intl/scriptabledateformat;1"]. + getService(Ci.nsIScriptableDateFormat); + return dateService.FormatDateTime("", dateService.dateFormatLong, dateService.timeFormatNoSeconds, + dateObj.getFullYear(), dateObj.getMonth()+1, dateObj.getDate(), + dateObj.getHours(), dateObj.getMinutes(), dateObj.getSeconds()); + }, + + /** + * Returns the feed type. + */ + __feedType: null, + _getFeedType: function FW__getFeedType() { + if (this.__feedType != null) + return this.__feedType; + + try { + // grab the feed because it's got the feed.type in it. + var container = this._getContainer(); + var feed = container.QueryInterface(Ci.nsIFeed); + this.__feedType = feed.type; + return feed.type; + } catch (ex) { } + + return Ci.nsIFeed.TYPE_FEED; + }, + + /** + * Maps a feed type to a maybe-feed mimetype. + */ + _getMimeTypeForFeedType: function FW__getMimeTypeForFeedType() { + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + return TYPE_MAYBE_VIDEO_FEED; + + case Ci.nsIFeed.TYPE_AUDIO: + return TYPE_MAYBE_AUDIO_FEED; + + default: + return TYPE_MAYBE_FEED; + } + }, + + /** + * Writes the feed title into the preview document. + * @param container + * The feed container + */ + _setTitleText: function FW__setTitleText(container) { + if (container.title) { + var title = container.title.plainText(); + this._setContentText(TITLE_ID, container.title); + this._contentSandbox.document = this._document; + this._contentSandbox.title = title; + var codeStr = "document.title = title;" + Cu.evalInSandbox(codeStr, this._contentSandbox); + } + + var feed = container.QueryInterface(Ci.nsIFeed); + if (feed && feed.subtitle) + this._setContentText(SUBTITLE_ID, container.subtitle); + }, + + /** + * Writes the title image into the preview document if one is present. + * @param container + * The feed container + */ + _setTitleImage: function FW__setTitleImage(container) { + try { + var parts = container.image; + + // Set up the title image (supplied by the feed) + var feedTitleImage = this._document.getElementById("feedTitleImage"); + this._safeSetURIAttribute(feedTitleImage, "src", + parts.getPropertyAsAString("url")); + + // Set up the title image link + var feedTitleLink = this._document.getElementById("feedTitleLink"); + + var titleText = this._getFormattedString("linkTitleTextFormat", + [parts.getPropertyAsAString("title")]); + this._contentSandbox.feedTitleLink = feedTitleLink; + this._contentSandbox.titleText = titleText; + this._contentSandbox.feedTitleText = this._document.getElementById("feedTitleText"); + this._contentSandbox.titleImageWidth = parseInt(parts.getPropertyAsAString("width")) + 15; + + // Fix the margin on the main title, so that the image doesn't run over + // the underline + var codeStr = "feedTitleLink.setAttribute('title', titleText); " + + "feedTitleText.style.marginRight = titleImageWidth + 'px';"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + this._contentSandbox.feedTitleLink = null; + this._contentSandbox.titleText = null; + this._contentSandbox.feedTitleText = null; + this._contentSandbox.titleImageWidth = null; + + this._safeSetURIAttribute(feedTitleLink, "href", + parts.getPropertyAsAString("link")); + } + catch (e) { + LOG("Failed to set Title Image (this is benign): " + e); + } + }, + + /** + * Writes all entries contained in the feed. + * @param container + * The container of entries in the feed + */ + _writeFeedContent: function FW__writeFeedContent(container) { + // Build the actual feed content + var feed = container.QueryInterface(Ci.nsIFeed); + if (feed.items.length == 0) + return; + + this._contentSandbox.feedContent = + this._document.getElementById("feedContent"); + + for (var i = 0; i < feed.items.length; ++i) { + var entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry); + entry.QueryInterface(Ci.nsIFeedContainer); + + var entryContainer = this._document.createElementNS(HTML_NS, "div"); + entryContainer.className = "entry"; + + // If the entry has a title, make it a link + if (entry.title) { + var a = this._document.createElementNS(HTML_NS, "a"); + var span = this._document.createElementNS(HTML_NS, "span"); + a.appendChild(span); + if (entry.title.base) + span.setAttributeNS(XML_NS, "base", entry.title.base.spec); + span.appendChild(entry.title.createDocumentFragment(a)); + + // Entries are not required to have links, so entry.link can be null. + if (entry.link) + this._safeSetURIAttribute(a, "href", entry.link.spec); + + var title = this._document.createElementNS(HTML_NS, "h3"); + title.appendChild(a); + + var lastUpdated = this._parseDate(entry.updated); + if (lastUpdated) { + var dateDiv = this._document.createElementNS(HTML_NS, "div"); + dateDiv.className = "lastUpdated"; + dateDiv.textContent = lastUpdated; + title.appendChild(dateDiv); + } + + entryContainer.appendChild(title); + } + + var body = this._document.createElementNS(HTML_NS, "div"); + var summary = entry.summary || entry.content; + var docFragment = null; + if (summary) { + if (summary.base) + body.setAttributeNS(XML_NS, "base", summary.base.spec); + else + LOG("no base?"); + docFragment = summary.createDocumentFragment(body); + if (docFragment) + body.appendChild(docFragment); + + // If the entry doesn't have a title, append a # permalink + // See http://scripting.com/rss.xml for an example + if (!entry.title && entry.link) { + var a = this._document.createElementNS(HTML_NS, "a"); + a.appendChild(this._document.createTextNode("#")); + this._safeSetURIAttribute(a, "href", entry.link.spec); + body.appendChild(this._document.createTextNode(" ")); + body.appendChild(a); + } + + } + body.className = "feedEntryContent"; + entryContainer.appendChild(body); + + if (entry.enclosures && entry.enclosures.length > 0) { + var enclosuresDiv = this._buildEnclosureDiv(entry); + entryContainer.appendChild(enclosuresDiv); + } + + this._contentSandbox.entryContainer = entryContainer; + this._contentSandbox.clearDiv = + this._document.createElementNS(HTML_NS, "div"); + this._contentSandbox.clearDiv.style.clear = "both"; + + var codeStr = "feedContent.appendChild(entryContainer); " + + "feedContent.appendChild(clearDiv);" + Cu.evalInSandbox(codeStr, this._contentSandbox); + } + + this._contentSandbox.feedContent = null; + this._contentSandbox.entryContainer = null; + this._contentSandbox.clearDiv = null; + }, + + /** + * Takes a url to a media item and returns the best name it can come up with. + * Frequently this is the filename portion (e.g. passing in + * http://example.com/foo.mpeg would return "foo.mpeg"), but in more complex + * cases, this will return the entire url (e.g. passing in + * http://example.com/somedirectory/ would return + * http://example.com/somedirectory/). + * @param aURL + * The URL string from which to create a display name + * @returns a string + */ + _getURLDisplayName: function FW__getURLDisplayName(aURL) { + var url = makeURI(aURL); + url.QueryInterface(Ci.nsIURL); + if (url == null || url.fileName.length == 0) + return decodeURIComponent(aURL); + + return decodeURIComponent(url.fileName); + }, + + /** + * Takes a FeedEntry with enclosures, generates the HTML code to represent + * them, and returns that. + * @param entry + * FeedEntry with enclosures + * @returns element + */ + _buildEnclosureDiv: function FW__buildEnclosureDiv(entry) { + var enclosuresDiv = this._document.createElementNS(HTML_NS, "div"); + enclosuresDiv.className = "enclosures"; + + enclosuresDiv.appendChild(this._document.createTextNode(this._getString("mediaLabel"))); + + var roundme = function(n) { + return (Math.round(n * 100) / 100).toLocaleString(); + } + + for (var i_enc = 0; i_enc < entry.enclosures.length; ++i_enc) { + var enc = entry.enclosures.queryElementAt(i_enc, Ci.nsIWritablePropertyBag2); + + if (!(enc.hasKey("url"))) + continue; + + var enclosureDiv = this._document.createElementNS(HTML_NS, "div"); + enclosureDiv.setAttribute("class", "enclosure"); + + var mozicon = "moz-icon://.txt?size=16"; + var type_text = null; + var size_text = null; + + if (enc.hasKey("type")) { + type_text = enc.get("type"); + try { + var handlerInfoWrapper = this._mimeSvc.getFromTypeAndExtension(enc.get("type"), null); + + if (handlerInfoWrapper) + type_text = handlerInfoWrapper.description; + + if (type_text && type_text.length > 0) + mozicon = "moz-icon://goat?size=16&contentType=" + enc.get("type"); + + } catch (ex) { } + + } + + if (enc.hasKey("length") && /^[0-9]+$/.test(enc.get("length"))) { + var enc_size = convertByteUnits(parseInt(enc.get("length"))); + + var size_text = this._getFormattedString("enclosureSizeText", + [enc_size[0], this._getString(enc_size[1])]); + } + + var iconimg = this._document.createElementNS(HTML_NS, "img"); + iconimg.setAttribute("src", mozicon); + iconimg.setAttribute("class", "type-icon"); + enclosureDiv.appendChild(iconimg); + + enclosureDiv.appendChild(this._document.createTextNode( " " )); + + var enc_href = this._document.createElementNS(HTML_NS, "a"); + enc_href.appendChild(this._document.createTextNode(this._getURLDisplayName(enc.get("url")))); + this._safeSetURIAttribute(enc_href, "href", enc.get("url")); + enclosureDiv.appendChild(enc_href); + + if (type_text && size_text) + enclosureDiv.appendChild(this._document.createTextNode( " (" + type_text + ", " + size_text + ")")); + + else if (type_text) + enclosureDiv.appendChild(this._document.createTextNode( " (" + type_text + ")")) + + else if (size_text) + enclosureDiv.appendChild(this._document.createTextNode( " (" + size_text + ")")) + + enclosuresDiv.appendChild(enclosureDiv); + } + + return enclosuresDiv; + }, + + /** + * Gets a valid nsIFeedContainer object from the parsed nsIFeedResult. + * Displays error information if there was one. + * @param result + * The parsed feed result + * @returns A valid nsIFeedContainer object containing the contents of + * the feed. + */ + _getContainer: function FW__getContainer(result) { + var feedService = + Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + + result = null; + try { + result = + feedService.getFeedResult(this._getOriginalURI(this._window)); + } + catch (e) { + // Ignore. + } + + if (!result) { + LOG("Subscribe Preview: feed not available?!"); + return null; + } + + if (result.bozo) { + LOG("Subscribe Preview: feed result is bozo?!"); + } + + try { + var container = result.doc; + } + catch (e) { + LOG("Subscribe Preview: no result.doc? Why didn't the original reload?"); + return null; + } + return container; + }, + + /** + * Get the human-readable display name of a file. This could be the + * application name. + * @param file + * A nsIFile to look up the name of + * @returns The display name of the application represented by the file. + */ + _getFileDisplayName: function FW__getFileDisplayName(file) { +#ifdef XP_WIN + if (file instanceof Ci.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } +#endif +#ifdef XP_MACOSX + if (file instanceof Ci.nsILocalFileMac) { + try { + return file.bundleDisplayName; + } catch (e) {} + } +#endif + return file.leafName; + }, + + /** + * Helper method to set the selected application and system default + * reader menuitems details from a file object + * @param aMenuItem + * The menuitem on which the attributes should be set + * @param aFile + * The menuitem's associated file + */ + _initMenuItemWithFile: function(aMenuItem, aFile) { + this._contentSandbox.menuitem = aMenuItem; + this._contentSandbox.label = this._getFileDisplayName(aFile); + // For security reasons, access to moz-icon:file://... URIs is + // no longer allowed (indirect file system access from content). + // We use a dummy application instead to get a generic icon. + this._contentSandbox.image = "moz-icon://dummy.exe?size=16"; + var codeStr = "menuitem.setAttribute('label', label); " + + "menuitem.setAttribute('image', image);" + Cu.evalInSandbox(codeStr, this._contentSandbox); + }, + + /** + * Helper method to get an element in the XBL binding where the handler + * selection UI lives + */ + _getUIElement: function FW__getUIElement(id) { + return this._document.getAnonymousElementByAttribute( + this._document.getElementById("feedSubscribeLine"), "anonid", id); + }, + + /** + * Displays a prompt from which the user may choose a (client) feed reader. + * @param aCallback the callback method, passes in true if a feed reader was + * selected, false otherwise. + */ + _chooseClientApp: function FW__chooseClientApp(aCallback) { + try { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == Ci.nsIFilePicker.returnOK) { + this._selectedApp = fp.file; + if (this._selectedApp) { + // XXXben - we need to compare this with the running instance + // executable just don't know how to do that via script + // XXXmano TBD: can probably add this to nsIShellService +#ifdef XP_WIN +#expand if (fp.file.leafName != "__MOZ_APP_NAME__.exe") { +#else +#ifdef XP_MACOSX +#expand if (fp.file.leafName != "__MOZ_MACBUNDLE_NAME__") { +#else +#expand if (fp.file.leafName != "__MOZ_APP_NAME__-bin") { +#endif +#endif + this._initMenuItemWithFile(this._contentSandbox.selectedAppMenuItem, + this._selectedApp); + + // Show and select the selected application menuitem + let codeStr = "selectedAppMenuItem.hidden = false;" + + "selectedAppMenuItem.doCommand();" + Cu.evalInSandbox(codeStr, this._contentSandbox); + if (aCallback) { + aCallback(true); + return; + } + } + } + } + if (aCallback) { + aCallback(false); + } + }.bind(this); + + fp.init(this._window, this._getString("chooseApplicationDialogTitle"), + Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + fp.open(fpCallback); + } catch(ex) { + } + }, + + _setAlwaysUseCheckedState: function FW__setAlwaysUseCheckedState(feedType) { + var checkbox = this._getUIElement("alwaysUse"); + if (checkbox) { + var alwaysUse = false; + try { + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + if (prefs.getCharPref(getPrefActionForType(feedType)) != "ask") + alwaysUse = true; + } + catch(ex) { } + this._setCheckboxCheckedState(checkbox, alwaysUse); + } + }, + + _setSubscribeUsingLabel: function FW__setSubscribeUsingLabel() { + var stringLabel = "subscribeFeedUsing"; + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + stringLabel = "subscribeVideoPodcastUsing"; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + stringLabel = "subscribeAudioPodcastUsing"; + break; + } + + this._contentSandbox.subscribeUsing = + this._getUIElement("subscribeUsingDescription"); + this._contentSandbox.label = this._getString(stringLabel); + var codeStr = "subscribeUsing.setAttribute('value', label);" + Cu.evalInSandbox(codeStr, this._contentSandbox); + }, + + _setAlwaysUseLabel: function FW__setAlwaysUseLabel() { + var checkbox = this._getUIElement("alwaysUse"); + if (checkbox) { + if (this._handlersMenuList) { + var handlerName = this._getSelectedItemFromMenulist(this._handlersMenuList) + .getAttribute("label"); + var stringLabel = "alwaysUseForFeeds"; + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + stringLabel = "alwaysUseForVideoPodcasts"; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + stringLabel = "alwaysUseForAudioPodcasts"; + break; + } + + this._contentSandbox.checkbox = checkbox; + this._contentSandbox.label = this._getFormattedString(stringLabel, [handlerName]); + + var codeStr = "checkbox.setAttribute('label', label);"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + } + } + }, + + // nsIDomEventListener + handleEvent: function(event) { + if (event.target.ownerDocument != this._document) { + LOG("FeedWriter.handleEvent: Someone passed the feed writer as a listener to the events of another document!"); + return; + } + + if (event.type == "command") { + switch (event.target.getAttribute("anonid")) { + case "subscribeButton": + this.subscribe(); + break; + case "chooseApplicationMenuItem": + /* Bug 351263: Make sure to not steal focus if the "Choose + * Application" item is being selected with the keyboard. We do this + * by ignoring command events while the dropdown is closed (user + * arrowing through the combobox), but handling them while the + * combobox dropdown is open (user pressed enter when an item was + * selected). If we don't show the filepicker here, it will be shown + * when clicking "Subscribe Now". + */ + var popupbox = this._handlersMenuList.firstChild.boxObject; + if (popupbox.popupState == "hiding") { + this._chooseClientApp(function(aResult) { + if (!aResult) { + // Select the (per-prefs) selected handler if no application + // was selected + this._setSelectedHandler(this._getFeedType()); + } + }.bind(this)); + } + break; + default: + this._setAlwaysUseLabel(); + } + } + }, + + _setSelectedHandler: function FW__setSelectedHandler(feedType) { + var prefs = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + + var handler = prefs.getCharPref(getPrefReaderForType(feedType), "bookmarks"); + + switch (handler) { + case "web": { + if (this._handlersMenuList) { + var url; + try { + url = prefs.getComplexValue(getPrefWebForType(feedType), Ci.nsISupportsString).data; + } catch (ex) { + LOG("FeedWriter._setSelectedHandler: invalid or no handler in prefs"); + return; + } + var handlers = + this._handlersMenuList.getElementsByAttribute("webhandlerurl", url); + if (handlers.length == 0) { + LOG("FeedWriter._setSelectedHandler: selected web handler isn't in the menulist") + return; + } + + this._safeDoCommand(handlers[0]); + } + break; + } + case "client": { + try { + this._selectedApp = + prefs.getComplexValue(getPrefAppForType(feedType), Ci.nsILocalFile); + } + catch(ex) { + this._selectedApp = null; + } + + if (this._selectedApp) { + this._initMenuItemWithFile(this._contentSandbox.selectedAppMenuItem, + this._selectedApp); + var codeStr = "selectedAppMenuItem.hidden = false; " + + "selectedAppMenuItem.doCommand(); "; + + // Only show the default reader menuitem if the default reader + // isn't the selected application + if (this._defaultSystemReader) { + var shouldHide = + this._defaultSystemReader.path == this._selectedApp.path; + codeStr += "defaultHandlerMenuItem.hidden = " + shouldHide + ";" + } + Cu.evalInSandbox(codeStr, this._contentSandbox); + break; + } + } + case "bookmarks": + default: { + var liveBookmarksMenuItem = this._getUIElement("liveBookmarksMenuItem"); + if (liveBookmarksMenuItem) + this._safeDoCommand(liveBookmarksMenuItem); + } + } + }, + + _initSubscriptionUI: function FW__initSubscriptionUI() { + var handlersMenuPopup = this._getUIElement("handlersMenuPopup"); + if (!handlersMenuPopup) + return; + + var feedType = this._getFeedType(); + var codeStr; + + // change the background + var header = this._document.getElementById("feedHeader"); + this._contentSandbox.header = header; + switch (feedType) { + case Ci.nsIFeed.TYPE_VIDEO: + codeStr = "header.className = 'videoPodcastBackground'; "; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + codeStr = "header.className = 'audioPodcastBackground'; "; + break; + + default: + codeStr = "header.className = 'feedBackground'; "; + } + + var liveBookmarksMenuItem = this._getUIElement("liveBookmarksMenuItem"); + + // Last-selected application + var menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("anonid", "selectedAppMenuItem"); + menuItem.className = "menuitem-iconic selectedAppMenuItem"; + menuItem.setAttribute("handlerType", "client"); + try { + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + this._selectedApp = prefs.getComplexValue(getPrefAppForType(feedType), + Ci.nsILocalFile); + + if (this._selectedApp.exists()) + this._initMenuItemWithFile(menuItem, this._selectedApp); + else { + // Hide the menuitem if the last selected application doesn't exist + menuItem.setAttribute("hidden", true); + } + } + catch(ex) { + // Hide the menuitem until an application is selected + menuItem.setAttribute("hidden", true); + } + this._contentSandbox.handlersMenuPopup = handlersMenuPopup; + this._contentSandbox.selectedAppMenuItem = menuItem; + + codeStr += "handlersMenuPopup.appendChild(selectedAppMenuItem); "; + + // List the default feed reader + try { + this._defaultSystemReader = Cc["@mozilla.org/browser/shell-service;1"]. + getService(Ci.nsIShellService). + defaultFeedReader; + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("anonid", "defaultHandlerMenuItem"); + menuItem.className = "menuitem-iconic defaultHandlerMenuItem"; + menuItem.setAttribute("handlerType", "client"); + + this._initMenuItemWithFile(menuItem, this._defaultSystemReader); + + // Hide the default reader item if it points to the same application + // as the last-selected application + if (this._selectedApp && + this._selectedApp.path == this._defaultSystemReader.path) + menuItem.hidden = true; + } + catch(ex) { menuItem = null; /* no default reader */ } + + if (menuItem) { + this._contentSandbox.defaultHandlerMenuItem = menuItem; + codeStr += "handlersMenuPopup.appendChild(defaultHandlerMenuItem); "; + } + + // "Choose Application..." menuitem + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("anonid", "chooseApplicationMenuItem"); + menuItem.className = "menuitem-iconic chooseApplicationMenuItem"; + menuItem.setAttribute("label", this._getString("chooseApplicationMenuItem")); + + this._contentSandbox.chooseAppMenuItem = menuItem; + codeStr += "handlersMenuPopup.appendChild(chooseAppMenuItem); "; + + // separator + this._contentSandbox.chooseAppSep = + menuItem = liveBookmarksMenuItem.nextSibling.cloneNode(false); + codeStr += "handlersMenuPopup.appendChild(chooseAppSep); "; + + Cu.evalInSandbox(codeStr, this._contentSandbox); + + // List of web handlers + var wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + var handlers = wccr.getContentHandlers(this._getMimeTypeForFeedType(feedType)); + if (handlers.length != 0) { + for (var i = 0; i < handlers.length; ++i) { + if (!handlers[i].uri) { + LOG("Handler with name " + handlers[i].name + " has no URI!? Skipping..."); + continue; + } + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.className = "menuitem-iconic"; + menuItem.setAttribute("label", handlers[i].name); + menuItem.setAttribute("handlerType", "web"); + menuItem.setAttribute("webhandlerurl", handlers[i].uri); + this._contentSandbox.menuItem = menuItem; + codeStr = "handlersMenuPopup.appendChild(menuItem);"; + Cu.evalInSandbox(codeStr, this._contentSandbox); + + this._setFaviconForWebReader(handlers[i].uri, menuItem); + } + this._contentSandbox.menuItem = null; + } + + this._setSelectedHandler(feedType); + + // "Subscribe using..." + this._setSubscribeUsingLabel(); + + // "Always use..." checkbox initial state + this._setAlwaysUseCheckedState(feedType); + this._setAlwaysUseLabel(); + + // We update the "Always use.." checkbox label whenever the selected item + // in the list is changed + handlersMenuPopup.addEventListener("command", this, false); + + // Set up the "Subscribe Now" button + this._getUIElement("subscribeButton") + .addEventListener("command", this, false); + + // first-run ui + var showFirstRunUI = prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI, true); + if (showFirstRunUI) { + var textfeedinfo1, textfeedinfo2; + switch (feedType) { + case Ci.nsIFeed.TYPE_VIDEO: + textfeedinfo1 = "feedSubscriptionVideoPodcast1"; + textfeedinfo2 = "feedSubscriptionVideoPodcast2"; + break; + case Ci.nsIFeed.TYPE_AUDIO: + textfeedinfo1 = "feedSubscriptionAudioPodcast1"; + textfeedinfo2 = "feedSubscriptionAudioPodcast2"; + break; + default: + textfeedinfo1 = "feedSubscriptionFeed1"; + textfeedinfo2 = "feedSubscriptionFeed2"; + } + + this._contentSandbox.feedinfo1 = + this._document.getElementById("feedSubscriptionInfo1"); + this._contentSandbox.feedinfo1Str = this._getString(textfeedinfo1); + this._contentSandbox.feedinfo2 = + this._document.getElementById("feedSubscriptionInfo2"); + this._contentSandbox.feedinfo2Str = this._getString(textfeedinfo2); + this._contentSandbox.header = header; + codeStr = "feedinfo1.textContent = feedinfo1Str; " + + "feedinfo2.textContent = feedinfo2Str; " + + "header.setAttribute('firstrun', 'true');" + Cu.evalInSandbox(codeStr, this._contentSandbox); + prefs.setBoolPref(PREF_SHOW_FIRST_RUN_UI, false); + } + }, + + /** + * Returns the original URI object of the feed and ensures that this + * component is only ever invoked from the preview document. + * @param aWindow + * The window of the document invoking the BrowserFeedWriter + */ + _getOriginalURI: function FW__getOriginalURI(aWindow) { + var chan = aWindow.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIDocShell).currentDocumentChannel; + + var nullPrincipal = Cc["@mozilla.org/nullprincipal;1"]. + createInstance(Ci.nsIPrincipal); + + // this channel is not going to be openend, use a nullPrincipal + // and the most restrctive securityFlag. + let resolvedURI = NetUtil.newChannel({ + uri: "about:feeds", + loadingPrincipal: nullPrincipal, + securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER + }).URI; + + if (resolvedURI.equals(chan.URI)) + return chan.originalURI; + + return null; + }, + + _window: null, + _document: null, + _feedURI: null, + _feedPrincipal: null, + _handlersMenuList: null, + + // BrowserFeedWriter WebIDL methods + init: function FW_init(aWindow) { + var window = aWindow; + this._feedURI = this._getOriginalURI(window); + if (!this._feedURI) + return; + + this._window = window; + this._document = window.document; + this._document.getElementById("feedSubscribeLine").offsetTop; + this._handlersMenuList = this._getUIElement("handlersMenuList"); + + var secman = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + this._feedPrincipal = secman.createCodebasePrincipal(this._feedURI, {}); + + LOG("Subscribe Preview: feed uri = " + this._window.location.href); + + // Set up the subscription UI + this._initSubscriptionUI(); + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefs.addObserver(PREF_SELECTED_ACTION, this, false); + prefs.addObserver(PREF_SELECTED_READER, this, false); + prefs.addObserver(PREF_SELECTED_WEB, this, false); + prefs.addObserver(PREF_SELECTED_APP, this, false); + prefs.addObserver(PREF_VIDEO_SELECTED_ACTION, this, false); + prefs.addObserver(PREF_VIDEO_SELECTED_READER, this, false); + prefs.addObserver(PREF_VIDEO_SELECTED_WEB, this, false); + prefs.addObserver(PREF_VIDEO_SELECTED_APP, this, false); + + prefs.addObserver(PREF_AUDIO_SELECTED_ACTION, this, false); + prefs.addObserver(PREF_AUDIO_SELECTED_READER, this, false); + prefs.addObserver(PREF_AUDIO_SELECTED_WEB, this, false); + prefs.addObserver(PREF_AUDIO_SELECTED_APP, this, false); + }, + + writeContent: function FW_writeContent() { + if (!this._window) + return; + + try { + // Set up the feed content + var container = this._getContainer(); + if (!container) + return; + + this._setTitleText(container); + this._setTitleImage(container); + this._writeFeedContent(container); + } + finally { + this._removeFeedFromCache(); + } + }, + + close: function FW_close() { + this._getUIElement("handlersMenuPopup") + .removeEventListener("command", this, false); + this._getUIElement("subscribeButton") + .removeEventListener("command", this, false); + this._document = null; + this._window = null; + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefs.removeObserver(PREF_SELECTED_ACTION, this); + prefs.removeObserver(PREF_SELECTED_READER, this); + prefs.removeObserver(PREF_SELECTED_WEB, this); + prefs.removeObserver(PREF_SELECTED_APP, this); + prefs.removeObserver(PREF_VIDEO_SELECTED_ACTION, this); + prefs.removeObserver(PREF_VIDEO_SELECTED_READER, this); + prefs.removeObserver(PREF_VIDEO_SELECTED_WEB, this); + prefs.removeObserver(PREF_VIDEO_SELECTED_APP, this); + + prefs.removeObserver(PREF_AUDIO_SELECTED_ACTION, this); + prefs.removeObserver(PREF_AUDIO_SELECTED_READER, this); + prefs.removeObserver(PREF_AUDIO_SELECTED_WEB, this); + prefs.removeObserver(PREF_AUDIO_SELECTED_APP, this); + + this._removeFeedFromCache(); + this.__faviconService = null; + this.__bundle = null; + this._feedURI = null; + this.__contentSandbox = null; + }, + + _removeFeedFromCache: function FW__removeFeedFromCache() { + if (this._feedURI) { + var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + feedService.removeFeedResult(this._feedURI); + this._feedURI = null; + } + }, + + subscribe: function FW_subscribe() { + var feedType = this._getFeedType(); + + // Subscribe to the feed using the selected handler and save prefs + var prefs = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + var defaultHandler = "reader"; + var useAsDefault = this._getUIElement("alwaysUse").getAttribute("checked"); + + var selectedItem = this._getSelectedItemFromMenulist(this._handlersMenuList); + let subscribeCallback = function() { + if (selectedItem.hasAttribute("webhandlerurl")) { + var webURI = selectedItem.getAttribute("webhandlerurl"); + prefs.setCharPref(getPrefReaderForType(feedType), "web"); + + var supportsString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + supportsString.data = webURI; + prefs.setComplexValue(getPrefWebForType(feedType), Ci.nsISupportsString, + supportsString); + + var wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + var handler = wccr.getWebContentHandlerByURI(this._getMimeTypeForFeedType(feedType), webURI); + if (handler) { + if (useAsDefault) { + wccr.setAutoHandler(this._getMimeTypeForFeedType(feedType), handler); + } + + this._window.location.href = handler.getHandlerURI(this._window.location.href); + } + } else { + switch (selectedItem.getAttribute("anonid")) { + case "selectedAppMenuItem": + prefs.setComplexValue(getPrefAppForType(feedType), Ci.nsILocalFile, + this._selectedApp); + prefs.setCharPref(getPrefReaderForType(feedType), "client"); + break; + case "defaultHandlerMenuItem": + prefs.setComplexValue(getPrefAppForType(feedType), Ci.nsILocalFile, + this._defaultSystemReader); + prefs.setCharPref(getPrefReaderForType(feedType), "client"); + break; + case "liveBookmarksMenuItem": + defaultHandler = "bookmarks"; + prefs.setCharPref(getPrefReaderForType(feedType), "bookmarks"); + break; + } + var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + + // Pull the title and subtitle out of the document + var feedTitle = this._document.getElementById(TITLE_ID).textContent; + var feedSubtitle = this._document.getElementById(SUBTITLE_ID).textContent; + feedService.addToClientReader(this._window.location.href, feedTitle, feedSubtitle, feedType); + } + + // If "Always use..." is checked, we should set PREF_*SELECTED_ACTION + // to either "reader" (If a web reader or if an application is selected), + // or to "bookmarks" (if the live bookmarks option is selected). + // Otherwise, we should set it to "ask" + if (useAsDefault) { + prefs.setCharPref(getPrefActionForType(feedType), defaultHandler); + } else { + prefs.setCharPref(getPrefActionForType(feedType), "ask"); + } + }.bind(this); + + // Show the file picker before subscribing if the + // choose application menuitem was chosen using the keyboard + if (selectedItem.getAttribute("anonid") == "chooseApplicationMenuItem") { + this._chooseClientApp(function(aResult) { + if (aResult) { + selectedItem = + this._getSelectedItemFromMenulist(this._handlersMenuList); + subscribeCallback(); + } + }.bind(this)); + } else { + subscribeCallback(); + } + }, + + // nsIObserver + observe: function FW_observe(subject, topic, data) { + if (!this._window) { + // this._window is null unless this.init was called with a trusted + // window object. + return; + } + + var feedType = this._getFeedType(); + + if (topic == "nsPref:changed") { + switch (data) { + case PREF_SELECTED_READER: + case PREF_SELECTED_WEB: + case PREF_SELECTED_APP: + case PREF_VIDEO_SELECTED_READER: + case PREF_VIDEO_SELECTED_WEB: + case PREF_VIDEO_SELECTED_APP: + case PREF_AUDIO_SELECTED_READER: + case PREF_AUDIO_SELECTED_WEB: + case PREF_AUDIO_SELECTED_APP: + this._setSelectedHandler(feedType); + break; + case PREF_SELECTED_ACTION: + case PREF_VIDEO_SELECTED_ACTION: + case PREF_AUDIO_SELECTED_ACTION: + this._setAlwaysUseCheckedState(feedType); + } + } + }, + + /** + * Sets the icon for the given web-reader item in the readers menu. + * The icon is fetched and stored through the favicon service. + * + * @param aReaderUrl + * the reader url. + * @param aMenuItem + * the reader item in the readers menulist. + * + * @note For privacy reasons we cannot set the image attribute directly + * to the icon url. See Bug 358878 for details. + */ + _setFaviconForWebReader: + function FW__setFaviconForWebReader(aReaderUrl, aMenuItem) { + var readerURI = makeURI(aReaderUrl); + if (!/^https?$/.test(readerURI.scheme)) { + // Don't try to get a favicon for non http(s) URIs. + return; + } + var faviconURI = makeURI(readerURI.prePath + "/favicon.ico"); + var self = this; + var usePrivateBrowsing = this._window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsILoadContext) + .usePrivateBrowsing; + var nullPrincipal = Cc["@mozilla.org/nullprincipal;1"] + .createInstance(Ci.nsIPrincipal); + this._faviconService.setAndFetchFaviconForPage(readerURI, faviconURI, false, + usePrivateBrowsing ? this._faviconService.FAVICON_LOAD_PRIVATE + : this._faviconService.FAVICON_LOAD_NON_PRIVATE, + function (aURI, aDataLen, aData, aMimeType) { + if (aDataLen > 0) { + var dataURL = "data:" + aMimeType + ";base64," + + btoa(String.fromCharCode.apply(null, aData)); + self._contentSandbox.menuItem = aMenuItem; + self._contentSandbox.dataURL = dataURL; + var codeStr = "menuItem.setAttribute('image', dataURL);"; + Cu.evalInSandbox(codeStr, self._contentSandbox); + self._contentSandbox.menuItem = null; + self._contentSandbox.dataURL = null; + } + }, nullPrincipal); + }, + + classID: FEEDWRITER_CID, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener, Ci.nsIObserver, + Ci.nsINavHistoryObserver, + Ci.nsIDOMGlobalPropertyInitializer]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FeedWriter]); diff --git a/components/feeds/WebContentConverter.js b/components/feeds/WebContentConverter.js new file mode 100644 index 0000000..42e2ede --- /dev/null +++ b/components/feeds/WebContentConverter.js @@ -0,0 +1,927 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +function LOG(str) { + dump("*** " + str + "\n"); +} + +const WCCR_CONTRACTID = "@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"; +const WCCR_CLASSID = Components.ID("{792a7e82-06a0-437c-af63-b2d12e808acc}"); + +const WCC_CLASSID = Components.ID("{db7ebf28-cc40-415f-8a51-1b111851df1e}"); +const WCC_CLASSNAME = "Web Service Handler"; + +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_ANY = "*/*"; +const TYPE_BLACKLIST = [ + "application/x-www-form-urlencoded", + "application/xhtml+xml", + "application/xml", + "application/mathml+xml", + "application/xslt+xml", + "application/x-xpinstall", + "image/gif", + "image/jpg", + "image/jpeg", + "image/png", + "image/x-png", + "image/webp", +#ifdef MOZ_JXR + "image/jxr", + "image/vnd.ms-photo", +#endif + "image/svg+xml", + "image/bmp", + "image/x-ms-bmp", + "image/icon", + "image/x-icon", + "image/vnd.microsoft.icon", + "multipart/x-mixed-replace", + "multipart/form-data", + "text/cache-manifest", + "text/css", + "text/xsl", + "text/html", + "text/ping", + "text/plain", + "text/xml", + "text/javascript", // To prevent malicious intent blocking scripting. + "text/ecmascript"]; + +const PREF_CONTENTHANDLERS_AUTO = "browser.contentHandlers.auto."; +const PREF_CONTENTHANDLERS_BRANCH = "browser.contentHandlers.types."; +const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_SELECTED_READER = "browser.feeds.handler.default"; +const PREF_HANDLER_EXTERNAL_PREFIX = "network.protocol-handler.external"; +const PREF_ALLOW_DIFFERENT_HOST = "gecko.handlerService.allowRegisterFromDifferentHost"; + +const STRING_BUNDLE_URI = "chrome://browser/locale/feeds/subscribe.properties"; + +const NS_ERROR_MODULE_DOM = 2152923136; +const NS_ERROR_DOM_SYNTAX_ERR = NS_ERROR_MODULE_DOM + 12; + +function WebContentConverter() { +} +WebContentConverter.prototype = { + convert: function WCC_convert() { }, + asyncConvertData: function WCC_asyncConvertData() { }, + onDataAvailable: function WCC_onDataAvailable() { }, + onStopRequest: function WCC_onStopRequest() { }, + + onStartRequest: function WCC_onStartRequest(request, context) { + var wccr = + Cc[WCCR_CONTRACTID]. + getService(Ci.nsIWebContentConverterService); + wccr.loadPreferredHandler(request); + }, + + QueryInterface: function WCC_QueryInterface(iid) { + if (iid.equals(Ci.nsIStreamConverter) || + iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +var WebContentConverterFactory = { + createInstance: function WCCF_createInstance(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return new WebContentConverter().QueryInterface(iid); + }, + + QueryInterface: function WCC_QueryInterface(iid) { + if (iid.equals(Ci.nsIFactory) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +function ServiceInfo(contentType, uri, name) { + this._contentType = contentType; + this._uri = uri; + this._name = name; +} +ServiceInfo.prototype = { + /** + * See nsIHandlerApp + */ + get name() { + return this._name; + }, + + /** + * See nsIHandlerApp + */ + equals: function SI_equals(aHandlerApp) { + if (!aHandlerApp) + throw Cr.NS_ERROR_NULL_POINTER; + + if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo && + aHandlerApp.contentType == this.contentType && + aHandlerApp.uri == this.uri) + return true; + + return false; + }, + + /** + * See nsIWebContentHandlerInfo + */ + get contentType() { + return this._contentType; + }, + + /** + * See nsIWebContentHandlerInfo + */ + get uri() { + return this._uri; + }, + + /** + * See nsIWebContentHandlerInfo + */ + getHandlerURI: function SI_getHandlerURI(uri) { + return this._uri.replace(/%s/gi, encodeURIComponent(uri)); + }, + + QueryInterface: function SI_QueryInterface(iid) { + if (iid.equals(Ci.nsIWebContentHandlerInfo) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +function WebContentConverterRegistrar() { + this._contentTypes = { }; + this._autoHandleContentTypes = { }; +} + +WebContentConverterRegistrar.prototype = { + get stringBundle() { + var sb = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(STRING_BUNDLE_URI); + delete WebContentConverterRegistrar.prototype.stringBundle; + return WebContentConverterRegistrar.prototype.stringBundle = sb; + }, + + _getFormattedString: function WCCR__getFormattedString(key, params) { + return this.stringBundle.formatStringFromName(key, params, params.length); + }, + + _getString: function WCCR_getString(key) { + return this.stringBundle.GetStringFromName(key); + }, + + /** + * See nsIWebContentConverterService + */ + getAutoHandler: + function WCCR_getAutoHandler(contentType) { + contentType = this._resolveContentType(contentType); + if (contentType in this._autoHandleContentTypes) + return this._autoHandleContentTypes[contentType]; + return null; + }, + + /** + * See nsIWebContentConverterService + */ + setAutoHandler: + function WCCR_setAutoHandler(contentType, handler) { + if (handler && !this._typeIsRegistered(contentType, handler.uri)) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + contentType = this._resolveContentType(contentType); + this._setAutoHandler(contentType, handler); + + var ps = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + var autoBranch = ps.getBranch(PREF_CONTENTHANDLERS_AUTO); + if (handler) + autoBranch.setCharPref(contentType, handler.uri); + else if (autoBranch.prefHasUserValue(contentType)) + autoBranch.clearUserPref(contentType); + + ps.savePrefFile(null); + }, + + /** + * Update the internal data structure (not persistent) + */ + _setAutoHandler: + function WCCR__setAutoHandler(contentType, handler) { + if (handler) + this._autoHandleContentTypes[contentType] = handler; + else if (contentType in this._autoHandleContentTypes) + delete this._autoHandleContentTypes[contentType]; + }, + + /** + * See nsIWebContentConverterService + */ + getWebContentHandlerByURI: + function WCCR_getWebContentHandlerByURI(contentType, uri) { + var handlers = this.getContentHandlers(contentType, { }); + for (var i = 0; i < handlers.length; ++i) { + if (handlers[i].uri == uri) + return handlers[i]; + } + return null; + }, + + /** + * See nsIWebContentConverterService + */ + loadPreferredHandler: + function WCCR_loadPreferredHandler(request) { + var channel = request.QueryInterface(Ci.nsIChannel); + var contentType = this._resolveContentType(channel.contentType); + var handler = this.getAutoHandler(contentType); + if (handler) { + request.cancel(Cr.NS_ERROR_FAILURE); + + var webNavigation = + channel.notificationCallbacks.getInterface(Ci.nsIWebNavigation); + webNavigation.loadURI(handler.getHandlerURI(channel.URI.spec), + Ci.nsIWebNavigation.LOAD_FLAGS_NONE, + null, null, null); + } + }, + + /** + * See nsIWebContentConverterService + */ + removeProtocolHandler: + function WCCR_removeProtocolHandler(aProtocol, aURITemplate) { + var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + var handlerInfo = eps.getProtocolHandlerInfo(aProtocol); + var handlers = handlerInfo.possibleApplicationHandlers; + for (let i = 0; i < handlers.length; i++) { + try { // We only want to test web handlers + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + if (handler.uriTemplate == aURITemplate) { + handlers.removeElementAt(i); + var hs = Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService); + hs.store(handlerInfo); + return; + } + } catch (e) { /* it wasn't a web handler */ } + } + }, + + /** + * See nsIWebContentConverterService + */ + removeContentHandler: + function WCCR_removeContentHandler(contentType, uri) { + function notURI(serviceInfo) { + return serviceInfo.uri != uri; + } + + if (contentType in this._contentTypes) { + this._contentTypes[contentType] = + this._contentTypes[contentType].filter(notURI); + } + }, + + /** + * + */ + _mappings: { + "application/rss+xml": TYPE_MAYBE_FEED, + "application/atom+xml": TYPE_MAYBE_FEED, + }, + + /** + * These are types for which there is a separate content converter aside + * from our built in generic one. We should not automatically register + * a factory for creating a converter for these types. + */ + _blockedTypes: { + "application/vnd.mozilla.maybe.feed": true, + }, + + /** + * Determines the "internal" content type based on the _mappings. + * @param contentType + * @returns The resolved contentType value. + */ + _resolveContentType: + function WCCR__resolveContentType(contentType) { + if (contentType in this._mappings) + return this._mappings[contentType]; + return contentType; + }, + + _makeURI: function(aURL, aOriginCharset, aBaseURI) { + var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + return ioService.newURI(aURL, aOriginCharset, aBaseURI); + }, + + _checkAndGetURI: + function WCCR_checkAndGetURI(aURIString, aContentWindow) + { + try { + let baseURI = aContentWindow.document.baseURIObject; + var uri = this._makeURI(aURIString, null, baseURI); + } catch (ex) { + // not supposed to throw according to spec + return; + } + + // For security reasons we reject non-http(s) urls (see bug 354316), + // we may need to revise this once we support more content types + // XXX this should be a "security exception" according to spec, but that + // isn't defined yet. + if (uri.scheme != "http" && uri.scheme != "https") + throw("Permission denied to add " + uri.spec + " as a content or protocol handler"); + + // We also reject handlers registered from a different host (see bug 402287) + // The pref allows us to test the feature + var pb = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + if (!pb.getBoolPref(PREF_ALLOW_DIFFERENT_HOST) && + (!["http:", "https:"].includes(aContentWindow.location.protocol) || + aContentWindow.location.hostname != uri.host)) { + throw("Permission denied to add " + uri.spec + " as a content or protocol handler"); + } + + // If the uri doesn't contain '%s', it won't be a good handler + if (uri.spec.indexOf("%s") < 0) + throw NS_ERROR_DOM_SYNTAX_ERR; + + return uri; + }, + + /** + * Determines if a web handler is already registered. + * + * @param aProtocol + * The scheme of the web handler we are checking for. + * @param aURITemplate + * The URI template that the handler uses to handle the protocol. + * @return true if it is already registered, false otherwise. + */ + _protocolHandlerRegistered: + function WCCR_protocolHandlerRegistered(aProtocol, aURITemplate) { + var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + var handlerInfo = eps.getProtocolHandlerInfo(aProtocol); + var handlers = handlerInfo.possibleApplicationHandlers; + for (let i = 0; i < handlers.length; i++) { + try { // We only want to test web handlers + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + if (handler.uriTemplate == aURITemplate) + return true; + } catch (e) { /* it wasn't a web handler */ } + } + return false; + }, + + /** + * See nsIWebContentHandlerRegistrar + */ + registerProtocolHandler: + function WCCR_registerProtocolHandler(aProtocol, aURIString, aTitle, aContentWindow) { + LOG("registerProtocolHandler(" + aProtocol + "," + aURIString + "," + aTitle + ")"); + + var uri = this._checkAndGetURI(aURIString, aContentWindow); + + // If the protocol handler is already registered, just return early. + if (this._protocolHandlerRegistered(aProtocol, uri.spec)) { + return; + } + + var browserWindow = this._getBrowserWindowForContentWindow(aContentWindow); + if (PrivateBrowsingUtils.isWindowPrivate(browserWindow)) { + // Inside the private browsing mode, we don't want to alert the user to save + // a protocol handler. We log it to the error console so that web developers + // would have some way to tell what's going wrong. + Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService). + logStringMessage("Web page denied access to register a protocol handler inside private browsing mode"); + return; + } + + // First, check to make sure this isn't already handled internally (we don't + // want to let them take over, say "chrome"). + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var handler = ios.getProtocolHandler(aProtocol); + if (!(handler instanceof Ci.nsIExternalProtocolHandler)) { + // This is handled internally, so we don't want them to register + // XXX this should be a "security exception" according to spec, but that + // isn't defined yet. + throw("Permission denied to add " + aURIString + "as a protocol handler"); + } + + // check if it is in the black list + var pb = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + var allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol, + pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default")); + if (!allowed) { + // XXX this should be a "security exception" according to spec + throw("Not allowed to register a protocol handler for " + aProtocol); + } + + // Now Ask the user and provide the proper callback + var message = this._getFormattedString("addProtocolHandler", + [aTitle, uri.host, aProtocol]); + + var notificationIcon = uri.prePath + "/favicon.ico"; + var notificationValue = "Protocol Registration: " + aProtocol; + var addButton = { + label: this._getString("addProtocolHandlerAddButton"), + accessKey: this._getString("addHandlerAddButtonAccesskey"), + protocolInfo: { protocol: aProtocol, uri: uri.spec, name: aTitle }, + + callback: + function WCCR_addProtocolHandlerButtonCallback(aNotification, aButtonInfo) { + var protocol = aButtonInfo.protocolInfo.protocol; + var uri = aButtonInfo.protocolInfo.uri; + var name = aButtonInfo.protocolInfo.name; + + var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"]. + createInstance(Ci.nsIWebHandlerApp); + handler.name = name; + handler.uriTemplate = uri; + + var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + var handlerInfo = eps.getProtocolHandlerInfo(protocol); + handlerInfo.possibleApplicationHandlers.appendElement(handler, false); + + // Since the user has agreed to add a new handler, chances are good + // that the next time they see a handler of this type, they're going + // to want to use it. Reset the handlerInfo to ask before the next + // use. + handlerInfo.alwaysAskBeforeHandling = true; + + var hs = Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService); + hs.store(handlerInfo); + } + }; + var browserElement = this._getBrowserForContentWindow(browserWindow, aContentWindow); + var notificationBox = browserWindow.gBrowser.getNotificationBox(browserElement); + notificationBox.appendNotification(message, + notificationValue, + notificationIcon, + notificationBox.PRIORITY_INFO_LOW, + [addButton]); + }, + + /** + * See nsIWebContentHandlerRegistrar + * If a DOM window is provided, then the request came from content, so we + * prompt the user to confirm the registration. + */ + registerContentHandler: + function WCCR_registerContentHandler(aContentType, aURIString, aTitle, aContentWindow) { + LOG("registerContentHandler(" + aContentType + "," + aURIString + "," + aTitle + ")"); + + // Check against the type blacklist. + // XXX this should be a "security exception" according to spec, but that + // isn't defined yet. + var contentType = this._resolveContentType(aContentType); + for (let blacklistType of TYPE_BLACKLIST) { + if (contentType == blacklistType) { + console.error("Unable to register content handler for prohibited MIME type %s.", contentType); + return; + } + } + + if (aContentWindow) { + var uri = this._checkAndGetURI(aURIString, aContentWindow); + + var browserWindow = this._getBrowserWindowForContentWindow(aContentWindow); + var browserElement = this._getBrowserForContentWindow(browserWindow, aContentWindow); + var notificationBox = browserWindow.gBrowser.getNotificationBox(browserElement); + this._appendFeedReaderNotification(uri, aTitle, notificationBox); + } + else + this._registerContentHandler(contentType, aURIString, aTitle); + }, + + /** + * Returns the browser chrome window in which the content window is in + */ + _getBrowserWindowForContentWindow: + function WCCR__getBrowserWindowForContentWindow(aContentWindow) { + return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .wrappedJSObject; + }, + + /** + * Returns the element associated with the given content + * window. + * + * @param aBrowserWindow + * The browser window in which the content window is in. + * @param aContentWindow + * The content window. It's possible to pass a child content window + * (i.e. the content window of a frame/iframe). + */ + _getBrowserForContentWindow: + function WCCR__getBrowserForContentWindow(aBrowserWindow, aContentWindow) { + // This depends on pseudo APIs of browser.js and tabbrowser.xml + aContentWindow = aContentWindow.top; + var browsers = aBrowserWindow.gBrowser.browsers; + for (var i = 0; i < browsers.length; ++i) { + if (browsers[i].contentWindow == aContentWindow) + return browsers[i]; + } + }, + + /** + * Appends a notifcation for the given feed reader details. + * + * The notification could be either a pseudo-dialog which lets + * the user to add the feed reader: + * [ [icon] Add %feed-reader-name% (%feed-reader-host%) as a Feed Reader? (Add) [x] ] + * + * or a simple message for the case where the feed reader is already registered: + * [ [icon] %feed-reader-name% is already registered as a Feed Reader [x] ] + * + * A new notification isn't appended if the given notificationbox has a + * notification for the same feed reader. + * + * @param aURI + * The url of the feed reader as a nsIURI object + * @param aName + * The feed reader name as it was passed to registerContentHandler + * @param aNotificationBox + * The notification box to which a notification might be appended + * @return true if a notification has been appended, false otherwise. + */ + _appendFeedReaderNotification: + function WCCR__appendFeedReaderNotification(aURI, aName, aNotificationBox) { + var uriSpec = aURI.spec; + var notificationValue = "feed reader notification: " + uriSpec; + var notificationIcon = aURI.prePath + "/favicon.ico"; + + // Don't append a new notification if the notificationbox + // has a notification for the given feed reader already + if (aNotificationBox.getNotificationWithValue(notificationValue)) + return false; + + var buttons, message; + if (this.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uriSpec)) + message = this._getFormattedString("handlerRegistered", [aName]); + else { + message = this._getFormattedString("addHandler", [aName, aURI.host]); + var self = this; + var addButton = { + _outer: self, + label: self._getString("addHandlerAddButton"), + accessKey: self._getString("addHandlerAddButtonAccesskey"), + feedReaderInfo: { uri: uriSpec, name: aName }, + + /* static */ + callback: + function WCCR__addFeedReaderButtonCallback(aNotification, aButtonInfo) { + var uri = aButtonInfo.feedReaderInfo.uri; + var name = aButtonInfo.feedReaderInfo.name; + var outer = aButtonInfo._outer; + + // The reader could have been added from another window mean while + if (!outer.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uri)) + outer._registerContentHandler(TYPE_MAYBE_FEED, uri, name); + + // avoid reference cycles + aButtonInfo._outer = null; + + return false; + } + }; + buttons = [addButton]; + } + + aNotificationBox.appendNotification(message, + notificationValue, + notificationIcon, + aNotificationBox.PRIORITY_INFO_LOW, + buttons); + return true; + }, + + /** + * Save Web Content Handler metadata to persistent preferences. + * @param contentType + * The content Type being handled + * @param uri + * The uri of the web service + * @param title + * The human readable name of the web service + * + * This data is stored under: + * + * browser.contentHandlers.type0 = content/type + * browser.contentHandlers.uri0 = http://www.foo.com/q=%s + * browser.contentHandlers.title0 = Foo 2.0alphr + */ + _saveContentHandlerToPrefs: + function WCCR__saveContentHandlerToPrefs(contentType, uri, title) { + var ps = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + var i = 0; + var typeBranch = null; + while (true) { + typeBranch = + ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + i + "."); + try { + typeBranch.getCharPref("type"); + ++i; + } + catch (e) { + // No more handlers + break; + } + } + if (typeBranch) { + typeBranch.setCharPref("type", contentType); + var pls = + Cc["@mozilla.org/pref-localizedstring;1"]. + createInstance(Ci.nsIPrefLocalizedString); + pls.data = uri; + typeBranch.setComplexValue("uri", Ci.nsIPrefLocalizedString, pls); + pls.data = title; + typeBranch.setComplexValue("title", Ci.nsIPrefLocalizedString, pls); + + ps.savePrefFile(null); + } + }, + + /** + * Determines if there is a type with a particular uri registered for the + * specified content type already. + * @param contentType + * The content type that the uri handles + * @param uri + * The uri of the + */ + _typeIsRegistered: function WCCR__typeIsRegistered(contentType, uri) { + if (!(contentType in this._contentTypes)) + return false; + + var services = this._contentTypes[contentType]; + for (var i = 0; i < services.length; ++i) { + // This uri has already been registered + if (services[i].uri == uri) + return true; + } + return false; + }, + + /** + * Gets a stream converter contract id for the specified content type. + * @param contentType + * The source content type for the conversion. + * @returns A contract id to construct a converter to convert between the + * contentType and *\/*. + */ + _getConverterContractID: function WCCR__getConverterContractID(contentType) { + const template = "@mozilla.org/streamconv;1?from=%s&to=*/*"; + return template.replace(/%s/, contentType); + }, + + /** + * Register a web service handler for a content type. + * + * @param contentType + * the content type being handled + * @param uri + * the URI of the web service + * @param title + * the human readable name of the web service + */ + _registerContentHandler: + function WCCR__registerContentHandler(contentType, uri, title) { + this._updateContentTypeHandlerMap(contentType, uri, title); + this._saveContentHandlerToPrefs(contentType, uri, title); + + if (contentType == TYPE_MAYBE_FEED) { + // Make the new handler the last-selected reader in the preview page + // and make sure the preview page is shown the next time a feed is visited + var pb = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService).getBranch(null); + pb.setCharPref(PREF_SELECTED_READER, "web"); + + var supportsString = + Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + supportsString.data = uri; + pb.setComplexValue(PREF_SELECTED_WEB, Ci.nsISupportsString, + supportsString); + pb.setCharPref(PREF_SELECTED_ACTION, "ask"); + this._setAutoHandler(TYPE_MAYBE_FEED, null); + } + }, + + /** + * Update the content type -> handler map. This mapping is not persisted, use + * registerContentHandler or _saveContentHandlerToPrefs for that purpose. + * @param contentType + * The content Type being handled + * @param uri + * The uri of the web service + * @param title + * The human readable name of the web service + */ + _updateContentTypeHandlerMap: + function WCCR__updateContentTypeHandlerMap(contentType, uri, title) { + if (!(contentType in this._contentTypes)) + this._contentTypes[contentType] = []; + + // Avoid adding duplicates + if (this._typeIsRegistered(contentType, uri)) + return; + + this._contentTypes[contentType].push(new ServiceInfo(contentType, uri, title)); + + if (!(contentType in this._blockedTypes)) { + var converterContractID = this._getConverterContractID(contentType); + var cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + cr.registerFactory(WCC_CLASSID, WCC_CLASSNAME, converterContractID, + WebContentConverterFactory); + } + }, + + /** + * See nsIWebContentConverterService + */ + getContentHandlers: + function WCCR_getContentHandlers(contentType, countRef) { + countRef.value = 0; + if (!(contentType in this._contentTypes)) + return []; + + var handlers = this._contentTypes[contentType]; + countRef.value = handlers.length; + return handlers; + }, + + /** + * See nsIWebContentConverterService + */ + resetHandlersForType: + function WCCR_resetHandlersForType(contentType) { + // currently unused within the tree, so only useful for extensions; previous + // impl. was buggy (and even infinite-looped!), so I argue that this is a + // definite improvement + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Registers a handler from the settings on a preferences branch. + * + * @param branch + * an nsIPrefBranch containing "type", "uri", and "title" preferences + * corresponding to the content handler to be registered + */ + _registerContentHandlerWithBranch: function(branch) { + /** + * Since we support up to six predefined readers, we need to handle gaps + * better, since the first branch with user-added values will be .6 + * + * How we deal with that is to check to see if there's no prefs in the + * branch and stop cycling once that's true. This doesn't fix the case + * where a user manually removes a reader, but that's not supported yet! + */ + var vals = branch.getChildList(""); + if (vals.length == 0) + return; + + try { + var type = branch.getCharPref("type"); + var uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data; + var title = branch.getComplexValue("title", + Ci.nsIPrefLocalizedString).data; + this._updateContentTypeHandlerMap(type, uri, title); + } + catch(ex) { + // do nothing, the next branch might have values + } + }, + + /** + * Load the auto handler, content handler and protocol tables from + * preferences. + */ + _init: function WCCR__init() { + var ps = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + + var kids = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH) + .getChildList(""); + + // first get the numbers of the providers by getting all ###.uri prefs + var nums = []; + for (var i = 0; i < kids.length; i++) { + var match = /^(\d+)\.uri$/.exec(kids[i]); + if (!match) + continue; + else + nums.push(match[1]); + } + + // sort them, to get them back in order + nums.sort(function(a, b) {return a - b;}); + + // now register them + for (var i = 0; i < nums.length; i++) { + var branch = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + nums[i] + "."); + this._registerContentHandlerWithBranch(branch); + } + + // We need to do this _after_ registering all of the available handlers, + // so that getWebContentHandlerByURI can return successfully. + try { + var autoBranch = ps.getBranch(PREF_CONTENTHANDLERS_AUTO); + var childPrefs = autoBranch.getChildList(""); + for (var i = 0; i < childPrefs.length; ++i) { + var type = childPrefs[i]; + var uri = autoBranch.getCharPref(type); + if (uri) { + var handler = this.getWebContentHandlerByURI(type, uri); + this._setAutoHandler(type, handler); + } + } + } + catch (e) { + // No auto branch yet, that's fine + //LOG("WCCR.init: There is no auto branch, benign"); + } + }, + + /** + * See nsIObserver + */ + observe: function WCCR_observe(subject, topic, data) { + var os = + Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + switch (topic) { + case "app-startup": + os.addObserver(this, "browser-ui-startup-complete", false); + break; + case "browser-ui-startup-complete": + os.removeObserver(this, "browser-ui-startup-complete"); + this._init(); + break; + } + }, + + /** + * See nsIFactory + */ + createInstance: function WCCR_createInstance(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + + classID: WCCR_CLASSID, + + /** + * See nsISupports + */ + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIWebContentConverterService, + Ci.nsIWebContentHandlerRegistrar, + Ci.nsIObserver, + Ci.nsIFactory]), + + _xpcom_categories: [{ + category: "app-startup", + service: true + }] +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebContentConverterRegistrar]); diff --git a/components/feeds/content/subscribe.css b/components/feeds/content/subscribe.css new file mode 100644 index 0000000..bf2524d --- /dev/null +++ b/components/feeds/content/subscribe.css @@ -0,0 +1,7 @@ +/* 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/. */ + +#feedSubscribeLine { + -moz-binding: url(subscribe.xml#feedreaderUI); +} diff --git a/components/feeds/content/subscribe.js b/components/feeds/content/subscribe.js new file mode 100644 index 0000000..ab2eac4 --- /dev/null +++ b/components/feeds/content/subscribe.js @@ -0,0 +1,23 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +var SubscribeHandler = { + /** + * The nsIFeedWriter object that produces the UI + */ + _feedWriter: null, + + init: function SH_init() { + this._feedWriter = new BrowserFeedWriter(); + }, + + writeContent: function SH_writeContent() { + this._feedWriter.writeContent(); + }, + + uninit: function SH_uninit() { + this._feedWriter.close(); + } +}; diff --git a/components/feeds/content/subscribe.xhtml b/components/feeds/content/subscribe.xhtml new file mode 100644 index 0000000..8ad069f --- /dev/null +++ b/components/feeds/content/subscribe.xhtml @@ -0,0 +1,65 @@ + + + + + + %htmlDTD; + + %globalDTD; + + %feedDTD; +]> + + + + + + &feedPage.title; + + + + +
+
+ + + +
+

+

+

+
+
+
+ + diff --git a/components/feeds/content/subscribe.xml b/components/feeds/content/subscribe.xml new file mode 100644 index 0000000..949bcfd --- /dev/null +++ b/components/feeds/content/subscribe.xml @@ -0,0 +1,40 @@ + + + + + %feedDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/feeds/jar.mn b/components/feeds/jar.mn new file mode 100644 index 0000000..f8896f8 --- /dev/null +++ b/components/feeds/jar.mn @@ -0,0 +1,9 @@ +# 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/. + +browser.jar: + content/browser/feeds/subscribe.xhtml (content/subscribe.xhtml) + content/browser/feeds/subscribe.js (content/subscribe.js) + content/browser/feeds/subscribe.xml (content/subscribe.xml) + content/browser/feeds/subscribe.css (content/subscribe.css) diff --git a/components/feeds/moz.build b/components/feeds/moz.build new file mode 100644 index 0000000..736920a --- /dev/null +++ b/components/feeds/moz.build @@ -0,0 +1,33 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ['jar.mn'] + +XPIDL_SOURCES += [ + 'nsIFeedResultService.idl', + 'nsIWebContentConverterRegistrar.idl', +] + +XPIDL_MODULE = 'browser-feeds' + +SOURCES += ['nsFeedSniffer.cpp'] + +EXTRA_COMPONENTS += [ + 'BrowserFeeds.manifest', + 'FeedConverter.js', +] + +EXTRA_PP_COMPONENTS += [ + 'FeedWriter.js', + 'WebContentConverter.js', +] + +FINAL_LIBRARY = 'browsercomps' + +for var in ('MOZ_APP_NAME', 'MOZ_MACBUNDLE_NAME'): + DEFINES[var] = CONFIG[var] + +LOCAL_INCLUDES += ['../build'] diff --git a/components/feeds/nsFeedSniffer.cpp b/components/feeds/nsFeedSniffer.cpp new file mode 100644 index 0000000..f314d3d --- /dev/null +++ b/components/feeds/nsFeedSniffer.cpp @@ -0,0 +1,363 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "nsFeedSniffer.h" + + +#include "nsNetCID.h" +#include "nsXPCOM.h" +#include "nsCOMPtr.h" +#include "nsStringStream.h" + +#include "nsBrowserCompsCID.h" + +#include "nsICategoryManager.h" +#include "nsIServiceManager.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" + +#include "nsIStreamConverterService.h" +#include "nsIStreamConverter.h" + +#include "nsIStreamListener.h" + +#include "nsIHttpChannel.h" +#include "nsIMIMEHeaderParam.h" + +#include "nsMimeTypes.h" +#include "nsIURI.h" +#include + +#define TYPE_ATOM "application/atom+xml" +#define TYPE_RSS "application/rss+xml" +#define TYPE_MAYBE_FEED "application/vnd.mozilla.maybe.feed" + +#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +#define NS_RSS "http://purl.org/rss/1.0/" + +#define MAX_BYTES 512u + +NS_IMPL_ISUPPORTS(nsFeedSniffer, + nsIContentSniffer, + nsIStreamListener, + nsIRequestObserver) + +nsresult +nsFeedSniffer::ConvertEncodedData(nsIRequest* request, + const uint8_t* data, + uint32_t length) +{ + nsresult rv = NS_OK; + + mDecodedData = ""; + nsCOMPtr httpChannel(do_QueryInterface(request)); + if (!httpChannel) + return NS_ERROR_NO_INTERFACE; + + nsAutoCString contentEncoding; + httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("Content-Encoding"), + contentEncoding); + if (!contentEncoding.IsEmpty()) { + nsCOMPtr converterService(do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID)); + if (converterService) { + ToLowerCase(contentEncoding); + + nsCOMPtr converter; + rv = converterService->AsyncConvertData(contentEncoding.get(), + "uncompressed", this, nullptr, + getter_AddRefs(converter)); + NS_ENSURE_SUCCESS(rv, rv); + + converter->OnStartRequest(request, nullptr); + + nsCOMPtr rawStream = + do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID); + if (!rawStream) + return NS_ERROR_FAILURE; + + rv = rawStream->SetData((const char*)data, length); + NS_ENSURE_SUCCESS(rv, rv); + + rv = converter->OnDataAvailable(request, nullptr, rawStream, 0, length); + NS_ENSURE_SUCCESS(rv, rv); + + converter->OnStopRequest(request, nullptr, NS_OK); + } + } + return rv; +} + +template +static bool +StringBeginsWithLowercaseLiteral(nsAString& aString, + const char (&aSubstring)[N]) +{ + return StringHead(aString, N).LowerCaseEqualsLiteral(aSubstring); +} + +bool +HasAttachmentDisposition(nsIHttpChannel* httpChannel) +{ + if (!httpChannel) + return false; + + uint32_t disp; + nsresult rv = httpChannel->GetContentDisposition(&disp); + + if (NS_SUCCEEDED(rv) && disp == nsIChannel::DISPOSITION_ATTACHMENT) + return true; + + return false; +} + +/** + * @return the first occurrence of a character within a string buffer, + * or nullptr if not found + */ +static const char* +FindChar(char c, const char *begin, const char *end) +{ + for (; begin < end; ++begin) { + if (*begin == c) + return begin; + } + return nullptr; +} + +/** + * + * Determine if a substring is the "documentElement" in the document. + * + * All of our sniffed substrings: = end) + return false; + + // Check to see if the character following the '<' is either '?' or '!' + // (processing instruction or doctype or comment)... these are valid nodes + // to have in the prologue. + if (*start != '?' && *start != '!') + return false; + + // Now advance the iterator until the '>' (We do this because we don't want + // to sniff indicator substrings that are embedded within other nodes, e.g. + // comments: + start = FindChar('>', start, end); + if (!start) + return false; + + ++start; + } + return true; +} + +/** + * Determines whether or not a string exists as the root element in an XML data + * string buffer. + * @param dataString + * The data being sniffed + * @param substring + * The substring being tested for existence and root-ness. + * @returns true if the substring exists and is the documentElement, false + * otherwise. + */ +static bool +ContainsTopLevelSubstring(nsACString& dataString, const char *substring) +{ + int32_t offset = dataString.Find(substring); + if (offset == -1) + return false; + + const char *begin = dataString.BeginReading(); + + // Only do the validation when we find the substring. + return IsDocumentElement(begin, begin + offset); +} + +NS_IMETHODIMP +nsFeedSniffer::GetMIMETypeFromContent(nsIRequest* request, + const uint8_t* data, + uint32_t length, + nsACString& sniffedType) +{ + nsCOMPtr channel(do_QueryInterface(request)); + if (!channel) + return NS_ERROR_NO_INTERFACE; + + // Check that this is a GET request, since you can't subscribe to a POST... + nsAutoCString method; + channel->GetRequestMethod(method); + if (!method.EqualsLiteral("GET")) { + sniffedType.Truncate(); + return NS_OK; + } + + // We need to find out if this is a load of a view-source document. In this + // case we do not want to override the content type, since the source display + // does not need to be converted from feed format to XUL. More importantly, + // we don't want to change the content type from something + // nsContentDLF::CreateInstance knows about (e.g. application/xml, text/html + // etc) to something that only the application fe knows about (maybe.feed) + // thus deactivating syntax highlighting. + nsCOMPtr originalURI; + channel->GetOriginalURI(getter_AddRefs(originalURI)); + + nsAutoCString scheme; + originalURI->GetScheme(scheme); + if (scheme.EqualsLiteral("view-source")) { + sniffedType.Truncate(); + return NS_OK; + } + + // Check the Content-Type to see if it is set correctly. If it is set to + // something specific that we think is a reliable indication of a feed, don't + // bother sniffing since we assume the site maintainer knows what they're + // doing. + nsAutoCString contentType; + channel->GetContentType(contentType); + bool noSniff = contentType.EqualsLiteral(TYPE_RSS) || + contentType.EqualsLiteral(TYPE_ATOM); + + // Check to see if this was a feed request from the location bar or from + // the feed: protocol. This is also a reliable indication. + // The value of the header doesn't matter. + if (!noSniff) { + nsAutoCString sniffHeader; + nsresult foundHeader = + channel->GetRequestHeader(NS_LITERAL_CSTRING("X-Moz-Is-Feed"), + sniffHeader); + noSniff = NS_SUCCEEDED(foundHeader); + } + + if (noSniff) { + // check for an attachment after we have a likely feed. + if(HasAttachmentDisposition(channel)) { + sniffedType.Truncate(); + return NS_OK; + } + + // set the feed header as a response header, since we have good metadata + // telling us that the feed is supposed to be RSS or Atom + channel->SetResponseHeader(NS_LITERAL_CSTRING("X-Moz-Is-Feed"), + NS_LITERAL_CSTRING("1"), false); + sniffedType.AssignLiteral(TYPE_MAYBE_FEED); + return NS_OK; + } + + // Don't sniff arbitrary types. Limit sniffing to situations that + // we think can reasonably arise. + if (!contentType.EqualsLiteral(TEXT_HTML) && + !contentType.EqualsLiteral(APPLICATION_OCTET_STREAM) && + // Same criterion as XMLHttpRequest. Should we be checking for "+xml" + // and check for text/xml and application/xml by hand instead? + contentType.Find("xml") == -1) { + sniffedType.Truncate(); + return NS_OK; + } + + // Now we need to potentially decompress data served with + // Content-Encoding: gzip + nsresult rv = ConvertEncodedData(request, data, length); + if (NS_FAILED(rv)) + return rv; + + // We cap the number of bytes to scan at MAX_BYTES to prevent picking up + // false positives by accidentally reading document content, e.g. a "how to + // make a feed" page. + const char* testData; + if (mDecodedData.IsEmpty()) { + testData = (const char*)data; + length = std::min(length, MAX_BYTES); + } else { + testData = mDecodedData.get(); + length = std::min(mDecodedData.Length(), MAX_BYTES); + } + + // The strategy here is based on that described in: + // http://blogs.msdn.com/rssteam/articles/PublishersGuide.aspx + // for interoperarbility purposes. + + // Thus begins the actual sniffing. + nsDependentCSubstring dataString((const char*)testData, length); + + bool isFeed = false; + + // RSS 0.91/0.92/2.0 + isFeed = ContainsTopLevelSubstring(dataString, "(closure); + decodedData->Append(rawSegment, count); + *writeCount = count; + return NS_OK; +} + +NS_IMETHODIMP +nsFeedSniffer::OnDataAvailable(nsIRequest* request, nsISupports* context, + nsIInputStream* stream, uint64_t offset, + uint32_t count) +{ + uint32_t read; + return stream->ReadSegments(AppendSegmentToString, &mDecodedData, count, + &read); +} + +NS_IMETHODIMP +nsFeedSniffer::OnStopRequest(nsIRequest* request, nsISupports* context, + nsresult status) +{ + return NS_OK; +} diff --git a/components/feeds/nsFeedSniffer.h b/components/feeds/nsFeedSniffer.h new file mode 100644 index 0000000..a0eb986 --- /dev/null +++ b/components/feeds/nsFeedSniffer.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + + +#include "nsIContentSniffer.h" +#include "nsIStreamListener.h" +#include "nsStringAPI.h" +#include "mozilla/Attributes.h" + +class nsFeedSniffer final : public nsIContentSniffer, + nsIStreamListener +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTSNIFFER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + static nsresult AppendSegmentToString(nsIInputStream* inputStream, + void* closure, + const char* rawSegment, + uint32_t toOffset, + uint32_t count, + uint32_t* writeCount); + +protected: + ~nsFeedSniffer() {} + + nsresult ConvertEncodedData(nsIRequest* request, const uint8_t* data, + uint32_t length); + +private: + nsCString mDecodedData; +}; + diff --git a/components/feeds/nsIFeedResultService.idl b/components/feeds/nsIFeedResultService.idl new file mode 100644 index 0000000..cb0f332 --- /dev/null +++ b/components/feeds/nsIFeedResultService.idl @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "nsISupports.idl" +interface nsIURI; +interface nsIRequest; +interface nsIFeedResult; + +/** + * nsIFeedResultService provides a globally-accessible object for retrieving + * the results of feed processing. + */ +[scriptable, uuid(950a829e-c20e-4dc3-b447-f8b753ae54da)] +interface nsIFeedResultService : nsISupports +{ + /** + * When set to true, forces the preview page to be displayed, regardless + * of the user's preferences. + */ + attribute boolean forcePreviewPage; + + /** + * Adds a URI to the user's specified external feed handler, or live + * bookmarks. + * @param uri + * The uri of the feed to add. + * @param title + * The title of the feed to add. + * @param subtitle + * The subtitle of the feed to add. + * @param feedType + * The nsIFeed type of the feed. See nsIFeed.idl + */ + void addToClientReader(in AUTF8String uri, + in AString title, + in AString subtitle, + in unsigned long feedType); + + /** + * Registers a Feed Result object with a globally accessible service + * so that it can be accessed by a singleton method outside the usual + * flow of control in document loading. + * + * @param feedResult + * An object implementing nsIFeedResult representing the feed. + */ + void addFeedResult(in nsIFeedResult feedResult); + + /** + * Gets a Feed Handler object registered using addFeedResult. + * + * @param uri + * The URI of the feed a handler is being requested for + */ + nsIFeedResult getFeedResult(in nsIURI uri); + + /** + * Unregisters a Feed Handler object registered using addFeedResult. + * @param uri + * The feed URI the handler was registered under. This must be + * the same *instance* the feed was registered under. + */ + void removeFeedResult(in nsIURI uri); +}; diff --git a/components/feeds/nsIWebContentConverterRegistrar.idl b/components/feeds/nsIWebContentConverterRegistrar.idl new file mode 100644 index 0000000..08ce2f4 --- /dev/null +++ b/components/feeds/nsIWebContentConverterRegistrar.idl @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "nsIMIMEInfo.idl" +#include "nsIWebContentHandlerRegistrar.idl" + +interface nsIRequest; + +[scriptable, uuid(eb361098-5158-4b21-8f98-50b445f1f0b2)] +interface nsIWebContentHandlerInfo : nsIHandlerApp +{ + /** + * The content type handled by the handler + */ + readonly attribute AString contentType; + + /** + * The uri of the handler, with an embedded %s where the URI of the loaded + * document will be encoded. + */ + readonly attribute AString uri; + + /** + * Gets the service URL Spec, with the loading document URI encoded in it. + * @param uri + * The URI of the document being loaded + * @returns The URI of the service with the loading document URI encoded in + * it. + */ + AString getHandlerURI(in AString uri); +}; + +[scriptable, uuid(de7cc06e-e778-45cb-b7db-7a114e1e75b1)] +interface nsIWebContentConverterService : nsIWebContentHandlerRegistrar +{ + /** + * Specifies the handler to be used to automatically handle all links of a + * certain content type from now on. + * @param contentType + * The content type to automatically load with the specified handler + * @param handler + * A web service handler. If this is null, no automatic action is + * performed and the user must choose. + * @throws NS_ERROR_NOT_AVAILABLE if the service refered to by |handler| is + * not already registered. + */ + void setAutoHandler(in AString contentType, in nsIWebContentHandlerInfo handler); + + /** + * Gets the auto handler specified for a particular content type + * @param contentType + * The content type to look up an auto handler for. + * @returns The web service handler that will automatically handle all + * documents of the specified type. null if there is no automatic + * handler. (Handlers may be registered, just none of them specified + * as "automatic"). + */ + nsIWebContentHandlerInfo getAutoHandler(in AString contentType); + + /** + * Gets a web handler for the specified service URI + * @param contentType + * The content type of the service being located + * @param uri + * The service URI of the handler to locate. + * @returns A web service handler that uses the specified uri. + */ + nsIWebContentHandlerInfo getWebContentHandlerByURI(in AString contentType, + in AString uri); + + /** + * Loads the preferred handler when content of a registered type is about + * to be loaded. + * @param request + * The nsIRequest for the load of the content + */ + void loadPreferredHandler(in nsIRequest request); + + /** + * Removes a registered protocol handler + * @param protocol + * The protocol scheme to remove a service handler for + * @param uri + * The uri of the service handler to remove + */ + void removeProtocolHandler(in AString protocol, in AString uri); + + /** + * Removes a registered content handler + * @param contentType + * The content type to remove a service handler for + * @param uri + * The uri of the service handler to remove + */ + void removeContentHandler(in AString contentType, in AString uri); + + /** + * Gets the list of content handlers for a particular type. + * @param contentType + * The content type to get handlers for + * @returns An array of nsIWebContentHandlerInfo objects + */ + void getContentHandlers(in AString contentType, + [optional] out unsigned long count, + [retval,array,size_is(count)] out nsIWebContentHandlerInfo handlers); + + /** + * Resets the list of available content handlers to the default set from + * the distribution. + * @param contentType + * The content type to reset handlers for + */ + void resetHandlersForType(in AString contentType); +}; + -- cgit v1.2.3