1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568
|
/* -*- 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(sourceStream, sourceType, destinationType,
context) {
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
},
/**
* See nsIStreamConverter.idl
*/
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() {
this._listener = null;
this._request = null;
this._processor = null;
},
/**
* See nsIFeedResultListener.idl
*/
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 {
let feedService =
Cc["@mozilla.org/browser/feeds/result-service;1"].
getService(Ci.nsIFeedResultService);
if (!this._forcePreviewPage && result.doc) {
let feed = result.doc.QueryInterface(Ci.nsIFeed);
let handler = safeGetCharPref(getPrefActionForType(feed.type), "ask");
if (handler != "ask") {
if (handler == "reader")
handler = safeGetCharPref(getPrefReaderForType(feed.type), "bookmarks");
switch (handler) {
case "web":
let 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":
case "default":
try {
let title = feed.title ? feed.title.plainText() : "";
let desc = feed.subtitle ? feed.subtitle.plainText() : "";
feedService.addToClientReader(result.uri.spec, title, desc, feed.type, handler);
return;
} catch (ex) { /* fallback to preview mode */ }
}
}
}
let ios =
Cc["@mozilla.org/network/io-service;1"].
getService(Ci.nsIIOService);
let chromeChannel;
// handling a redirect, hence forwarding the loadInfo from the old channel
// to the newchannel.
let oldChannel = this._request.QueryInterface(Ci.nsIChannel);
let 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.
let aboutFeedsURI = ios.newURI("about:feeds", null, null);
chromeChannel = ios.newChannelFromURIWithLoadInfo(aboutFeedsURI, loadInfo);
chromeChannel.originalURI = result.uri;
// carry the origin attributes from the channel that loaded the feed.
chromeChannel.owner =
Services.scriptSecurityManager.createCodebasePrincipal(aboutFeedsURI,
loadInfo.originAttributes);
} else {
chromeChannel = ios.newChannelFromURIWithLoadInfo(result.uri, loadInfo);
}
chromeChannel.loadGroup = this._request.loadGroup;
chromeChannel.asyncOpen(this._listener, null);
}
finally {
this._releaseHandles();
}
},
/**
* See nsIStreamListener.idl
*/
onDataAvailable(request, context, inputStream,
sourceOffset, count) {
if (this._processor)
this._processor.onDataAvailable(request, context, inputStream,
sourceOffset, count);
},
/**
* See nsIRequestObserver.idl
*/
onStartRequest(request, context) {
let channel = request.QueryInterface(Ci.nsIChannel);
// Check for a header that tells us there was no sniffing
// The value doesn't matter.
try {
let 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;
}
// Note: this throws if the header is not set.
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.
let 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(request, context, status) {
if (this._processor)
this._processor.onStopRequest(request, context, status);
},
/**
* See nsISupports.idl
*/
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(spec, title, subtitle, feedType, feedReader) {
if (!feedReader) {
feedReader = "default";
}
let handler = safeGetCharPref(getPrefActionForType(feedType), "bookmarks");
if (handler == "ask" || handler == "reader")
handler = feedReader;
switch (handler) {
case "client":
Services.cpmm.sendAsyncMessage("FeedConverter:ExecuteClientApp",
{ spec,
title,
subtitle,
feedHandler: getPrefAppForType(feedType) });
break;
case "default":
// Default system feed reader
Services.cpmm.sendAsyncMessage("FeedConverter:ExecuteClientApp",
{ spec,
title,
subtitle,
feedHandler: "default" });
break;
default:
// "web" should have been handled elsewhere
LOG("unexpected handler: " + handler);
// fall through
case "bookmarks":
Services.cpmm.sendAsyncMessage("FeedConverter:addLiveBookmark",
{ spec, title, subtitle });
break;
}
},
/**
* See nsIFeedResultService.idl
*/
addFeedResult(feedResult) {
NS_ASSERT(feedResult.uri != null, "null URI!");
NS_ASSERT(feedResult.uri != null, "null feedResult!");
let spec = feedResult.uri.spec;
if (!this._results[spec])
this._results[spec] = [];
this._results[spec].push(feedResult);
},
/**
* See nsIFeedResultService.idl
*/
getFeedResult(uri) {
NS_ASSERT(uri != null, "null URI!");
let resultList = this._results[uri.spec];
for (let result of resultList) {
if (result.uri == uri)
return result;
}
return null;
},
/**
* See nsIFeedResultService.idl
*/
removeFeedResult(uri) {
NS_ASSERT(uri != null, "null URI!");
let resultList = this._results[uri.spec];
if (!resultList)
return;
let deletions = 0;
for (let 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(outer, iid) {
if (outer != null)
throw Cr.NS_ERROR_NO_AGGREGATION;
return this.QueryInterface(iid);
},
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(scheme) {
let 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() {
let {URI_DANGEROUS_TO_LOAD, ALLOWS_PROXY_HTTP, ALLOWS_PROXY} =
Ci.nsIProtocolHandler;
return URI_DANGEROUS_TO_LOAD | ALLOWS_PROXY | ALLOWS_PROXY_HTTP;
},
get defaultPort() {
return this._http.defaultPort;
},
allowPort(port, scheme) {
return this._http.allowPort(port, scheme);
},
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.
let scheme = this._scheme + ":";
if (spec.substr(0, scheme.length) != scheme)
throw Cr.NS_ERROR_MALFORMED_URI;
let prefix = spec.substr(scheme.length, 2) == "//" ? "http:" : "";
let inner = Services.io.newURI(spec.replace(scheme, prefix),
originalCharset, baseURI);
if (!["http", "https"].includes(inner.scheme))
throw Cr.NS_ERROR_MALFORMED_URI;
let uri = Services.io.QueryInterface(Ci.nsINetUtil).newSimpleNestedURI(inner);
uri.spec = inner.spec.replace(prefix, scheme);
return uri;
},
newChannel2(aUri, aLoadInfo) {
let inner = aUri.QueryInterface(Ci.nsINestedURI).innerURI;
let 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(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);
|