diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/components/feeds | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/suite/components/feeds')
-rw-r--r-- | comm/suite/components/feeds/FeedConverter.js | 461 | ||||
-rw-r--r-- | comm/suite/components/feeds/FeedWriter.js | 1211 | ||||
-rw-r--r-- | comm/suite/components/feeds/SuiteFeeds.manifest | 11 | ||||
-rw-r--r-- | comm/suite/components/feeds/WebContentConverter.js | 818 | ||||
-rw-r--r-- | comm/suite/components/feeds/content/subscribe.css | 7 | ||||
-rw-r--r-- | comm/suite/components/feeds/content/subscribe.xhtml | 59 | ||||
-rw-r--r-- | comm/suite/components/feeds/content/subscribe.xml | 42 | ||||
-rw-r--r-- | comm/suite/components/feeds/jar.mn | 8 | ||||
-rw-r--r-- | comm/suite/components/feeds/moz.build | 27 | ||||
-rw-r--r-- | comm/suite/components/feeds/nsFeedSniffer.cpp | 356 | ||||
-rw-r--r-- | comm/suite/components/feeds/nsFeedSniffer.h | 35 | ||||
-rw-r--r-- | comm/suite/components/feeds/nsIFeedResultService.idl | 66 | ||||
-rw-r--r-- | comm/suite/components/feeds/nsIWebContentConverterRegistrar.idl | 116 |
13 files changed, 3217 insertions, 0 deletions
diff --git a/comm/suite/components/feeds/FeedConverter.js b/comm/suite/components/feeds/FeedConverter.js new file mode 100644 index 0000000000..153f2a803d --- /dev/null +++ b/comm/suite/components/feeds/FeedConverter.js @@ -0,0 +1,461 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +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 FEEDHANDLER_URI = "about:feeds"; + +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 LOG(str) { + if (Services.prefs.getBoolPref("feeds.log", false)) + dump("*** Feeds: " + str + "\n"); +} + +function FeedConverter() { +} + +FeedConverter.prototype = { + /** + * 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 nsISupports.idl + */ + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIFeedResultListener, + Ci.nsIStreamConverter, + Ci.nsIStreamListener, + Ci.nsIRequestObserver]), + classID: Components.ID("{88592f45-3866-4c8e-9d8a-ab58b290fcf7}"), + + /** + * See nsIStreamConverter.idl + */ + convert: function convert(sourceStream, sourceType, destinationType, + context) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * See nsIStreamConverter.idl + */ + asyncConvertData: function 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 _releaseHandles() { + this._listener = null; + this._request = null; + this._processor = null; + }, + + /** + * See nsIFeedResultListener.idl + */ + handleResult: function 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. Therefore 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 judgement 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 = Services.prefs.getCharPref(getPrefActionForType(feed.type), "ask"); + + if (handler != "ask") { + if (handler == "reader") + handler = Services.prefs.getCharPref(getPrefReaderForType(feed.type), "messenger"); + 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": + case "messenger": + 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 chromeChannel; + 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 chromeURI = Services.io.newURI(FEEDHANDLER_URI); + chromeChannel = Services.io.newChannelFromURIWithLoadInfo(chromeURI, loadInfo); + // carry the origin attributes from the channel that loaded the feed. + chromeChannel.owner = Services.scriptSecurityManager + .createCodebasePrincipal(chromeURI, + loadInfo.originAttributes); + chromeChannel.originalURI = result.uri; + } + else + chromeChannel = Services.io.newChannelFromURIWithLoadInfo(result.uri, loadInfo); + + chromeChannel.loadGroup = this._request.loadGroup; + chromeChannel.asyncOpen2(this._listener); + } + finally { + this._releaseHandles(); + } + }, + + /** + * See nsIStreamListener.idl + */ + onDataAvailable: function onDataAvailable(request, context, inputStream, + sourceOffset, count) { + if (this._processor) + this._processor.onDataAvailable(request, context, inputStream, + sourceOffset, count); + }, + + /** + * See nsIRequestObserver.idl + */ + onStartRequest: function 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; + } + // 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. + 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 onStopRequest(request, context, status) { + if (this._processor) + this._processor.onStopRequest(request, context, status); + } + +}; + +/** + * Keeps parsed FeedResults around for use elsewhere in the UI after the stream + * converter completes. + */ +function FeedResultService() { +} + +FeedResultService.prototype = { + /** + * 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 addToClientReader(spec, title, subtitle, feedType) { + var handler = Services.prefs.getCharPref(getPrefActionForType(feedType), "reader"); + if (handler == "ask" || handler == "reader") + handler = Services.prefs.getCharPref(getPrefReaderForType(feedType), "messenger"); + + switch (handler) { + case "client": + var clientApp = Services.prefs.getComplexValue(getPrefAppForType(feedType), + Ci.nsIFile); + + // 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 feedURI = Services.io.newURI(spec); + 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/suite/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 topWindow = Services.wm.getMostRecentWindow("navigator:browser"); + topWindow.PlacesCommandHook.addLiveBookmark(spec, title, subtitle) + .catch(Cu.reportError); + break; + case "messenger": + Cc["@mozilla.org/newsblog-feed-downloader;1"] + .getService(Ci.nsINewsBlogFeedDownloader) + .subscribeToFeed("feed:" + spec, null, null); + break; + + } + }, + + /** + * See nsIFeedResultService.idl + */ + addFeedResult: function addFeedResult(feedResult) { + if (feedResult == null) + throw new Error("null feedResult!"); + if (feedResult.uri == null) + throw new Error("null URI!"); + var spec = feedResult.uri.spec; + if (!this._results[spec]) + this._results[spec] = []; + this._results[spec].push(feedResult); + }, + + /** + * See nsIFeedResultService.idl + */ + getFeedResult: function getFeedResult(uri) { + if (uri == null) + throw new Error("null URI!"); + var resultList = this._results[uri.spec]; + for (let i = 0; i < resultList.length; ++i) { + if (resultList[i].uri == uri) + return resultList[i]; + } + return null; + }, + + /** + * See nsIFeedResultService.idl + */ + removeFeedResult: function removeFeedResult(uri) { + if (uri == null) + throw new Error("null URI!"); + var resultList = this._results[uri.spec]; + if (!resultList) + return; + var 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]; + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFeedResultService]), + classID: Components.ID("{e5b05e9d-f037-48e4-b9a4-b99476582927}") +}; + +var components = [FeedConverter, + FeedResultService]; + +var NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/comm/suite/components/feeds/FeedWriter.js b/comm/suite/components/feeds/FeedWriter.js new file mode 100644 index 0000000000..c02e914caf --- /dev/null +++ b/comm/suite/components/feeds/FeedWriter.js @@ -0,0 +1,1211 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +var {NetUtil} = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +var {AppConstants} = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +const FEEDWRITER_CID = Components.ID("{49bb6593-3aff-4eb3-a068-2712c28bd58e}"); +const FEEDWRITER_CONTRACTID = "@mozilla.org/browser/feeds/result-writer;1"; + +const XML_NS = "http://www.w3.org/XML/1998/namespace"; +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; +const STRING_BUNDLE_URI = "chrome://communicator/locale/feeds/subscribe.properties"; + +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"; + +const PREF_SHOW_FIRST_RUN_UI = "browser.feeds.showFirstRunUI"; + +const TITLE_ID = "feedTitleText"; +const SUBTITLE_ID = "feedSubtitleText"; + +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 LOG(str) { + if (Services.prefs.getBoolPref("feeds.log", false)) + dump("*** Feeds: " + str + "\n"); +} + +/** + * Wrapper function for nsIIOService::newURI. + * @param aURLSpec + * The URL string from which to create an nsIURI. + * @returns an nsIURI object, or null if the creation of the URI failed. + */ +function makeURI(aURLSpec, aCharset) { + try { + return Services.io.newURI(aURLSpec, aCharset); + } catch (ex) { + } + + return null; +} + +/** + * Converts a number of bytes to the appropriate unit that results in a + * number that needs fewer than 4 digits + * + * @return a pair: [new value with 3 sig. figs., its unit] + */ +function convertByteUnits(aBytes) { + var units = ["bytes", "kilobytes", "megabytes", "gigabytes"]; + var unitIndex = 0; + + // convert to next unit if it needs 4 digits (after rounding), but only if + // we know the name of the next unit + while ((aBytes >= 999.5) && (unitIndex < units.length - 1)) { + aBytes /= 1024; + unitIndex++; + } + + // Get rid of insignificant bits by truncating to 1 or 0 decimal points + // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235 + aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) ? 1 : 0); + + return [aBytes, units[unitIndex]]; +} + +function FeedWriter() { + this._mimeSvc = Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService); +} + +FeedWriter.prototype = { + _getPropertyAsBag: function getPropertyAsBag(container, property) { + return container.fields.getProperty(property) + .QueryInterface(Ci.nsIPropertyBag2); + }, + + _getPropertyAsString: function getPropertyAsString(container, property) { + try { + return container.fields.getPropertyAsAString(property); + } + catch (e) { + } + return ""; + }, + + /** + * @param element + * The element to add the text content to. + * @param text + * An nsIFeedTextConstruct + */ + _setContentText: function setContentText(element, text) { + if (typeof element == "string") + element = this._document.getElementById(element); + + // Takes the content of the nsIFeedTextConstruct and creates a + // sanitized documentFragment. + var docFragment = text.createDocumentFragment(element); + element.innerHTML = ""; + element.appendChild(docFragment); + if (text.base) + element.setAttributeNS(XML_NS, "base", text.base.spec); + }, + + /** + * Safely sets the href attribute on an anchor tag, providing the URI + * specified can be loaded according to rules. + * @param element + * The element to set a URI attribute on + * @param attribute + * The attribute of the element to set the URI to, e.g. href or src + * @param uri + * The URI spec to set as the href + */ + _safeSetURIAttribute: function safeSetURIAttribute(element, attribute, uri) { + const flags = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; + try { + Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(this._feedPrincipal, uri, flags); + // checkLoadURIStrWithPrincipal will throw if the link URI should not be + // loaded, either because our feedURI isn't allowed to load it or per + // the rules specified in |flags|, so we'll never "linkify" the link... + element.setAttribute(attribute, uri); + } + catch (e) { + // Not allowed to load this link because checkLoadURIStrWithPrincipal threw + } + }, + + __bundle: null, + get _bundle() { + if (!this.__bundle) { + this.__bundle = Services.strings.createBundle(STRING_BUNDLE_URI); + } + + return this.__bundle; + }, + + _getFormattedString: function getFormattedString(key, params) { + return this._bundle.formatStringFromName(key, params, params.length); + }, + + _getString: function getString(key) { + try { + return this._bundle.GetStringFromName(key); + } catch(e) { + LOG("Couldn't retrieve key from bundle"); + } + + return null; + }, + + /* Magic helper methods to be used instead of xbl properties */ + _getSelectedItemFromMenulist: function getSelectedItemFromList(aList) { + return aList.getElementsByAttribute("selected", "true").item(0); + }, + + _setCheckboxCheckedState: function setCheckboxCheckedState(aCheckbox, aValue) { + // see checkbox.xml, xbl bindings are not visible through xrays! + var change = (aValue != (aCheckbox.getAttribute('checked') == 'true')); + if (aValue) + aCheckbox.setAttribute("checked", "true"); + else + aCheckbox.removeAttribute("checked"); + + if (change) { + aCheckbox.dispatchEvent(new this._document.defaultView.Event( + "CheckboxStateChange", { bubbles: true, cancelable: true })); + } + }, + + /** + * Returns a date suitable for displaying in the feed preview. + * If the date cannot be parsed, the return value is "null". + * @param dateString + * A date as extracted from a feed entry. (entry.updated) + */ + _parseDate: function parseDate(dateString) { + // Convert the date into the user's local time zone. + var dateObj = new Date(dateString); + // Make sure the date we're given is valid. + if (!dateObj.getTime()) + return false; + + return this._dateFormatter.format(dateObj); + }, + + __dateFormatter: null, + get _dateFormatter() { + if (!this.__dateFormatter) { + const dtOptions = { timeStyle: "short", dateStyle: "long" }; + this.__dateFormatter = new Services.intl.DateTimeFormat(undefined, dtOptions); + } + return this.__dateFormatter; + }, + + /** + * Returns the feed type. + */ + __feedType: null, + _getFeedType: function getFeedType() { + if (this.__feedType != null) + return this.__feedType; + + try { + // grab the feed because it's got the feed.type in it. + var container = this._getContainer(); + var feed = container.QueryInterface(Ci.nsIFeed); + this.__feedType = feed.type; + return feed.type; + } catch (ex) { + } + + return Ci.nsIFeed.TYPE_FEED; + }, + + /** + * Maps a feed type to a maybe-feed mimetype. + */ + _getMimeTypeForFeedType: function getMimeTypeForFeedType() { + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + return TYPE_MAYBE_VIDEO_FEED; + + case Ci.nsIFeed.TYPE_AUDIO: + return TYPE_MAYBE_AUDIO_FEED; + + default: + return TYPE_MAYBE_FEED; + } + }, + + /** + * Writes the feed title into the preview document. + * @param container + * The feed container, an nsIFeedContainer + */ + _setTitleText: function setTitleText(container) { + if (container.title) { + this._setContentText(TITLE_ID, container.title); + this._document.title = container.title.plainText(); + } + + var feed = container.QueryInterface(Ci.nsIFeed); + if (feed && feed.subtitle) + this._setContentText(SUBTITLE_ID, feed.subtitle); + }, + + /** + * Writes the title image into the preview document if one is present. + * @param container + * The feed container + */ + _setTitleImage: function setTitleImage(container) { + try { + var parts = container.image; + + // Set up the title image (supplied by the feed) + var feedTitleImage = this._document.getElementById("feedTitleImage"); + this._safeSetURIAttribute(feedTitleImage, "src", + parts.getPropertyAsAString("url")); + + // Set up the title image link + var feedTitleLink = this._document.getElementById("feedTitleLink"); + + var titleText = this._getFormattedString("linkTitleTextFormat", + [parts.getPropertyAsAString("title")]); + feedTitleLink.setAttribute("title", titleText); + var titleImageWidth = parseInt(parts.getPropertyAsAString("width")) + 15; + feedTitleLink.style.MozMarginEnd = titleImageWidth + "px"; + + this._safeSetURIAttribute(feedTitleLink, "href", + parts.getPropertyAsAString("link")); + } + catch (e) { + LOG("Failed to set Title Image (this is benign): " + e); + } + }, + + /** + * Writes all entries contained in the feed. + * @param container + * The container of entries in the feed + */ + _writeFeedContent: function writeFeedContent(container) { + // Build the actual feed content + var feed = container.QueryInterface(Ci.nsIFeed); + if (feed.items.length == 0) + return; + + var feedContent = this._document.getElementById("feedContent"); + + for (let i = 0; i < feed.items.length; ++i) { + let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry); + entry.QueryInterface(Ci.nsIFeedContainer); + + let entryContainer = this._document.createElementNS(HTML_NS, "div"); + entryContainer.className = "entry"; + + // If the entry has a title, make it a link + if (entry.title) { + let a = this._document.createElementNS(HTML_NS, "a"); + let span = this._document.createElementNS(HTML_NS, "span"); + a.appendChild(span); + this._setContentText(span, entry.title); + + // Entries are not required to have links, so entry.link can be null. + if (entry.link) + this._safeSetURIAttribute(a, "href", entry.link.spec); + + let title = this._document.createElementNS(HTML_NS, "h3"); + title.appendChild(a); + + let lastUpdated = this._parseDate(entry.updated); + if (lastUpdated) { + let dateDiv = this._document.createElementNS(HTML_NS, "div"); + dateDiv.className = "lastUpdated"; + dateDiv.textContent = lastUpdated; + title.appendChild(dateDiv); + } + + entryContainer.appendChild(title); + } + + var body = this._document.createElementNS(HTML_NS, "div"); + var summary = entry.summary || entry.content; + var docFragment = null; + if (summary) { + if (summary.base) + body.setAttributeNS(XML_NS, "base", summary.base.spec); + else + LOG("no base?"); + docFragment = summary.createDocumentFragment(body); + if (docFragment) + body.appendChild(docFragment); + + // If the entry doesn't have a title, append a # permalink + // See http://scripting.com/rss.xml for an example + if (!entry.title && entry.link) { + var a = this._document.createElementNS(HTML_NS, "a"); + a.appendChild(this._document.createTextNode("#")); + this._safeSetURIAttribute(a, "href", entry.link.spec); + body.appendChild(this._document.createTextNode(" ")); + body.appendChild(a); + } + + } + body.className = "feedEntryContent"; + entryContainer.appendChild(body); + + if (entry.enclosures && entry.enclosures.length > 0) { + var enclosuresDiv = this._buildEnclosureDiv(entry); + entryContainer.appendChild(enclosuresDiv); + } + + feedContent.appendChild(entryContainer); + + var clearDiv = this._document.createElementNS(HTML_NS, "div"); + clearDiv.style.clear = "both"; + feedContent.appendChild(clearDiv); + } + }, + + /** + * Takes a url to a media item and returns the best name it can come up with. + * Frequently this is the filename portion (e.g. passing in + * http://example.com/foo.mpeg would return "foo.mpeg"), but in more complex + * cases, this will return the entire url (e.g. passing in + * http://example.com/somedirectory/ would return + * http://example.com/somedirectory/). + * @param aURL + * The URL string from which to create a display name + * @returns a string + */ + _getURLDisplayName: function getURLDisplayName(aURL) { + var url = makeURI(aURL); + + if ((url instanceof Ci.nsIURL) && url.fileName) + return decodeURIComponent(url.fileName); + return aURL; + }, + + /** + * Takes a FeedEntry with enclosures, generates the HTML code to represent + * them, and returns that. + * @param entry + * FeedEntry with enclosures + * @returns element + */ + _buildEnclosureDiv: function buildEnclosureDiv(entry) { + var enclosuresDiv = this._document.createElementNS(HTML_NS, "div"); + enclosuresDiv.className = "enclosures"; + + enclosuresDiv.appendChild(this._document.createTextNode(this._getString("mediaLabel"))); + + for (let i_enc = 0; i_enc < entry.enclosures.length; ++i_enc) { + let enc = entry.enclosures.queryElementAt(i_enc, Ci.nsIWritablePropertyBag2); + + if (!(enc.hasKey("url"))) + continue; + + let enclosureDiv = this._document.createElementNS(HTML_NS, "div"); + enclosureDiv.setAttribute("class", "enclosure"); + + let mozicon = "moz-icon://.txt?size=16"; + let type_text = null; + let size_text = null; + + if (enc.hasKey("type")) { + type_text = enc.get("type"); + try { + let handlerInfoWrapper = this._mimeSvc.getFromTypeAndExtension(enc.get("type"), null); + + if (handlerInfoWrapper) + type_text = handlerInfoWrapper.description; + + if (type_text && type_text.length > 0) + mozicon = "moz-icon://goat?size=16&contentType=" + enc.get("type"); + + } catch (ex) { + } + + } + + if (enc.hasKey("length") && /^[0-9]+$/.test(enc.get("length"))) { + let enc_size = convertByteUnits(parseInt(enc.get("length"))); + + size_text = this._getFormattedString("enclosureSizeText", + [enc_size[0], this._getString(enc_size[1])]); + } + + let iconimg = this._document.createElementNS(HTML_NS, "img"); + iconimg.setAttribute("src", mozicon); + iconimg.setAttribute("class", "type-icon"); + enclosureDiv.appendChild(iconimg); + + enclosureDiv.appendChild(this._document.createTextNode( " " )); + + let enc_href = this._document.createElementNS(HTML_NS, "a"); + enc_href.appendChild(this._document.createTextNode(this._getURLDisplayName(enc.get("url")))); + this._safeSetURIAttribute(enc_href, "href", enc.get("url")); + enclosureDiv.appendChild(enc_href); + + if (type_text && size_text) + enclosureDiv.appendChild(this._document.createTextNode( " (" + type_text + ", " + size_text + ")")); + + else if (type_text) + enclosureDiv.appendChild(this._document.createTextNode( " (" + type_text + ")")) + + else if (size_text) + enclosureDiv.appendChild(this._document.createTextNode( " (" + size_text + ")")) + + enclosuresDiv.appendChild(enclosureDiv); + } + + return enclosuresDiv; + }, + + /** + * Gets a valid nsIFeedContainer object from the parsed nsIFeedResult. + * Displays error information if there was one. + * @param result + * The parsed feed result + * @returns A valid nsIFeedContainer object containing the contents of + * the feed. + */ + _getContainer: function getContainer(result) { + var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"] + .getService(Ci.nsIFeedResultService); + + try { + var result = feedService.getFeedResult(this._getOriginalURI(this._window)); + + if (result.bozo) { + LOG("Subscribe Preview: feed result is bozo?!"); + } + } + catch (e) { + LOG("Subscribe Preview: feed not available?!"); + } + + try { + var container = result.doc; + } + catch (e) { + LOG("Subscribe Preview: no result.doc? Why didn't the original reload?"); + return null; + } + return container; + }, + + /** + * Get the human-readable display name of a file. This could be the + * application name. + * @param file + * A nsIFile to look up the name of + * @returns The display name of the application represented by the file. + */ + _getFileDisplayName: function getFileDisplayName(file) { + if (AppConstants.platform == "win" && + file instanceof Ci.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } else if (AppConstants.platform == "macosx" && + file instanceof Ci.nsILocalFileMac) { + try { + return file.bundleDisplayName; + } catch (e) {} + } + return file.leafName; + }, + + /** + * Get moz-icon url for a file + * @param file + * A nsIFile object for which the moz-icon:// is returned + * @returns moz-icon url of the given file as a string + */ + _getFileIconURL: function getFileIconURL(file) { + var fph = Services.io.getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + var urlSpec = fph.getURLSpecFromFile(file); + return "moz-icon://" + urlSpec + "?size=16"; + }, + + /** + * Helper method to set the selected application and system default + * reader menuitems details from a file object + * @param aMenuItem + * The menuitem on which the attributes should be set + * @param aFile + * The menuitem's associated file + */ + _initMenuItemWithFile: function(aMenuItem, aFile) { + aMenuItem.setAttribute("label", this._getFileDisplayName(aFile)); + aMenuItem.setAttribute("image", this._getFileIconURL(aFile)); + }, + + /** + * Helper method to get an element in the XBL binding where the handler + * selection UI lives + */ + _getUIElement: function getUIElement(id) { + return this._document.getAnonymousElementByAttribute( + this._document.getElementById("feedSubscribeLine"), "anonid", id); + }, + + /** + * Displays a prompt from which the user may choose a (client) feed reader. + * @param aCallback the callback method, passes in true if a feed reader was + * selected, false otherwise. + */ + _chooseClientApp: function chooseClientApp(aCallback) { + try { + let fp = Cc["@mozilla.org/filepicker;1"] + .createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == Ci.nsIFilePicker.returnOK) { + this._selectedApp = fp.file; + if (this._selectedApp) { + let file = Services.dirsvc.get("XREExeF", Ci.nsIFile); + if (fp.file.leafName != file.leafName) { + this._initMenuItemWithFile(this._selectedAppMenuItem, + this._selectedApp); + + // Show and select the selected application menuitem + this._selectedAppMenuItem.hidden = false; + this._selectedAppMenuItem.doCommand(); + if (aCallback) { + aCallback(true); + return; + } + } + } + } + if (aCallback) { + aCallback(false); + } + }.bind(this); + + fp.init(this._window, + this._getString("chooseApplicationDialogTitle"), + Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + fp.open(fpCallback); + } catch(ex) {} + }, + + _setAlwaysUseCheckedState: function setAlwaysUseCheckedState(feedType) { + var checkbox = this._getUIElement("alwaysUse"); + if (checkbox) { + var alwaysUse = (Services.prefs.getCharPref(getPrefActionForType(feedType), "ask") != "ask"); + this._setCheckboxCheckedState(checkbox, alwaysUse); + } + }, + + _setSubscribeUsingLabel: function setSubscribeUsingLabel() { + var stringLabel = "subscribeFeedUsing"; + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + stringLabel = "subscribeVideoPodcastUsing"; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + stringLabel = "subscribeAudioPodcastUsing"; + break; + } + + var subscribeUsing = this._getUIElement("subscribeUsingDescription"); + subscribeUsing.setAttribute("value", this._getString(stringLabel)); + }, + + _setAlwaysUseLabel: function setAlwaysUseLabel() { + var checkbox = this._getUIElement("alwaysUse"); + if (checkbox) { + if (this._handlersMenuList) { + var handlerName = this._getSelectedItemFromMenulist(this._handlersMenuList) + .getAttribute("label"); + var stringLabel = "alwaysUseForFeeds"; + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + stringLabel = "alwaysUseForVideoPodcasts"; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + stringLabel = "alwaysUseForAudioPodcasts"; + break; + } + + checkbox.setAttribute("label", this._getFormattedString(stringLabel, [handlerName])); + } + } + }, + + // nsIDOMEventListener + handleEvent: function(event) { + if (event.target != this._document && + event.target.ownerDocument != this._document) { + LOG("FeedWriter.handleEvent: Someone passed the feed writer as a listener to the events of another document!"); + return; + } + + if (event.type == "load") + this._writeContent(); + else if (event.type == "unload") + this._close(); + else if (event.type == "command") { + switch (event.target.getAttribute("anonid")) { + case "subscribeButton": + this._subscribe(); + break; + case "chooseApplicationMenuItem": + /* Bug 351263: Make sure to not steal focus if the "Choose + * Application" item is being selected with the keyboard. We do this + * by ignoring command events while the dropdown is closed (user + * arrowing through the combobox), but handling them while the + * combobox dropdown is open (user pressed enter when an item was + * selected). If we don't show the filepicker here, it will be shown + * when clicking "Subscribe Now". + */ + var popupbox = this._handlersMenuList.firstChild.boxObject; + if (popupbox.popupState == "hiding") { + this._chooseClientApp(function(aResult) { + if (!aResult) { + // Select the (per-prefs) selected handler if no application + // was selected + this._setSelectedHandler(this._getFeedType()); + } + }.bind(this)); + } + break; + default: + this._setAlwaysUseLabel(); + } + } + }, + + _setSelectedHandler: function setSelectedHandler(feedType) { + var handler = Services.prefs.getCharPref(getPrefReaderForType(feedType), "messenger"); + + switch (handler) { + case "web": + if (this._handlersMenuList) { + var url = Services.prefs.getStringPref(getPrefWebForType(feedType)); + var handlers = this._handlersMenuList.getElementsByAttribute("webhandlerurl", url); + if (handlers.length == 0) { + LOG("FeedWriter._setSelectedHandler: selected web handler isn't in the menulist"); + return; + } + + handlers[0].doCommand(); + } + break; + case "client": + try { + this._selectedApp = + Services.prefs.getComplexValue(getPrefAppForType(feedType), + Ci.nsIFile); + } + catch(ex) { + this._selectedApp = null; + } + + if (this._selectedApp) { + this._initMenuItemWithFile(this._selectedAppMenuItem, + this._selectedApp); + this._selectedAppMenuItem.hidden = false; + this._selectedAppMenuItem.doCommand(); + + // Only show the default reader menuitem if the default reader + // isn't the selected application + if (this._defaultSystemReader) { + var shouldHide = this._defaultSystemReader.path == this._selectedApp.path; + this._defaultHandlerMenuItem.hidden = shouldHide; + } + break; + } + case "bookmarks": + var liveBookmarksMenuItem = this._getUIElement("liveBookmarksMenuItem"); + if (liveBookmarksMenuItem) + liveBookmarksMenuItem.doCommand(); + break; + // fall through if this._selectedApp is null + default: + var messengerFeedsMenuItem = this._getUIElement("messengerFeedsMenuItem"); + if (messengerFeedsMenuItem) + messengerFeedsMenuItem.doCommand(); + break; + } + }, + + _initSubscriptionUI: function initSubscriptionUI() { + var handlersMenuPopup = this._getUIElement("handlersMenuPopup"); + if (!handlersMenuPopup) + return; + + var feedType = this._getFeedType(); + + // change the background + var header = this._document.getElementById("feedHeader"); + switch (feedType) { + case Ci.nsIFeed.TYPE_VIDEO: + header.className = "videoPodcastBackground"; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + header.className = "audioPodcastBackground"; + break; + + default: + header.className = "feedBackground"; + } + + var liveBookmarksMenuItem = this._getUIElement("liveBookmarksMenuItem"); + + // Last-selected application + var menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("anonid", "selectedAppMenuItem"); + menuItem.className = "menuitem-iconic selectedAppMenuItem"; + menuItem.setAttribute("handlerType", "client"); + try { + this._selectedApp = Services.prefs.getComplexValue(getPrefAppForType(feedType), + Ci.nsIFile); + + if (this._selectedApp.exists()) + this._initMenuItemWithFile(menuItem, this._selectedApp); + else { + // Hide the menuitem if the last selected application doesn't exist + menuItem.hidden = true; + } + } + catch(ex) { + // Hide the menuitem until an application is selected + menuItem.hidden = true; + } + this._selectedAppMenuItem = menuItem; + handlersMenuPopup.appendChild(menuItem); + + // List the default feed reader + try { + this._defaultSystemReader = Cc["@mozilla.org/suite/shell-service;1"] + .getService(Ci.nsIShellService) + .defaultFeedReader; + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("anonid", "defaultHandlerMenuItem"); + menuItem.className = "menuitem-iconic defaultHandlerMenuItem"; + menuItem.setAttribute("handlerType", "client"); + + this._initMenuItemWithFile(menuItem, this._defaultSystemReader); + + // Hide the default reader item if it points to the same application + // as the last-selected application + if (this._selectedApp && + this._selectedApp.path == this._defaultSystemReader.path) + menuItem.hidden = true; + } + catch(ex) { + } + + if (menuItem) { + this._defaultHandlerMenuItem = menuItem; + handlersMenuPopup.appendChild(menuItem); + } + + // "Choose Application..." menuitem + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("anonid", "chooseApplicationMenuItem"); + menuItem.className = "menuitem-iconic chooseApplicationMenuItem"; + menuItem.setAttribute("label", this._getString("chooseApplicationMenuItem")); + handlersMenuPopup.appendChild(menuItem); + + // separator + menuItem = liveBookmarksMenuItem.nextSibling.cloneNode(false); + handlersMenuPopup.appendChild(menuItem); + + // List of web handlers + var wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"] + .getService(Ci.nsIWebContentConverterService); + var handlers = wccr.getContentHandlers(this._getMimeTypeForFeedType(feedType)); + if (handlers.length != 0) { + for (let i = 0; i < handlers.length; ++i) { + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.className = "menuitem-iconic"; + menuItem.setAttribute("label", handlers[i].name); + menuItem.setAttribute("handlerType", "web"); + menuItem.setAttribute("webhandlerurl", handlers[i].uri); + handlersMenuPopup.appendChild(menuItem); + } + } + + this._setSelectedHandler(feedType); + + // "Subscribe using..." + this._setSubscribeUsingLabel(); + + // "Always use..." checkbox initial state + this._setAlwaysUseCheckedState(feedType); + this._setAlwaysUseLabel(); + + // We update the "Always use.." checkbox label whenever the selected item + // in the list is changed + handlersMenuPopup.addEventListener("command", this); + + // Set up the "Subscribe Now" button + this._getUIElement("subscribeButton") + .addEventListener("command", this); + + // first-run ui + var showFirstRunUI = true; + try { + showFirstRunUI = Services.prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI); + } + catch (ex) { + } + if (showFirstRunUI) { + var textfeedinfo1, textfeedinfo2; + switch (feedType) { + case Ci.nsIFeed.TYPE_VIDEO: + textfeedinfo1 = "feedSubscriptionVideoPodcast1"; + textfeedinfo2 = "feedSubscriptionVideoPodcast2"; + break; + case Ci.nsIFeed.TYPE_AUDIO: + textfeedinfo1 = "feedSubscriptionAudioPodcast1"; + textfeedinfo2 = "feedSubscriptionAudioPodcast2"; + break; + default: + textfeedinfo1 = "feedSubscriptionFeed1"; + textfeedinfo2 = "feedSubscriptionFeed2"; + } + + this._document.getElementById("feedSubscriptionInfo1").textContent = + this._getString(textfeedinfo1); + this._document.getElementById("feedSubscriptionInfo2").textContent = + this._getString(textfeedinfo2); + header.setAttribute("firstrun", "true"); + Services.prefs.setBoolPref(PREF_SHOW_FIRST_RUN_UI, false); + } + }, + + /** + * Returns the original URI object of the feed and ensures that this + * component is only ever invoked from the preview document. + * @param aWindow + * The window of the document invoking the BrowserFeedWriter + */ + _getOriginalURI: function getOriginalURI(aWindow) { + let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + let chan = docShell.currentDocumentChannel; + + // We probably need to call Inherit() for this, but right now we can't call + // it from JS. + let attrs = docShell.getOriginAttributes(); + let nullPrincipal = Services.scriptSecurityManager + .createNullPrincipal(attrs); + + // This channel is not going to be opened, use a nullPrincipal + // and the most restrictive securityFlag. + let resolvedURI = NetUtil.newChannel({ + uri: "about:feeds", + loadingPrincipal: nullPrincipal, + securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER + }).URI; + + if (resolvedURI.equals(chan.URI)) + return chan.originalURI; + + return null; + }, + + _window: null, + _document: null, + _feedURI: null, + _feedPrincipal: null, + _handlersMenuList: null, + _selectedAppMenuItem: null, + _defaultHandlerMenuItem: null, + + // nsIDOMGlobalPropertyInitializer + init: function init(aWindow) { + this._feedURI = this._getOriginalURI(aWindow); + if (!this._feedURI) + return; + + this._window = aWindow; + this._document = aWindow.document; + this._handlersMenuList = this._getUIElement("handlersMenuList"); + + this._feedPrincipal = Services.scriptSecurityManager + .createCodebasePrincipal(this._feedURI, {}); + + LOG("Subscribe Preview: feed uri = " + this._window.location.href); + + // Set up the subscription UI + this._initSubscriptionUI(); + Services.prefs.addObserver(PREF_SELECTED_ACTION, this); + Services.prefs.addObserver(PREF_SELECTED_READER, this); + Services.prefs.addObserver(PREF_SELECTED_WEB, this); + Services.prefs.addObserver(PREF_SELECTED_APP, this); + Services.prefs.addObserver(PREF_VIDEO_SELECTED_ACTION, this); + Services.prefs.addObserver(PREF_VIDEO_SELECTED_READER, this); + Services.prefs.addObserver(PREF_VIDEO_SELECTED_WEB, this); + Services.prefs.addObserver(PREF_VIDEO_SELECTED_APP, this); + + Services.prefs.addObserver(PREF_AUDIO_SELECTED_ACTION, this); + Services.prefs.addObserver(PREF_AUDIO_SELECTED_READER, this); + Services.prefs.addObserver(PREF_AUDIO_SELECTED_WEB, this); + Services.prefs.addObserver(PREF_AUDIO_SELECTED_APP, this); + + this._window.addEventListener("load", this); + this._window.addEventListener("unload", this); + }, + + _writeContent: function writeContent() { + if (!this._window) + return; + + try { + // Set up the feed content + var container = this._getContainer(); + if (!container) + return; + + this._setTitleText(container); + this._setTitleImage(container); + this._writeFeedContent(container); + } + finally { + this._removeFeedFromCache(); + } + }, + + _close: function close() { + this._window.removeEventListener("load", this); + this._window.removeEventListener("unload", this); + this._getUIElement("handlersMenuPopup") + .removeEventListener("command", this); + this._getUIElement("subscribeButton") + .removeEventListener("command", this); + this._document = null; + this._window = null; + Services.prefs.removeObserver(PREF_SELECTED_ACTION, this); + Services.prefs.removeObserver(PREF_SELECTED_READER, this); + Services.prefs.removeObserver(PREF_SELECTED_WEB, this); + Services.prefs.removeObserver(PREF_SELECTED_APP, this); + Services.prefs.removeObserver(PREF_VIDEO_SELECTED_ACTION, this); + Services.prefs.removeObserver(PREF_VIDEO_SELECTED_READER, this); + Services.prefs.removeObserver(PREF_VIDEO_SELECTED_WEB, this); + Services.prefs.removeObserver(PREF_VIDEO_SELECTED_APP, this); + + Services.prefs.removeObserver(PREF_AUDIO_SELECTED_ACTION, this); + Services.prefs.removeObserver(PREF_AUDIO_SELECTED_READER, this); + Services.prefs.removeObserver(PREF_AUDIO_SELECTED_WEB, this); + Services.prefs.removeObserver(PREF_AUDIO_SELECTED_APP, this); + + this._removeFeedFromCache(); + this.__bundle = null; + this._feedURI = null; + this._selectedAppMenuItem = null; + this._defaultHandlerMenuItem = null; + }, + + _removeFeedFromCache: function removeFeedFromCache() { + if (this._feedURI) { + var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"] + .getService(Ci.nsIFeedResultService); + feedService.removeFeedResult(this._feedURI); + this._feedURI = null; + } + }, + + _subscribe: function subscribe() { + var feedType = this._getFeedType(); + + // Subscribe to the feed using the selected handler and save prefs + var defaultHandler = "reader"; + var useAsDefault = this._getUIElement("alwaysUse").getAttribute("checked"); + + var selectedItem = this._getSelectedItemFromMenulist(this._handlersMenuList); + let subscribeCallback = function() { + if (selectedItem.hasAttribute("webhandlerurl")) { + var webURI = selectedItem.getAttribute("webhandlerurl"); + Services.prefs.setCharPref(getPrefReaderForType(feedType), "web"); + + Services.prefs.setStringPref(getPrefWebForType(feedType), webURI); + + var wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"] + .getService(Ci.nsIWebContentConverterService); + var handler = wccr.getWebContentHandlerByURI(this._getMimeTypeForFeedType(feedType), webURI); + if (handler) { + if (useAsDefault) + wccr.setAutoHandler(this._getMimeTypeForFeedType(feedType), handler); + + this._window.location.href = handler.getHandlerURI(this._window.location.href); + } + } + else { + switch (selectedItem.getAttribute("anonid")) { + case "selectedAppMenuItem": + Services.prefs.setComplexValue(getPrefAppForType(feedType), Ci.nsIFile, + this._selectedApp); + Services.prefs.setCharPref(getPrefReaderForType(feedType), "client"); + break; + case "defaultHandlerMenuItem": + Services.prefs.setComplexValue(getPrefAppForType(feedType), Ci.nsIFile, + this._defaultSystemReader); + Services.prefs.setCharPref(getPrefReaderForType(feedType), "client"); + break; + case "liveBookmarksMenuItem": + defaultHandler = "bookmarks"; + Services.prefs.setCharPref(getPrefReaderForType(feedType), "bookmarks"); + break; + case "messengerFeedsMenuItem": + defaultHandler = "messenger"; + Services.prefs.setCharPref(getPrefReaderForType(feedType), "messenger"); + break; + } + var feedService = Cc["@mozilla.org/browser/feeds/result-service;1"] + .getService(Ci.nsIFeedResultService); + + // Pull the title and subtitle out of the document + var feedTitle = this._document.getElementById(TITLE_ID).textContent; + var feedSubtitle = this._document.getElementById(SUBTITLE_ID).textContent; + feedService.addToClientReader(this._window.location.href, feedTitle, feedSubtitle, feedType); + } + + // If "Always use..." is checked, we should set PREF_*SELECTED_ACTION + // to either "reader" (If a web reader or if an application is selected), + // or to "messenger" (if the messenger feeds option is selected). + // Otherwise, we should set it to "ask" + if (useAsDefault) { + Services.prefs.setCharPref(getPrefActionForType(feedType), defaultHandler); + } else { + Services.prefs.setCharPref(getPrefActionForType(feedType), "ask"); + } + }.bind(this); + + // Show the file picker before subscribing if the + // choose application menuitem was chosen using the keyboard + if (selectedItem.getAttribute("anonid") == "chooseApplicationMenuItem") { + this._chooseClientApp(function(aResult) { + if (aResult) { + selectedItem = + this._getSelectedItemFromMenulist(this._handlersMenuList); + subscribeCallback(); + } + }.bind(this)); + } else { + subscribeCallback(); + } + }, + + // nsIObserver + observe: function observe(subject, topic, data) { + if (!this._window) { + // this._window is null unless this.init was called with a trusted + // window object. + return; + } + + var feedType = this._getFeedType(); + + if (topic == "nsPref:changed") { + switch (data) { + case PREF_SELECTED_READER: + case PREF_SELECTED_WEB: + case PREF_SELECTED_APP: + case PREF_VIDEO_SELECTED_READER: + case PREF_VIDEO_SELECTED_WEB: + case PREF_VIDEO_SELECTED_APP: + case PREF_AUDIO_SELECTED_READER: + case PREF_AUDIO_SELECTED_WEB: + case PREF_AUDIO_SELECTED_APP: + this._setSelectedHandler(feedType); + break; + case PREF_SELECTED_ACTION: + case PREF_VIDEO_SELECTED_ACTION: + case PREF_AUDIO_SELECTED_ACTION: + this._setAlwaysUseCheckedState(feedType); + } + } + }, + + classID: FEEDWRITER_CID, + QueryInterface: XPCOMUtils.generateQI([ Ci.nsIDOMGlobalPropertyInitializer, + Ci.nsIDOMEventListener, + Ci.nsIObserver]) + +}; + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([FeedWriter]); diff --git a/comm/suite/components/feeds/SuiteFeeds.manifest b/comm/suite/components/feeds/SuiteFeeds.manifest new file mode 100644 index 0000000000..e44e0c4c31 --- /dev/null +++ b/comm/suite/components/feeds/SuiteFeeds.manifest @@ -0,0 +1,11 @@ +component {88592f45-3866-4c8e-9d8a-ab58b290fcf7} FeedConverter.js +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.feed&to=*/* {88592f45-3866-4c8e-9d8a-ab58b290fcf7} +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.video.feed&to=*/* {88592f45-3866-4c8e-9d8a-ab58b290fcf7} +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.audio.feed&to=*/* {88592f45-3866-4c8e-9d8a-ab58b290fcf7} +component {e5b05e9d-f037-48e4-b9a4-b99476582927} FeedConverter.js +contract @mozilla.org/browser/feeds/result-service;1 {e5b05e9d-f037-48e4-b9a4-b99476582927} +component {49bb6593-3aff-4eb3-a068-2712c28bd58e} FeedWriter.js +contract @mozilla.org/browser/feeds/result-writer;1 {49bb6593-3aff-4eb3-a068-2712c28bd58e} +component {792a7e82-06a0-437c-af63-b2d12e808acc} WebContentConverter.js +contract @mozilla.org/embeddor.implemented/web-content-handler-registrar;1 {792a7e82-06a0-437c-af63-b2d12e808acc} +category app-startup WebContentConverter service,@mozilla.org/embeddor.implemented/web-content-handler-registrar;1 diff --git a/comm/suite/components/feeds/WebContentConverter.js b/comm/suite/components/feeds/WebContentConverter.js new file mode 100644 index 0000000000..ef519c7df2 --- /dev/null +++ b/comm/suite/components/feeds/WebContentConverter.js @@ -0,0 +1,818 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +const WCCR_CONTRACTID = "@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"; +const WCCR_CLASSID = Components.ID("{792a7e82-06a0-437c-af63-b2d12e808acc}"); + +const WCC_CLASSID = Components.ID("{db7ebf28-cc40-415f-8a51-1b111851df1e}"); +const WCC_CLASSNAME = "Web Service Handler"; + +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_ANY = "*/*"; + +const PREF_CONTENTHANDLERS_AUTO = "browser.contentHandlers.auto."; +const PREF_CONTENTHANDLERS_BRANCH = "browser.contentHandlers.types."; +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_HANDLER_EXTERNAL_PREFIX = "network.protocol-handler.external"; +const PREF_ALLOW_DIFFERENT_HOST = "gecko.handlerService.allowRegisterFromDifferentHost"; + +const STRING_BUNDLE_URI = "chrome://communicator/locale/feeds/subscribe.properties"; + +const NS_ERROR_MODULE_DOM = 0x80530000; +const NS_ERROR_DOM_SYNTAX_ERR = NS_ERROR_MODULE_DOM + 12; + +function LOG(str) { + try { + if (Services.prefs.getBoolPref("feeds.log")) + dump("*** Feeds: " + str + "\n"); + } + catch (ex) { + } +} + +function getNotificationBox(aWindow) +{ + return aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.parentNode.wrappedJSObject; +} + +function WebContentConverter() { +} + +WebContentConverter.prototype = { + convert: function convert() { }, + asyncConvertData: function asyncConvertData() { }, + onDataAvailable: function onDataAvailable() { }, + onStopRequest: function onStopRequest() { }, + + onStartRequest: function onStartRequest(request, context) { + var wccr = Cc[WCCR_CONTRACTID] + .getService(Ci.nsIWebContentConverterService); + wccr.loadPreferredHandler(request); + }, + + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIStreamConverter, + Ci.nsIStreamListener]) +}; + +var WebContentConverterFactory = { + createInstance: function createInstance(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return new WebContentConverter().QueryInterface(iid); + }, + + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIFactory]) +}; + +function ServiceInfo(contentType, uri, name) { + this._contentType = contentType; + this._uri = uri; + this._name = name; +} + +ServiceInfo.prototype = { + /** + * See nsIHandlerApp + */ + get name() { + return this._name; + }, + + /** + * See nsIHandlerApp + */ + equals: function equals(aHandlerApp) { + if (!aHandlerApp) + throw Cr.NS_ERROR_NULL_POINTER; + + if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo && + aHandlerApp.contentType == this.contentType && + aHandlerApp.uri == this.uri) + return true; + + return false; + }, + + /** + * See nsIWebContentHandlerInfo + */ + get contentType() { + return this._contentType; + }, + + /** + * See nsIWebContentHandlerInfo + */ + get uri() { + return this._uri; + }, + + /** + * See nsIWebContentHandlerInfo + */ + getHandlerURI: function getHandlerURI(uri) { + return this._uri.replace(/%s/gi, encodeURIComponent(uri)); + }, + + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIWebContentHandlerInfo]) +}; + +function WebContentConverterRegistrar() { + this._contentTypes = { }; + this._autoHandleContentTypes = { }; +} + +WebContentConverterRegistrar.prototype = { + __bundle: null, + get _bundle() { + if (!this.__bundle) { + this.__bundle = Services.strings.createBundle(STRING_BUNDLE_URI); + } + return this.__bundle; + }, + + _getFormattedString: function getFormattedString(key, params) { + return this._bundle.formatStringFromName(key, params, params.length); + }, + + _getString: function getString(key) { + try { + return this._bundle.GetStringFromName(key); + } catch(e) { + LOG("Couldn't retrieve key from bundle"); + } + + return null; + }, + + /** + * See nsIWebContentConverterService + */ + getAutoHandler: function getAutoHandler(contentType) { + contentType = this._resolveContentType(contentType); + if (contentType in this._autoHandleContentTypes) + return this._autoHandleContentTypes[contentType]; + return null; + }, + + /** + * See nsIWebContentConverterService + */ + setAutoHandler: function setAutoHandler(contentType, handler) { + if (handler && !this._typeIsRegistered(contentType, handler.uri)) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + contentType = this._resolveContentType(contentType); + this._setAutoHandler(contentType, handler); + + var autoBranch = Services.prefs.getBranch(PREF_CONTENTHANDLERS_AUTO); + if (handler) + autoBranch.setCharPref(contentType, handler.uri); + else if (autoBranch.prefHasUserValue(contentType)) + autoBranch.clearUserPref(contentType); + + Services.prefs.savePrefFile(null); + }, + + /** + * Update the internal data structure (not persistent) + */ + _setAutoHandler: function setAutoHandler(contentType, handler) { + if (handler) + this._autoHandleContentTypes[contentType] = handler; + else if (contentType in this._autoHandleContentTypes) + delete this._autoHandleContentTypes[contentType]; + }, + + /** + * See nsIWebContentConverterService + */ + getWebContentHandlerByURI: function getWebContentHandlerByURI(contentType, uri) { + var handlers = this.getContentHandlers(contentType, { }); + for (let i = 0; i < handlers.length; ++i) { + if (handlers[i].uri == uri) + return handlers[i]; + } + return null; + }, + + /** + * See nsIWebContentConverterService + */ + loadPreferredHandler: function loadPreferredHandler(request) { + var channel = request.QueryInterface(Ci.nsIChannel); + var contentType = this._resolveContentType(channel.contentType); + var handler = this.getAutoHandler(contentType); + if (handler) { + request.cancel(Cr.NS_ERROR_FAILURE); + + let triggeringPrincipal = channel.loadInfo + ? channel.loadInfo.triggeringPrincipal + : Services.scriptSecurityManager.getSystemPrincipal(); + + let webNavigation = channel.notificationCallbacks + .getInterface(Ci.nsIWebNavigation); + webNavigation.loadURI(handler.getHandlerURI(channel.URI.spec), + Ci.nsIWebNavigation.LOAD_FLAGS_NONE, + null, null, null, triggeringPrincipal); + } + }, + + /** + * See nsIWebContentConverterService + */ + removeProtocolHandler: function removeProtocolHandler(aProtocol, aURITemplate) { + var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + var handlerInfo = eps.getProtocolHandlerInfo(aProtocol); + var handlers = handlerInfo.possibleApplicationHandlers; + for (let i = 0; i < handlers.length; i++) { + try { // We only want to test web handlers + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + if (handler.uriTemplate == aURITemplate) { + handlers.removeElementAt(i); + let hs = Cc["@mozilla.org/uriloader/handler-service;1"] + .getService(Ci.nsIHandlerService); + hs.store(handlerInfo); + return; + } + } catch (e) { + /* it wasn't a web handler */ + } + } + }, + + /** + * See nsIWebContentConverterService + */ + removeContentHandler: function removeContentHandler(contentType, uri) { + function notURI(serviceInfo) { + return serviceInfo.uri != uri; + } + + if (contentType in this._contentTypes) { + this._contentTypes[contentType] = this._contentTypes[contentType] + .filter(notURI); + } + }, + + /** + * + */ + _mappings: { + "application/rss+xml": TYPE_MAYBE_FEED, + "application/atom+xml": TYPE_MAYBE_FEED, + }, + + /** + * These are types for which there is a separate content converter aside + * from our built in generic one. We should not automatically register + * a factory for creating a converter for these types. + */ + _blockedTypes: { + "application/vnd.mozilla.maybe.feed": true, + }, + + /** + * Determines the "internal" content type based on the _mappings. + * @param contentType + * @returns The resolved contentType value. + */ + _resolveContentType: function resolveContentType(contentType) { + if (contentType in this._mappings) + return this._mappings[contentType]; + return contentType; + }, + + _makeURI: function(aURL, aOriginCharset, aBaseURI) { + return Services.io.newURI(aURL, aOriginCharset, aBaseURI); + }, + + _checkAndGetURI: function checkAndGetURI(aURIString, aContentWindow) { + try { + var uri = this._makeURI(aURIString); + } catch (ex) { + // not supposed to throw according to spec + return; + } + + // For security reasons we reject non-http(s) urls (see bug 354316), + // we may need to revise this once we support more content types + // XXX this should be a "security exception" according to spec, but that + // isn't defined yet. + if (uri.scheme != "http" && uri.scheme != "https") + throw("Permission denied to add " + uri.spec + " as a content or protocol handler"); + + // We also reject handlers registered from a different host (see bug 402287) + // The pref allows us to test the feature + if ((!Services.prefs.prefHasUserValue(PREF_ALLOW_DIFFERENT_HOST) || + !Services.prefs.getBoolPref(PREF_ALLOW_DIFFERENT_HOST)) && + aContentWindow.location.hostname != uri.host) + throw("Permission denied to add " + uri.spec + " as a content or protocol handler"); + + // If the uri doesn't contain '%s', it won't be a good handler + if (!uri.spec.includes("%s")) + throw NS_ERROR_DOM_SYNTAX_ERR; + + return uri; + }, + + /** + * Determines if a web handler is already registered. + * + * @param aProtocol + * The scheme of the web handler we are checking for. + * @param aURITemplate + * The URI template that the handler uses to handle the protocol. + * @return true if it is already registered, false otherwise. + */ + _protocolHandlerRegistered: function protocolHandlerRegistered(aProtocol, aURITemplate) { + var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + var handlerInfo = eps.getProtocolHandlerInfo(aProtocol); + var handlers = handlerInfo.possibleApplicationHandlers; + for (let i = 0; i < handlers.length; i++) { + try { // We only want to test web handlers + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + if (handler.uriTemplate == aURITemplate) + return true; + } catch (e) { /* it wasn't a web handler */ } + } + return false; + }, + + /** + * See nsIWebContentHandlerRegistrar + */ + registerProtocolHandler: function registerProtocolHandler(aProtocol, aURIString, + aTitle, aContentWindow) { + LOG("registerProtocolHandler(" + aProtocol + "," + aURIString + "," + aTitle + ")"); + + var win = aContentWindow; + var isPB = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsILoadContext) + .usePrivateBrowsing; + if (isPB) { + // Inside the private browsing mode, we don't want to alert the user to save + // a protocol handler. We log it to the error console so that web developers + // would have some way to tell what's going wrong. + Services.console.logStringMessage("Web page denied access to register a protocol handler inside private browsing mode"); + return; + } + + // First, check to make sure this isn't already handled internally (we don't + // want to let them take over, say "chrome"). + var handler = Services.io.getProtocolHandler(aProtocol); + if (!(handler instanceof Ci.nsIExternalProtocolHandler)) { + // This is handled internally, so we don't want them to register + Services.console.logStringMessage("Permission denied to add " + aURIString + " as a protocol handler"); + return; + } + + // check if it is in the black list + var allowed; + try { + allowed = Services.prefs.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol); + } + catch (e) { + allowed = Services.prefs.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default"); + } + if (!allowed) { + Services.console.logStringMessage("Not allowed to register a protocol handler for " + aProtocol); + return; + } + + var uri = this._checkAndGetURI(aURIString, aContentWindow); + + var buttons, message; + if (this._protocolHandlerRegistered(aProtocol, uri.spec)) + message = this._getFormattedString("protocolHandlerRegistered", + [aTitle, aProtocol]); + else { + // Now Ask the user and provide the proper callback + message = this._getFormattedString("addProtocolHandler", + [aTitle, uri.host, aProtocol]); + var notificationIcon = uri.resolve("/favicon.ico"); + var notificationValue = "Protocol Registration: " + aProtocol; + var addButton = { + label: this._getString("addProtocolHandlerAddButton"), + accessKey: this._getString("addHandlerAddButtonAccesskey"), + protocolInfo: { protocol: aProtocol, uri: uri.spec, name: aTitle }, + + callback: function addProtocolHandlerButtonCallback(aNotification, aButtonInfo) { + var protocol = aButtonInfo.protocolInfo.protocol; + var uri = aButtonInfo.protocolInfo.uri; + var name = aButtonInfo.protocolInfo.name; + + var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"] + .createInstance(Ci.nsIWebHandlerApp); + handler.name = name; + handler.uriTemplate = uri; + + var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + var handlerInfo = eps.getProtocolHandlerInfo(protocol); + handlerInfo.possibleApplicationHandlers.appendElement(handler); + + // Since the user has agreed to add a new handler, chances are good + // that the next time they see a handler of this type, they're going + // to want to use it. Reset the handlerInfo to ask before the next + // use. + handlerInfo.alwaysAskBeforeHandling = true; + + var hs = Cc["@mozilla.org/uriloader/handler-service;1"] + .getService(Ci.nsIHandlerService); + hs.store(handlerInfo); + } + }; + buttons = [addButton]; + } + + var notificationBox = getNotificationBox(aContentWindow); + notificationBox.appendNotification(message, + notificationValue, + notificationIcon, + notificationBox.PRIORITY_INFO_LOW, + buttons); + }, + + /** + * See nsIWebContentHandlerRegistrar + * If a DOM window is provided, then the request came from content, so we + * prompt the user to confirm the registration. + */ + registerContentHandler: function registerContentHandler(aContentType, aURIString, + aTitle, aContentWindow) { + LOG("registerContentHandler(" + aContentType + "," + aURIString + "," + aTitle + ")"); + + // We only support feed types at present. + // XXX this should be a "security exception" according to spec, but that + // isn't defined yet. + var contentType = this._resolveContentType(aContentType); + if (contentType != TYPE_MAYBE_FEED) + return; + + if (aContentWindow) { + var uri = this._checkAndGetURI(aURIString, aContentWindow); + + this._appendFeedReaderNotification(uri, aTitle, getNotificationBox(aContentWindow)); + } + else + this._registerContentHandler(contentType, aURIString, aTitle); + }, + + /** + * Appends a notifcation for the given feed reader details. + * + * The notification could be either a pseudo-dialog which lets + * the user to add the feed reader: + * [ [icon] Add %feed-reader-name% (%feed-reader-host%) as a Feed Reader? (Add) [x] ] + * + * or a simple message for the case where the feed reader is already registered: + * [ [icon] %feed-reader-name% is already registered as a Feed Reader [x] ] + * + * A new notification isn't appended if the given notificationbox has a + * notification for the same feed reader. + * + * @param aURI + * The url of the feed reader as a nsIURI object + * @param aName + * The feed reader name as it was passed to registerContentHandler + * @param aNotificationBox + * The notification box to which a notification might be appended + * @return true if a notification has been appended, false otherwise. + */ + _appendFeedReaderNotification: function appendFeedReaderNotification(aURI, aName, aNotificationBox) { + var uriSpec = aURI.spec; + var notificationValue = "feed reader notification: " + uriSpec; + var notificationIcon = aURI.resolve("/favicon.ico"); + + // Don't append a new notification if the notificationbox + // has a notification for the given feed reader already + if (aNotificationBox.getNotificationWithValue(notificationValue)) + return false; + + var buttons, message; + if (this.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uriSpec)) + message = this._getFormattedString("handlerRegistered", [aName]); + else { + message = this._getFormattedString("addHandler", [aName, aURI.host]); + var self = this; + var addButton = { + _outer: self, + label: self._getString("addHandlerAddButton"), + accessKey: self._getString("addHandlerAddButtonAccesskey"), + feedReaderInfo: { uri: uriSpec, name: aName }, + + /* static */ + callback: function addFeedReaderButtonCallback(aNotification, aButtonInfo) { + var uri = aButtonInfo.feedReaderInfo.uri; + var name = aButtonInfo.feedReaderInfo.name; + var outer = aButtonInfo._outer; + + // The reader could have been added from another window mean while + if (!outer.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uri)) + outer._registerContentHandler(TYPE_MAYBE_FEED, uri, name); + + // avoid reference cycles + aButtonInfo._outer = null; + + return false; + } + }; + buttons = [addButton]; + } + + aNotificationBox.appendNotification(message, + notificationValue, + notificationIcon, + aNotificationBox.PRIORITY_INFO_LOW, + buttons); + return true; + }, + + /** + * Save Web Content Handler metadata to persistent preferences. + * @param contentType + * The content Type being handled + * @param uri + * The uri of the web service + * @param title + * The human readable name of the web service + * + * This data is stored under: + * + * browser.contentHandlers.type0 = content/type + * browser.contentHandlers.uri0 = http://www.foo.com/q=%s + * browser.contentHandlers.title0 = Foo 2.0alphr + */ + _saveContentHandlerToPrefs: function saveContentHandlerToPrefs(contentType, uri, title) { + var i = 0; + var typeBranch = null; + while (true) { + typeBranch = Services.prefs.getBranch(PREF_CONTENTHANDLERS_BRANCH + i + "."); + try { + typeBranch.getCharPref("type"); + ++i; + } + catch (e) { + // No more handlers + break; + } + } + if (typeBranch) { + typeBranch.setCharPref("type", contentType); + var pls = Cc["@mozilla.org/pref-localizedstring;1"] + .createInstance(Ci.nsIPrefLocalizedString); + pls.data = uri; + typeBranch.setComplexValue("uri", + Ci.nsIPrefLocalizedString, pls); + pls.data = title; + typeBranch.setComplexValue("title", + Ci.nsIPrefLocalizedString, pls); + + Services.prefs.savePrefFile(null); + } + }, + + /** + * Determines if there is a type with a particular uri registered for the + * specified content type already. + * @param contentType + * The content type that the uri handles + * @param uri + * The uri of the + */ + _typeIsRegistered: function typeIsRegistered(contentType, uri) { + if (!(contentType in this._contentTypes)) + return false; + + var services = this._contentTypes[contentType]; + for (let i = 0; i < services.length; ++i) { + // This uri has already been registered + if (services[i].uri == uri) + return true; + } + return false; + }, + + /** + * Gets a stream converter contract id for the specified content type. + * @param contentType + * The source content type for the conversion. + * @returns A contract id to construct a converter to convert between the + * contentType and *\/*. + */ + _getConverterContractID: function getConverterContractID(contentType) { + const template = "@mozilla.org/streamconv;1?from=%s&to=*/*"; + return template.replace(/%s/, contentType); + }, + + /** + * Register a web service handler for a content type. + * + * @param contentType + * the content type being handled + * @param uri + * the URI of the web service + * @param title + * the human readable name of the web service + */ + _registerContentHandler: function registerContentHandler(contentType, uri, title) { + this._updateContentTypeHandlerMap(contentType, uri, title); + this._saveContentHandlerToPrefs(contentType, uri, title); + + if (contentType == TYPE_MAYBE_FEED) { + // Make the new handler the last-selected reader in the preview page + // and make sure the preview page is shown the next time a feed is visited + Services.prefs.setCharPref(PREF_SELECTED_READER, "web"); + + Services.prefs.setStringPref(PREF_SELECTED_WEB, uri); + Services.prefs.setCharPref(PREF_SELECTED_ACTION, "ask"); + this._setAutoHandler(TYPE_MAYBE_FEED, null); + } + }, + + /** + * Update the content type -> handler map. This mapping is not persisted, use + * registerContentHandler or _saveContentHandlerToPrefs for that purpose. + * @param contentType + * The content Type being handled + * @param uri + * The uri of the web service + * @param title + * The human readable name of the web service + */ + _updateContentTypeHandlerMap: function updateContentTypeHandlerMap(contentType, uri, title) { + if (!(contentType in this._contentTypes)) + this._contentTypes[contentType] = []; + + // Avoid adding duplicates + if (this._typeIsRegistered(contentType, uri)) + return; + + this._contentTypes[contentType].push(new ServiceInfo(contentType, uri, title)); + + if (!(contentType in this._blockedTypes)) { + var converterContractID = this._getConverterContractID(contentType); + var cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + cr.registerFactory(WCC_CLASSID, WCC_CLASSNAME, converterContractID, + WebContentConverterFactory); + } + }, + + /** + * See nsIWebContentConverterService + */ + getContentHandlers: function getContentHandlers(contentType, countRef) { + countRef.value = 0; + if (!(contentType in this._contentTypes)) + return []; + + var handlers = this._contentTypes[contentType]; + countRef.value = handlers.length; + return handlers; + }, + + /** + * See nsIWebContentConverterService + */ + resetHandlersForType: function resetHandlersForType(contentType) { + // currently unused within the tree, so only useful for extensions; previous + // impl. was buggy (and even infinite-looped!), so I argue that this is a + // definite improvement + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * Registers a handler from the settings on a preferences branch. + * + * @param branch + * an nsIPrefBranch containing "type", "uri", and "title" preferences + * corresponding to the content handler to be registered + */ + _registerContentHandlerWithBranch: function(branch) { + /** + * Since we support up to six predefined readers, we need to handle gaps + * better, since the first branch with user-added values will be .6 + * + * How we deal with that is to check to see if there's no prefs in the + * branch and stop cycling once that's true. This doesn't fix the case + * where a user manually removes a reader, but that's not supported yet! + */ + var vals = branch.getChildList(""); + if (vals.length == 0) + return; + + try { + var type = branch.getCharPref("type"); + var uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data; + var title = branch.getComplexValue("title", + Ci.nsIPrefLocalizedString).data; + this._updateContentTypeHandlerMap(type, uri, title); + } + catch(ex) { + // do nothing, the next branch might have values + } + }, + + /** + * Load the auto handler, content handler and protocol tables from + * preferences. + */ + _init: function init() { + var kids = Services.prefs.getBranch(PREF_CONTENTHANDLERS_BRANCH) + .getChildList(""); + // first get the numbers of the providers by getting all ###.uri prefs + var nums = []; + for (let i = 0; i < kids.length; i++) { + let match = /^(\d+)\.uri$/.exec(kids[i]); + if (match) + nums.push(match[1]); + } + // sort them, to get them back in order + nums.sort(function(a, b) {return a - b;}); + // now register them + for (let i = 0; i < nums.length; i++) { + let branch = Services.prefs.getBranch(PREF_CONTENTHANDLERS_BRANCH + nums[i] + "."); + this._registerContentHandlerWithBranch(branch); + } + + // We need to do this _after_ registering all of the available handlers, + // so that getWebContentHandlerByURI can return successfully. + try { + var autoBranch = Services.prefs.getBranch(PREF_CONTENTHANDLERS_AUTO); + var childPrefs = autoBranch.getChildList(""); + for (let i = 0; i < childPrefs.length; ++i) { + let type = childPrefs[i]; + let uri = autoBranch.getCharPref(type); + if (uri) { + let handler = this.getWebContentHandlerByURI(type, uri); + this._setAutoHandler(type, handler); + } + } + } + catch (e) { + } + }, + + /** + * See nsIObserver + */ + observe: function observe(subject, topic, data) { + switch (topic) { + case "app-startup": + Services.obs.addObserver(this, "final-ui-startup"); + break; + case "final-ui-startup": + Services.obs.removeObserver(this, "final-ui-startup"); + this._init(); + break; + } + }, + + /** + * See nsIFactory + */ + createInstance: function createInstance(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + + classID: WCCR_CLASSID, + classInfo: XPCOMUtils.generateCI({ + classID: WCCR_CLASSID, + contractID: WCCR_CONTRACTID, + interfaces: [Ci.nsIWebContentConverterService, + Ci.nsIWebContentHandlerRegistrar, + Ci.nsIObserver, + Ci.nsIFactory], + flags: Ci.nsIClassInfo.DOM_OBJECT}), + + /** + * See nsISupports + */ + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIWebContentConverterService, + Ci.nsIWebContentHandlerRegistrar, + Ci.nsIObserver, + Ci.nsIFactory]) +}; + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([WebContentConverterRegistrar]); diff --git a/comm/suite/components/feeds/content/subscribe.css b/comm/suite/components/feeds/content/subscribe.css new file mode 100644 index 0000000000..1ba7611b80 --- /dev/null +++ b/comm/suite/components/feeds/content/subscribe.css @@ -0,0 +1,7 @@ +/* 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/. */ + +#feedSubscribeLine { + -moz-binding: url(chrome://communicator/content/feeds/subscribe.xml#feedreaderUI); +} diff --git a/comm/suite/components/feeds/content/subscribe.xhtml b/comm/suite/components/feeds/content/subscribe.xhtml new file mode 100644 index 0000000000..c1c4b90765 --- /dev/null +++ b/comm/suite/components/feeds/content/subscribe.xhtml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> + + +<!DOCTYPE html [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % globalDTD + SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % feedDTD + SYSTEM "chrome://communicator/locale/feeds/subscribe.dtd"> + %feedDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<html id="feedHandler" + xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&feedPage.title;</title> + <link rel="stylesheet" + href="chrome://communicator/content/feeds/subscribe.css" + type="text/css" + media="all"/> + <link rel="stylesheet" + href="chrome://communicator/skin/feed-subscribe.css" + type="text/css" + media="all"/> + </head> + <body> + <div id="feedHeaderContainer"> + <div id="feedHeader" dir="&locale.dir;"> + <div id="feedIntroText"> + <p id="feedSubscriptionInfo1" /> + <p id="feedSubscriptionInfo2" /> + </div> + <div id="feedSubscribeLine" /> + </div> + </div> + + <div id="feedBody"> + <div id="feedTitle"> + <a id="feedTitleLink"> + <img id="feedTitleImage"/> + </a> + <div id="feedTitleContainer"> + <h1 id="feedTitleText"/> + <h2 id="feedSubtitleText"/> + </div> + </div> + <div id="feedContent"/> + </div> + </body> +</html> diff --git a/comm/suite/components/feeds/content/subscribe.xml b/comm/suite/components/feeds/content/subscribe.xml new file mode 100644 index 0000000000..33a9e539df --- /dev/null +++ b/comm/suite/components/feeds/content/subscribe.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE bindings SYSTEM "chrome://communicator/locale/feeds/subscribe.dtd"> + +<bindings id="feedBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <binding id="feedreaderUI" bindToUntrustedContent="true"> + <content> + <xul:vbox> + <xul:hbox align="center"> + <xul:description anonid="subscribeUsingDescription" class="subscribeUsingDescription"/> + <xul:menulist anonid="handlersMenuList" class="handlersMenuList" aria-labelledby="subscribeUsingDescription"> + <xul:menupopup anonid="handlersMenuPopup" class="handlersMenuPopup"> + <xul:menuitem anonid="messengerFeedsMenuItem" label="&feedMessenger;" class="menuitem-iconic messengerFeedsMenuItem" image="chrome://communicator/skin/icons/feedIcon16.png" selected="true"/> + <xul:menuitem anonid="liveBookmarksMenuItem" label="&feedLiveBookmarks;" class="menuitem-iconic liveBookmarksMenuItem" image="chrome://communicator/skin/icons/feedIcon16.png"/> + <xul:menuseparator/> + </xul:menupopup> + </xul:menulist> + </xul:hbox> + <xul:hbox> + <xul:checkbox anonid="alwaysUse" class="alwaysUse" checked="false"/> + </xul:hbox> + <xul:hbox align="center"> + <xul:spacer flex="1"/> + <xul:button label="&feedSubscribeNow;" anonid="subscribeButton" class="subscribeButton"/> + </xul:hbox> + </xul:vbox> + </content> + <implementation> + <constructor> + new BrowserFeedWriter(); + </constructor> + </implementation> + <resources> + <stylesheet src="chrome://communicator/skin/feed-subscribe-ui.css"/> + </resources> + </binding> +</bindings> diff --git a/comm/suite/components/feeds/jar.mn b/comm/suite/components/feeds/jar.mn new file mode 100644 index 0000000000..b1df80fa40 --- /dev/null +++ b/comm/suite/components/feeds/jar.mn @@ -0,0 +1,8 @@ +# 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/. + +comm.jar: + content/communicator/feeds/subscribe.css (content/subscribe.css) + content/communicator/feeds/subscribe.xhtml (content/subscribe.xhtml) + content/communicator/feeds/subscribe.xml (content/subscribe.xml) diff --git a/comm/suite/components/feeds/moz.build b/comm/suite/components/feeds/moz.build new file mode 100644 index 0000000000..07571081e3 --- /dev/null +++ b/comm/suite/components/feeds/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +# XPIDL_SOURCES += [ +# 'nsIFeedResultService.idl', +# 'nsIWebContentConverterRegistrar.idl', +# ] + +# XPIDL_MODULE = 'suite-feeds' + +SOURCES += [ + "nsFeedSniffer.cpp", +] + +EXTRA_COMPONENTS += [ + "FeedConverter.js", + "FeedWriter.js", + "SuiteFeeds.manifest", + "WebContentConverter.js", +] + +FINAL_LIBRARY = "suite" + +JAR_MANIFESTS += ["jar.mn"] diff --git a/comm/suite/components/feeds/nsFeedSniffer.cpp b/comm/suite/components/feeds/nsFeedSniffer.cpp new file mode 100644 index 0000000000..eba3a012ff --- /dev/null +++ b/comm/suite/components/feeds/nsFeedSniffer.cpp @@ -0,0 +1,356 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsFeedSniffer.h" + +#include "mozilla/Unused.h" + +#include "nsNetCID.h" +#include "nsXPCOM.h" +#include "nsCOMPtr.h" +#include "nsStringStream.h" + +#include "nsICategoryManager.h" +#include "nsIServiceManager.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" + +#include "nsIStreamConverterService.h" +#include "nsIStreamConverter.h" + +#include "nsIStreamListener.h" + +#include "nsIHttpChannel.h" +#include "nsIMIMEHeaderParam.h" + +#include "nsMimeTypes.h" +#include "nsIURI.h" +#include <algorithm> + +#define TYPE_ATOM "application/atom+xml" +#define TYPE_RSS "application/rss+xml" +#define TYPE_MAYBE_FEED "application/vnd.mozilla.maybe.feed" + +#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +#define NS_RSS "http://purl.org/rss/1.0/" + +#define MAX_BYTES 512u + +NS_IMPL_ISUPPORTS(nsFeedSniffer, + nsIContentSniffer, + nsIStreamListener, + nsIRequestObserver) + +nsresult +nsFeedSniffer::ConvertEncodedData(nsIRequest* request, + const uint8_t* data, + uint32_t length) +{ + nsresult rv = NS_OK; + + mDecodedData = ""; + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(request)); + if (!httpChannel) + return NS_ERROR_NO_INTERFACE; + + nsAutoCString contentEncoding; + + mozilla::Unused << httpChannel->GetResponseHeader("Content-Encoding"_ns, + contentEncoding); + if (!contentEncoding.IsEmpty()) { + nsCOMPtr<nsIStreamConverterService> converterService(do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID)); + if (converterService) { + ToLowerCase(contentEncoding); + + nsCOMPtr<nsIStreamListener> converter; + rv = converterService->AsyncConvertData(contentEncoding.get(), + "uncompressed", this, nullptr, + getter_AddRefs(converter)); + NS_ENSURE_SUCCESS(rv, rv); + + converter->OnStartRequest(request); + + nsCOMPtr<nsIStringInputStream> rawStream = + do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID); + if (!rawStream) + return NS_ERROR_FAILURE; + + rv = rawStream->SetData((const char*)data, length); + NS_ENSURE_SUCCESS(rv, rv); + + rv = converter->OnDataAvailable(request, rawStream, 0, length); + NS_ENSURE_SUCCESS(rv, rv); + + converter->OnStopRequest(request, NS_OK); + } + } + return rv; +} + +template<int N> +static bool +StringBeginsWithLowercaseLiteral(nsAString& aString, + const char (&aSubstring)[N]) +{ + return StringHead(aString, N).LowerCaseEqualsLiteral(aSubstring); +} + +bool +HasAttachmentDisposition(nsIHttpChannel* httpChannel) +{ + if (!httpChannel) + return false; + + uint32_t disp; + nsresult rv = httpChannel->GetContentDisposition(&disp); + + if (NS_SUCCEEDED(rv) && disp == nsIChannel::DISPOSITION_ATTACHMENT) + return true; + + return false; +} + +/** + * @return the first occurrence of a character within a string buffer, + * or nullptr if not found + */ +inline const char* +FindChar(char c, const char *begin, const char *end) +{ + return static_cast<const char *>(memchr(begin, c, end - begin)); +} + +/** + * + * Determine if a substring is the "documentElement" in the document. + * + * All of our sniffed substrings: <rss, <feed, <rdf:RDF must be the "document" + * element within the XML DOM, i.e. the root container element. Otherwise, + * it's possible that someone embedded one of these tags inside a document of + * another type, e.g. a HTML document, and we don't want to show the preview + * page if the document isn't actually a feed. + * + * @param start + * The beginning of the data being sniffed + * @param end + * The end of the data being sniffed, right before the substring that + * was found. + * @returns true if the found substring is the documentElement, false + * otherwise. + */ +static bool +IsDocumentElement(const char *start, const char* end) +{ + // For every tag in the buffer, check to see if it's a PI, Doctype or + // comment, our desired substring or something invalid. + while ( (start = FindChar('<', start, end)) ) { + ++start; + if (start >= end) + return false; + + // Check to see if the character following the '<' is either '?' or '!' + // (processing instruction or doctype or comment)... these are valid nodes + // to have in the prologue. + if (*start != '?' && *start != '!') + return false; + + // Now advance the iterator until the '>' (We do this because we don't want + // to sniff indicator substrings that are embedded within other nodes, e.g. + // comments: <!-- <rdf:RDF .. > --> + start = FindChar('>', start, end); + if (!start) + return false; + + ++start; + } + return true; +} + +/** + * Determines whether or not a string exists as the root element in an XML data + * string buffer. + * @param dataString + * The data being sniffed + * @param substring + * The substring being tested for existence and root-ness. + * @returns true if the substring exists and is the documentElement, false + * otherwise. + */ +static bool +ContainsTopLevelSubstring(nsACString& dataString, const char *substring) +{ + nsACString::const_iterator start, end; + dataString.BeginReading(start); + dataString.EndReading(end); + + if (!FindInReadable(nsCString(substring), start, end)){ + return false; + } + + auto offset = start.get() - dataString.Data(); + + const char *begin = dataString.BeginReading(); + + // Only do the validation when we find the substring. + return IsDocumentElement(begin, begin + offset); +} + +NS_IMETHODIMP +nsFeedSniffer::GetMIMETypeFromContent(nsIRequest* request, + const uint8_t* data, + uint32_t length, + nsACString& sniffedType) +{ + nsCOMPtr<nsIHttpChannel> channel(do_QueryInterface(request)); + if (!channel) + return NS_ERROR_NO_INTERFACE; + + // Check that this is a GET request, since you can't subscribe to a POST... + nsAutoCString method; + nsresult rv; + mozilla::Unused << channel->GetRequestMethod(method); + if (!method.EqualsLiteral("GET")) { + sniffedType.Truncate(); + return NS_OK; + } + + // We need to find out if this is a load of a view-source document. In this + // case we do not want to override the content type, since the source display + // does not need to be converted from feed format to XUL. More importantly, + // we don't want to change the content type from something + // nsContentDLF::CreateInstance knows about (e.g. application/xml, text/html + // etc) to something that only the application fe knows about (maybe.feed) + // thus deactivating syntax highlighting. + nsCOMPtr<nsIURI> originalURI; + channel->GetOriginalURI(getter_AddRefs(originalURI)); + + nsAutoCString scheme; + originalURI->GetScheme(scheme); + if (scheme.EqualsLiteral("view-source")) { + sniffedType.Truncate(); + return NS_OK; + } + + // Check the Content-Type to see if it is set correctly. If it is set to + // something specific that we think is a reliable indication of a feed, don't + // bother sniffing since we assume the site maintainer knows what they're + // doing. + nsAutoCString contentType; + channel->GetContentType(contentType); + bool noSniff = contentType.EqualsLiteral(TYPE_RSS) || + contentType.EqualsLiteral(TYPE_ATOM); + + if (noSniff) { + // check for an attachment after we have a likely feed. + if(HasAttachmentDisposition(channel)) { + sniffedType.Truncate(); + return NS_OK; + } + + // set the feed header as a response header, since we have good metadata + // telling us that the feed is supposed to be RSS or Atom + mozilla::DebugOnly<nsresult> rv = + channel->SetResponseHeader("X-Moz-Is-Feed"_ns, + "1"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + sniffedType.AssignLiteral(TYPE_MAYBE_FEED); + return NS_OK; + } + + // Don't sniff arbitrary types. Limit sniffing to situations that + // we think can reasonably arise. + if (!contentType.EqualsLiteral(TEXT_HTML) && + !contentType.EqualsLiteral(APPLICATION_OCTET_STREAM) && + // Same criterion as XMLHttpRequest. Should we be checking for "+xml" + // and check for text/xml and application/xml by hand instead? + contentType.Find("xml") == -1) { + sniffedType.Truncate(); + return NS_OK; + } + + // Now we need to potentially decompress data served with + // Content-Encoding: gzip + rv = ConvertEncodedData(request, data, length); + if (NS_FAILED(rv)) + return rv; + + // We cap the number of bytes to scan at MAX_BYTES to prevent picking up + // false positives by accidentally reading document content, e.g. a "how to + // make a feed" page. + const char* testData; + if (mDecodedData.IsEmpty()) { + testData = (const char*)data; + length = std::min<uint32_t>(length, MAX_BYTES); + } else { + testData = mDecodedData.get(); + length = std::min<uint32_t>(mDecodedData.Length(), MAX_BYTES); + } + + // The strategy here is based on that described in: + // http://blogs.msdn.com/rssteam/articles/PublishersGuide.aspx + // for interoperarbility purposes. + + // Thus begins the actual sniffing. + nsDependentCSubstring dataString((const char*)testData, length); + + bool isFeed = false; + + // RSS 0.91/0.92/2.0 + isFeed = ContainsTopLevelSubstring(dataString, "<rss"); + + // Atom 1.0 + if (!isFeed) + isFeed = ContainsTopLevelSubstring(dataString, "<feed"); + + // RSS 1.0 + if (!isFeed) { + bool foundNS_RDF = FindInReadable(nsLiteralCString(NS_RDF), dataString); + bool foundNS_RSS = FindInReadable(nsLiteralCString(NS_RSS), dataString); + isFeed = ContainsTopLevelSubstring(dataString, "<rdf:RDF") && + foundNS_RDF && foundNS_RSS; + } + + // If we sniffed a feed, coerce our internal type + if (isFeed && !HasAttachmentDisposition(channel)) + sniffedType.AssignLiteral(TYPE_MAYBE_FEED); + else + sniffedType.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +nsFeedSniffer::OnStartRequest(nsIRequest* request) +{ + return NS_OK; +} + +nsresult +nsFeedSniffer::AppendSegmentToString(nsIInputStream* inputStream, + void* closure, + const char* rawSegment, + uint32_t toOffset, + uint32_t count, + uint32_t* writeCount) +{ + nsCString* decodedData = static_cast<nsCString*>(closure); + decodedData->Append(rawSegment, count); + *writeCount = count; + return NS_OK; +} + +NS_IMETHODIMP +nsFeedSniffer::OnDataAvailable(nsIRequest* request, nsIInputStream* stream, + uint64_t offset, uint32_t count) +{ + uint32_t read; + return stream->ReadSegments(AppendSegmentToString, &mDecodedData, count, + &read); +} + +NS_IMETHODIMP +nsFeedSniffer::OnStopRequest(nsIRequest* request, nsresult status) +{ + return NS_OK; +} diff --git a/comm/suite/components/feeds/nsFeedSniffer.h b/comm/suite/components/feeds/nsFeedSniffer.h new file mode 100644 index 0000000000..0128de2483 --- /dev/null +++ b/comm/suite/components/feeds/nsFeedSniffer.h @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsIContentSniffer.h" +#include "nsIStreamListener.h" +#include "nsString.h" +#include "nsSuiteCID.h" +#include "mozilla/Attributes.h" + +class nsFeedSniffer final : public nsIContentSniffer, nsIStreamListener +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTSNIFFER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + static nsresult AppendSegmentToString(nsIInputStream* inputStream, + void* closure, + const char* rawSegment, + uint32_t toOffset, + uint32_t count, + uint32_t* writeCount); + +protected: + ~nsFeedSniffer() {} + + nsresult ConvertEncodedData(nsIRequest* request, const uint8_t* data, + uint32_t length); + +private: + nsCString mDecodedData; +}; diff --git a/comm/suite/components/feeds/nsIFeedResultService.idl b/comm/suite/components/feeds/nsIFeedResultService.idl new file mode 100644 index 0000000000..a72292abd6 --- /dev/null +++ b/comm/suite/components/feeds/nsIFeedResultService.idl @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" +interface nsIURI; +interface nsIRequest; +interface nsIFeedResult; + +/** + * nsIFeedResultService provides a globally-accessible object for retrieving + * the results of feed processing. + */ +[scriptable, uuid(950a829e-c20e-4dc3-b447-f8b753ae54da)] +interface nsIFeedResultService : nsISupports +{ + /** + * When set to true, forces the preview page to be displayed, regardless + * of the user's preferences. + */ + attribute boolean forcePreviewPage; + + /** + * Adds a URI to the user's specified external feed handler, or live + * bookmarks. + * @param uri + * The uri of the feed to add. + * @param title + * The title of the feed to add. + * @param subtitle + * The subtitle of the feed to add. + * @param feedType + * The nsIFeed type of the feed. See nsIFeed.idl + */ + void addToClientReader(in AUTF8String uri, + in AString title, + in AString subtitle, + in unsigned long feedType); + + /** + * Registers a Feed Result object with a globally accessible service + * so that it can be accessed by a singleton method outside the usual + * flow of control in document loading. + * + * @param feedResult + * An object implementing nsIFeedResult representing the feed. + */ + void addFeedResult(in nsIFeedResult feedResult); + + /** + * Gets a Feed Handler object registered using addFeedResult. + * + * @param uri + * The URI of the feed a handler is being requested for + */ + nsIFeedResult getFeedResult(in nsIURI uri); + + /** + * Unregisters a Feed Handler object registered using addFeedResult. + * @param uri + * The feed URI the handler was registered under. This must be + * the same *instance* the feed was registered under. + */ + void removeFeedResult(in nsIURI uri); +}; diff --git a/comm/suite/components/feeds/nsIWebContentConverterRegistrar.idl b/comm/suite/components/feeds/nsIWebContentConverterRegistrar.idl new file mode 100644 index 0000000000..68ec48a936 --- /dev/null +++ b/comm/suite/components/feeds/nsIWebContentConverterRegistrar.idl @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsIMIMEInfo.idl" +#include "nsIWebContentHandlerRegistrar.idl" + +interface nsIRequest; + +[scriptable, uuid(eb361098-5158-4b21-8f98-50b445f1f0b2)] +interface nsIWebContentHandlerInfo : nsIHandlerApp +{ + /** + * The content type handled by the handler + */ + readonly attribute AString contentType; + + /** + * The uri of the handler, with an embedded %s where the URI of the loaded + * document will be encoded. + */ + readonly attribute AString uri; + + /** + * Gets the service URL Spec, with the loading document URI encoded in it. + * @param uri + * The URI of the document being loaded + * @returns The URI of the service with the loading document URI encoded in + * it. + */ + AString getHandlerURI(in AString uri); +}; + +[scriptable, uuid(de7cc06e-e778-45cb-b7db-7a114e1e75b1)] +interface nsIWebContentConverterService : nsIWebContentHandlerRegistrar +{ + /** + * Specifies the handler to be used to automatically handle all links of a + * certain content type from now on. + * @param contentType + * The content type to automatically load with the specified handler + * @param handler + * A web service handler. If this is null, no automatic action is + * performed and the user must choose. + * @throws NS_ERROR_NOT_AVAILABLE if the service refered to by |handler| is + * not already registered. + */ + void setAutoHandler(in AString contentType, in nsIWebContentHandlerInfo handler); + + /** + * Gets the auto handler specified for a particular content type + * @param contentType + * The content type to look up an auto handler for. + * @returns The web service handler that will automatically handle all + * documents of the specified type. null if there is no automatic + * handler. (Handlers may be registered, just none of them specified + * as "automatic"). + */ + nsIWebContentHandlerInfo getAutoHandler(in AString contentType); + + /** + * Gets a web handler for the specified service URI + * @param contentType + * The content type of the service being located + * @param uri + * The service URI of the handler to locate. + * @returns A web service handler that uses the specified uri. + */ + nsIWebContentHandlerInfo getWebContentHandlerByURI(in AString contentType, + in AString uri); + + /** + * Loads the preferred handler when content of a registered type is about + * to be loaded. + * @param request + * The nsIRequest for the load of the content + */ + void loadPreferredHandler(in nsIRequest request); + + /** + * Removes a registered protocol handler + * @param protocol + * The protocol scheme to remove a service handler for + * @param uri + * The uri of the service handler to remove + */ + void removeProtocolHandler(in AString protocol, in AString uri); + + /** + * Removes a registered content handler + * @param contentType + * The content type to remove a service handler for + * @param uri + * The uri of the service handler to remove + */ + void removeContentHandler(in AString contentType, in AString uri); + + /** + * Gets the list of content handlers for a particular type. + * @param contentType + * The content type to get handlers for + * @returns An array of nsIWebContentHandlerInfo objects + */ + void getContentHandlers(in AString contentType, + [optional] out unsigned long count, + [retval,array,size_is(count)] out nsIWebContentHandlerInfo handlers); + + /** + * Resets the list of available content handlers to the default set from + * the distribution. + * @param contentType + * The content type to reset handlers for + */ + void resetHandlersForType(in AString contentType); +}; |