diff options
Diffstat (limited to 'application/basilisk/base/content/browser-feeds.js')
-rw-r--r-- | application/basilisk/base/content/browser-feeds.js | 646 |
1 files changed, 646 insertions, 0 deletions
diff --git a/application/basilisk/base/content/browser-feeds.js b/application/basilisk/base/content/browser-feeds.js new file mode 100644 index 000000000..6f29d8915 --- /dev/null +++ b/application/basilisk/base/content/browser-feeds.js @@ -0,0 +1,646 @@ +/* -*- 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/. */ + +XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", + "resource://gre/modules/DeferredTask.jsm"); + +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; + +const PREF_SHOW_FIRST_RUN_UI = "browser.feeds.showFirstRunUI"; + +const PREF_SELECTED_APP = "browser.feeds.handlers.application"; +const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_SELECTED_READER = "browser.feeds.handler.default"; + +const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application"; +const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; +const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler"; +const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default"; + +const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application"; +const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; +const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler"; +const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default"; + +const PREF_UPDATE_DELAY = 2000; + +const SETTABLE_PREFS = new Set([ + PREF_VIDEO_SELECTED_ACTION, + PREF_AUDIO_SELECTED_ACTION, + PREF_SELECTED_ACTION, + PREF_VIDEO_SELECTED_READER, + PREF_AUDIO_SELECTED_READER, + PREF_SELECTED_READER, + PREF_VIDEO_SELECTED_WEB, + PREF_AUDIO_SELECTED_WEB, + PREF_SELECTED_WEB +]); + +const EXECUTABLE_PREFS = new Set([ + PREF_SELECTED_APP, + PREF_VIDEO_SELECTED_APP, + PREF_AUDIO_SELECTED_APP +]); + +const VALID_ACTIONS = new Set(["ask", "reader", "bookmarks"]); +const VALID_READERS = new Set(["web", "client", "default", "bookmarks"]); + +XPCOMUtils.defineLazyPreferenceGetter(this, "SHOULD_LOG", + "feeds.log", false); + +function LOG(str) { + if (SHOULD_LOG) + dump("*** Feeds: " + str + "\n"); +} + +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 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 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; + } +} + +/** + * Maps a feed type to a maybe-feed mimetype. + */ +function getMimeTypeForFeedType(aFeedType) { + switch (aFeedType) { + case Ci.nsIFeed.TYPE_VIDEO: + return TYPE_MAYBE_VIDEO_FEED; + + case Ci.nsIFeed.TYPE_AUDIO: + return TYPE_MAYBE_AUDIO_FEED; + + default: + return TYPE_MAYBE_FEED; + } +} + +/** + * The Feed Handler object manages discovery of RSS/ATOM feeds in web pages + * and shows UI when they are discovered. + */ +var FeedHandler = { + _prefChangeCallback: null, + + /** Called when the user clicks on the Subscribe to This Page... menu item, + * or when the user clicks the feed button when the page contains multiple + * feeds. + * Builds a menu of unique feeds associated with the page, and if there + * is only one, shows the feed inline in the browser window. + * @param container + * The feed list container (menupopup or subview) to be populated. + * @param isSubview + * Whether we're creating a subview (true) or menu (false/undefined) + * @return true if the menu/subview should be shown, false if there was only + * one feed and the feed should be shown inline in the browser + * window (do not show the menupopup/subview). + */ + buildFeedList(container, isSubview) { + let feeds = gBrowser.selectedBrowser.feeds; + if (!isSubview && feeds == null) { + // XXX hack -- menu opening depends on setting of an "open" + // attribute, and the menu refuses to open if that attribute is + // set (because it thinks it's already open). onpopupshowing gets + // called after the attribute is unset, and it doesn't get unset + // if we return false. so we unset it here; otherwise, the menu + // refuses to work past this point. + container.parentNode.removeAttribute("open"); + return false; + } + + for (let i = container.childNodes.length - 1; i >= 0; --i) { + let node = container.childNodes[i]; + if (isSubview && node.localName == "label") + continue; + container.removeChild(node); + } + + if (!feeds || feeds.length <= 1) + return false; + + // Build the menu showing the available feed choices for viewing. + let itemNodeType = isSubview ? "toolbarbutton" : "menuitem"; + for (let feedInfo of feeds) { + let item = document.createElement(itemNodeType); + let baseTitle = feedInfo.title || feedInfo.href; + item.setAttribute("label", baseTitle); + item.setAttribute("feed", feedInfo.href); + item.setAttribute("tooltiptext", feedInfo.href); + item.setAttribute("crop", "center"); + let className = "feed-" + itemNodeType; + if (isSubview) { + className += " subviewbutton"; + } + item.setAttribute("class", className); + container.appendChild(item); + } + return true; + }, + + /** + * Subscribe to a given feed. Called when + * 1. Page has a single feed and user clicks feed icon in location bar + * 2. Page has a single feed and user selects Subscribe menu item + * 3. Page has multiple feeds and user selects from feed icon popup (or subview) + * 4. Page has multiple feeds and user selects from Subscribe submenu + * @param href + * The feed to subscribe to. May be null, in which case the + * event target's feed attribute is examined. + * @param event + * The event this method is handling. Used to decide where + * to open the preview UI. (Optional, unless href is null) + */ + subscribeToFeed(href, event) { + // Just load the feed in the content area to either subscribe or show the + // preview UI + if (!href) + href = event.target.getAttribute("feed"); + urlSecurityCheck(href, gBrowser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + let feedURI = makeURI(href, document.characterSet); + // Use the feed scheme so X-Moz-Is-Feed will be set + // The value doesn't matter + if (/^https?$/.test(feedURI.scheme)) + href = "feed:" + href; + this.loadFeed(href, event); + }, + + loadFeed(href, event) { + let feeds = gBrowser.selectedBrowser.feeds; + try { + openUILink(href, event, { ignoreAlt: true }); + } + finally { + // We might default to a livebookmarks modal dialog, + // so reset that if the user happens to click it again + gBrowser.selectedBrowser.feeds = feeds; + } + }, + + get _feedMenuitem() { + delete this._feedMenuitem; + return this._feedMenuitem = document.getElementById("singleFeedMenuitemState"); + }, + + get _feedMenupopup() { + delete this._feedMenupopup; + return this._feedMenupopup = document.getElementById("multipleFeedsMenuState"); + }, + + /** + * Update the browser UI to show whether or not feeds are available when + * a page is loaded or the user switches tabs to a page that has feeds. + */ + updateFeeds() { + if (this._updateFeedTimeout) + clearTimeout(this._updateFeedTimeout); + + let feeds = gBrowser.selectedBrowser.feeds; + let haveFeeds = feeds && feeds.length > 0; + + let feedButton = document.getElementById("feed-button"); + if (feedButton) { + if (haveFeeds) { + feedButton.removeAttribute("disabled"); + } else { + feedButton.setAttribute("disabled", "true"); + } + } + + if (!haveFeeds) { + this._feedMenuitem.setAttribute("disabled", "true"); + this._feedMenuitem.removeAttribute("hidden"); + this._feedMenupopup.setAttribute("hidden", "true"); + return; + } + + if (feeds.length > 1) { + this._feedMenuitem.setAttribute("hidden", "true"); + this._feedMenupopup.removeAttribute("hidden"); + } else { + this._feedMenuitem.setAttribute("feed", feeds[0].href); + this._feedMenuitem.removeAttribute("disabled"); + this._feedMenuitem.removeAttribute("hidden"); + this._feedMenupopup.setAttribute("hidden", "true"); + } + }, + + addFeed(link, browserForLink) { + if (!browserForLink.feeds) + browserForLink.feeds = []; + + browserForLink.feeds.push({ href: link.href, title: link.title }); + + // If this addition was for the current browser, update the UI. For + // background browsers, we'll update on tab switch. + if (browserForLink == gBrowser.selectedBrowser) { + // Batch updates to avoid updating the UI for multiple onLinkAdded events + // fired within 100ms of each other. + if (this._updateFeedTimeout) + clearTimeout(this._updateFeedTimeout); + this._updateFeedTimeout = setTimeout(this.updateFeeds.bind(this), 100); + } + }, + + /** + * Get the human-readable display name of a file. This could be the + * application name. + * @param file + * A nsIFile to look up the name of + * @return The display name of the application represented by the file. + */ + _getFileDisplayName(file) { + switch (AppConstants.platform) { + case "win": + if (file instanceof Ci.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } + break; + case "macosx": + if (file instanceof Ci.nsILocalFileMac) { + try { + return file.bundleDisplayName; + } catch (e) {} + } + break; + } + + return file.leafName; + }, + + _chooseClientApp(aTitle, aTypeName, aBrowser) { + const prefName = getPrefAppForType(aTypeName); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + + fp.init(window, aTitle, Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + + fp.open((aResult) => { + if (aResult == Ci.nsIFilePicker.returnOK) { + let selectedApp = fp.file; + if (selectedApp) { + // XXXben - we need to compare this with the running instance + // executable just don't know how to do that via script + // XXXmano TBD: can probably add this to nsIShellService + let appName = ""; + switch (AppConstants.platform) { + case "win": + appName = AppConstants.MOZ_APP_NAME + ".exe"; + break; + case "macosx": + appName = AppConstants.MOZ_MACBUNDLE_NAME; + break; + default: + appName = AppConstants.MOZ_APP_NAME + "-bin"; + break; + } + + if (fp.file.leafName != appName) { + Services.prefs.setComplexValue(prefName, Ci.nsILocalFile, selectedApp); + aBrowser.messageManager.sendAsyncMessage("FeedWriter:SetApplicationLauncherMenuItem", + { name: this._getFileDisplayName(selectedApp), + type: "SelectedAppMenuItem" }); + } + } + } + }); + + }, + + executeClientApp(aSpec, aTitle, aSubtitle, aFeedHandler) { + // aFeedHandler is either "default", indicating the system default reader, or a pref-name containing + // an nsILocalFile pointing to the feed handler's executable. + + let clientApp = null; + if (aFeedHandler == "default") { + clientApp = Cc["@mozilla.org/browser/shell-service;1"] + .getService(Ci.nsIShellService) + .defaultFeedReader; + } else { + clientApp = Services.prefs.getComplexValue(aFeedHandler, Ci.nsILocalFile); + } + + // For the benefit of applications that might know how to deal with more + // URLs than just feeds, send feed: URLs in the following format: + // + // http urls: replace scheme with feed, e.g. + // http://foo.com/index.rdf -> feed://foo.com/index.rdf + // other urls: prepend feed: scheme, e.g. + // https://foo.com/index.rdf -> feed:https://foo.com/index.rdf + let feedURI = NetUtil.newURI(aSpec); + if (feedURI.schemeIs("http")) { + feedURI.scheme = "feed"; + aSpec = feedURI.spec; + } else { + aSpec = "feed:" + aSpec; + } + + // Retrieving the shell service might fail on some systems, most + // notably systems where GNOME is not installed. + try { + let ss = Cc["@mozilla.org/browser/shell-service;1"] + .getService(Ci.nsIShellService); + ss.openApplicationWithURI(clientApp, aSpec); + } catch (e) { + // If we couldn't use the shell service, fallback to using a + // nsIProcess instance + let p = Cc["@mozilla.org/process/util;1"] + .createInstance(Ci.nsIProcess); + p.init(clientApp); + p.run(false, [aSpec], 1); + } + }, + + // nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + init() { + window.messageManager.addMessageListener("FeedWriter:ChooseClientApp", this); + window.messageManager.addMessageListener("FeedWriter:GetSubscriptionUI", this); + window.messageManager.addMessageListener("FeedWriter:SetFeedPrefsAndSubscribe", this); + window.messageManager.addMessageListener("FeedWriter:ShownFirstRun", this); + + Services.ppmm.addMessageListener("FeedConverter:ExecuteClientApp", this); + + const prefs = Services.prefs; + prefs.addObserver(PREF_SELECTED_ACTION, this, true); + prefs.addObserver(PREF_SELECTED_READER, this, true); + prefs.addObserver(PREF_SELECTED_WEB, this, true); + prefs.addObserver(PREF_VIDEO_SELECTED_ACTION, this, true); + prefs.addObserver(PREF_VIDEO_SELECTED_READER, this, true); + prefs.addObserver(PREF_VIDEO_SELECTED_WEB, this, true); + prefs.addObserver(PREF_AUDIO_SELECTED_ACTION, this, true); + prefs.addObserver(PREF_AUDIO_SELECTED_READER, this, true); + prefs.addObserver(PREF_AUDIO_SELECTED_WEB, this, true); + }, + + uninit() { + Services.ppmm.removeMessageListener("FeedConverter:ExecuteClientApp", this); + + this._prefChangeCallback = null; + }, + + // nsIObserver + observe(subject, topic, data) { + if (topic == "nsPref:changed") { + LOG(`Pref changed ${data}`) + if (this._prefChangeCallback) { + this._prefChangeCallback.disarm(); + } + // Multiple prefs are set at the same time, debounce to reduce noise + // This can happen in one feed and we want to message all feed pages + this._prefChangeCallback = new DeferredTask(() => { + this._prefChanged(data); + }, PREF_UPDATE_DELAY); + this._prefChangeCallback.arm(); + } + }, + + _prefChanged(prefName) { + // Don't observe for PREF_*SELECTED_APP as user likely just picked one + // That is also handled by SetApplicationLauncherMenuItem call + // Rather than the others which happen on subscription + switch (prefName) { + case PREF_SELECTED_READER: + case PREF_SELECTED_WEB: + case PREF_VIDEO_SELECTED_READER: + case PREF_VIDEO_SELECTED_WEB: + case PREF_AUDIO_SELECTED_READER: + case PREF_AUDIO_SELECTED_WEB: + case PREF_SELECTED_ACTION: + case PREF_VIDEO_SELECTED_ACTION: + case PREF_AUDIO_SELECTED_ACTION: + const response = { + default: this._getReaderForType(Ci.nsIFeed.TYPE_FEED), + [Ci.nsIFeed.TYPE_AUDIO]: this._getReaderForType(Ci.nsIFeed.TYPE_AUDIO), + [Ci.nsIFeed.TYPE_VIDEO]: this._getReaderForType(Ci.nsIFeed.TYPE_VIDEO) + }; + Services.mm.broadcastAsyncMessage("FeedWriter:PreferenceUpdated", + response); + break; + } + }, + + _initSubscriptionUIResponse(feedType) { + const wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + const handlersRaw = wccr.getContentHandlers(getMimeTypeForFeedType(feedType)); + const handlers = []; + for (let handler of handlersRaw) { + LOG(`Handler found: ${handler}`); + handlers.push({ + name: handler.name, + uri: handler.uri + }); + } + let showFirstRunUI = true; + // eslint-disable-next-line mozilla/use-default-preference-values + try { + showFirstRunUI = Services.prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI); + } catch (ex) { } + const response = { handlers, showFirstRunUI }; + let selectedClientApp; + const feedTypePref = getPrefAppForType(feedType); + try { + selectedClientApp = Services.prefs.getComplexValue(feedTypePref, Ci.nsILocalFile); + } catch (ex) { + // Just do nothing, then we won't bother populating + } + + let defaultClientApp = null; + try { + // This can sometimes not exist + defaultClientApp = Cc["@mozilla.org/browser/shell-service;1"] + .getService(Ci.nsIShellService) + .defaultFeedReader; + } catch (ex) { + // Just do nothing, then we don't bother populating + } + + if (selectedClientApp && selectedClientApp.exists()) { + if (defaultClientApp && selectedClientApp.path != defaultClientApp.path) { + // Only set the default menu item if it differs from the selected one + response.defaultMenuItem = this._getFileDisplayName(defaultClientApp); + } + response.selectedMenuItem = this._getFileDisplayName(selectedClientApp); + } + response.reader = this._getReaderForType(feedType); + return response; + }, + + _setPref(aPrefName, aPrefValue, aIsComplex = false) { + LOG(`FeedWriter._setPref ${aPrefName}`); + // Ensure we have a pref that is settable + if (aPrefName && SETTABLE_PREFS.has(aPrefName)) { + if (aIsComplex) { + const supportsString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + supportsString.data = aPrefValue; + Services.prefs.setComplexValue(aPrefName, Ci.nsISupportsString, supportsString); + } else { + Services.prefs.setCharPref(aPrefName, aPrefValue); + } + } else { + LOG(`FeedWriter._setPref ${aPrefName} not allowed`); + } + }, + + _getReaderForType(feedType) { + let prefs = Services.prefs; + let handler = "bookmarks"; + let url; + // eslint-disable-next-line mozilla/use-default-preference-values + try { + handler = prefs.getCharPref(getPrefReaderForType(feedType)); + } catch (ex) { } + + if (handler === "web") { + try { + url = prefs.getComplexValue(getPrefWebForType(feedType), Ci.nsISupportsString).data; + } catch (ex) { + LOG("FeedWriter._setSelectedHandler: invalid or no handler in prefs"); + url = null; + } + } + const alwaysUse = this._getAlwaysUseState(feedType); + const action = prefs.getCharPref(getPrefActionForType(feedType)); + return { handler, url, alwaysUse, action }; + }, + + _getAlwaysUseState(feedType) { + try { + return Services.prefs.getCharPref(getPrefActionForType(feedType)) != "ask"; + } catch (ex) { } + return false; + }, + + receiveMessage(msg) { + let handler; + switch (msg.name) { + case "FeedWriter:GetSubscriptionUI": + const response = this._initSubscriptionUIResponse(msg.data.feedType); + msg.target.messageManager + .sendAsyncMessage("FeedWriter:GetSubscriptionUIResponse", + response); + break; + case "FeedWriter:ChooseClientApp": + this._chooseClientApp(msg.data.title, msg.data.feedType, msg.target); + break; + case "FeedWriter:ShownFirstRun": + Services.prefs.setBoolPref(PREF_SHOW_FIRST_RUN_UI, false); + break; + case "FeedWriter:SetFeedPrefsAndSubscribe": + const settings = msg.data; + if (!settings.action || !VALID_ACTIONS.has(settings.action)) { + LOG(`Invalid action ${settings.action}`); + return; + } + if (!settings.reader || !VALID_READERS.has(settings.reader)) { + LOG(`Invalid reader ${settings.reader}`); + return; + } + const actionPref = getPrefActionForType(settings.feedType); + this._setPref(actionPref, settings.action); + const readerPref = getPrefReaderForType(settings.feedType); + this._setPref(readerPref, settings.reader); + handler = null; + + switch (settings.reader) { + case "web": + // This is a web set URI by content using window.registerContentHandler() + // Lets make sure we know about it before setting it + const webPref = getPrefWebForType(settings.feedType); + let wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + // If the user provided an invalid web URL this function won't give us a reference + handler = wccr.getWebContentHandlerByURI(getMimeTypeForFeedType(settings.feedType), settings.uri); + if (handler) { + this._setPref(webPref, settings.uri, true); + if (settings.useAsDefault) { + wccr.setAutoHandler(getMimeTypeForFeedType(settings.feedType), handler); + } + msg.target.messageManager + .sendAsyncMessage("FeedWriter:SetFeedPrefsAndSubscribeResponse", + { redirect: handler.getHandlerURI(settings.feedLocation) }); + } else { + LOG(`No handler found for web ${settings.feedType} ${settings.uri}`); + } + break; + default: + const feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + + feedService.addToClientReader(settings.feedLocation, + settings.feedTitle, + settings.feedSubtitle, + settings.feedType, + settings.reader); + } + break; + case "FeedConverter:ExecuteClientApp": + // Always check feedHandler is from a set array of executable prefs + if (EXECUTABLE_PREFS.has(msg.data.feedHandler)) { + this.executeClientApp(msg.data.spec, msg.data.title, + msg.data.subtitle, msg.data.feedHandler); + } else { + LOG(`FeedConverter:ExecuteClientApp - Will not exec ${msg.data.feedHandler}`); + } + break; + } + }, +}; |