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 // hook up UI through progress listener
gBrowser.addProgressListener(window.XULBrowserWindow, Components.interfaces.nsIWebProgress.NOTIFY_ALL); gBrowser.addProgressListener(window.XULBrowserWindow, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
// Initialize the feedhandler // setup our common DOMLinkAdded listener
FeedHandler.init(); gBrowser.addEventListener("DOMLinkAdded",
function (event) { DOMLinkHandler.onLinkAdded(event); },
false);
// Initialize the searchbar gBrowser.addEventListener("pagehide", FeedHandler.onPageHide, false);
BrowserSearch.init();
} }
function delayedStartup() function delayedStartup()
@ -2647,84 +2648,145 @@ var DownloadsButtonDNDObserver = {
} }
} }
const BrowserSearch = { const DOMLinkHandler = {
/**
* 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.
*/
onLinkAdded: function(event) { onLinkAdded: function(event) {
// XXX this event listener can/should probably be combined with the onLinkAdded var link = event.originalTarget;
// listener in tabbrowser.xml. See comments in FeedHandler.onLinkAdded(). var rel = link.rel && link.rel.toLowerCase();
const target = event.target; if (!link || !link.ownerDocument || !rel || !link.href)
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)
return; return;
if (etype == "application/opensearchdescription+xml" && var feedAdded = false;
searchRelRegex.test(target.rel) && searchHrefRegex.test(target.href)) var iconAdded = false;
{ var searchAdded = false;
const targetDoc = target.ownerDocument; var relStrings = rel.split(/\s+/);
// Set the attribute of the (first) search-engine button. var rels = {};
var searchButton = document.getAnonymousElementByAttribute(this.getSearchBar(), for (let i = 0; i < relStrings.length; i++)
"anonid", "searchbar-engine-button"); rels[relStrings[i]] = true;
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; for (let relVal in rels) {
// If this engine (identified by title) is already in the list, add it switch (relVal) {
// to the list of hidden engines rather than to the main list. case "feed":
// XXX This will need to be changed when engines are identified by URL; case "alternate":
// see bug 335102. if (!feedAdded) {
var searchService = if (!rels.feed && rels.alternate && rels.stylesheet)
Components.classes["@mozilla.org/browser/search-service;1"] break;
.getService(Components.interfaces.nsIBrowserSearchService);
if (searchService.getEngineByName(target.title))
hidden = true;
var engines = []; var feed = { title: link.title, href: link.href, type: link.type };
if (hidden) { if (isValidFeed(feed, link.ownerDocument.nodePrincipal, rels.feed)) {
if (browser.hiddenEngines) FeedHandler.addFeed(feed, link.ownerDocument);
engines = browser.hiddenEngines; feedAdded = true;
} }
else { }
if (browser.engines) break;
engines = browser.engines; case "icon":
} if (!iconAdded) {
if (!gBrowser.mPrefs.getBoolPref("browser.chrome.site_icons"))
break;
engines.push({ uri: target.href, try {
title: target.title, var contentPolicy = Cc["@mozilla.org/layout/content-policy;1"].
icon: iconURL }); getService(Ci.nsIContentPolicy);
} catch(e) {
break; // Refuse to load if we can't do a security check.
}
if (hidden) { var targetDoc = link.ownerDocument;
browser.hiddenEngines = engines; var ios = Cc["@mozilla.org/network/io-service;1"].
} getService(Ci.nsIIOService);
else { var uri = ios.newURI(link.href, targetDoc.characterSet, null);
browser.engines = engines; try {
if (browser == gBrowser || browser == gBrowser.mCurrentBrowser) // Verify that the load of this icon is legal.
this.updateSearchButton(); // 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. * and shows UI when they are discovered.
*/ */
var FeedHandler = { 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) { onPageHide: function(event) {
var theBrowser = gBrowser.getBrowserForDocument(event.target); var theBrowser = gBrowser.getBrowserForDocument(event.target);
if (theBrowser) if (theBrowser)
@ -5237,25 +5289,9 @@ 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) { if (feed) {
const targetDoc = event.target.ownerDocument;
// find which tab this is for, and set the attribute on the browser // find which tab this is for, and set the attribute on the browser
var browserForLink = gBrowser.getBrowserForDocument(targetDoc); var browserForLink = gBrowser.getBrowserForDocument(targetDoc);
if (!browserForLink) { if (!browserForLink) {

View File

@ -50,14 +50,23 @@ function initFeedTab()
var linkNodes = gDocument.getElementsByTagName("link"); var linkNodes = gDocument.getElementsByTagName("link");
var length = linkNodes.length; var length = linkNodes.length;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
var feed = recognizeFeedFromLink(linkNodes[i], gDocument.nodePrincipal); var link = linkNodes[i];
if (feed) { if (!link.href)
var type = feed.type; continue;
if (type in feedTypes)
type = feedTypes[type]; var rel = link.rel && link.rel.toLowerCase();
else var rels = {};
type = feedTypes["application/rss+xml"]; if (rel) {
addRow(feed.title, type, feed.href); 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);
}
} }
} }

View File

@ -853,73 +853,6 @@
</body> </body>
</method> </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"> <method name="onTitleChanged">
<parameter name="evt"/> <parameter name="evt"/>
<body> <body>
@ -2400,8 +2333,6 @@
</implementation> </implementation>
<handlers> <handlers>
<handler event="DOMLinkAdded" phase="capturing" action="this.onLinkAdded(event);"/>
<handler event="DOMWindowClose" phase="capturing"> <handler event="DOMWindowClose" phase="capturing">
<![CDATA[ <![CDATA[
if (!event.isTrusted) if (!event.isTrusted)

View File

@ -50,6 +50,9 @@ _TEST_FILES = test_feed_discovery.html \
$(NULL) $(NULL)
_BROWSER_FILES = browser_bug321000.js \ _BROWSER_FILES = browser_bug321000.js \
browser_autodiscovery.js \
autodiscovery.html \
moz.png \
$(NULL) $(NULL)
libs:: $(_TEST_FILES) 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 * @param aData
* The DOM link element to check for representing a feed. * An object representing a feed with title, href and type.
* @param aPrincipal * @param aPrincipal
* The principal of the document, used for security check. * The principal of the document, used for security check.
* @return object * @param aIsFeed
* The feed object containing href, type, and title properties, * Whether this is already a known feed or not, if true only a security
* if successful, otherwise null. * check will be performed.
*/ */
function recognizeFeedFromLink(aLink, aPrincipal) function isValidFeed(aData, aPrincipal, aIsFeed)
{ {
if (!aLink || !aPrincipal) if (!aData || !aPrincipal)
return null; return false;
var erel = aLink.rel && aLink.rel.toLowerCase(); if (!aIsFeed) {
var etype = aLink.type && aLink.type.toLowerCase(); var type = aData.type && aData.type.toLowerCase();
var etitle = aLink.title; type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
const rssTitleRegex = /(^|\s)rss($|\s)/i;
var rels = {};
if (erel) { aIsFeed = (type == "application/rss+xml" ||
for each (var relValue in erel.split(/\s+/)) type == "application/atom+xml");
rels[relValue] = true;
}
var isFeed = rels.feed;
if (!isFeed && (!rels.alternate || rels.stylesheet || !etype)) if (!aIsFeed) {
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) {
// really slimy: general XML types with magic letters in the title // really slimy: general XML types with magic letters in the title
isFeed = ((etype == "text/xml" || etype == "application/xml" || const titleRegex = /(^|\s)rss($|\s)/i;
etype == "application/rdf+xml") && rssTitleRegex.test(etitle)); aIsFeed = ((type == "text/xml" || type == "application/rdf+xml" ||
type == "application/xml") && titleRegex.test(aData.title));
} }
} }
if (isFeed) { if (aIsFeed) {
try { try {
urlSecurityCheck(aLink.href, urlSecurityCheck(aData.href, aPrincipal,
aPrincipal,
Components.interfaces.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); Components.interfaces.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
} }
catch (ex) { catch(ex) {
dump(ex.message); aIsFeed = false;
return null; // doesn't pass security check
} }
// successful! return the feed
return {
href: aLink.href,
type: etype,
title: aLink.title
};
} }
return null; if (type)
aData.type = type;
return aIsFeed;
} }