FeedConverter.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  6. Components.utils.import("resource://gre/modules/debug.js");
  7. Components.utils.import("resource://gre/modules/Services.jsm");
  8. const Cc = Components.classes;
  9. const Ci = Components.interfaces;
  10. const Cr = Components.results;
  11. function LOG(str) {
  12. dump("*** " + str + "\n");
  13. }
  14. const FS_CONTRACTID = "@mozilla.org/browser/feeds/result-service;1";
  15. const FPH_CONTRACTID = "@mozilla.org/network/protocol;1?name=feed";
  16. const PCPH_CONTRACTID = "@mozilla.org/network/protocol;1?name=pcast";
  17. const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
  18. const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed";
  19. const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed";
  20. const TYPE_ANY = "*/*";
  21. const PREF_SELECTED_APP = "browser.feeds.handlers.application";
  22. const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice";
  23. const PREF_SELECTED_ACTION = "browser.feeds.handler";
  24. const PREF_SELECTED_READER = "browser.feeds.handler.default";
  25. const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application";
  26. const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice";
  27. const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler";
  28. const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default";
  29. const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application";
  30. const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice";
  31. const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler";
  32. const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default";
  33. function getPrefAppForType(t) {
  34. switch (t) {
  35. case Ci.nsIFeed.TYPE_VIDEO:
  36. return PREF_VIDEO_SELECTED_APP;
  37. case Ci.nsIFeed.TYPE_AUDIO:
  38. return PREF_AUDIO_SELECTED_APP;
  39. default:
  40. return PREF_SELECTED_APP;
  41. }
  42. }
  43. function getPrefWebForType(t) {
  44. switch (t) {
  45. case Ci.nsIFeed.TYPE_VIDEO:
  46. return PREF_VIDEO_SELECTED_WEB;
  47. case Ci.nsIFeed.TYPE_AUDIO:
  48. return PREF_AUDIO_SELECTED_WEB;
  49. default:
  50. return PREF_SELECTED_WEB;
  51. }
  52. }
  53. function getPrefActionForType(t) {
  54. switch (t) {
  55. case Ci.nsIFeed.TYPE_VIDEO:
  56. return PREF_VIDEO_SELECTED_ACTION;
  57. case Ci.nsIFeed.TYPE_AUDIO:
  58. return PREF_AUDIO_SELECTED_ACTION;
  59. default:
  60. return PREF_SELECTED_ACTION;
  61. }
  62. }
  63. function getPrefReaderForType(t) {
  64. switch (t) {
  65. case Ci.nsIFeed.TYPE_VIDEO:
  66. return PREF_VIDEO_SELECTED_READER;
  67. case Ci.nsIFeed.TYPE_AUDIO:
  68. return PREF_AUDIO_SELECTED_READER;
  69. default:
  70. return PREF_SELECTED_READER;
  71. }
  72. }
  73. function safeGetCharPref(pref, defaultValue) {
  74. var prefs =
  75. Cc["@mozilla.org/preferences-service;1"].
  76. getService(Ci.nsIPrefBranch);
  77. try {
  78. return prefs.getCharPref(pref);
  79. }
  80. catch (e) {
  81. }
  82. return defaultValue;
  83. }
  84. function FeedConverter() {
  85. }
  86. FeedConverter.prototype = {
  87. classID: Components.ID("{229fa115-9412-4d32-baf3-2fc407f76fb1}"),
  88. /**
  89. * This is the downloaded text data for the feed.
  90. */
  91. _data: null,
  92. /**
  93. * This is the object listening to the conversion, which is ultimately the
  94. * docshell for the load.
  95. */
  96. _listener: null,
  97. /**
  98. * Records if the feed was sniffed
  99. */
  100. _sniffed: false,
  101. /**
  102. * See nsIStreamConverter.idl
  103. */
  104. convert: function(sourceStream, sourceType, destinationType,
  105. context) {
  106. throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  107. },
  108. /**
  109. * See nsIStreamConverter.idl
  110. */
  111. asyncConvertData: function(sourceType, destinationType,
  112. listener, context) {
  113. this._listener = listener;
  114. },
  115. /**
  116. * Whether or not the preview page is being forced.
  117. */
  118. _forcePreviewPage: false,
  119. /**
  120. * Release our references to various things once we're done using them.
  121. */
  122. _releaseHandles: function() {
  123. this._listener = null;
  124. this._request = null;
  125. this._processor = null;
  126. },
  127. /**
  128. * See nsIFeedResultListener.idl
  129. */
  130. handleResult: function(result) {
  131. // Feeds come in various content types, which our feed sniffer coerces to
  132. // the maybe.feed type. However, feeds are used as a transport for
  133. // different data types, e.g. news/blogs (traditional feed), video/audio
  134. // (podcasts) and photos (photocasts, photostreams). Each of these is
  135. // different in that there's a different class of application suitable for
  136. // handling feeds of that type, but without a content-type differentiation
  137. // it is difficult for us to disambiguate.
  138. //
  139. // The other problem is that if the user specifies an auto-action handler
  140. // for one feed application, the fact that the content type is shared means
  141. // that all other applications will auto-load with that handler too,
  142. // regardless of the content-type.
  143. //
  144. // This means that content-type alone is not enough to determine whether
  145. // or not a feed should be auto-handled. This means that for feeds we need
  146. // to always use this stream converter, even when an auto-action is
  147. // specified, not the basic one provided by WebContentConverter. This
  148. // converter needs to consume all of the data and parse it, and based on
  149. // that determination make a judgment about type.
  150. //
  151. // Since there are no content types for this content, and I'm not going to
  152. // invent any, the upshot is that while a user can set an auto-handler for
  153. // generic feed content, the system will prevent them from setting an auto-
  154. // handler for other stream types. In those cases, the user will always see
  155. // the preview page and have to select a handler. We can guess and show
  156. // a client handler, but will not be able to show web handlers for those
  157. // types.
  158. //
  159. // If this is just a feed, not some kind of specialized application, then
  160. // auto-handlers can be set and we should obey them.
  161. try {
  162. var feedService =
  163. Cc["@mozilla.org/browser/feeds/result-service;1"].
  164. getService(Ci.nsIFeedResultService);
  165. if (!this._forcePreviewPage && result.doc) {
  166. var feed = result.doc.QueryInterface(Ci.nsIFeed);
  167. var handler = safeGetCharPref(getPrefActionForType(feed.type), "ask");
  168. if (handler != "ask") {
  169. if (handler == "reader")
  170. handler = safeGetCharPref(getPrefReaderForType(feed.type), "bookmarks");
  171. switch (handler) {
  172. case "web":
  173. var wccr =
  174. Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
  175. getService(Ci.nsIWebContentConverterService);
  176. if ((feed.type == Ci.nsIFeed.TYPE_FEED &&
  177. wccr.getAutoHandler(TYPE_MAYBE_FEED)) ||
  178. (feed.type == Ci.nsIFeed.TYPE_VIDEO &&
  179. wccr.getAutoHandler(TYPE_MAYBE_VIDEO_FEED)) ||
  180. (feed.type == Ci.nsIFeed.TYPE_AUDIO &&
  181. wccr.getAutoHandler(TYPE_MAYBE_AUDIO_FEED))) {
  182. wccr.loadPreferredHandler(this._request);
  183. return;
  184. }
  185. break;
  186. default:
  187. LOG("unexpected handler: " + handler);
  188. // fall through -- let feed service handle error
  189. case "bookmarks":
  190. case "client":
  191. try {
  192. var title = feed.title ? feed.title.plainText() : "";
  193. var desc = feed.subtitle ? feed.subtitle.plainText() : "";
  194. feedService.addToClientReader(result.uri.spec, title, desc, feed.type);
  195. return;
  196. } catch(ex) { /* fallback to preview mode */ }
  197. }
  198. }
  199. }
  200. var ios =
  201. Cc["@mozilla.org/network/io-service;1"].
  202. getService(Ci.nsIIOService);
  203. var chromeChannel;
  204. // handling a redirect, hence forwarding the loadInfo from the old channel
  205. // to the newchannel.
  206. var oldChannel = this._request.QueryInterface(Ci.nsIChannel);
  207. var loadInfo = oldChannel.loadInfo;
  208. // If there was no automatic handler, or this was a podcast,
  209. // photostream or some other kind of application, show the preview page
  210. // if the parser returned a document.
  211. if (result.doc) {
  212. // Store the result in the result service so that the display
  213. // page can access it.
  214. feedService.addFeedResult(result);
  215. // Now load the actual XUL document.
  216. var aboutFeedsURI = ios.newURI("about:feeds", null, null);
  217. chromeChannel = ios.newChannelFromURIWithLoadInfo(aboutFeedsURI, loadInfo);
  218. chromeChannel.originalURI = result.uri;
  219. chromeChannel.owner =
  220. Services.scriptSecurityManager.getNoAppCodebasePrincipal(aboutFeedsURI);
  221. } else {
  222. chromeChannel = ios.newChannelFromURIWithLoadInfo(result.uri, loadInfo);
  223. }
  224. chromeChannel.loadGroup = this._request.loadGroup;
  225. chromeChannel.asyncOpen2(this._listener);
  226. }
  227. finally {
  228. this._releaseHandles();
  229. }
  230. },
  231. /**
  232. * See nsIStreamListener.idl
  233. */
  234. onDataAvailable: function(request, context, inputStream,
  235. sourceOffset, count) {
  236. if (this._processor)
  237. this._processor.onDataAvailable(request, context, inputStream,
  238. sourceOffset, count);
  239. },
  240. /**
  241. * See nsIRequestObserver.idl
  242. */
  243. onStartRequest: function(request, context) {
  244. var channel = request.QueryInterface(Ci.nsIChannel);
  245. // Check for a header that tells us there was no sniffing
  246. // The value doesn't matter.
  247. try {
  248. var httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
  249. // Make sure to check requestSucceeded before the potentially-throwing
  250. // getResponseHeader.
  251. if (!httpChannel.requestSucceeded) {
  252. // Just give up, but don't forget to cancel the channel first!
  253. request.cancel(Cr.NS_BINDING_ABORTED);
  254. return;
  255. }
  256. var noSniff = httpChannel.getResponseHeader("X-Moz-Is-Feed");
  257. }
  258. catch (ex) {
  259. this._sniffed = true;
  260. }
  261. this._request = request;
  262. // Save and reset the forced state bit early, in case there's some kind of
  263. // error.
  264. var feedService =
  265. Cc["@mozilla.org/browser/feeds/result-service;1"].
  266. getService(Ci.nsIFeedResultService);
  267. this._forcePreviewPage = feedService.forcePreviewPage;
  268. feedService.forcePreviewPage = false;
  269. // Parse feed data as it comes in
  270. this._processor =
  271. Cc["@mozilla.org/feed-processor;1"].
  272. createInstance(Ci.nsIFeedProcessor);
  273. this._processor.listener = this;
  274. this._processor.parseAsync(null, channel.URI);
  275. this._processor.onStartRequest(request, context);
  276. },
  277. /**
  278. * See nsIRequestObserver.idl
  279. */
  280. onStopRequest: function(request, context, status) {
  281. if (this._processor)
  282. this._processor.onStopRequest(request, context, status);
  283. },
  284. /**
  285. * See nsISupports.idl
  286. */
  287. QueryInterface: function(iid) {
  288. if (iid.equals(Ci.nsIFeedResultListener) ||
  289. iid.equals(Ci.nsIStreamConverter) ||
  290. iid.equals(Ci.nsIStreamListener) ||
  291. iid.equals(Ci.nsIRequestObserver)||
  292. iid.equals(Ci.nsISupports))
  293. return this;
  294. throw Cr.NS_ERROR_NO_INTERFACE;
  295. },
  296. };
  297. /**
  298. * Keeps parsed FeedResults around for use elsewhere in the UI after the stream
  299. * converter completes.
  300. */
  301. function FeedResultService() {
  302. }
  303. FeedResultService.prototype = {
  304. classID: Components.ID("{2376201c-bbc6-472f-9b62-7548040a61c6}"),
  305. /**
  306. * A URI spec -> [nsIFeedResult] hash. We have to keep a list as the
  307. * value in case the same URI is requested concurrently.
  308. */
  309. _results: { },
  310. /**
  311. * See nsIFeedResultService.idl
  312. */
  313. forcePreviewPage: false,
  314. /**
  315. * See nsIFeedResultService.idl
  316. */
  317. addToClientReader: function(spec, title, subtitle, feedType) {
  318. var prefs =
  319. Cc["@mozilla.org/preferences-service;1"].
  320. getService(Ci.nsIPrefBranch);
  321. var handler = safeGetCharPref(getPrefActionForType(feedType), "bookmarks");
  322. if (handler == "ask" || handler == "reader")
  323. handler = safeGetCharPref(getPrefReaderForType(feedType), "bookmarks");
  324. switch (handler) {
  325. case "client":
  326. var clientApp = prefs.getComplexValue(getPrefAppForType(feedType), Ci.nsILocalFile);
  327. // For the benefit of applications that might know how to deal with more
  328. // URLs than just feeds, send feed: URLs in the following format:
  329. //
  330. // http urls: replace scheme with feed, e.g.
  331. // http://foo.com/index.rdf -> feed://foo.com/index.rdf
  332. // other urls: prepend feed: scheme, e.g.
  333. // https://foo.com/index.rdf -> feed:https://foo.com/index.rdf
  334. var ios =
  335. Cc["@mozilla.org/network/io-service;1"].
  336. getService(Ci.nsIIOService);
  337. var feedURI = ios.newURI(spec, null, null);
  338. if (feedURI.schemeIs("http")) {
  339. feedURI.scheme = "feed";
  340. spec = feedURI.spec;
  341. }
  342. else
  343. spec = "feed:" + spec;
  344. // Retrieving the shell service might fail on some systems, most
  345. // notably systems where GNOME is not installed.
  346. try {
  347. var ss =
  348. Cc["@mozilla.org/browser/shell-service;1"].
  349. getService(Ci.nsIShellService);
  350. ss.openApplicationWithURI(clientApp, spec);
  351. } catch(e) {
  352. // If we couldn't use the shell service, fallback to using a
  353. // nsIProcess instance
  354. var p =
  355. Cc["@mozilla.org/process/util;1"].
  356. createInstance(Ci.nsIProcess);
  357. p.init(clientApp);
  358. p.run(false, [spec], 1);
  359. }
  360. break;
  361. default:
  362. // "web" should have been handled elsewhere
  363. LOG("unexpected handler: " + handler);
  364. // fall through
  365. case "bookmarks":
  366. var wm =
  367. Cc["@mozilla.org/appshell/window-mediator;1"].
  368. getService(Ci.nsIWindowMediator);
  369. var topWindow = wm.getMostRecentWindow("navigator:browser");
  370. topWindow.PlacesCommandHook.addLiveBookmark(spec, title, subtitle);
  371. break;
  372. }
  373. },
  374. /**
  375. * See nsIFeedResultService.idl
  376. */
  377. addFeedResult: function(feedResult) {
  378. NS_ASSERT(feedResult.uri != null, "null URI!");
  379. NS_ASSERT(feedResult.uri != null, "null feedResult!");
  380. var spec = feedResult.uri.spec;
  381. if(!this._results[spec])
  382. this._results[spec] = [];
  383. this._results[spec].push(feedResult);
  384. },
  385. /**
  386. * See nsIFeedResultService.idl
  387. */
  388. getFeedResult: function(uri) {
  389. NS_ASSERT(uri != null, "null URI!");
  390. var resultList = this._results[uri.spec];
  391. for (var i in resultList) {
  392. if (resultList[i].uri == uri)
  393. return resultList[i];
  394. }
  395. return null;
  396. },
  397. /**
  398. * See nsIFeedResultService.idl
  399. */
  400. removeFeedResult: function(uri) {
  401. NS_ASSERT(uri != null, "null URI!");
  402. var resultList = this._results[uri.spec];
  403. if (!resultList)
  404. return;
  405. var deletions = 0;
  406. for (var i = 0; i < resultList.length; ++i) {
  407. if (resultList[i].uri == uri) {
  408. delete resultList[i];
  409. ++deletions;
  410. }
  411. }
  412. // send the holes to the end
  413. resultList.sort();
  414. // and trim the list
  415. resultList.splice(resultList.length - deletions, deletions);
  416. if (resultList.length == 0)
  417. delete this._results[uri.spec];
  418. },
  419. createInstance: function(outer, iid) {
  420. if (outer != null)
  421. throw Cr.NS_ERROR_NO_AGGREGATION;
  422. return this.QueryInterface(iid);
  423. },
  424. QueryInterface: function(iid) {
  425. if (iid.equals(Ci.nsIFeedResultService) ||
  426. iid.equals(Ci.nsIFactory) ||
  427. iid.equals(Ci.nsISupports))
  428. return this;
  429. throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  430. },
  431. };
  432. /**
  433. * A protocol handler that attempts to deal with the variant forms of feed:
  434. * URIs that are actually either http or https.
  435. */
  436. function GenericProtocolHandler() {
  437. }
  438. GenericProtocolHandler.prototype = {
  439. _init: function(scheme) {
  440. var ios =
  441. Cc["@mozilla.org/network/io-service;1"].
  442. getService(Ci.nsIIOService);
  443. this._http = ios.getProtocolHandler("http");
  444. this._scheme = scheme;
  445. },
  446. get scheme() {
  447. return this._scheme;
  448. },
  449. get protocolFlags() {
  450. return this._http.protocolFlags;
  451. },
  452. get defaultPort() {
  453. return this._http.defaultPort;
  454. },
  455. allowPort: function(port, scheme) {
  456. return this._http.allowPort(port, scheme);
  457. },
  458. newURI: function(spec, originalCharset, baseURI) {
  459. // Feed URIs can be either nested URIs of the form feed:realURI (in which
  460. // case we create a nested URI for the realURI) or feed://example.com, in
  461. // which case we create a nested URI for the real protocol which is http.
  462. var scheme = this._scheme + ":";
  463. if (spec.substr(0, scheme.length) != scheme)
  464. throw Cr.NS_ERROR_MALFORMED_URI;
  465. var prefix = spec.substr(scheme.length, 2) == "//" ? "http:" : "";
  466. var inner = Cc["@mozilla.org/network/io-service;1"].
  467. getService(Ci.nsIIOService).newURI(spec.replace(scheme, prefix),
  468. originalCharset, baseURI);
  469. var netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
  470. const URI_INHERITS_SECURITY_CONTEXT = Ci.nsIProtocolHandler
  471. .URI_INHERITS_SECURITY_CONTEXT;
  472. if (netutil.URIChainHasFlags(inner, URI_INHERITS_SECURITY_CONTEXT))
  473. throw Cr.NS_ERROR_MALFORMED_URI;
  474. var uri = netutil.newSimpleNestedURI(inner);
  475. uri.spec = inner.spec.replace(prefix, scheme);
  476. return uri;
  477. },
  478. newChannel2: function(aUri, aLoadInfo) {
  479. var inner = aUri.QueryInterface(Ci.nsINestedURI).innerURI;
  480. var channel = Cc["@mozilla.org/network/io-service;1"].
  481. getService(Ci.nsIIOService).
  482. newChannelFromURIWithLoadInfo(inner, aLoadInfo);
  483. if (channel instanceof Components.interfaces.nsIHttpChannel)
  484. // Set this so we know this is supposed to be a feed
  485. channel.setRequestHeader("X-Moz-Is-Feed", "1", false);
  486. channel.originalURI = aUri;
  487. return channel;
  488. },
  489. QueryInterface: function(iid) {
  490. if (iid.equals(Ci.nsIProtocolHandler) ||
  491. iid.equals(Ci.nsISupports))
  492. return this;
  493. throw Cr.NS_ERROR_NO_INTERFACE;
  494. }
  495. };
  496. function FeedProtocolHandler() {
  497. this._init('feed');
  498. }
  499. FeedProtocolHandler.prototype = new GenericProtocolHandler();
  500. FeedProtocolHandler.prototype.classID = Components.ID("{4f91ef2e-57ba-472e-ab7a-b4999e42d6c0}");
  501. function PodCastProtocolHandler() {
  502. this._init('pcast');
  503. }
  504. PodCastProtocolHandler.prototype = new GenericProtocolHandler();
  505. PodCastProtocolHandler.prototype.classID = Components.ID("{1c31ed79-accd-4b94-b517-06e0c81999d5}");
  506. var components = [FeedConverter,
  507. FeedResultService,
  508. FeedProtocolHandler,
  509. PodCastProtocolHandler];
  510. this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);