/* -*- 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; } }, };