michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/debug.js"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: function LOG(str) { michael@0: dump("*** " + str + "\n"); michael@0: } michael@0: michael@0: const FS_CONTRACTID = "@mozilla.org/browser/feeds/result-service;1"; michael@0: const FPH_CONTRACTID = "@mozilla.org/network/protocol;1?name=feed"; michael@0: const PCPH_CONTRACTID = "@mozilla.org/network/protocol;1?name=pcast"; michael@0: michael@0: const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; michael@0: const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; michael@0: const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; michael@0: const TYPE_ANY = "*/*"; michael@0: michael@0: const PREF_SELECTED_APP = "browser.feeds.handlers.application"; michael@0: const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; michael@0: const PREF_SELECTED_ACTION = "browser.feeds.handler"; michael@0: const PREF_SELECTED_READER = "browser.feeds.handler.default"; michael@0: michael@0: const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application"; michael@0: const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; michael@0: const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler"; michael@0: const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default"; michael@0: michael@0: const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application"; michael@0: const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; michael@0: const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler"; michael@0: const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default"; michael@0: michael@0: function getPrefAppForType(t) { michael@0: switch (t) { michael@0: case Ci.nsIFeed.TYPE_VIDEO: michael@0: return PREF_VIDEO_SELECTED_APP; michael@0: michael@0: case Ci.nsIFeed.TYPE_AUDIO: michael@0: return PREF_AUDIO_SELECTED_APP; michael@0: michael@0: default: michael@0: return PREF_SELECTED_APP; michael@0: } michael@0: } michael@0: michael@0: function getPrefWebForType(t) { michael@0: switch (t) { michael@0: case Ci.nsIFeed.TYPE_VIDEO: michael@0: return PREF_VIDEO_SELECTED_WEB; michael@0: michael@0: case Ci.nsIFeed.TYPE_AUDIO: michael@0: return PREF_AUDIO_SELECTED_WEB; michael@0: michael@0: default: michael@0: return PREF_SELECTED_WEB; michael@0: } michael@0: } michael@0: michael@0: function getPrefActionForType(t) { michael@0: switch (t) { michael@0: case Ci.nsIFeed.TYPE_VIDEO: michael@0: return PREF_VIDEO_SELECTED_ACTION; michael@0: michael@0: case Ci.nsIFeed.TYPE_AUDIO: michael@0: return PREF_AUDIO_SELECTED_ACTION; michael@0: michael@0: default: michael@0: return PREF_SELECTED_ACTION; michael@0: } michael@0: } michael@0: michael@0: function getPrefReaderForType(t) { michael@0: switch (t) { michael@0: case Ci.nsIFeed.TYPE_VIDEO: michael@0: return PREF_VIDEO_SELECTED_READER; michael@0: michael@0: case Ci.nsIFeed.TYPE_AUDIO: michael@0: return PREF_AUDIO_SELECTED_READER; michael@0: michael@0: default: michael@0: return PREF_SELECTED_READER; michael@0: } michael@0: } michael@0: michael@0: function safeGetCharPref(pref, defaultValue) { michael@0: var prefs = michael@0: Cc["@mozilla.org/preferences-service;1"]. michael@0: getService(Ci.nsIPrefBranch); michael@0: try { michael@0: return prefs.getCharPref(pref); michael@0: } michael@0: catch (e) { michael@0: } michael@0: return defaultValue; michael@0: } michael@0: michael@0: function FeedConverter() { michael@0: } michael@0: FeedConverter.prototype = { michael@0: classID: Components.ID("{229fa115-9412-4d32-baf3-2fc407f76fb1}"), michael@0: michael@0: /** michael@0: * This is the downloaded text data for the feed. michael@0: */ michael@0: _data: null, michael@0: michael@0: /** michael@0: * This is the object listening to the conversion, which is ultimately the michael@0: * docshell for the load. michael@0: */ michael@0: _listener: null, michael@0: michael@0: /** michael@0: * Records if the feed was sniffed michael@0: */ michael@0: _sniffed: false, michael@0: michael@0: /** michael@0: * See nsIStreamConverter.idl michael@0: */ michael@0: convert: function FC_convert(sourceStream, sourceType, destinationType, michael@0: context) { michael@0: throw Cr.NS_ERROR_NOT_IMPLEMENTED; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIStreamConverter.idl michael@0: */ michael@0: asyncConvertData: function FC_asyncConvertData(sourceType, destinationType, michael@0: listener, context) { michael@0: this._listener = listener; michael@0: }, michael@0: michael@0: /** michael@0: * Whether or not the preview page is being forced. michael@0: */ michael@0: _forcePreviewPage: false, michael@0: michael@0: /** michael@0: * Release our references to various things once we're done using them. michael@0: */ michael@0: _releaseHandles: function FC__releaseHandles() { michael@0: this._listener = null; michael@0: this._request = null; michael@0: this._processor = null; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIFeedResultListener.idl michael@0: */ michael@0: handleResult: function FC_handleResult(result) { michael@0: // Feeds come in various content types, which our feed sniffer coerces to michael@0: // the maybe.feed type. However, feeds are used as a transport for michael@0: // different data types, e.g. news/blogs (traditional feed), video/audio michael@0: // (podcasts) and photos (photocasts, photostreams). Each of these is michael@0: // different in that there's a different class of application suitable for michael@0: // handling feeds of that type, but without a content-type differentiation michael@0: // it is difficult for us to disambiguate. michael@0: // michael@0: // The other problem is that if the user specifies an auto-action handler michael@0: // for one feed application, the fact that the content type is shared means michael@0: // that all other applications will auto-load with that handler too, michael@0: // regardless of the content-type. michael@0: // michael@0: // This means that content-type alone is not enough to determine whether michael@0: // or not a feed should be auto-handled. This means that for feeds we need michael@0: // to always use this stream converter, even when an auto-action is michael@0: // specified, not the basic one provided by WebContentConverter. This michael@0: // converter needs to consume all of the data and parse it, and based on michael@0: // that determination make a judgment about type. michael@0: // michael@0: // Since there are no content types for this content, and I'm not going to michael@0: // invent any, the upshot is that while a user can set an auto-handler for michael@0: // generic feed content, the system will prevent them from setting an auto- michael@0: // handler for other stream types. In those cases, the user will always see michael@0: // the preview page and have to select a handler. We can guess and show michael@0: // a client handler, but will not be able to show web handlers for those michael@0: // types. michael@0: // michael@0: // If this is just a feed, not some kind of specialized application, then michael@0: // auto-handlers can be set and we should obey them. michael@0: try { michael@0: var feedService = michael@0: Cc["@mozilla.org/browser/feeds/result-service;1"]. michael@0: getService(Ci.nsIFeedResultService); michael@0: if (!this._forcePreviewPage && result.doc) { michael@0: var feed = result.doc.QueryInterface(Ci.nsIFeed); michael@0: var handler = safeGetCharPref(getPrefActionForType(feed.type), "ask"); michael@0: michael@0: if (handler != "ask") { michael@0: if (handler == "reader") michael@0: handler = safeGetCharPref(getPrefReaderForType(feed.type), "bookmarks"); michael@0: switch (handler) { michael@0: case "web": michael@0: var wccr = michael@0: Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. michael@0: getService(Ci.nsIWebContentConverterService); michael@0: if ((feed.type == Ci.nsIFeed.TYPE_FEED && michael@0: wccr.getAutoHandler(TYPE_MAYBE_FEED)) || michael@0: (feed.type == Ci.nsIFeed.TYPE_VIDEO && michael@0: wccr.getAutoHandler(TYPE_MAYBE_VIDEO_FEED)) || michael@0: (feed.type == Ci.nsIFeed.TYPE_AUDIO && michael@0: wccr.getAutoHandler(TYPE_MAYBE_AUDIO_FEED))) { michael@0: wccr.loadPreferredHandler(this._request); michael@0: return; michael@0: } michael@0: break; michael@0: michael@0: default: michael@0: LOG("unexpected handler: " + handler); michael@0: // fall through -- let feed service handle error michael@0: case "bookmarks": michael@0: case "client": michael@0: try { michael@0: var title = feed.title ? feed.title.plainText() : ""; michael@0: var desc = feed.subtitle ? feed.subtitle.plainText() : ""; michael@0: feedService.addToClientReader(result.uri.spec, title, desc, feed.type); michael@0: return; michael@0: } catch(ex) { /* fallback to preview mode */ } michael@0: } michael@0: } michael@0: } michael@0: michael@0: var ios = michael@0: Cc["@mozilla.org/network/io-service;1"]. michael@0: getService(Ci.nsIIOService); michael@0: var chromeChannel; michael@0: michael@0: // If there was no automatic handler, or this was a podcast, michael@0: // photostream or some other kind of application, show the preview page michael@0: // if the parser returned a document. michael@0: if (result.doc) { michael@0: michael@0: // Store the result in the result service so that the display michael@0: // page can access it. michael@0: feedService.addFeedResult(result); michael@0: michael@0: // Now load the actual XUL document. michael@0: var aboutFeedsURI = ios.newURI("about:feeds", null, null); michael@0: chromeChannel = ios.newChannelFromURI(aboutFeedsURI, null); michael@0: chromeChannel.originalURI = result.uri; michael@0: chromeChannel.owner = michael@0: Services.scriptSecurityManager.getNoAppCodebasePrincipal(aboutFeedsURI); michael@0: } else { michael@0: chromeChannel = ios.newChannelFromURI(result.uri, null); michael@0: } michael@0: michael@0: chromeChannel.loadGroup = this._request.loadGroup; michael@0: chromeChannel.asyncOpen(this._listener, null); michael@0: } michael@0: finally { michael@0: this._releaseHandles(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * See nsIStreamListener.idl michael@0: */ michael@0: onDataAvailable: function FC_onDataAvailable(request, context, inputStream, michael@0: sourceOffset, count) { michael@0: if (this._processor) michael@0: this._processor.onDataAvailable(request, context, inputStream, michael@0: sourceOffset, count); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIRequestObserver.idl michael@0: */ michael@0: onStartRequest: function FC_onStartRequest(request, context) { michael@0: var channel = request.QueryInterface(Ci.nsIChannel); michael@0: michael@0: // Check for a header that tells us there was no sniffing michael@0: // The value doesn't matter. michael@0: try { michael@0: var httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); michael@0: // Make sure to check requestSucceeded before the potentially-throwing michael@0: // getResponseHeader. michael@0: if (!httpChannel.requestSucceeded) { michael@0: // Just give up, but don't forget to cancel the channel first! michael@0: request.cancel(Cr.NS_BINDING_ABORTED); michael@0: return; michael@0: } michael@0: var noSniff = httpChannel.getResponseHeader("X-Moz-Is-Feed"); michael@0: } michael@0: catch (ex) { michael@0: this._sniffed = true; michael@0: } michael@0: michael@0: this._request = request; michael@0: michael@0: // Save and reset the forced state bit early, in case there's some kind of michael@0: // error. michael@0: var feedService = michael@0: Cc["@mozilla.org/browser/feeds/result-service;1"]. michael@0: getService(Ci.nsIFeedResultService); michael@0: this._forcePreviewPage = feedService.forcePreviewPage; michael@0: feedService.forcePreviewPage = false; michael@0: michael@0: // Parse feed data as it comes in michael@0: this._processor = michael@0: Cc["@mozilla.org/feed-processor;1"]. michael@0: createInstance(Ci.nsIFeedProcessor); michael@0: this._processor.listener = this; michael@0: this._processor.parseAsync(null, channel.URI); michael@0: michael@0: this._processor.onStartRequest(request, context); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIRequestObserver.idl michael@0: */ michael@0: onStopRequest: function FC_onStopRequest(request, context, status) { michael@0: if (this._processor) michael@0: this._processor.onStopRequest(request, context, status); michael@0: }, michael@0: michael@0: /** michael@0: * See nsISupports.idl michael@0: */ michael@0: QueryInterface: function FC_QueryInterface(iid) { michael@0: if (iid.equals(Ci.nsIFeedResultListener) || michael@0: iid.equals(Ci.nsIStreamConverter) || michael@0: iid.equals(Ci.nsIStreamListener) || michael@0: iid.equals(Ci.nsIRequestObserver)|| michael@0: iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Keeps parsed FeedResults around for use elsewhere in the UI after the stream michael@0: * converter completes. michael@0: */ michael@0: function FeedResultService() { michael@0: } michael@0: michael@0: FeedResultService.prototype = { michael@0: classID: Components.ID("{2376201c-bbc6-472f-9b62-7548040a61c6}"), michael@0: michael@0: /** michael@0: * A URI spec -> [nsIFeedResult] hash. We have to keep a list as the michael@0: * value in case the same URI is requested concurrently. michael@0: */ michael@0: _results: { }, michael@0: michael@0: /** michael@0: * See nsIFeedResultService.idl michael@0: */ michael@0: forcePreviewPage: false, michael@0: michael@0: /** michael@0: * See nsIFeedResultService.idl michael@0: */ michael@0: addToClientReader: function FRS_addToClientReader(spec, title, subtitle, feedType) { michael@0: var prefs = michael@0: Cc["@mozilla.org/preferences-service;1"]. michael@0: getService(Ci.nsIPrefBranch); michael@0: michael@0: var handler = safeGetCharPref(getPrefActionForType(feedType), "bookmarks"); michael@0: if (handler == "ask" || handler == "reader") michael@0: handler = safeGetCharPref(getPrefReaderForType(feedType), "bookmarks"); michael@0: michael@0: switch (handler) { michael@0: case "client": michael@0: var clientApp = prefs.getComplexValue(getPrefAppForType(feedType), Ci.nsILocalFile); michael@0: michael@0: // For the benefit of applications that might know how to deal with more michael@0: // URLs than just feeds, send feed: URLs in the following format: michael@0: // michael@0: // http urls: replace scheme with feed, e.g. michael@0: // http://foo.com/index.rdf -> feed://foo.com/index.rdf michael@0: // other urls: prepend feed: scheme, e.g. michael@0: // https://foo.com/index.rdf -> feed:https://foo.com/index.rdf michael@0: var ios = michael@0: Cc["@mozilla.org/network/io-service;1"]. michael@0: getService(Ci.nsIIOService); michael@0: var feedURI = ios.newURI(spec, null, null); michael@0: if (feedURI.schemeIs("http")) { michael@0: feedURI.scheme = "feed"; michael@0: spec = feedURI.spec; michael@0: } michael@0: else michael@0: spec = "feed:" + spec; michael@0: michael@0: // Retrieving the shell service might fail on some systems, most michael@0: // notably systems where GNOME is not installed. michael@0: try { michael@0: var ss = michael@0: Cc["@mozilla.org/browser/shell-service;1"]. michael@0: getService(Ci.nsIShellService); michael@0: ss.openApplicationWithURI(clientApp, spec); michael@0: } catch(e) { michael@0: // If we couldn't use the shell service, fallback to using a michael@0: // nsIProcess instance michael@0: var p = michael@0: Cc["@mozilla.org/process/util;1"]. michael@0: createInstance(Ci.nsIProcess); michael@0: p.init(clientApp); michael@0: p.run(false, [spec], 1); michael@0: } michael@0: break; michael@0: michael@0: default: michael@0: // "web" should have been handled elsewhere michael@0: LOG("unexpected handler: " + handler); michael@0: // fall through michael@0: case "bookmarks": michael@0: var wm = michael@0: Cc["@mozilla.org/appshell/window-mediator;1"]. michael@0: getService(Ci.nsIWindowMediator); michael@0: var topWindow = wm.getMostRecentWindow("navigator:browser"); michael@0: topWindow.PlacesCommandHook.addLiveBookmark(spec, title, subtitle); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * See nsIFeedResultService.idl michael@0: */ michael@0: addFeedResult: function FRS_addFeedResult(feedResult) { michael@0: NS_ASSERT(feedResult.uri != null, "null URI!"); michael@0: NS_ASSERT(feedResult.uri != null, "null feedResult!"); michael@0: var spec = feedResult.uri.spec; michael@0: if(!this._results[spec]) michael@0: this._results[spec] = []; michael@0: this._results[spec].push(feedResult); michael@0: }, michael@0: michael@0: /** michael@0: * See nsIFeedResultService.idl michael@0: */ michael@0: getFeedResult: function RFS_getFeedResult(uri) { michael@0: NS_ASSERT(uri != null, "null URI!"); michael@0: var resultList = this._results[uri.spec]; michael@0: for (var i in resultList) { michael@0: if (resultList[i].uri == uri) michael@0: return resultList[i]; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * See nsIFeedResultService.idl michael@0: */ michael@0: removeFeedResult: function FRS_removeFeedResult(uri) { michael@0: NS_ASSERT(uri != null, "null URI!"); michael@0: var resultList = this._results[uri.spec]; michael@0: if (!resultList) michael@0: return; michael@0: var deletions = 0; michael@0: for (var i = 0; i < resultList.length; ++i) { michael@0: if (resultList[i].uri == uri) { michael@0: delete resultList[i]; michael@0: ++deletions; michael@0: } michael@0: } michael@0: michael@0: // send the holes to the end michael@0: resultList.sort(); michael@0: // and trim the list michael@0: resultList.splice(resultList.length - deletions, deletions); michael@0: if (resultList.length == 0) michael@0: delete this._results[uri.spec]; michael@0: }, michael@0: michael@0: createInstance: function FRS_createInstance(outer, iid) { michael@0: if (outer != null) michael@0: throw Cr.NS_ERROR_NO_AGGREGATION; michael@0: return this.QueryInterface(iid); michael@0: }, michael@0: michael@0: QueryInterface: function FRS_QueryInterface(iid) { michael@0: if (iid.equals(Ci.nsIFeedResultService) || michael@0: iid.equals(Ci.nsIFactory) || michael@0: iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: throw Cr.NS_ERROR_NOT_IMPLEMENTED; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * A protocol handler that attempts to deal with the variant forms of feed: michael@0: * URIs that are actually either http or https. michael@0: */ michael@0: function GenericProtocolHandler() { michael@0: } michael@0: GenericProtocolHandler.prototype = { michael@0: _init: function GPH_init(scheme) { michael@0: var ios = michael@0: Cc["@mozilla.org/network/io-service;1"]. michael@0: getService(Ci.nsIIOService); michael@0: this._http = ios.getProtocolHandler("http"); michael@0: this._scheme = scheme; michael@0: }, michael@0: michael@0: get scheme() { michael@0: return this._scheme; michael@0: }, michael@0: michael@0: get protocolFlags() { michael@0: return this._http.protocolFlags; michael@0: }, michael@0: michael@0: get defaultPort() { michael@0: return this._http.defaultPort; michael@0: }, michael@0: michael@0: allowPort: function GPH_allowPort(port, scheme) { michael@0: return this._http.allowPort(port, scheme); michael@0: }, michael@0: michael@0: newURI: function GPH_newURI(spec, originalCharset, baseURI) { michael@0: // Feed URIs can be either nested URIs of the form feed:realURI (in which michael@0: // case we create a nested URI for the realURI) or feed://example.com, in michael@0: // which case we create a nested URI for the real protocol which is http. michael@0: michael@0: var scheme = this._scheme + ":"; michael@0: if (spec.substr(0, scheme.length) != scheme) michael@0: throw Cr.NS_ERROR_MALFORMED_URI; michael@0: michael@0: var prefix = spec.substr(scheme.length, 2) == "//" ? "http:" : ""; michael@0: var inner = Cc["@mozilla.org/network/io-service;1"]. michael@0: getService(Ci.nsIIOService).newURI(spec.replace(scheme, prefix), michael@0: originalCharset, baseURI); michael@0: var netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil); michael@0: const URI_INHERITS_SECURITY_CONTEXT = Ci.nsIProtocolHandler michael@0: .URI_INHERITS_SECURITY_CONTEXT; michael@0: if (netutil.URIChainHasFlags(inner, URI_INHERITS_SECURITY_CONTEXT)) michael@0: throw Cr.NS_ERROR_MALFORMED_URI; michael@0: michael@0: var uri = netutil.newSimpleNestedURI(inner); michael@0: uri.spec = inner.spec.replace(prefix, scheme); michael@0: return uri; michael@0: }, michael@0: michael@0: newChannel: function GPH_newChannel(aUri) { michael@0: var inner = aUri.QueryInterface(Ci.nsINestedURI).innerURI; michael@0: var channel = Cc["@mozilla.org/network/io-service;1"]. michael@0: getService(Ci.nsIIOService).newChannelFromURI(inner, null); michael@0: if (channel instanceof Components.interfaces.nsIHttpChannel) michael@0: // Set this so we know this is supposed to be a feed michael@0: channel.setRequestHeader("X-Moz-Is-Feed", "1", false); michael@0: channel.originalURI = aUri; michael@0: return channel; michael@0: }, michael@0: michael@0: QueryInterface: function GPH_QueryInterface(iid) { michael@0: if (iid.equals(Ci.nsIProtocolHandler) || michael@0: iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: throw Cr.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: }; michael@0: michael@0: function FeedProtocolHandler() { michael@0: this._init('feed'); michael@0: } michael@0: FeedProtocolHandler.prototype = new GenericProtocolHandler(); michael@0: FeedProtocolHandler.prototype.classID = Components.ID("{4f91ef2e-57ba-472e-ab7a-b4999e42d6c0}"); michael@0: michael@0: function PodCastProtocolHandler() { michael@0: this._init('pcast'); michael@0: } michael@0: PodCastProtocolHandler.prototype = new GenericProtocolHandler(); michael@0: PodCastProtocolHandler.prototype.classID = Components.ID("{1c31ed79-accd-4b94-b517-06e0c81999d5}"); michael@0: michael@0: var components = [FeedConverter, michael@0: FeedResultService, michael@0: FeedProtocolHandler, michael@0: PodCastProtocolHandler]; michael@0: michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);