diff options
author | wolfbeast <mcwerewolf@gmail.com> | 2018-07-18 08:24:24 +0200 |
---|---|---|
committer | wolfbeast <mcwerewolf@gmail.com> | 2018-07-18 08:24:24 +0200 |
commit | fc61780b35af913801d72086456f493f63197da6 (patch) | |
tree | f85891288a7bd988da9f0f15ae64e5c63f00d493 /application/basilisk/components/feeds | |
parent | 69f7f9e5f1475891ce11cc4f431692f965b0cd30 (diff) | |
parent | 50d3e596bbe89c95615f96eb71f6bc5be737a1db (diff) | |
download | UXP-9ccb235f04529c1ec345d87dad6521cb567d20bb.tar UXP-9ccb235f04529c1ec345d87dad6521cb567d20bb.tar.gz UXP-9ccb235f04529c1ec345d87dad6521cb567d20bb.tar.lz UXP-9ccb235f04529c1ec345d87dad6521cb567d20bb.tar.xz UXP-9ccb235f04529c1ec345d87dad6521cb567d20bb.zip |
Merge commit '50d3e596bbe89c95615f96eb71f6bc5be737a1db' into Basilisk-releasev2018.07.18
# Conflicts:
# browser/app/profile/firefox.js
# browser/components/preferences/jar.mn
Diffstat (limited to 'application/basilisk/components/feeds')
12 files changed, 3398 insertions, 0 deletions
diff --git a/application/basilisk/components/feeds/BrowserFeeds.manifest b/application/basilisk/components/feeds/BrowserFeeds.manifest new file mode 100644 index 000000000..ac5c299fa --- /dev/null +++ b/application/basilisk/components/feeds/BrowserFeeds.manifest @@ -0,0 +1,25 @@ +# This component must restrict its registration for the app-startup category +# to the specific list of apps that use it so it doesn't get loaded in xpcshell. +# Thus we restrict it to these apps: +# +# b2g: {3c2e2abc-06d4-11e1-ac3b-374f68613e61} +# browser: {ec8030f7-c20a-464f-9b0e-13a3a9e97384} +# mobile/android: {aa3c5121-dab2-40e2-81ca-7ea25febc110} +# mobile/xul: {a23983c0-fd0e-11dc-95ff-0800200c9a66} +# graphene: {d1bfe7d9-c01e-4237-998b-7b5f960a4314} + +component {229fa115-9412-4d32-baf3-2fc407f76fb1} FeedConverter.js +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.feed&to=*/* {229fa115-9412-4d32-baf3-2fc407f76fb1} +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.video.feed&to=*/* {229fa115-9412-4d32-baf3-2fc407f76fb1} +contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.maybe.audio.feed&to=*/* {229fa115-9412-4d32-baf3-2fc407f76fb1} +component {2376201c-bbc6-472f-9b62-7548040a61c6} FeedConverter.js +contract @mozilla.org/browser/feeds/result-service;1 {2376201c-bbc6-472f-9b62-7548040a61c6} +component {4f91ef2e-57ba-472e-ab7a-b4999e42d6c0} FeedConverter.js +contract @mozilla.org/network/protocol;1?name=feed {4f91ef2e-57ba-472e-ab7a-b4999e42d6c0} +component {1c31ed79-accd-4b94-b517-06e0c81999d5} FeedConverter.js +contract @mozilla.org/network/protocol;1?name=pcast {1c31ed79-accd-4b94-b517-06e0c81999d5} +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 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} application={d1bfe7d9-c01e-4237-998b-7b5f960a4314} diff --git a/application/basilisk/components/feeds/FeedConverter.js b/application/basilisk/components/feeds/FeedConverter.js new file mode 100644 index 000000000..c2c565608 --- /dev/null +++ b/application/basilisk/components/feeds/FeedConverter.js @@ -0,0 +1,568 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/debug.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +function LOG(str) { + dump("*** " + str + "\n"); +} + +const FS_CONTRACTID = "@mozilla.org/browser/feeds/result-service;1"; +const FPH_CONTRACTID = "@mozilla.org/network/protocol;1?name=feed"; +const PCPH_CONTRACTID = "@mozilla.org/network/protocol;1?name=pcast"; + +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; +const TYPE_ANY = "*/*"; + +const PREF_SELECTED_APP = "browser.feeds.handlers.application"; +const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_SELECTED_READER = "browser.feeds.handler.default"; + +const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application"; +const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; +const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler"; +const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default"; + +const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application"; +const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; +const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler"; +const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default"; + +function getPrefAppForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_APP; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_APP; + + default: + return PREF_SELECTED_APP; + } +} + +function getPrefWebForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_WEB; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_WEB; + + default: + return PREF_SELECTED_WEB; + } +} + +function getPrefActionForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_ACTION; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_ACTION; + + default: + return PREF_SELECTED_ACTION; + } +} + +function getPrefReaderForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_READER; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_READER; + + default: + return PREF_SELECTED_READER; + } +} + +function safeGetCharPref(pref, defaultValue) { + var prefs = + Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + try { + return prefs.getCharPref(pref); + } + catch (e) { + } + return defaultValue; +} + +function FeedConverter() { +} +FeedConverter.prototype = { + classID: Components.ID("{229fa115-9412-4d32-baf3-2fc407f76fb1}"), + + /** + * This is the downloaded text data for the feed. + */ + _data: null, + + /** + * This is the object listening to the conversion, which is ultimately the + * docshell for the load. + */ + _listener: null, + + /** + * Records if the feed was sniffed + */ + _sniffed: false, + + /** + * See nsIStreamConverter.idl + */ + convert(sourceStream, sourceType, destinationType, + context) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + /** + * See nsIStreamConverter.idl + */ + asyncConvertData(sourceType, destinationType, + listener, context) { + this._listener = listener; + }, + + /** + * Whether or not the preview page is being forced. + */ + _forcePreviewPage: false, + + /** + * Release our references to various things once we're done using them. + */ + _releaseHandles() { + this._listener = null; + this._request = null; + this._processor = null; + }, + + /** + * See nsIFeedResultListener.idl + */ + handleResult(result) { + // Feeds come in various content types, which our feed sniffer coerces to + // the maybe.feed type. However, feeds are used as a transport for + // different data types, e.g. news/blogs (traditional feed), video/audio + // (podcasts) and photos (photocasts, photostreams). Each of these is + // different in that there's a different class of application suitable for + // handling feeds of that type, but without a content-type differentiation + // it is difficult for us to disambiguate. + // + // The other problem is that if the user specifies an auto-action handler + // for one feed application, the fact that the content type is shared means + // that all other applications will auto-load with that handler too, + // regardless of the content-type. + // + // This means that content-type alone is not enough to determine whether + // or not a feed should be auto-handled. This means that for feeds we need + // to always use this stream converter, even when an auto-action is + // specified, not the basic one provided by WebContentConverter. This + // converter needs to consume all of the data and parse it, and based on + // that determination make a judgment about type. + // + // Since there are no content types for this content, and I'm not going to + // invent any, the upshot is that while a user can set an auto-handler for + // generic feed content, the system will prevent them from setting an auto- + // handler for other stream types. In those cases, the user will always see + // the preview page and have to select a handler. We can guess and show + // a client handler, but will not be able to show web handlers for those + // types. + // + // If this is just a feed, not some kind of specialized application, then + // auto-handlers can be set and we should obey them. + try { + let feedService = + Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + if (!this._forcePreviewPage && result.doc) { + let feed = result.doc.QueryInterface(Ci.nsIFeed); + let handler = safeGetCharPref(getPrefActionForType(feed.type), "ask"); + + if (handler != "ask") { + if (handler == "reader") + handler = safeGetCharPref(getPrefReaderForType(feed.type), "bookmarks"); + switch (handler) { + case "web": + let wccr = + Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + if ((feed.type == Ci.nsIFeed.TYPE_FEED && + wccr.getAutoHandler(TYPE_MAYBE_FEED)) || + (feed.type == Ci.nsIFeed.TYPE_VIDEO && + wccr.getAutoHandler(TYPE_MAYBE_VIDEO_FEED)) || + (feed.type == Ci.nsIFeed.TYPE_AUDIO && + wccr.getAutoHandler(TYPE_MAYBE_AUDIO_FEED))) { + wccr.loadPreferredHandler(this._request); + return; + } + break; + + default: + LOG("unexpected handler: " + handler); + // fall through -- let feed service handle error + case "bookmarks": + case "client": + case "default": + try { + let title = feed.title ? feed.title.plainText() : ""; + let desc = feed.subtitle ? feed.subtitle.plainText() : ""; + feedService.addToClientReader(result.uri.spec, title, desc, feed.type, handler); + return; + } catch (ex) { /* fallback to preview mode */ } + } + } + } + + let ios = + Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let chromeChannel; + + // handling a redirect, hence forwarding the loadInfo from the old channel + // to the newchannel. + let oldChannel = this._request.QueryInterface(Ci.nsIChannel); + let loadInfo = oldChannel.loadInfo; + + // If there was no automatic handler, or this was a podcast, + // photostream or some other kind of application, show the preview page + // if the parser returned a document. + if (result.doc) { + + // Store the result in the result service so that the display + // page can access it. + feedService.addFeedResult(result); + + // Now load the actual XUL document. + let aboutFeedsURI = ios.newURI("about:feeds", null, null); + chromeChannel = ios.newChannelFromURIWithLoadInfo(aboutFeedsURI, loadInfo); + chromeChannel.originalURI = result.uri; + + // carry the origin attributes from the channel that loaded the feed. + chromeChannel.owner = + Services.scriptSecurityManager.createCodebasePrincipal(aboutFeedsURI, + loadInfo.originAttributes); + } else { + chromeChannel = ios.newChannelFromURIWithLoadInfo(result.uri, loadInfo); + } + + chromeChannel.loadGroup = this._request.loadGroup; + chromeChannel.asyncOpen2(this._listener); + } + finally { + this._releaseHandles(); + } + }, + + /** + * See nsIStreamListener.idl + */ + onDataAvailable(request, context, inputStream, + sourceOffset, count) { + if (this._processor) + this._processor.onDataAvailable(request, context, inputStream, + sourceOffset, count); + }, + + /** + * See nsIRequestObserver.idl + */ + onStartRequest(request, context) { + let channel = request.QueryInterface(Ci.nsIChannel); + + // Check for a header that tells us there was no sniffing + // The value doesn't matter. + try { + let httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + // Make sure to check requestSucceeded before the potentially-throwing + // getResponseHeader. + if (!httpChannel.requestSucceeded) { + // Just give up, but don't forget to cancel the channel first! + request.cancel(Cr.NS_BINDING_ABORTED); + return; + } + + // Note: this throws if the header is not set. + httpChannel.getResponseHeader("X-Moz-Is-Feed"); + } + catch (ex) { + this._sniffed = true; + } + + this._request = request; + + // Save and reset the forced state bit early, in case there's some kind of + // error. + let feedService = + Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + this._forcePreviewPage = feedService.forcePreviewPage; + feedService.forcePreviewPage = false; + + // Parse feed data as it comes in + this._processor = + Cc["@mozilla.org/feed-processor;1"]. + createInstance(Ci.nsIFeedProcessor); + this._processor.listener = this; + this._processor.parseAsync(null, channel.URI); + + this._processor.onStartRequest(request, context); + }, + + /** + * See nsIRequestObserver.idl + */ + onStopRequest(request, context, status) { + if (this._processor) + this._processor.onStopRequest(request, context, status); + }, + + /** + * See nsISupports.idl + */ + QueryInterface(iid) { + if (iid.equals(Ci.nsIFeedResultListener) || + iid.equals(Ci.nsIStreamConverter) || + iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver)|| + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, +}; + +/** + * Keeps parsed FeedResults around for use elsewhere in the UI after the stream + * converter completes. + */ +function FeedResultService() { +} + +FeedResultService.prototype = { + classID: Components.ID("{2376201c-bbc6-472f-9b62-7548040a61c6}"), + + /** + * A URI spec -> [nsIFeedResult] hash. We have to keep a list as the + * value in case the same URI is requested concurrently. + */ + _results: { }, + + /** + * See nsIFeedResultService.idl + */ + forcePreviewPage: false, + + /** + * See nsIFeedResultService.idl + */ + addToClientReader(spec, title, subtitle, feedType, feedReader) { + if (!feedReader) { + feedReader = "default"; + } + + let handler = safeGetCharPref(getPrefActionForType(feedType), "bookmarks"); + if (handler == "ask" || handler == "reader") + handler = feedReader; + + switch (handler) { + case "client": + Services.cpmm.sendAsyncMessage("FeedConverter:ExecuteClientApp", + { spec, + title, + subtitle, + feedHandler: getPrefAppForType(feedType) }); + break; + case "default": + // Default system feed reader + Services.cpmm.sendAsyncMessage("FeedConverter:ExecuteClientApp", + { spec, + title, + subtitle, + feedHandler: "default" }); + break; + default: + // "web" should have been handled elsewhere + LOG("unexpected handler: " + handler); + // fall through + case "bookmarks": + Services.cpmm.sendAsyncMessage("FeedConverter:addLiveBookmark", + { spec, title, subtitle }); + break; + } + }, + + /** + * See nsIFeedResultService.idl + */ + addFeedResult(feedResult) { + NS_ASSERT(feedResult.uri != null, "null URI!"); + NS_ASSERT(feedResult.uri != null, "null feedResult!"); + let spec = feedResult.uri.spec; + if (!this._results[spec]) + this._results[spec] = []; + this._results[spec].push(feedResult); + }, + + /** + * See nsIFeedResultService.idl + */ + getFeedResult(uri) { + NS_ASSERT(uri != null, "null URI!"); + let resultList = this._results[uri.spec]; + for (let result of resultList) { + if (result.uri == uri) + return result; + } + return null; + }, + + /** + * See nsIFeedResultService.idl + */ + removeFeedResult(uri) { + NS_ASSERT(uri != null, "null URI!"); + let resultList = this._results[uri.spec]; + if (!resultList) + return; + let deletions = 0; + for (let i = 0; i < resultList.length; ++i) { + if (resultList[i].uri == uri) { + delete resultList[i]; + ++deletions; + } + } + + // send the holes to the end + resultList.sort(); + // and trim the list + resultList.splice(resultList.length - deletions, deletions); + if (resultList.length == 0) + delete this._results[uri.spec]; + }, + + createInstance(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + + QueryInterface(iid) { + if (iid.equals(Ci.nsIFeedResultService) || + iid.equals(Ci.nsIFactory) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, +}; + +/** + * A protocol handler that attempts to deal with the variant forms of feed: + * URIs that are actually either http or https. + */ +function GenericProtocolHandler() { +} +GenericProtocolHandler.prototype = { + _init(scheme) { + let ios = + Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + this._http = ios.getProtocolHandler("http"); + this._scheme = scheme; + }, + + get scheme() { + return this._scheme; + }, + + get protocolFlags() { + let {URI_DANGEROUS_TO_LOAD, ALLOWS_PROXY_HTTP, ALLOWS_PROXY} = + Ci.nsIProtocolHandler; + return URI_DANGEROUS_TO_LOAD | ALLOWS_PROXY | ALLOWS_PROXY_HTTP; + }, + + get defaultPort() { + return this._http.defaultPort; + }, + + allowPort(port, scheme) { + return this._http.allowPort(port, scheme); + }, + + newURI(spec, originalCharset, baseURI) { + // Feed URIs can be either nested URIs of the form feed:realURI (in which + // case we create a nested URI for the realURI) or feed://example.com, in + // which case we create a nested URI for the real protocol which is http. + + let scheme = this._scheme + ":"; + if (spec.substr(0, scheme.length) != scheme) + throw Cr.NS_ERROR_MALFORMED_URI; + + let prefix = spec.substr(scheme.length, 2) == "//" ? "http:" : ""; + let inner = Services.io.newURI(spec.replace(scheme, prefix), + originalCharset, baseURI); + + if (!["http", "https"].includes(inner.scheme)) + throw Cr.NS_ERROR_MALFORMED_URI; + + let uri = Services.io.QueryInterface(Ci.nsINetUtil).newSimpleNestedURI(inner); + uri.spec = inner.spec.replace(prefix, scheme); + return uri; + }, + + newChannel2(aUri, aLoadInfo) { + let inner = aUri.QueryInterface(Ci.nsINestedURI).innerURI; + let channel = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService). + newChannelFromURIWithLoadInfo(inner, aLoadInfo); + + if (channel instanceof Components.interfaces.nsIHttpChannel) + // Set this so we know this is supposed to be a feed + channel.setRequestHeader("X-Moz-Is-Feed", "1", false); + channel.originalURI = aUri; + return channel; + }, + + QueryInterface(iid) { + if (iid.equals(Ci.nsIProtocolHandler) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +function FeedProtocolHandler() { + this._init('feed'); +} +FeedProtocolHandler.prototype = new GenericProtocolHandler(); +FeedProtocolHandler.prototype.classID = Components.ID("{4f91ef2e-57ba-472e-ab7a-b4999e42d6c0}"); + +function PodCastProtocolHandler() { + this._init('pcast'); +} +PodCastProtocolHandler.prototype = new GenericProtocolHandler(); +PodCastProtocolHandler.prototype.classID = Components.ID("{1c31ed79-accd-4b94-b517-06e0c81999d5}"); + +var components = [FeedConverter, + FeedResultService, + FeedProtocolHandler, + PodCastProtocolHandler]; + + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/application/basilisk/components/feeds/FeedWriter.js b/application/basilisk/components/feeds/FeedWriter.js new file mode 100644 index 000000000..20f1399b0 --- /dev/null +++ b/application/basilisk/components/feeds/FeedWriter.js @@ -0,0 +1,1007 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +const FEEDWRITER_CID = Components.ID("{49bb6593-3aff-4eb3-a068-2712c28bd58e}"); +const FEEDWRITER_CONTRACTID = "@mozilla.org/browser/feeds/result-writer;1"; + +function LOG(str) { + let prefB = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + + let shouldLog = false; + try { + shouldLog = prefB.getBoolPref("feeds.log"); + } + catch (ex) { + } + + if (shouldLog) + 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) { + let ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + try { + return ios.newURI(aURLSpec, aCharset, null); + } catch (ex) { } + + return null; +} + +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 URI_BUNDLE = "chrome://browser/locale/feeds/subscribe.properties"; + +const TITLE_ID = "feedTitleText"; +const SUBTITLE_ID = "feedSubtitleText"; + +/** + * 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) { + let units = ["bytes", "kilobyte", "megabyte", "gigabyte"]; + let 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._selectedApp = undefined; + this._selectedAppMenuItem = null; + this._subscribeCallback = null; + this._defaultHandlerMenuItem = null; +} + +FeedWriter.prototype = { + _getPropertyAsBag(container, property) { + return container.fields.getProperty(property). + QueryInterface(Ci.nsIPropertyBag2); + }, + + _getPropertyAsString(container, property) { + try { + return container.fields.getPropertyAsAString(property); + } + catch (e) { + } + return ""; + }, + + _setContentText(id, text) { + let element = this._document.getElementById(id); + let textNode = text.createDocumentFragment(element); + while (element.hasChildNodes()) + element.removeChild(element.firstChild); + element.appendChild(textNode); + 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(element, attribute, uri) { + let secman = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + const flags = Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; + try { + // TODO Is this necessary? + secman.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... + } + catch (e) { + // Not allowed to load this link because secman.checkLoadURIStr threw + return; + } + + element.setAttribute(attribute, uri); + }, + + __bundle: null, + get _bundle() { + if (!this.__bundle) { + this.__bundle = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle(URI_BUNDLE); + } + return this.__bundle; + }, + + _getFormattedString(key, params) { + return this._bundle.formatStringFromName(key, params, params.length); + }, + + _getString(key) { + return this._bundle.GetStringFromName(key); + }, + + _setCheckboxCheckedState(aValue) { + let checkbox = this._document.getElementById("alwaysUse"); + if (checkbox) { + // see checkbox.xml, xbl bindings are not applied within the sandbox! TODO + let change = (aValue != (checkbox.getAttribute("checked") == "true")); + if (aValue) + checkbox.setAttribute("checked", "true"); + else + checkbox.removeAttribute("checked"); + + if (change) { + let event = this._document.createEvent("Events"); + event.initEvent("CheckboxStateChange", true, true); + checkbox.dispatchEvent(event); + } + } + }, + + /** + * Returns a date suitable for displaying in the feed preview. + * If the date cannot be parsed, the return value is "false". + * @param dateString + * A date as extracted from a feed entry. (entry.updated) + */ + _parseDate(dateString) { + // Convert the date into the user's local time zone + let 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 locale = Cc["@mozilla.org/chrome/chrome-registry;1"] + .getService(Ci.nsIXULChromeRegistry) + .getSelectedLocale("global", true); + const dtOptions = { year: 'numeric', month: 'long', day: 'numeric', + hour: 'numeric', minute: 'numeric' }; + this.__dateFormatter = new Intl.DateTimeFormat(locale, dtOptions); + } + return this.__dateFormatter; + }, + + /** + * Returns the feed type. + */ + __feedType: null, + _getFeedType() { + if (this.__feedType != null) + return this.__feedType; + + try { + // grab the feed because it's got the feed.type in it. + let container = this._getContainer(); + let feed = container.QueryInterface(Ci.nsIFeed); + this.__feedType = feed.type; + return feed.type; + } catch (ex) { } + + return Ci.nsIFeed.TYPE_FEED; + }, + + /** + * Writes the feed title into the preview document. + * @param container + * The feed container + */ + _setTitleText(container) { + if (container.title) { + let title = container.title.plainText(); + this._setContentText(TITLE_ID, container.title); + this._document.title = title; + } + + let feed = container.QueryInterface(Ci.nsIFeed); + if (feed && feed.subtitle) + this._setContentText(SUBTITLE_ID, container.subtitle); + }, + + /** + * Writes the title image into the preview document if one is present. + * @param container + * The feed container + */ + _setTitleImage(container) { + try { + let parts = container.image; + + // Set up the title image (supplied by the feed) + let feedTitleImage = this._document.getElementById("feedTitleImage"); + this._safeSetURIAttribute(feedTitleImage, "src", + parts.getPropertyAsAString("url")); + + // Set up the title image link + let feedTitleLink = this._document.getElementById("feedTitleLink"); + + let titleText = this._getFormattedString("linkTitleTextFormat", + [parts.getPropertyAsAString("title")]); + let feedTitleText = this._document.getElementById("feedTitleText"); + let titleImageWidth = parseInt(parts.getPropertyAsAString("width")) + 15; + + // Fix the margin on the main title, so that the image doesn't run over + // the underline + feedTitleLink.setAttribute('title', titleText); + feedTitleText.style.marginRight = 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(container) { + // Build the actual feed content + let feed = container.QueryInterface(Ci.nsIFeed); + if (feed.items.length == 0) + return; + + let 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); + if (entry.title.base) + span.setAttributeNS(XML_NS, "base", entry.title.base.spec); + span.appendChild(entry.title.createDocumentFragment(a)); + + // 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); + } + + let body = this._document.createElementNS(HTML_NS, "div"); + let summary = entry.summary || entry.content; + let 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) { + let 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) { + let enclosuresDiv = this._buildEnclosureDiv(entry); + entryContainer.appendChild(enclosuresDiv); + } + + let clearDiv = this._document.createElementNS(HTML_NS, "div"); + clearDiv.style.clear = "both"; + + feedContent.appendChild(entryContainer); + 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(aURL) { + let url = makeURI(aURL); + url.QueryInterface(Ci.nsIURL); + if (url == null || url.fileName.length == 0) + return decodeURIComponent(aURL); + + return decodeURIComponent(url.fileName); + }, + + /** + * Takes a FeedEntry with enclosures, generates the HTML code to represent + * them, and returns that. + * @param entry + * FeedEntry with enclosures + * @returns element + */ + _buildEnclosureDiv(entry) { + let 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"); + if (enc.hasKey("typeDesc")) + type_text = enc.get("typeDesc"); + + if (type_text && type_text.length > 0) + mozicon = "moz-icon://goat?size=16&contentType=" + enc.get("type"); + } + + 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. + * @returns A valid nsIFeedContainer object containing the contents of + * the feed. + */ + _getContainer() { + let feedService = + Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + + let result; + try { + result = + feedService.getFeedResult(this._getOriginalURI(this._window)); + } + catch (e) { + LOG("Subscribe Preview: feed not available?!"); + } + + if (result.bozo) { + LOG("Subscribe Preview: feed result is bozo?!"); + } + + let container; + try { + container = result.doc; + } + catch (e) { + LOG("Subscribe Preview: no result.doc? Why didn't the original reload?"); + return null; + } + return container; + }, + + /** + * 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(file) { + let ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let fph = ios.getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + let urlSpec = fph.getURLSpecFromFile(file); + return "moz-icon://" + urlSpec + "?size=16"; + }, + + /** + * 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(aCallback) { + this._subscribeCallback = aCallback; + this._mm.sendAsyncMessage("FeedWriter:ChooseClientApp", + { title: this._getString("chooseApplicationDialogTitle"), + feedType: this._getFeedType() }); + }, + + _setSubscribeUsingLabel() { + let stringLabel = "subscribeFeedUsing"; + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + stringLabel = "subscribeVideoPodcastUsing"; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + stringLabel = "subscribeAudioPodcastUsing"; + break; + } + + let subscribeUsing = this._document.getElementById("subscribeUsingDescription"); + let textNode = this._document.createTextNode(this._getString(stringLabel)); + subscribeUsing.insertBefore(textNode, subscribeUsing.firstChild); + }, + + _setAlwaysUseLabel() { + let checkbox = this._document.getElementById("alwaysUse"); + if (checkbox && this._handlersList) { + let handlerName = this._handlersList.selectedOptions[0] + .textContent; + let stringLabel = "alwaysUseForFeeds"; + switch (this._getFeedType()) { + case Ci.nsIFeed.TYPE_VIDEO: + stringLabel = "alwaysUseForVideoPodcasts"; + break; + + case Ci.nsIFeed.TYPE_AUDIO: + stringLabel = "alwaysUseForAudioPodcasts"; + break; + } + + let label = this._getFormattedString(stringLabel, [handlerName]); + + let checkboxText = this._document.getElementById("checkboxText"); + if (checkboxText.lastChild.nodeType == checkboxText.TEXT_NODE) { + checkboxText.lastChild.textContent = label; + } else { + LOG("FeedWriter._setAlwaysUseLabel: Expected textNode as lastChild of alwaysUse label"); + let textNode = this._document.createTextNode(label); + checkboxText.appendChild(textNode); + } + } + }, + + // nsIDomEventListener + handleEvent(event) { + if (event.target.ownerDocument != this._document) { + LOG("FeedWriter.handleEvent: Someone passed the feed writer as a listener to the events of another document!"); + return; + } + + switch (event.type) { + case "click": + if (event.target.id == "subscribeButton") { + this.subscribe(); + } + break; + case "change": + LOG("Change fired"); + if (event.target.selectedOptions[0].id == "chooseApplicationMenuItem") { + this._chooseClientApp(() => { + // Select the (per-prefs) selected handler if no application + // was selected + LOG("Selected handler after callback"); + this._setAlwaysUseLabel(); + }); + } else { + this._setAlwaysUseLabel(); + } + break; + } + }, + + _getWebHandlerElementsForURL(aURL) { + return this._handlersList.querySelectorAll('[webhandlerurl="' + aURL + '"]'); + }, + + _setSelectedHandlerResponse(handler, url) { + LOG(`Selecting handler response ${handler} ${url}`); + switch (handler) { + case "web": { + if (this._handlersList) { + let handlers = + this._getWebHandlerElementsForURL(url); + if (handlers.length == 0) { + LOG(`Selected web handler isn't in the menulist ${url}`); + return; + } + + handlers[0].selected = true; + } + break; + } + case "client": + case "default": + // do nothing, these are handled by the onchange event + break; + case "bookmarks": + default: { + let liveBookmarksMenuItem = this._document.getElementById("liveBookmarksMenuItem"); + if (liveBookmarksMenuItem) + liveBookmarksMenuItem.selected = true; + } + } + }, + + _initSubscriptionUI(setupMessage) { + if (!this._handlersList) + return; + LOG("UI init"); + + let feedType = this._getFeedType(); + + // change the background + let 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'; + } + + let liveBookmarksMenuItem = this._document.getElementById("liveBookmarksMenuItem"); + + // Last-selected application + let menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("id", "selectedAppMenuItem"); + menuItem.setAttribute("handlerType", "client"); + + // Hide the menuitem until we select an app + menuItem.style.display = "none"; + this._selectedAppMenuItem = menuItem; + + this._handlersList.appendChild(this._selectedAppMenuItem); + + // Create the menuitem for the default reader, but don't show/populate it until + // we get confirmation of what it is from the parent + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("id", "defaultHandlerMenuItem"); + menuItem.setAttribute("handlerType", "client"); + menuItem.style.display = "none"; + + this._defaultHandlerMenuItem = menuItem; + this._handlersList.appendChild(this._defaultHandlerMenuItem); + + // "Choose Application..." menuitem + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.setAttribute("id", "chooseApplicationMenuItem"); + menuItem.textContent = this._getString("chooseApplicationMenuItem"); + + this._handlersList.appendChild(menuItem); + + // separator + let chooseAppSep = liveBookmarksMenuItem.nextElementSibling.cloneNode(false); + chooseAppSep.textContent = liveBookmarksMenuItem.nextElementSibling.textContent; + this._handlersList.appendChild(chooseAppSep); + + for (let handler of setupMessage.handlers) { + if (!handler.uri) { + LOG("Handler with name " + handler.name + " has no URI!? Skipping..."); + continue; + } + menuItem = liveBookmarksMenuItem.cloneNode(false); + menuItem.removeAttribute("selected"); + menuItem.className = "menuitem-iconic"; + menuItem.textContent = handler.name; + menuItem.setAttribute("handlerType", "web"); + menuItem.setAttribute("webhandlerurl", handler.uri); + this._handlersList.appendChild(menuItem); + } + + this._setSelectedHandlerResponse(setupMessage.reader.handler, setupMessage.reader.url); + + if (setupMessage.defaultMenuItem) { + LOG(`Setting default menu item ${setupMessage.defaultMenuItem}`); + this._setApplicationLauncherMenuItem(this._defaultHandlerMenuItem, setupMessage.defaultMenuItem); + } + if (setupMessage.selectedMenuItem) { + LOG(`Setting selected menu item ${setupMessage.selectedMenuItem}`); + this._setApplicationLauncherMenuItem(this._selectedAppMenuItem, setupMessage.selectedMenuItem); + } + + // "Subscribe using..." + this._setSubscribeUsingLabel(); + + // "Always use..." checkbox initial state + this._setCheckboxCheckedState(setupMessage.reader.alwaysUse); + this._setAlwaysUseLabel(); + + // We update the "Always use.." checkbox label whenever the selected item + // in the list is changed + this._handlersList.addEventListener("change", this); + + // Set up the "Subscribe Now" button + this._document.getElementById("subscribeButton") + .addEventListener("click", this); + + // first-run ui + if (setupMessage.showFirstRunUI) { + let 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"; + } + + let feedinfo1 = this._document.getElementById("feedSubscriptionInfo1"); + let feedinfo1Str = this._getString(textfeedinfo1); + let feedinfo2 = this._document.getElementById("feedSubscriptionInfo2"); + let feedinfo2Str = this._getString(textfeedinfo2); + + feedinfo1.textContent = feedinfo1Str; + feedinfo2.textContent = feedinfo2Str; + + header.setAttribute('firstrun', 'true'); + + this._mm.sendAsyncMessage("FeedWriter:ShownFirstRun"); + } + }, + + /** + * 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(aWindow) { + let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + let chan = docShell.currentDocumentChannel; + + // We probably need to call InheritFromDocShellToDoc for this, but right now + // we can't call it from JS. + let attrs = docShell.getOriginAttributes(); + let ssm = Services.scriptSecurityManager; + let nullPrincipal = ssm.createNullPrincipal(attrs); + + // this channel is not going to be openend, use a nullPrincipal + // and the most restrctive 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, + _handlersList: null, + + // BrowserFeedWriter WebIDL methods + init(aWindow) { + let window = aWindow; + this._feedURI = this._getOriginalURI(window); + if (!this._feedURI) + return; + + this._window = window; + this._document = window.document; + this._handlersList = this._document.getElementById("handlersMenuList"); + + let secman = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + this._feedPrincipal = secman.createCodebasePrincipal(this._feedURI, {}); + + LOG("Subscribe Preview: feed uri = " + this._window.location.href); + + + this._mm.addMessageListener("FeedWriter:PreferenceUpdated", this); + this._mm.addMessageListener("FeedWriter:SetApplicationLauncherMenuItem", this); + this._mm.addMessageListener("FeedWriter:GetSubscriptionUIResponse", this); + this._mm.addMessageListener("FeedWriter:SetFeedPrefsAndSubscribeResponse", this); + + const feedType = this._getFeedType(); + this._mm.sendAsyncMessage("FeedWriter:GetSubscriptionUI", + { feedType }); + }, + + receiveMessage(msg) { + if (!this._window) { + // this._window is null unless this.init was called with a trusted + // window object. + return; + } + LOG(`received message from parent ${msg.name}`); + switch (msg.name) { + case "FeedWriter:PreferenceUpdated": + // This is called when browser-feeds.js spots a pref change + // This will happen when + // - about:preferences#applications changes + // - another feed reader page changes the preference + // - when this page itself changes the select and there isn't a redirect + // bookmarks and launching an external app means the page stays open after subscribe + const feedType = this._getFeedType(); + LOG(`Got prefChange! ${JSON.stringify(msg.data)} current type: ${feedType}`); + let feedTypePref = msg.data.default; + if (feedType in msg.data) { + feedTypePref = msg.data[feedType]; + } + LOG(`Got pref ${JSON.stringify(feedTypePref)}`); + this._setCheckboxCheckedState(feedTypePref.alwaysUse); + this._setSelectedHandlerResponse(feedTypePref.handler, feedTypePref.url); + this._setAlwaysUseLabel(); + break; + case "FeedWriter:SetFeedPrefsAndSubscribeResponse": + LOG(`FeedWriter:SetFeedPrefsAndSubscribeResponse - Redirecting ${msg.data.redirect}`); + this._window.location.href = msg.data.redirect; + break; + case "FeedWriter:GetSubscriptionUIResponse": + // Set up the subscription UI + this._initSubscriptionUI(msg.data); + break; + case "FeedWriter:SetApplicationLauncherMenuItem": + LOG(`FeedWriter:SetApplicationLauncherMenuItem - picked ${msg.data.name}`); + this._setApplicationLauncherMenuItem(this._selectedAppMenuItem, msg.data.name); + // Potentially a bit racy, but I don't think we can get into a state where this callback is set and + // we're not coming back from ChooseClientApp in browser-feeds.js + if (this._subscribeCallback) { + this._subscribeCallback(); + this._subscribeCallback = null; + } + break; + } + }, + + _setApplicationLauncherMenuItem(menuItem, aName) { + /* unselect all handlers */ + [...this._handlersList.children].forEach((option) => { + option.removeAttribute("selected"); + }); + menuItem.textContent = aName; + menuItem.style.display = ""; + menuItem.selected = true; + }, + + writeContent() { + if (!this._window) + return; + + try { + // Set up the feed content + let container = this._getContainer(); + if (!container) + return; + + this._setTitleText(container); + this._setTitleImage(container); + this._writeFeedContent(container); + } + finally { + this._removeFeedFromCache(); + } + }, + + close() { + this._document.getElementById("subscribeButton") + .removeEventListener("click", this, false); + this._handlersList + .removeEventListener("change", this, false); + this._document = null; + this._window = null; + this._handlersList = null; + + this._removeFeedFromCache(); + this.__bundle = null; + this._feedURI = null; + + this._selectedApp = undefined; + this._selectedAppMenuItem = null; + this._defaultHandlerMenuItem = null; + }, + + _removeFeedFromCache() { + if (this._feedURI) { + let feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + feedService.removeFeedResult(this._feedURI); + this._feedURI = null; + } + }, + + subscribe() { + let feedType = this._getFeedType(); + + // Subscribe to the feed using the selected handler and save prefs + let defaultHandler = "reader"; + let useAsDefault = this._document.getElementById("alwaysUse").getAttribute("checked"); + + let selectedItem = this._handlersList.selectedOptions[0]; + let subscribeCallback = () => { + let feedReader = null; + let settings = { + feedType, + useAsDefault, + // Pull the title and subtitle out of the document + feedTitle: this._document.getElementById(TITLE_ID).textContent, + feedSubtitle: this._document.getElementById(SUBTITLE_ID).textContent, + feedLocation: this._window.location.href + }; + if (selectedItem.hasAttribute("webhandlerurl")) { + feedReader = "web"; + settings.uri = selectedItem.getAttribute("webhandlerurl"); + } else { + switch (selectedItem.id) { + case "selectedAppMenuItem": + feedReader = "client"; + break; + case "defaultHandlerMenuItem": + feedReader = "default"; + break; + case "liveBookmarksMenuItem": + defaultHandler = "bookmarks"; + feedReader = "bookmarks"; + break; + } + } + settings.reader = feedReader; + + // 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 "bookmarks" (if the live bookmarks option is selected). + // Otherwise, we should set it to "ask" + if (!useAsDefault) { + defaultHandler = "ask"; + } + settings.action = defaultHandler; + LOG(`FeedWriter:SetFeedPrefsAndSubscribe - ${JSON.stringify(settings)}`); + this._mm.sendAsyncMessage("FeedWriter:SetFeedPrefsAndSubscribe", + settings); + } + + // Show the file picker before subscribing if the + // choose application menuitem was chosen using the keyboard + if (selectedItem.id == "chooseApplicationMenuItem") { + this._chooseClientApp(function(aResult) { + if (aResult) { + selectedItem = + this._handlersList.selectedOptions[0]; + subscribeCallback(); + } + }.bind(this)); + } else { + subscribeCallback(); + } + }, + + get _mm() { + let mm = this._window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDocShell). + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIContentFrameMessageManager); + delete this._mm; + return this._mm = mm; + }, + + classID: FEEDWRITER_CID, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener, Ci.nsIObserver, + Ci.nsINavHistoryObserver, + Ci.nsIDOMGlobalPropertyInitializer]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FeedWriter]); diff --git a/application/basilisk/components/feeds/WebContentConverter.js b/application/basilisk/components/feeds/WebContentConverter.js new file mode 100644 index 000000000..2cb5cd145 --- /dev/null +++ b/application/basilisk/components/feeds/WebContentConverter.js @@ -0,0 +1,1071 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +function LOG(str) { + dump("*** " + str + "\n"); +} + +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://browser/locale/feeds/subscribe.properties"; + +const NS_ERROR_MODULE_DOM = 2152923136; +const NS_ERROR_DOM_SYNTAX_ERR = NS_ERROR_MODULE_DOM + 12; + +function WebContentConverter() { +} +WebContentConverter.prototype = { + convert() { }, + asyncConvertData() { }, + onDataAvailable() { }, + onStopRequest() { }, + + onStartRequest(request, context) { + let wccr = + Cc[WCCR_CONTRACTID]. + getService(Ci.nsIWebContentConverterService); + wccr.loadPreferredHandler(request); + }, + + QueryInterface(iid) { + if (iid.equals(Ci.nsIStreamConverter) || + iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +let WebContentConverterFactory = { + createInstance(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return new WebContentConverter().QueryInterface(iid); + }, + + QueryInterface(iid) { + if (iid.equals(Ci.nsIFactory) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +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(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(uri) { + return this._uri.replace(/%s/gi, encodeURIComponent(uri)); + }, + + QueryInterface(iid) { + if (iid.equals(Ci.nsIWebContentHandlerInfo) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +const Utils = { + makeURI(aURL, aOriginCharset, aBaseURI) { + return Services.io.newURI(aURL, aOriginCharset, aBaseURI); + }, + + checkAndGetURI(aURIString, aContentWindow) { + let uri; + try { + let baseURI = aContentWindow.document.baseURIObject; + uri = this.makeURI(aURIString, null, baseURI); + } catch (ex) { + throw NS_ERROR_DOM_SYNTAX_ERR; + } + + // For security reasons we reject non-http(s) urls (see bug 354316), + // we may need to revise this once we support more content types + if (uri.scheme != "http" && uri.scheme != "https") { + throw this.getSecurityError( + "Permission denied to add " + uri.spec + " as a content or protocol handler", + aContentWindow); + } + + // We also reject handlers registered from a different host (see bug 402287) + // The pref allows us to test the feature + let pb = Services.prefs; + if (!pb.getBoolPref(PREF_ALLOW_DIFFERENT_HOST) && + (!["http:", "https:"].includes(aContentWindow.location.protocol) || + aContentWindow.location.hostname != uri.host)) { + throw this.getSecurityError( + "Permission denied to add " + uri.spec + " as a content or protocol handler", + aContentWindow); + } + + // If the uri doesn't contain '%s', it won't be a good handler + if (uri.spec.indexOf("%s") < 0) + throw NS_ERROR_DOM_SYNTAX_ERR; + + return uri; + }, + + // NB: Throws if aProtocol is not allowed. + checkProtocolHandlerAllowed(aProtocol, aURIString, aWindowOrNull) { + // First, check to make sure this isn't already handled internally (we don't + // want to let them take over, say "chrome"). + let handler = Services.io.getProtocolHandler(aProtocol); + if (!(handler instanceof Ci.nsIExternalProtocolHandler)) { + // This is handled internally, so we don't want them to register + throw this.getSecurityError( + `Permission denied to add ${aURIString} as a protocol handler`, + aWindowOrNull); + } + + // check if it is in the black list + let pb = Services.prefs; + let allowed; + try { + allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol); + } + catch (e) { + allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default"); + } + if (!allowed) { + throw this.getSecurityError( + `Not allowed to register a protocol handler for ${aProtocol}`, + aWindowOrNull); + } + }, + + // Return a SecurityError exception from the given Window if one is given. If + // none is given, just return the given error string, for lack of anything + // better. + getSecurityError(errorString, aWindowOrNull) { + if (!aWindowOrNull) { + return errorString; + } + + return new aWindowOrNull.DOMException(errorString, "SecurityError"); + }, + + /** + * Mappings from known feed types to our internal content type. + */ + _mappings: { + "application/rss+xml": TYPE_MAYBE_FEED, + "application/atom+xml": TYPE_MAYBE_FEED, + }, + + resolveContentType(aContentType) { + if (aContentType in this._mappings) + return this._mappings[aContentType]; + return aContentType; + } +}; + +function WebContentConverterRegistrar() { + this._contentTypes = {}; + this._autoHandleContentTypes = {}; +} + +WebContentConverterRegistrar.prototype = { + get stringBundle() { + let sb = Services.strings.createBundle(STRING_BUNDLE_URI); + delete WebContentConverterRegistrar.prototype.stringBundle; + return WebContentConverterRegistrar.prototype.stringBundle = sb; + }, + + _getFormattedString(key, params) { + return this.stringBundle.formatStringFromName(key, params, params.length); + }, + + _getString(key) { + return this.stringBundle.GetStringFromName(key); + }, + + /** + * See nsIWebContentConverterService + */ + getAutoHandler(contentType) { + contentType = Utils.resolveContentType(contentType); + if (contentType in this._autoHandleContentTypes) + return this._autoHandleContentTypes[contentType]; + return null; + }, + + /** + * See nsIWebContentConverterService + */ + setAutoHandler(contentType, handler) { + if (handler && !this._typeIsRegistered(contentType, handler.uri)) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + contentType = Utils.resolveContentType(contentType); + this._setAutoHandler(contentType, handler); + + let ps = Services.prefs; + let autoBranch = ps.getBranch(PREF_CONTENTHANDLERS_AUTO); + if (handler) + autoBranch.setCharPref(contentType, handler.uri); + else if (autoBranch.prefHasUserValue(contentType)) + autoBranch.clearUserPref(contentType); + + ps.savePrefFile(null); + }, + + /** + * Update the internal data structure (not persistent) + */ + _setAutoHandler(contentType, handler) { + if (handler) + this._autoHandleContentTypes[contentType] = handler; + else if (contentType in this._autoHandleContentTypes) + delete this._autoHandleContentTypes[contentType]; + }, + + /** + * See nsIWebContentConverterService + */ + getWebContentHandlerByURI(contentType, uri) { + return this.getContentHandlers(contentType) + .find(e => e.uri == uri) || null; + }, + + /** + * See nsIWebContentConverterService + */ + loadPreferredHandler(request) { + let channel = request.QueryInterface(Ci.nsIChannel); + let contentType = Utils.resolveContentType(channel.contentType); + let handler = this.getAutoHandler(contentType); + if (handler) { + request.cancel(Cr.NS_ERROR_FAILURE); + + let webNavigation = + channel.notificationCallbacks.getInterface(Ci.nsIWebNavigation); + webNavigation.loadURI(handler.getHandlerURI(channel.URI.spec), + Ci.nsIWebNavigation.LOAD_FLAGS_NONE, + null, null, null); + } + }, + + /** + * See nsIWebContentConverterService + */ + removeProtocolHandler(aProtocol, aURITemplate) { + let eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + let handlerInfo = eps.getProtocolHandlerInfo(aProtocol); + let 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(contentType, uri) { + function notURI(serviceInfo) { + return serviceInfo.uri != uri; + } + + if (contentType in this._contentTypes) { + this._contentTypes[contentType] = + this._contentTypes[contentType].filter(notURI); + } + }, + + /** + * 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 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(aProtocol, aURITemplate) { + let eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + let handlerInfo = eps.getProtocolHandlerInfo(aProtocol); + let 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(aProtocol, aURIString, aTitle, aBrowserOrWindow) { + LOG("registerProtocolHandler(" + aProtocol + "," + aURIString + "," + aTitle + ")"); + let haveWindow = (aBrowserOrWindow instanceof Ci.nsIDOMWindow); + let uri; + if (haveWindow) { + uri = Utils.checkAndGetURI(aURIString, aBrowserOrWindow); + } else { + // aURIString must not be a relative URI. + uri = Utils.makeURI(aURIString, null); + } + + // If the protocol handler is already registered, just return early. + if (this._protocolHandlerRegistered(aProtocol, uri.spec)) { + return; + } + + let browser; + if (haveWindow) { + let browserWindow = + this._getBrowserWindowForContentWindow(aBrowserOrWindow); + browser = this._getBrowserForContentWindow(browserWindow, + aBrowserOrWindow); + } else { + browser = aBrowserOrWindow; + } + if (PrivateBrowsingUtils.isBrowserPrivate(browser)) { + // 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; + } + + Utils.checkProtocolHandlerAllowed(aProtocol, aURIString, + haveWindow ? aBrowserOrWindow : null); + + // Now Ask the user and provide the proper callback + let message = this._getFormattedString("addProtocolHandler", + [aTitle, uri.host, aProtocol]); + + let notificationIcon = uri.prePath + "/favicon.ico"; + let notificationValue = "Protocol Registration: " + aProtocol; + let addButton = { + label: this._getString("addProtocolHandlerAddButton"), + accessKey: this._getString("addProtocolHandlerAddButtonAccesskey"), + protocolInfo: { protocol: aProtocol, uri: uri.spec, name: aTitle }, + + callback(aNotification, aButtonInfo) { + let protocol = aButtonInfo.protocolInfo.protocol; + let uri = aButtonInfo.protocolInfo.uri; + let name = aButtonInfo.protocolInfo.name; + + let handler = Cc["@mozilla.org/uriloader/web-handler-app;1"]. + createInstance(Ci.nsIWebHandlerApp); + handler.name = name; + handler.uriTemplate = uri; + + let eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + let handlerInfo = eps.getProtocolHandlerInfo(protocol); + handlerInfo.possibleApplicationHandlers.appendElement(handler, false); + + // 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; + + let hs = Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService); + hs.store(handlerInfo); + } + }; + let notificationBox = browser.getTabBrowser().getNotificationBox(browser); + notificationBox.appendNotification(message, + notificationValue, + notificationIcon, + notificationBox.PRIORITY_INFO_LOW, + [addButton]); + }, + + /** + * See nsIWebContentHandlerRegistrar + * If a DOM window is provided, then the request came from content, so we + * prompt the user to confirm the registration. + */ + registerContentHandler(aContentType, aURIString, aTitle, aWindowOrBrowser) { + LOG("registerContentHandler(" + aContentType + "," + aURIString + "," + aTitle + ")"); + + // Make sure to do our URL checks up front, before our content type check, + // just like the WebContentConverterRegistrarContent does. + let haveWindow = aWindowOrBrowser && + (aWindowOrBrowser instanceof Ci.nsIDOMWindow); + let uri; + if (haveWindow) { + uri = Utils.checkAndGetURI(aURIString, aWindowOrBrowser); + } else if (aWindowOrBrowser) { + // uri was vetted in the content process. + uri = Utils.makeURI(aURIString, null); + } + + // We only support feed types at present. + let contentType = Utils.resolveContentType(aContentType); + // XXX We should be throwing a Utils.getSecurityError() here in at least + // some cases. See bug 1266492. + if (contentType != TYPE_MAYBE_FEED) { + return; + } + + if (aWindowOrBrowser) { + let notificationBox; + if (haveWindow) { + let browserWindow = this._getBrowserWindowForContentWindow(aWindowOrBrowser); + let browserElement = this._getBrowserForContentWindow(browserWindow, aWindowOrBrowser); + notificationBox = browserElement.getTabBrowser().getNotificationBox(browserElement); + } else { + notificationBox = aWindowOrBrowser.getTabBrowser() + .getNotificationBox(aWindowOrBrowser); + } + + this._appendFeedReaderNotification(uri, aTitle, notificationBox); + } + else { + this._registerContentHandler(contentType, aURIString, aTitle); + } + }, + + /** + * Returns the browser chrome window in which the content window is in + */ + _getBrowserWindowForContentWindow(aContentWindow) { + return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .wrappedJSObject; + }, + + /** + * Returns the <xul:browser> element associated with the given content + * window. + * + * @param aBrowserWindow + * The browser window in which the content window is in. + * @param aContentWindow + * The content window. It's possible to pass a child content window + * (i.e. the content window of a frame/iframe). + */ + _getBrowserForContentWindow(aBrowserWindow, aContentWindow) { + // This depends on pseudo APIs of browser.js and tabbrowser.xml + aContentWindow = aContentWindow.top; + return aBrowserWindow.gBrowser.browsers.find((browser) => + browser.contentWindow == aContentWindow); + }, + + /** + * 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(aURI, aName, aNotificationBox) { + let uriSpec = aURI.spec; + let notificationValue = "feed reader notification: " + uriSpec; + let notificationIcon = aURI.prePath + "/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; + + let buttons; + let message; + if (this.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uriSpec)) + message = this._getFormattedString("handlerRegistered", [aName]); + else { + message = this._getFormattedString("addHandler", [aName, aURI.host]); + let self = this; + let addButton = { + _outer: self, + label: self._getString("addHandlerAddButton"), + accessKey: self._getString("addHandlerAddButtonAccesskey"), + feedReaderInfo: { uri: uriSpec, name: aName }, + + /* static */ + callback(aNotification, aButtonInfo) { + let uri = aButtonInfo.feedReaderInfo.uri; + let name = aButtonInfo.feedReaderInfo.name; + let 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(contentType, uri, title) { + let ps = Services.prefs; + let i = 0; + let typeBranch = null; + while (true) { + typeBranch = + ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + i + "."); + try { + typeBranch.getCharPref("type"); + ++i; + } + catch (e) { + // No more handlers + break; + } + } + if (typeBranch) { + typeBranch.setCharPref("type", contentType); + let 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); + + ps.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 content type + */ + _typeIsRegistered(contentType, uri) { + if (!(contentType in this._contentTypes)) + return false; + + return this._contentTypes[contentType] + .some(t => t.uri == uri); + }, + + /** + * 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(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(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 + let pb = Services.prefs.getBranch(null); + pb.setCharPref(PREF_SELECTED_READER, "web"); + + let supportsString = + Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + supportsString.data = uri; + pb.setComplexValue(PREF_SELECTED_WEB, Ci.nsISupportsString, + supportsString); + pb.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(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)) { + let converterContractID = this._getConverterContractID(contentType); + let cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + cr.registerFactory(WCC_CLASSID, WCC_CLASSNAME, converterContractID, + WebContentConverterFactory); + } + }, + + /** + * See nsIWebContentConverterService + */ + getContentHandlers(contentType, countRef) { + if (countRef) { + countRef.value = 0; + } + if (!(contentType in this._contentTypes)) + return []; + + let handlers = this._contentTypes[contentType]; + if (countRef) { + countRef.value = handlers.length; + } + return handlers; + }, + + /** + * See nsIWebContentConverterService + */ + 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. + * + * 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! + * + * @param branch + * an nsIPrefBranch containing "type", "uri", and "title" preferences + * corresponding to the content handler to be registered + */ + _registerContentHandlerHavingBranch(branch) { + let vals = branch.getChildList(""); + if (vals.length == 0) + return; + + let type = branch.getCharPref("type"); + let uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data; + let title = branch.getComplexValue("title", + Ci.nsIPrefLocalizedString).data; + this._updateContentTypeHandlerMap(type, uri, title); + }, + + /** + * Load the auto handler, content handler and protocol tables from + * preferences. + */ + _init() { + let ps = Services.prefs; + + let children = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH) + .getChildList(""); + + // first get the numbers of the providers by getting all ###.uri prefs + let nums = children.map((child) => { + let match = /^(\d+)\.uri$/.exec(child); + return match ? match[1] : ""; + }).filter(child => !!child) + .sort(); + + + // now register them + for (let num of nums) { + let branch = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + num + "."); + try { + this._registerContentHandlerHavingBranch(branch); + } catch (ex) { + // do nothing, the next branch might have values + } + } + + // We need to do this _after_ registering all of the available handlers, + // so that getWebContentHandlerByURI can return successfully. + let autoBranch; + try { + autoBranch = ps.getBranch(PREF_CONTENTHANDLERS_AUTO); + } catch (e) { + // No auto branch yet, that's fine + // LOG("WCCR.init: There is no auto branch, benign"); + } + + if (autoBranch) { + for (let type of autoBranch.getChildList("")) { + let uri = autoBranch.getCharPref(type); + if (uri) { + let handler = this.getWebContentHandlerByURI(type, uri); + if (handler) { + this._setAutoHandler(type, handler); + } + } + } + } + }, + + /** + * See nsIObserver + */ + observe(subject, topic, data) { + let os = Services.obs; + switch (topic) { + case "app-startup": + os.addObserver(this, "browser-ui-startup-complete", false); + break; + case "browser-ui-startup-complete": + os.removeObserver(this, "browser-ui-startup-complete"); + this._init(); + break; + } + }, + + /** + * See nsIFactory + */ + createInstance(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + + classID: WCCR_CLASSID, + + /** + * See nsISupports + */ + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIWebContentConverterService, + Ci.nsIWebContentHandlerRegistrar, + Ci.nsIObserver, + Ci.nsIFactory]), + + _xpcom_categories: [{ + category: "app-startup", + service: true + }] +}; + +function WebContentConverterRegistrarContent() { + this._contentTypes = {}; +} + +WebContentConverterRegistrarContent.prototype = { + + /** + * Load the auto handler, content handler and protocol tables from + * preferences. + */ + _init() { + let ps = Services.prefs; + + let children = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH) + .getChildList(""); + + // first get the numbers of the providers by getting all ###.uri prefs + let nums = children.map((child) => { + let match = /^(\d+)\.uri$/.exec(child); + return match ? match[1] : ""; + }).filter(child => !!child) + .sort(); + + // now register them + for (num of nums) { + let branch = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + num + "."); + try { + this._registerContentHandlerHavingBranch(branch); + } catch (ex) { + // do nothing, the next branch might have values + } + } + }, + + _typeIsRegistered(contentType, uri) { + return this._contentTypes[contentType] + .some(e => e.uri == uri); + }, + + /** + * 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! + * + * @param branch + * The pref branch to register the content handler under + * + */ + _registerContentHandlerHavingBranch(branch) { + let vals = branch.getChildList(""); + if (vals.length == 0) + return; + + let type = branch.getCharPref("type"); + let uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data; + let title = branch.getComplexValue("title", + Ci.nsIPrefLocalizedString).data; + this._updateContentTypeHandlerMap(type, uri, title); + }, + + _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)) { + let converterContractID = this._getConverterContractID(contentType); + let cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + cr.registerFactory(WCC_CLASSID, WCC_CLASSNAME, converterContractID, + WebContentConverterFactory); + } + }, + + /** + * See nsIWebContentConverterService + */ + getContentHandlers(contentType, countRef) { + this._init(); + if (countRef) { + countRef.value = 0; + } + + if (!(contentType in this._contentTypes)) + return []; + + let handlers = this._contentTypes[contentType]; + if (countRef) { + countRef.value = handlers.length; + } + return handlers; + }, + + setAutoHandler(contentType, handler) { + Services.cpmm.sendAsyncMessage("WCCR:setAutoHandler", + { contentType, handler }); + }, + + getWebContentHandlerByURI(contentType, uri) { + return this.getContentHandlers(contentType) + .find(e => e.uri == uri) || null; + }, + + /** + * See nsIWebContentHandlerRegistrar + */ + registerContentHandler(aContentType, aURIString, aTitle, aBrowserOrWindow) { + // aBrowserOrWindow must be a window. + let messageManager = aBrowserOrWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsITabChild) + .messageManager; + + let uri = Utils.checkAndGetURI(aURIString, aBrowserOrWindow); + // XXX We should be throwing a Utils.getSecurityError() here in at least + // some cases. See bug 1266492. + if (Utils.resolveContentType(aContentType) != TYPE_MAYBE_FEED) { + return; + } + + messageManager.sendAsyncMessage("WCCR:registerContentHandler", + { contentType: aContentType, + uri: uri.spec, + title: aTitle }); + }, + + registerProtocolHandler(aProtocol, aURIString, aTitle, aBrowserOrWindow) { + // aBrowserOrWindow must be a window. + let messageManager = aBrowserOrWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsITabChild) + .messageManager; + + let uri = Utils.checkAndGetURI(aURIString, aBrowserOrWindow); + Utils.checkProtocolHandlerAllowed(aProtocol, aURIString, aBrowserOrWindow); + + messageManager.sendAsyncMessage("WCCR:registerProtocolHandler", + { protocol: aProtocol, + uri: uri.spec, + title: aTitle }); + }, + + /** + * See nsIFactory + */ + createInstance(outer, iid) { + if (outer != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + + classID: WCCR_CLASSID, + + /** + * See nsISupports + */ + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIWebContentHandlerRegistrar, + Ci.nsIWebContentConverterService, + Ci.nsIFactory]) +}; + +this.NSGetFactory = + (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) ? + XPCOMUtils.generateNSGetFactory([WebContentConverterRegistrarContent]) : + XPCOMUtils.generateNSGetFactory([WebContentConverterRegistrar]); diff --git a/application/basilisk/components/feeds/content/subscribe.js b/application/basilisk/components/feeds/content/subscribe.js new file mode 100644 index 000000000..05a564bf1 --- /dev/null +++ b/application/basilisk/components/feeds/content/subscribe.js @@ -0,0 +1,25 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global BrowserFeedWriter */ + +var SubscribeHandler = { + /** + * The nsIFeedWriter object that produces the UI + */ + _feedWriter: null, + + init: function SH_init() { + this._feedWriter = new BrowserFeedWriter(); + }, + + writeContent: function SH_writeContent() { + this._feedWriter.writeContent(); + }, + + uninit: function SH_uninit() { + this._feedWriter.close(); + } +}; diff --git a/application/basilisk/components/feeds/content/subscribe.xhtml b/application/basilisk/components/feeds/content/subscribe.xhtml new file mode 100644 index 000000000..a55c80053 --- /dev/null +++ b/application/basilisk/components/feeds/content/subscribe.xhtml @@ -0,0 +1,74 @@ +<?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://browser/locale/feeds/subscribe.dtd"> + %feedDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<html id="feedHandler" + xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&feedPage.title;</title> + <link rel="stylesheet" + href="chrome://browser/skin/feeds/subscribe.css" + type="text/css" + media="all"/> + <script type="application/javascript" + src="chrome://browser/content/feeds/subscribe.js"/> + </head> + <body onload="SubscribeHandler.writeContent();" onunload="SubscribeHandler.uninit();"> + <div id="feedHeaderContainer"> + <div id="feedHeader" dir="&locale.dir;"> + <div id="feedIntroText"> + <p id="feedSubscriptionInfo1" /> + <p id="feedSubscriptionInfo2" /> + </div> + <div id="feedSubscribeLine"> + <label id="subscribeUsingDescription"> + <select id="handlersMenuList"> + <option id="liveBookmarksMenuItem" selected="true">&feedLiveBookmarks;</option> + <option disabled="true">━━━━━━━</option> + </select> + </label> + <label id="checkboxText"> + <input type="checkbox" id="alwaysUse" class="alwaysUse" checked="false"/> + </label> + <button id="subscribeButton">&feedSubscribeNow;</button> + </div> + </div> + <div id="feedHeaderContainerSpacer"/> + </div> + + <script type="application/javascript"> + /* import-globals-from subscribe.js */ + SubscribeHandler.init(); + </script> + + <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/application/basilisk/components/feeds/jar.mn b/application/basilisk/components/feeds/jar.mn new file mode 100644 index 000000000..8570112c1 --- /dev/null +++ b/application/basilisk/components/feeds/jar.mn @@ -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/. + +browser.jar: + content/browser/feeds/subscribe.xhtml (content/subscribe.xhtml) + content/browser/feeds/subscribe.js (content/subscribe.js) diff --git a/application/basilisk/components/feeds/moz.build b/application/basilisk/components/feeds/moz.build new file mode 100644 index 000000000..b8755fcc9 --- /dev/null +++ b/application/basilisk/components/feeds/moz.build @@ -0,0 +1,34 @@ +# -*- 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/. + +JAR_MANIFESTS += ['jar.mn'] + +XPIDL_SOURCES += [ + 'nsIFeedResultService.idl', + 'nsIWebContentConverterRegistrar.idl', +] + +XPIDL_MODULE = 'browser-feeds' + +SOURCES += [ + 'nsFeedSniffer.cpp', +] + +EXTRA_COMPONENTS += [ + 'BrowserFeeds.manifest', + 'FeedConverter.js', + 'FeedWriter.js', + 'WebContentConverter.js', +] + +FINAL_LIBRARY = 'browsercomps' + +for var in ('MOZ_APP_NAME', 'MOZ_MACBUNDLE_NAME'): + DEFINES[var] = CONFIG[var] + +LOCAL_INCLUDES += [ + '../build', +] diff --git a/application/basilisk/components/feeds/nsFeedSniffer.cpp b/application/basilisk/components/feeds/nsFeedSniffer.cpp new file mode 100644 index 000000000..f314d3d3b --- /dev/null +++ b/application/basilisk/components/feeds/nsFeedSniffer.cpp @@ -0,0 +1,363 @@ +/* -*- 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 "nsNetCID.h" +#include "nsXPCOM.h" +#include "nsCOMPtr.h" +#include "nsStringStream.h" + +#include "nsBrowserCompsCID.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; + httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("Content-Encoding"), + 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, nullptr); + + 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, nullptr, rawStream, 0, length); + NS_ENSURE_SUCCESS(rv, rv); + + converter->OnStopRequest(request, nullptr, 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 + */ +static const char* +FindChar(char c, const char *begin, const char *end) +{ + for (; begin < end; ++begin) { + if (*begin == c) + return begin; + } + return nullptr; +} + +/** + * + * 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) +{ + int32_t offset = dataString.Find(substring); + if (offset == -1) + return false; + + 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; + 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); + + // Check to see if this was a feed request from the location bar or from + // the feed: protocol. This is also a reliable indication. + // The value of the header doesn't matter. + if (!noSniff) { + nsAutoCString sniffHeader; + nsresult foundHeader = + channel->GetRequestHeader(NS_LITERAL_CSTRING("X-Moz-Is-Feed"), + sniffHeader); + noSniff = NS_SUCCEEDED(foundHeader); + } + + 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 + channel->SetResponseHeader(NS_LITERAL_CSTRING("X-Moz-Is-Feed"), + NS_LITERAL_CSTRING("1"), false); + 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 + nsresult 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(length, MAX_BYTES); + } else { + testData = mDecodedData.get(); + length = std::min(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) { + isFeed = ContainsTopLevelSubstring(dataString, "<rdf:RDF") && + dataString.Find(NS_RDF) != -1 && + dataString.Find(NS_RSS) != -1; + } + + // 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, nsISupports* context) +{ + 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, nsISupports* context, + nsIInputStream* stream, uint64_t offset, + uint32_t count) +{ + uint32_t read; + return stream->ReadSegments(AppendSegmentToString, &mDecodedData, count, + &read); +} + +NS_IMETHODIMP +nsFeedSniffer::OnStopRequest(nsIRequest* request, nsISupports* context, + nsresult status) +{ + return NS_OK; +} diff --git a/application/basilisk/components/feeds/nsFeedSniffer.h b/application/basilisk/components/feeds/nsFeedSniffer.h new file mode 100644 index 000000000..a0eb9862c --- /dev/null +++ b/application/basilisk/components/feeds/nsFeedSniffer.h @@ -0,0 +1,37 @@ +/* -*- 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 "nsStringAPI.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/application/basilisk/components/feeds/nsIFeedResultService.idl b/application/basilisk/components/feeds/nsIFeedResultService.idl new file mode 100644 index 000000000..f745fa693 --- /dev/null +++ b/application/basilisk/components/feeds/nsIFeedResultService.idl @@ -0,0 +1,70 @@ +/* -*- 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(95309fd2-7b3a-47fb-97f3-5c460d9473cd)] +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 + * @param feedReader + * The type of feed reader we're using (client, bookmarks, default) + * If this parameter is null, the type is set to default + */ + void addToClientReader(in AUTF8String uri, + in AString title, + in AString subtitle, + in unsigned long feedType, + [optional] in AString feedReader); + + /** + * 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/application/basilisk/components/feeds/nsIWebContentConverterRegistrar.idl b/application/basilisk/components/feeds/nsIWebContentConverterRegistrar.idl new file mode 100644 index 000000000..08ce2f4ae --- /dev/null +++ b/application/basilisk/components/feeds/nsIWebContentConverterRegistrar.idl @@ -0,0 +1,117 @@ +/* -*- 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); +}; + |