browser/components/feeds/src/FeedConverter.js

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

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

mercurial