Bug 396203 - Unify checks for DOMLinkAdded. r/a=mconnor

git-svn-id: svn://10.0.0.236/trunk@237088 18797224-902f-48f8-a5cc-f745e15eee43
This commit is contained in:
rflint%ryanflint.com 2007-10-02 17:54:36 +00:00
parent c22c43ae9e
commit e8e8de756b
8 changed files with 298 additions and 226 deletions

View File

@ -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,44 +2648,108 @@ var DownloadsButtonDNDObserver = {
}
}
const BrowserSearch = {
/**
* Initialize the BrowserSearch
*/
init: function() {
gBrowser.addEventListener("DOMLinkAdded",
function (event) { BrowserSearch.onLinkAdded(event); },
false);
},
/**
* A new <link> 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)
var link = event.originalTarget;
var rel = link.rel && link.rel.toLowerCase();
if (!link || !link.ownerDocument || !rel || !link.href)
return;
// Bug 349431: If the engine has no suggested title, ignore it rather
// than trying to find an alternative.
if (!target.title)
return;
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;
if (etype == "application/opensearchdescription+xml" &&
searchRelRegex.test(target.rel) && searchHrefRegex.test(target.href))
{
const targetDoc = target.ownerDocument;
for (let relVal in rels) {
switch (relVal) {
case "feed":
case "alternate":
if (!feedAdded) {
if (!rels.feed && rels.alternate && rels.stylesheet)
break;
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;
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.
}
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");
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.
@ -2697,10 +2762,9 @@ const BrowserSearch = {
// 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))
var searchService = Cc["@mozilla.org/browser/search-service;1"].
getService(Ci.nsIBrowserSearchService);
if (searchService.getEngineByName(engine.title))
hidden = true;
var engines = [];
@ -2713,20 +2777,18 @@ const BrowserSearch = {
engines = browser.engines;
}
engines.push({ uri: target.href,
title: target.title,
engines.push({ uri: engine.href,
title: engine.title,
icon: iconURL });
if (hidden) {
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)
@ -5238,24 +5290,8 @@ var FeedHandler = {
}
},
/**
* A new <link> 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) {

View File

@ -50,16 +50,25 @@ 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"];
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);
}
}
}
var feedListbox = document.getElementById("feedListbox");
document.getElementById("feedTab").hidden = feedListbox.getRowCount() == 0;

View File

@ -853,73 +853,6 @@
</body>
</method>
<method name="onLinkAdded">
<parameter name="event"/>
<body>
<![CDATA[
if (!this.mPrefs.getBoolPref("browser.chrome.site_icons"))
return;
if (!event.originalTarget.rel.match((/(?:^|\s)icon(?:\s|$)/i)))
return;
// We have an icon.
var href = event.originalTarget.href;
if (!href)
return;
const nsIContentPolicy = Components.interfaces.nsIContentPolicy;
try {
var contentPolicy =
Components.classes['@mozilla.org/layout/content-policy;1']
.getService(nsIContentPolicy);
} catch(e) {
return; // Refuse to load if we can't do a security check.
}
// Get the IOService so we can make URIs
const ioService =
Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
const targetDoc = event.target.ownerDocument;
// Make a URI out of our href.
var uri = ioService.newURI(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 nsIScriptSecMan =
Components.interfaces.nsIScriptSecurityManager;
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
.getService(nsIScriptSecMan);
const aboutNeterr = "about:neterror?";
if (targetDoc.documentURI.substr(0, aboutNeterr.length) != aboutNeterr ||
!uri.schemeIs("chrome"))
secMan.checkLoadURIWithPrincipal(targetDoc.nodePrincipal, uri,
nsIScriptSecMan.DISALLOW_SCRIPT);
} catch(e) {
return;
}
// Security says okay, now ask content policy
if (contentPolicy.shouldLoad(nsIContentPolicy.TYPE_IMAGE,
uri, targetDoc.documentURIObject,
event.target, event.target.type,
null) != nsIContentPolicy.ACCEPT)
return;
var browserIndex = this.getBrowserIndexForDocument(targetDoc);
// no browser? no favicon.
if (browserIndex == -1)
return;
var tab = this.mTabContainer.childNodes[browserIndex];
this.setIcon(tab, href);
]]>
</body>
</method>
<method name="onTitleChanged">
<parameter name="evt"/>
<body>
@ -2400,8 +2333,6 @@
</implementation>
<handlers>
<handler event="DOMLinkAdded" phase="capturing" action="this.onLinkAdded(event);"/>
<handler event="DOMWindowClose" phase="capturing">
<![CDATA[
if (!event.isTrusted)

View File

@ -50,6 +50,9 @@ _TEST_FILES = test_feed_discovery.html \
$(NULL)
_BROWSER_FILES = browser_bug321000.js \
browser_autodiscovery.js \
autodiscovery.html \
moz.png \
$(NULL)
libs:: $(_TEST_FILES)

View File

@ -0,0 +1,8 @@
<!DOCTYPE HTML>
<html>
<head id="linkparent">
<title>Autodiscovery Test</title>
</head>
<body>
</body>
</html>

View File

@ -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();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

View File

@ -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) {
if (aIsFeed) {
try {
urlSecurityCheck(aLink.href,
aPrincipal,
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
};
}
if (type)
aData.type = type;
return null;
return aIsFeed;
}