diff options
Diffstat (limited to 'components/feeds/FeedConverter.js')
-rw-r--r-- | components/feeds/FeedConverter.js | 591 |
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); |