summaryrefslogtreecommitdiffstats
path: root/components/feeds/FeedConverter.js
diff options
context:
space:
mode:
Diffstat (limited to 'components/feeds/FeedConverter.js')
-rw-r--r--components/feeds/FeedConverter.js591
1 files changed, 591 insertions, 0 deletions
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);