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 @@ + + utilityOverlay.js +/* import-globals-from ../utilityOverlay.js */ +/* import-globals-from ./pageInfo.js */ + +function initFeedTab(feeds) { + for (const [name, type, url] of feeds) { + addRow(name, type, url); + } + + const feedListbox = document.getElementById("feedListbox"); + document.getElementById("feedTab").hidden = feedListbox.getRowCount() == 0; +} + +function addRow(name, type, url) { + const item = document.createXULElement("richlistitem"); + + const top = document.createXULElement("hbox"); + top.setAttribute("flex", "1"); + item.appendChild(top); + + const bottom = document.createXULElement("hbox"); + bottom.setAttribute("flex", "1"); + item.appendChild(bottom); + + const nameLabel = document.createXULElement("label"); + nameLabel.className = "feedTitle"; + nameLabel.textContent = name; + nameLabel.setAttribute("flex", "1"); + top.appendChild(nameLabel); + + const typeLabel = document.createXULElement("label"); + typeLabel.textContent = type; + top.appendChild(typeLabel); + + const urlContainer = document.createXULElement("hbox"); + urlContainer.setAttribute("flex", "1"); + bottom.appendChild(urlContainer); + + const urlLabel = document.createXULElement("label"); + urlLabel.className = "text-link"; + urlLabel.textContent = url; + urlLabel.setAttribute("tooltiptext", url); + urlLabel.addEventListener("click", ev => openUILink(this.value, ev, {triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({})})); + urlContainer.appendChild(urlLabel); + + const subscribeButton = document.createXULElement("button"); + subscribeButton.className = "feed-subscribe"; + subscribeButton.addEventListener("click", + () => openWebLinkIn(url, "current", { ignoreAlt: true })); + subscribeButton.setAttribute("label", gBundle.getString("feedSubscribe")); + subscribeButton.setAttribute("accesskey", gBundle.getString("feedSubscribe.accesskey")); + bottom.appendChild(subscribeButton); + + document.getElementById("feedListbox").appendChild(item); +} diff --git a/browser/base/content/pageinfo/pageInfo.css b/browser/base/content/pageinfo/pageInfo.css index 26c7729da370..e8adeef192b7 100644 --- a/browser/base/content/pageinfo/pageInfo.css +++ b/browser/base/content/pageinfo/pageInfo.css @@ -12,6 +12,14 @@ display: none; } +#feedListbox richlistitem { + -moz-box-orient: vertical; +} + +#feedListbox richlistitem:not([selected="true"]) .feed-subscribe { + display: none; +} + groupbox[closed="true"] > .groupbox-body { visibility: collapse; } diff --git a/browser/base/content/pageinfo/pageInfo.js b/browser/base/content/pageinfo/pageInfo.js index b78e6390a59f..c37d237caa76 100644 --- a/browser/base/content/pageinfo/pageInfo.js +++ b/browser/base/content/pageinfo/pageInfo.js @@ -7,7 +7,7 @@ ChromeUtils.import("resource://gre/modules/Services.jsm"); /* import-globals-from ../../../../toolkit/content/globalOverlay.js */ /* import-globals-from ../../../../toolkit/content/contentAreaUtils.js */ /* import-globals-from ../../../../toolkit/content/treeUtils.js */ -/* import-globals-from ../utilityOverlay.js */ +/* import-globals-from feeds.js */ /* import-globals-from permissions.js */ /* import-globals-from security.js */ @@ -333,6 +333,12 @@ function loadPageInfo(frameOuterWindowID, imageElement, browser) { browser = browser || window.opener.gBrowser.selectedBrowser; let mm = browser.messageManager; + gStrings["application/rss+xml"] = gBundle.getString("feedRss"); + gStrings["application/atom+xml"] = gBundle.getString("feedAtom"); + gStrings["text/xml"] = gBundle.getString("feedXML"); + gStrings["application/xml"] = gBundle.getString("feedXML"); + gStrings["application/rdf+xml"] = gBundle.getString("feedXML"); + let imageInfo = imageElement; // Look for pageInfoListener in content.js. Sends message to listener with arguments. @@ -340,7 +346,7 @@ function loadPageInfo(frameOuterWindowID, imageElement, browser) { let pageInfoData; - // Get initial pageInfoData needed to display the general, permission and security tabs. + // Get initial pageInfoData needed to display the general, feeds, permission and security tabs. mm.addMessageListener("PageInfo:data", function onmessage(message) { mm.removeMessageListener("PageInfo:data", onmessage); pageInfoData = message.data; @@ -359,6 +365,7 @@ function loadPageInfo(frameOuterWindowID, imageElement, browser) { document.getElementById("main-window").setAttribute("relatedUrl", docInfo.location); makeGeneralTab(pageInfoData.metaViewRows, docInfo); + initFeedTab(pageInfoData.feeds); onLoadPermission(uri, principal); securityOnLoad(uri, windowInfo); }); @@ -402,6 +409,11 @@ function resetPageInfo(args) { gImageView.clear(); gImageHash = {}; + /* Reset Feeds Tab */ + var feedListbox = document.getElementById("feedListbox"); + while (feedListbox.firstChild) + feedListbox.firstChild.remove(); + /* Call registered overlay reset functions */ onResetRegistry.forEach(function(func) { func(); }); @@ -423,6 +435,7 @@ function doHelpButton() { const helpTopics = { "generalPanel": "pageinfo_general", "mediaPanel": "pageinfo_media", + "feedPanel": "pageinfo_feed", "permPanel": "pageinfo_permissions", "securityPanel": "pageinfo_security", }; diff --git a/browser/base/content/pageinfo/pageInfo.xul b/browser/base/content/pageinfo/pageInfo.xul index 5c6269162e01..2c7b872355ce 100644 --- a/browser/base/content/pageinfo/pageInfo.xul +++ b/browser/base/content/pageinfo/pageInfo.xul @@ -32,6 +32,7 @@ + + + +Mozilla Bug 589543 +

+ +
+
+
+ + diff --git a/browser/locales/en-US/chrome/browser/browser.dtd b/browser/locales/en-US/chrome/browser/browser.dtd index 6ba8dc639f0e..5fc63f85bb04 100644 --- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -194,6 +194,8 @@ These should match what Safari and other Apple applications use on OS X Lion. -- + + diff --git a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties index d7bfec7f7fb4..762068a1688b 100644 --- a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties +++ b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties @@ -73,6 +73,9 @@ paste-button.label = Paste # LOCALIZATION NOTE(paste-button.tooltiptext2): %S is the keyboard shortcut. paste-button.tooltiptext2 = Paste (%S) +feed-button.label = Subscribe +feed-button.tooltiptext2 = Subscribe to this page + # LOCALIZATION NOTE (characterencoding-button2.label): The \u00ad text at the beginning # of the string is used to disable auto hyphenation on the button text when it is displayed # in the menu panel. diff --git a/browser/locales/en-US/chrome/browser/pageInfo.dtd b/browser/locales/en-US/chrome/browser/pageInfo.dtd index 9126a8f8a97d..39fd57edd0c8 100644 --- a/browser/locales/en-US/chrome/browser/pageInfo.dtd +++ b/browser/locales/en-US/chrome/browser/pageInfo.dtd @@ -44,6 +44,9 @@ + + + diff --git a/browser/locales/en-US/chrome/browser/pageInfo.properties b/browser/locales/en-US/chrome/browser/pageInfo.properties index ef73dc820611..6cb2034a5edb 100644 --- a/browser/locales/en-US/chrome/browser/pageInfo.properties +++ b/browser/locales/en-US/chrome/browser/pageInfo.properties @@ -38,6 +38,12 @@ generalSize=%S KB (%S bytes) generalMetaTag=Meta (1 tag) generalMetaTags=Meta (%S tags) +feedRss=RSS +feedAtom=Atom +feedXML=XML +feedSubscribe=Subscribe +feedSubscribe.accesskey=u + securityNoOwner=This website does not supply ownership information. # LOCALIZATION NOTE (securityVisitsNumber): # Semi-colon list of plural forms. diff --git a/browser/modules/Feeds.jsm b/browser/modules/Feeds.jsm index a2dfc76e75ec..c2127e4a182c 100644 --- a/browser/modules/Feeds.jsm +++ b/browser/modules/Feeds.jsm @@ -6,6 +6,13 @@ var EXPORTED_SYMBOLS = [ "Feeds" ]; +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +ChromeUtils.defineModuleGetter(this, "BrowserWindowTracker", + "resource:///modules/BrowserWindowTracker.jsm"); + var Feeds = { // Listeners are added in nsBrowserGlue.js receiveMessage(aMessage) { @@ -18,6 +25,48 @@ var Feeds = { aMessage.target); break; } + + case "FeedConverter:addLiveBookmark": { + let topWindow = BrowserWindowTracker.getTopWindow(); + topWindow.PlacesCommandHook.addLiveBookmark(data.spec, data.title) + .catch(Cu.reportError); + break; + } } }, + + /** + * isValidFeed: checks whether the given data represents a valid feed. + * + * @param aLink + * An object representing a feed with title, href and type. + * @param aPrincipal + * The principal of the document, used for security check. + * @param aIsFeed + * Whether this is already a known feed or not, if true only a security + * check will be performed. + */ + isValidFeed(aLink, aPrincipal, aIsFeed) { + if (!aLink || !aPrincipal) + return false; + + var type = aLink.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, ""); + if (!aIsFeed) { + aIsFeed = (type == "application/rss+xml" || + type == "application/atom+xml"); + } + + if (aIsFeed) { + try { + let href = Services.io.newURI(aLink.href, aLink.ownerDocument.characterSet); + BrowserUtils.urlSecurityCheck(href, aPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + return type || "application/rss+xml"; + } catch (ex) { + } + } + + return null; + }, + }; diff --git a/browser/themes/shared/icons/feed.svg b/browser/themes/shared/icons/feed.svg new file mode 100644 index 000000000000..8235d22ce4d7 --- /dev/null +++ b/browser/themes/shared/icons/feed.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn index 8779d2a1df14..0c98ef738fbd 100644 --- a/browser/themes/shared/jar.inc.mn +++ b/browser/themes/shared/jar.inc.mn @@ -148,6 +148,7 @@ skin/classic/browser/edit-copy.svg (../shared/icons/edit-copy.svg) skin/classic/browser/edit-cut.svg (../shared/icons/edit-cut.svg) skin/classic/browser/edit-paste.svg (../shared/icons/edit-paste.svg) + skin/classic/browser/feed.svg (../shared/icons/feed.svg) skin/classic/browser/folder.svg (../shared/icons/folder.svg) skin/classic/browser/forget.svg (../shared/icons/forget.svg) skin/classic/browser/forward.svg (../shared/icons/forward.svg) diff --git a/browser/themes/shared/toolbarbutton-icons.inc.css b/browser/themes/shared/toolbarbutton-icons.inc.css index b913e11f19d0..2f15b9fd07b0 100644 --- a/browser/themes/shared/toolbarbutton-icons.inc.css +++ b/browser/themes/shared/toolbarbutton-icons.inc.css @@ -212,6 +212,10 @@ toolbar[brighttext] { list-style-image: url("chrome://browser/skin/tab.svg"); } +#feed-button { + list-style-image: url("chrome://browser/skin/feed.svg"); +} + #characterencoding-button { list-style-image: url("chrome://browser/skin/characterEncoding.svg"); } diff --git a/toolkit/content/widgets/browser.xml b/toolkit/content/widgets/browser.xml index a6f63ebdc5fd..40495a14dfc0 100644 --- a/toolkit/content/widgets/browser.xml +++ b/toolkit/content/widgets/browser.xml @@ -516,6 +516,10 @@