diff --git a/mozilla/browser/base/content/browser.js b/mozilla/browser/base/content/browser.js index 63de19b9492..12e40fd6ff4 100644 --- a/mozilla/browser/base/content/browser.js +++ b/mozilla/browser/base/content/browser.js @@ -935,11 +935,12 @@ function prepareForStartup() // hook up UI through progress listener gBrowser.addProgressListener(window.XULBrowserWindow, Components.interfaces.nsIWebProgress.NOTIFY_ALL); - // Initialize the feedhandler - FeedHandler.init(); + // setup our common DOMLinkAdded listener + gBrowser.addEventListener("DOMLinkAdded", + function (event) { DOMLinkHandler.onLinkAdded(event); }, + false); - // Initialize the searchbar - BrowserSearch.init(); + gBrowser.addEventListener("pagehide", FeedHandler.onPageHide, false); } function delayedStartup() @@ -2647,84 +2648,145 @@ var DownloadsButtonDNDObserver = { } } -const BrowserSearch = { - - /** - * Initialize the BrowserSearch - */ - init: function() { - gBrowser.addEventListener("DOMLinkAdded", - function (event) { BrowserSearch.onLinkAdded(event); }, - false); - }, - - /** - * A new tag has been discovered - check to see if it advertises - * a OpenSearch engine. - */ +const DOMLinkHandler = { onLinkAdded: function(event) { - // XXX this event listener can/should probably be combined with the onLinkAdded - // listener in tabbrowser.xml. See comments in FeedHandler.onLinkAdded(). - const target = event.target; - var etype = target.type; - const searchRelRegex = /(^|\s)search($|\s)/i; - const searchHrefRegex = /^(https?|ftp):\/\//i; - - if (!etype) - return; - - // Bug 349431: If the engine has no suggested title, ignore it rather - // than trying to find an alternative. - if (!target.title) + var link = event.originalTarget; + var rel = link.rel && link.rel.toLowerCase(); + if (!link || !link.ownerDocument || !rel || !link.href) return; - if (etype == "application/opensearchdescription+xml" && - searchRelRegex.test(target.rel) && searchHrefRegex.test(target.href)) - { - const targetDoc = target.ownerDocument; - // Set the attribute of the (first) search-engine button. - var searchButton = document.getAnonymousElementByAttribute(this.getSearchBar(), - "anonid", "searchbar-engine-button"); - if (searchButton) { - var browser = gBrowser.getBrowserForDocument(targetDoc); - // Append the URI and an appropriate title to the browser data. - var iconURL = null; - if (gBrowser.shouldLoadFavIcon(browser.currentURI)) - iconURL = browser.currentURI.prePath + "/favicon.ico"; + var feedAdded = false; + var iconAdded = false; + var searchAdded = false; + var relStrings = rel.split(/\s+/); + var rels = {}; + for (let i = 0; i < relStrings.length; i++) + rels[relStrings[i]] = true; - var hidden = false; - // If this engine (identified by title) is already in the list, add it - // to the list of hidden engines rather than to the main list. - // XXX This will need to be changed when engines are identified by URL; - // see bug 335102. - var searchService = - Components.classes["@mozilla.org/browser/search-service;1"] - .getService(Components.interfaces.nsIBrowserSearchService); - if (searchService.getEngineByName(target.title)) - hidden = true; + for (let relVal in rels) { + switch (relVal) { + case "feed": + case "alternate": + if (!feedAdded) { + if (!rels.feed && rels.alternate && rels.stylesheet) + break; - var engines = []; - if (hidden) { - if (browser.hiddenEngines) - engines = browser.hiddenEngines; - } - else { - if (browser.engines) - engines = browser.engines; - } + var feed = { title: link.title, href: link.href, type: link.type }; + if (isValidFeed(feed, link.ownerDocument.nodePrincipal, rels.feed)) { + FeedHandler.addFeed(feed, link.ownerDocument); + feedAdded = true; + } + } + break; + case "icon": + if (!iconAdded) { + if (!gBrowser.mPrefs.getBoolPref("browser.chrome.site_icons")) + break; - engines.push({ uri: target.href, - title: target.title, - icon: iconURL }); + try { + var contentPolicy = Cc["@mozilla.org/layout/content-policy;1"]. + getService(Ci.nsIContentPolicy); + } catch(e) { + break; // Refuse to load if we can't do a security check. + } - if (hidden) { - browser.hiddenEngines = engines; - } - else { - browser.engines = engines; - if (browser == gBrowser || browser == gBrowser.mCurrentBrowser) - this.updateSearchButton(); - } + var targetDoc = link.ownerDocument; + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var uri = ios.newURI(link.href, targetDoc.characterSet, null); + try { + // Verify that the load of this icon is legal. + // error pages can load their favicon, to be on the safe side, + // only allow chrome:// favicons + const aboutNeterr = "about:neterror?"; + if (targetDoc.documentURI.substr(0, aboutNeterr.length) != aboutNeterr || + !uri.schemeIs("chrome")) { + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"]. + getService(Ci.nsIScriptSecurityManager); + ssm.checkLoadURIWithPrincipal(targetDoc.nodePrincipal, uri, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + } + } catch(e) { + break; + } + + // Security says okay, now ask content policy + if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_IMAGE, + uri, targetDoc.documentURIObject, + link, link.type, null) + != Ci.nsIContentPolicy.ACCEPT) + break; + + var browserIndex = gBrowser.getBrowserIndexForDocument(targetDoc); + // no browser? no favicon. + if (browserIndex == -1) + break; + + var tab = gBrowser.mTabContainer.childNodes[browserIndex]; + gBrowser.setIcon(tab, link.href); + iconAdded = true; + } + break; + case "search": + if (!searchAdded) { + var type = link.type && link.type.toLowerCase(); + type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); + + if (type == "application/opensearchdescription+xml" && link.title && + /^(https?|ftp):/i.test(link.href)) { + var engine = { title: link.title, href: link.href }; + BrowserSearch.addEngine(engine, link.ownerDocument); + searchAdded = true; + } + } + break; + } + } + } +} + +const BrowserSearch = { + addEngine: function(engine, targetDoc) { + // Set the attribute of the (first) search-engine button. + var searchButton = document.getAnonymousElementByAttribute(this.getSearchBar(), "anonid", + "searchbar-engine-button"); + if (searchButton) { + var browser = gBrowser.getBrowserForDocument(targetDoc); + // Append the URI and an appropriate title to the browser data. + var iconURL = null; + if (gBrowser.shouldLoadFavIcon(browser.currentURI)) + iconURL = browser.currentURI.prePath + "/favicon.ico"; + + var hidden = false; + // If this engine (identified by title) is already in the list, add it + // to the list of hidden engines rather than to the main list. + // XXX This will need to be changed when engines are identified by URL; + // see bug 335102. + var searchService = Cc["@mozilla.org/browser/search-service;1"]. + getService(Ci.nsIBrowserSearchService); + if (searchService.getEngineByName(engine.title)) + hidden = true; + + var engines = []; + if (hidden) { + if (browser.hiddenEngines) + engines = browser.hiddenEngines; + } + else { + if (browser.engines) + engines = browser.engines; + } + + engines.push({ uri: engine.href, + title: engine.title, + icon: iconURL }); + + if (hidden) + browser.hiddenEngines = engines; + else { + browser.engines = engines; + if (browser == gBrowser || browser == gBrowser.mCurrentBrowser) + this.updateSearchButton(); } } }, @@ -5073,16 +5135,6 @@ function convertFromUnicode(charset, str) * and shows UI when they are discovered. */ var FeedHandler = { - /** - * Initialize the Feed Handler - */ - init: function() { - gBrowser.addEventListener("DOMLinkAdded", - function (event) { FeedHandler.onLinkAdded(event); }, - true); - gBrowser.addEventListener("pagehide", FeedHandler.onPageHide, true); - }, - onPageHide: function(event) { var theBrowser = gBrowser.getBrowserForDocument(event.target); if (theBrowser) @@ -5237,25 +5289,9 @@ var FeedHandler = { } } }, - - /** - * A new tag has been discovered - check to see if it advertises - * an RSS feed. - */ - onLinkAdded: function(event) { - // XXX this event listener can/should probably be combined with the onLinkAdded - // listener in tabbrowser.xml, which only listens for favicons and then passes - // them to onLinkIconAvailable in the ProgressListener. We could extend the - // progress listener to have a generic onLinkAvailable and have tabbrowser pass - // along all events. It should give us the browser for the tab, as well as - // the actual event. - - var feed = recognizeFeedFromLink(event.target, - event.target.ownerDocument.nodePrincipal); + addFeed: function(feed, targetDoc) { if (feed) { - const targetDoc = event.target.ownerDocument; - // find which tab this is for, and set the attribute on the browser var browserForLink = gBrowser.getBrowserForDocument(targetDoc); if (!browserForLink) { diff --git a/mozilla/browser/base/content/pageinfo/feeds.js b/mozilla/browser/base/content/pageinfo/feeds.js index e0d5dd28eab..f5f00f3146c 100644 --- a/mozilla/browser/base/content/pageinfo/feeds.js +++ b/mozilla/browser/base/content/pageinfo/feeds.js @@ -50,14 +50,23 @@ function initFeedTab() var linkNodes = gDocument.getElementsByTagName("link"); var length = linkNodes.length; for (var i = 0; i < length; i++) { - var feed = recognizeFeedFromLink(linkNodes[i], gDocument.nodePrincipal); - if (feed) { - var type = feed.type; - if (type in feedTypes) - type = feedTypes[type]; - else - type = feedTypes["application/rss+xml"]; - addRow(feed.title, type, feed.href); + var link = linkNodes[i]; + if (!link.href) + continue; + + var rel = link.rel && link.rel.toLowerCase(); + var rels = {}; + if (rel) { + for each (let relVal in rel.split(/\s+/)) + rels[relVal] = true; + } + + if (rels.feed || (link.type && rels.alternate && !rels.stylesheet)) { + var feed = { title: link.title, href: link.href, type: link.type || "" }; + if (isValidFeed(feed, gDocument.nodePrincipal, rels.feed)) { + var type = feedTypes[feed.type] || feedTypes["application/rss+xml"]; + addRow(feed.title, type, feed.href); + } } } diff --git a/mozilla/browser/base/content/tabbrowser.xml b/mozilla/browser/base/content/tabbrowser.xml index 4502b77cb75..333fdc88f49 100644 --- a/mozilla/browser/base/content/tabbrowser.xml +++ b/mozilla/browser/base/content/tabbrowser.xml @@ -853,73 +853,6 @@ - - - - - - - @@ -2400,8 +2333,6 @@ - - + + + Autodiscovery Test + + + + diff --git a/mozilla/browser/base/content/test/browser_autodiscovery.js b/mozilla/browser/base/content/test/browser_autodiscovery.js new file mode 100644 index 00000000000..f1e452ec4ee --- /dev/null +++ b/mozilla/browser/base/content/test/browser_autodiscovery.js @@ -0,0 +1,105 @@ +function url(spec) { + var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + return ios.newURI(spec, null, null); +} + +var gTestPage = null; +function test() { + waitForExplicitFinish(); + var activeWin = Application.activeWindow; + gTestPage = activeWin.open(url("chrome://mochikit/content/browser/browser/base/content/test/autodiscovery.html")); + gTestPage.focus(); + setTimeout(iconDiscovery, 1000); +} + +function iconDiscovery() { + var tests = [ + { text: "rel icon discovered" }, + { rel: "abcdefg icon qwerty", text: "rel may contain additional rels separated by spaces" }, + { rel: "ICON", text: "rel is case insensitive" }, + { rel: "shortcut-icon", pass: false, text: "rel shortcut-icon not discovered" }, + { href: "moz.png", text: "relative href works" }, + { href: "notthere.png", text: "404'd icon is removed properly" }, + { href: "data:image/x-icon,%00", type: "image/x-icon", text: "data: URIs work" }, + { type: "image/png; charset=utf-8", text: "type may have optional parameters (RFC2046)" } + ]; + + for (let i = 0; i < tests.length; i++) { + gProxyFavIcon.removeAttribute("src"); + + var test = tests[i]; + var head = gTestPage.document.getElementById("linkparent"); + var link = gTestPage.document.createElement("link"); + + var rel = test.rel || "icon"; + var href = test.href || "chrome://mochikit/content/browser/browser/base/content/test/moz.png"; + var type = test.type || "image/png"; + if (test.pass == undefined) + test.pass = true; + + link.rel = rel; + link.href = href; + link.type = type; + head.appendChild(link); + + var hasSrc = gProxyFavIcon.hasAttribute("src"); + if (test.pass) + ok(hasSrc, test.text); + else + ok(!hasSrc, test.text); + + head.removeChild(link); + } + searchDiscovery(); +} + +function searchDiscovery() { + var tests = [ + { text: "rel search discovered" }, + { rel: "SEARCH", text: "rel is case insensitive" }, + { rel: "-search-", pass: false, text: "rel -search- not discovered" }, + { rel: "foo bar baz search quux", text: "rel may contain additional rels separated by spaces" }, + { href: "https://not.mozilla.com", text: "HTTPS ok" }, + { href: "ftp://not.mozilla.com", text: "FTP ok" }, + { href: "data:text/foo,foo", pass: false, text: "data URI not permitted" }, + { href: "javascript:alert(0)", pass: false, text: "JS URI not permitted" }, + { type: "APPLICATION/OPENSEARCHDESCRIPTION+XML", text: "type is case insensitve" }, + { type: " application/opensearchdescription+xml ", text: "type may contain extra whitespace" }, + { type: "application/opensearchdescription+xml; charset=utf-8", text: "type may have optional parameters (RFC2046)" }, + { type: "aapplication/opensearchdescription+xml", pass: false, text: "type should not be loosely matched" }, + { rel: "search search search", count: 1, text: "only one engine should be added" } + ]; + + for (let i = 0; i < tests.length; i++) { + var test = tests[i]; + var head = gTestPage.document.getElementById("linkparent"); + var link = gTestPage.document.createElement("link"); + + var rel = test.rel || "search"; + var href = test.href || "http://so.not.here.mozilla.com/search.xml"; + var type = test.type || "application/opensearchdescription+xml"; + var title = test.title || i; + if (test.pass == undefined) + test.pass = true; + + link.rel = rel; + link.href = href; + link.type = type; + link.title = title; + head.appendChild(link); + + var browser = gBrowser.getBrowserForDocument(gTestPage.document); + if (browser.engines) { + var hasEngine = (test.count) ? (browser.engines[0].title == title && + browser.engines.length == test.count) : + (browser.engines[0].title == title); + ok(hasEngine, test.text); + browser.engines = null; + } + else + ok(!test.pass, test.text); + } + gTestPage.close(); + finish(); +} diff --git a/mozilla/browser/base/content/test/moz.png b/mozilla/browser/base/content/test/moz.png new file mode 100755 index 00000000000..769c636340e Binary files /dev/null and b/mozilla/browser/base/content/test/moz.png differ diff --git a/mozilla/browser/base/content/utilityOverlay.js b/mozilla/browser/base/content/utilityOverlay.js index fd10c04affb..175fb41ba82 100644 --- a/mozilla/browser/base/content/utilityOverlay.js +++ b/mozilla/browser/base/content/utilityOverlay.js @@ -586,68 +586,48 @@ function openNewWindowWith(aURL, aDocument, aPostData, aAllowThirdPartyFixup, } /** - * recognizeFeedFromLink: recognizes RSS/ATOM feeds from DOM link elements. + * isValidFeed: checks whether the given data represents a valid feed. * - * @param aLink - * The DOM link element to check for representing a feed. + * @param aData + * An object representing a feed with title, href and type. * @param aPrincipal * The principal of the document, used for security check. - * @return object - * The feed object containing href, type, and title properties, - * if successful, otherwise null. + * @param aIsFeed + * Whether this is already a known feed or not, if true only a security + * check will be performed. */ -function recognizeFeedFromLink(aLink, aPrincipal) +function isValidFeed(aData, aPrincipal, aIsFeed) { - if (!aLink || !aPrincipal) - return null; + if (!aData || !aPrincipal) + return false; - var erel = aLink.rel && aLink.rel.toLowerCase(); - var etype = aLink.type && aLink.type.toLowerCase(); - var etitle = aLink.title; - const rssTitleRegex = /(^|\s)rss($|\s)/i; - var rels = {}; + if (!aIsFeed) { + var type = aData.type && aData.type.toLowerCase(); + type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); - if (erel) { - for each (var relValue in erel.split(/\s+/)) - rels[relValue] = true; - } - var isFeed = rels.feed; + aIsFeed = (type == "application/rss+xml" || + type == "application/atom+xml"); - if (!isFeed && (!rels.alternate || rels.stylesheet || !etype)) - return null; - - if (!isFeed) { - // Use type value - etype = etype.replace(/^\s+/, ""); - etype = etype.replace(/\s+$/, ""); - etype = etype.replace(/\s*;.*/, ""); - isFeed = (etype == "application/rss+xml" || - etype == "application/atom+xml"); - if (!isFeed) { + if (!aIsFeed) { // really slimy: general XML types with magic letters in the title - isFeed = ((etype == "text/xml" || etype == "application/xml" || - etype == "application/rdf+xml") && rssTitleRegex.test(etitle)); + const titleRegex = /(^|\s)rss($|\s)/i; + aIsFeed = ((type == "text/xml" || type == "application/rdf+xml" || + type == "application/xml") && titleRegex.test(aData.title)); } } - if (isFeed) { - try { - urlSecurityCheck(aLink.href, - aPrincipal, + if (aIsFeed) { + try { + urlSecurityCheck(aData.href, aPrincipal, Components.interfaces.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); } - catch (ex) { - dump(ex.message); - return null; // doesn't pass security check + catch(ex) { + aIsFeed = false; } - - // successful! return the feed - return { - href: aLink.href, - type: etype, - title: aLink.title - }; } - return null; + if (type) + aData.type = type; + + return aIsFeed; }