diff --git a/browser/actors/LinkHandlerChild.jsm b/browser/actors/LinkHandlerChild.jsm index 98c3d584d72d..7b1f57055e5d 100644 --- a/browser/actors/LinkHandlerChild.jsm +++ b/browser/actors/LinkHandlerChild.jsm @@ -9,6 +9,8 @@ const EXPORTED_SYMBOLS = ["LinkHandlerChild"]; ChromeUtils.import("resource://gre/modules/Services.jsm"); ChromeUtils.import("resource://gre/modules/ActorChild.jsm"); +ChromeUtils.defineModuleGetter(this, "Feeds", + "resource:///modules/Feeds.jsm"); ChromeUtils.defineModuleGetter(this, "FaviconLoader", "resource:///modules/FaviconLoader.jsm"); @@ -95,6 +97,7 @@ class LinkHandlerChild extends ActorChild { // Note: following booleans only work for the current link, not for the // whole content + let feedAdded = false; let iconAdded = false; let searchAdded = false; let rels = {}; @@ -105,6 +108,22 @@ class LinkHandlerChild extends ActorChild { let isRichIcon = false; switch (relVal) { + case "feed": + case "alternate": + if (!feedAdded && event.type == "DOMLinkAdded") { + if (!rels.feed && rels.alternate && rels.stylesheet) + break; + + if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) { + this.mm.sendAsyncMessage("Link:AddFeed", { + type: link.type, + href: link.href, + title: link.title, + }); + feedAdded = true; + } + } + break; case "apple-touch-icon": case "apple-touch-icon-precomposed": case "fluid-icon": diff --git a/browser/actors/PageInfoChild.jsm b/browser/actors/PageInfoChild.jsm index 88cef18ae523..35cc40dbf0e9 100644 --- a/browser/actors/PageInfoChild.jsm +++ b/browser/actors/PageInfoChild.jsm @@ -10,6 +10,7 @@ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); ChromeUtils.import("resource://gre/modules/ActorChild.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { + Feeds: "resource:///modules/Feeds.jsm", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", setTimeout: "resource://gre/modules/Timer.jsm", }); @@ -33,6 +34,7 @@ class PageInfoChild extends ActorChild { let pageInfoData = {metaViewRows: this.getMetaInfo(document), docInfo: this.getDocumentInfo(document), + feeds: this.getFeedsInfo(document, strings), windowInfo: this.getWindowInfo(window)}; message.target.sendAsyncMessage("PageInfo:data", pageInfoData); @@ -96,6 +98,36 @@ class PageInfoChild extends ActorChild { return docInfo; } + getFeedsInfo(document, strings) { + let feeds = []; + // Get the feeds from the page. + let linkNodes = document.getElementsByTagName("link"); + let length = linkNodes.length; + for (let i = 0; i < length; i++) { + let link = linkNodes[i]; + if (!link.href) { + continue; + } + let rel = link.rel && link.rel.toLowerCase(); + let rels = {}; + + if (rel) { + for (let relVal of rel.split(/\s+/)) { + rels[relVal] = true; + } + } + + if (rels.feed || (link.type && rels.alternate && !rels.stylesheet)) { + let type = Feeds.isValidFeed(link, document.nodePrincipal, "feed" in rels); + if (type) { + type = strings[type] || strings["application/rss+xml"]; + feeds.push([link.title, type, link.href]); + } + } + } + return feeds; + } + // Only called once to get the media tab's media elements from the content page. getMediaInfo(document, window, strings, mm) { let frameList = this.goThroughFrames(document, window); diff --git a/browser/base/content/browser-feeds.js b/browser/base/content/browser-feeds.js index 5e4be6e79d1a..d5ab0b5bbeba 100644 --- a/browser/base/content/browser-feeds.js +++ b/browser/base/content/browser-feeds.js @@ -114,6 +114,170 @@ function getMimeTypeForFeedType(aFeedType) { 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); + this.loadFeed(href, event); + }, + + loadFeed(href, event) { + let feeds = gBrowser.selectedBrowser.feeds; + try { + openUILink(href, event, { + ignoreAlt: true, + triggeringPrincipal: gBrowser.contentPrincipal, + }); + } 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("subscribeToPageMenuitem"); + }, + + get _feedMenupopup() { + delete this._feedMenupopup; + return this._feedMenupopup = document.getElementById("subscribeToPageMenupopup"); + }, + + /** + * 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 = []; + + urlSecurityCheck(link.href, gBrowser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + + let feedURI = makeURI(link.href, document.characterSet); + if (!/^https?$/.test(feedURI.scheme)) + return; + + 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. diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc index 1a5054f37a39..0b95444aaa3b 100644 --- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -400,6 +400,26 @@ + +