diff options
13 files changed, 464 insertions, 2 deletions
diff --git a/chrome/browser/extensions/execute_script_apitest.cc b/chrome/browser/extensions/execute_script_apitest.cc index 5014ef43..82f3eab 100644 --- a/chrome/browser/extensions/execute_script_apitest.cc +++ b/chrome/browser/extensions/execute_script_apitest.cc @@ -95,6 +95,13 @@ IN_PROC_BROWSER_TEST_F(ExecuteScriptApiTest, ExecuteScriptFrameAfterLoad) { ASSERT_TRUE(RunExtensionTest("executescript/frame_after_load")) << message_; } +IN_PROC_BROWSER_TEST_F(ExecuteScriptApiTest, FrameWithHttp204) { + host_resolver()->AddRule("b.com", "127.0.0.1"); + host_resolver()->AddRule("c.com", "127.0.0.1"); + ASSERT_TRUE(StartEmbeddedTestServer()); + ASSERT_TRUE(RunExtensionTest("executescript/http204")) << message_; +} + IN_PROC_BROWSER_TEST_F(ExecuteScriptApiTest, ExecuteScriptRunAt) { SetupDelayedHostResolver(); ASSERT_TRUE(StartEmbeddedTestServer()); diff --git a/chrome/test/data/extensions/api_test/executescript/http204/at_document_end.js b/chrome/test/data/extensions/api_test/executescript/http204/at_document_end.js new file mode 100644 index 0000000..5b0e049 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/at_document_end.js @@ -0,0 +1,5 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +window.documentEnd = window.documentEnd ? documentEnd + 1 : 1; diff --git a/chrome/test/data/extensions/api_test/executescript/http204/at_document_end_unexpected.js b/chrome/test/data/extensions/api_test/executescript/http204/at_document_end_unexpected.js new file mode 100644 index 0000000..456b59e --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/at_document_end_unexpected.js @@ -0,0 +1,5 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +window.didRunAtDocumentEndUnexpected = true; diff --git a/chrome/test/data/extensions/api_test/executescript/http204/at_document_idle.js b/chrome/test/data/extensions/api_test/executescript/http204/at_document_idle.js new file mode 100644 index 0000000..1f86100 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/at_document_idle.js @@ -0,0 +1,5 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +window.documentIdle = window.documentIdle ? documentIdle + 1 : 1; diff --git a/chrome/test/data/extensions/api_test/executescript/http204/at_document_idle_unexpected.js b/chrome/test/data/extensions/api_test/executescript/http204/at_document_idle_unexpected.js new file mode 100644 index 0000000..9954502 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/at_document_idle_unexpected.js @@ -0,0 +1,5 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +window.didRunAtDocumentIdleUnexpected = true; diff --git a/chrome/test/data/extensions/api_test/executescript/http204/at_document_start.js b/chrome/test/data/extensions/api_test/executescript/http204/at_document_start.js new file mode 100644 index 0000000..c1fa003 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/at_document_start.js @@ -0,0 +1,5 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +window.documentStart = window.documentStart ? documentStart + 1 : 1; diff --git a/chrome/test/data/extensions/api_test/executescript/http204/at_document_start_unexpected.js b/chrome/test/data/extensions/api_test/executescript/http204/at_document_start_unexpected.js new file mode 100644 index 0000000..4f8dbf1 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/at_document_start_unexpected.js @@ -0,0 +1,5 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +window.didRunAtDocumentStartUnexpected = true; diff --git a/chrome/test/data/extensions/api_test/executescript/http204/background.js b/chrome/test/data/extensions/api_test/executescript/http204/background.js new file mode 100644 index 0000000..ad6280f --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/background.js @@ -0,0 +1,294 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var config; +var MAIN_HOST = 'b.com'; +var OTHER_HOST = 'c.com'; + +var DOMContentLoadedEventsInFrame = []; + +chrome.test.getConfig(function(config) { + window.config = config; + + var testUrl = 'http://' + MAIN_HOST + ':' + config.testServer.port + + '/extensions/api_test/executescript/http204/page_with_204_frame.html'; + chrome.runtime.onMessage.addListener(function listener(msg, sender) { + // This message should be sent when the frame and all sub frames have + // completely finished loading. + chrome.test.assertEq('start the test', msg); + // Should be the top-level frame with our test page. + chrome.test.assertEq(0, sender.frameId); + chrome.test.assertEq(testUrl, sender.url); + chrome.test.assertTrue(sender.tab.id > 0); + + chrome.runtime.onMessage.removeListener(listener); + chrome.webNavigation.onDOMContentLoaded.removeListener(onDOMContentLoaded); + // Avoid flakiness by excluding all events that are not from our tab. + DOMContentLoadedEventsInFrame = + DOMContentLoadedEventsInFrame.filter(function(details) { + return details.tabId === sender.tab.id; + }); + + startTest(sender.tab.id); + }); + + chrome.webNavigation.onDOMContentLoaded.addListener(onDOMContentLoaded); + chrome.tabs.create({ + url: testUrl + }); + + function onDOMContentLoaded(details) { + if (details.frameId > 0) { + DOMContentLoadedEventsInFrame.push(details); + } + } +}); + +function startTest(tabId) { + // The default font color of any document. + var kDefaultColor = getComputedStyle(document.body).color; + var kExpectedFontFamily = '\'expected font-family\''; + var kExpectedColor = 'rgb(123, 123, 123)'; + + // The page has a child frame containing a HTTP 204 page. + // In response to HTTP 204 (No Content), the browser stops navigating away and + // stays at the previous page. In this test, the URL leading to HTTP 204 was + // the initial URL of the frame, so in response to HTTP 204, the frame should + // end at about:blank. + + // Each chrome.tabs.insertCSS test is followed by a test using executeScript. + // These executeScript tests exists for two reasons: + // - They verify the result of insertCSS + // - They show that executeScript is working as intended. + + chrome.test.runTests([ + function insertCssTopLevelOnly() { + // Sanity check: insertCSS can change main frame's CSS. + chrome.tabs.insertCSS(tabId, { + code: 'body { font-family: ' + kExpectedFontFamily + ' !important;}', + }, chrome.test.callbackPass()); + // The result is verified hereafter, in executeScriptTopLevelOnly. + }, + + function executeScriptTopLevelOnly() { + // Sanity check: insertCSS should really have changed the CSS. + // Depends on insertCssTopLevelOnly. + chrome.tabs.executeScript(tabId, { + code: 'getComputedStyle(document.body).fontFamily', + }, chrome.test.callbackPass(function(results) { + chrome.test.assertEq([kExpectedFontFamily], results); + })); + }, + + // Now we know that executeScript works in the top-level frame, we will use + // it to check whether executeScript can execute code in the child frame. + function verifyManifestContentScriptInjected() { + // Check whether the content scripts from manifest.json ran in the frame. + chrome.tabs.executeScript(tabId, { + code: '[' + + '[window.documentStart,' + + ' window.documentEnd,' + + ' window.documentIdle],' + + '[frames[0].documentStart,' + + ' frames[0].documentEnd,' + + ' frames[0].documentIdle],' + + '[frames[0].didRunAtDocumentStartUnexpected,' + + ' frames[0].didRunAtDocumentEndUnexpected,' + + ' frames[0].didRunAtDocumentIdleUnexpected],' + + ']', + }, chrome.test.callbackPass(function(results) { + chrome.test.assertEq([[ + // Should always run in top frame because of matching match pattern. + [1, 1, 1], + + [ + // Before the response from the server is received, the frame + // displays an empty document. This document has a <html> element, + // it can be scripted by the parent frame and its URL as shown to + // scripts is about:blank. + // Because the content script's match_about_blank flag is set to + // true in manifest.json, and its URL pattern matches the parent + // frame's URL and, the document_start script should be run. + // TODO(robwu): This should be 1 for the reason above, but it is + // null because the script is not injected (crbug.com/511057). + null, + // Does not run at document_end and document_idle because the + // DOMContentLoaded event is not triggered either. + null, + null, + ], + + // Should not run scripts in child frame because the page load was + // not committed, and the URL pattern (204 page) doesn't match. + [null, null, null], + ]], results); + })); + }, + + // document_end and document_idle scripts are not run in the child frame + // because we assume that the DOMContentLoaded event is not triggered in + // frames after a failed provisional load. Verify that the DOMContentLoaded + // event was indeed NOT triggered. + function checkDOMContentLoadedEvent() { + chrome.test.assertEq([], DOMContentLoadedEventsInFrame); + chrome.test.succeed(); + }, + + function insertCss204NoAbout() { + // HTTP 204 = stay at previous page, which was a blank page, so insertCSS + // without matchAboutBlank shouldn't change the frame's CSS. + chrome.tabs.insertCSS(tabId, { + code: 'body { color: ' + kExpectedColor + '; }', + allFrames: true, + }, chrome.test.callbackPass()); + // The result is verified hereafter, in verifyInsertCss204NoAbout. + }, + + function verifyInsertCss204NoAbout() { + // Depends on insertCss204NoAbout. + chrome.tabs.executeScript(tabId, { + code: 'frames[0].getComputedStyle(frames[0].document.body).color', + }, chrome.test.callbackPass(function(results) { + // CSS should not be inserted in frame because it's about:blank. + chrome.test.assertEq([kDefaultColor], results); + })); + }, + + function insertCss204Blank() { + chrome.tabs.insertCSS(tabId, { + code: 'body { color: ' + kExpectedColor + '; }', + allFrames: true, + matchAboutBlank: true, + }, chrome.test.callbackPass()); + // The result is verified hereafter, in verifyInsertCss204Blank. + }, + + function verifyInsertCss204Blank() { + // Depends on insertCss204Blank. + chrome.tabs.executeScript(tabId, { + code: 'frames[0].getComputedStyle(frames[0].document.body).color', + }, chrome.test.callbackPass(function(results) { + // CSS should be inserted in frame because matchAboutBlank was true. + chrome.test.assertEq([kExpectedColor], results); + })); + }, + + function executeScript204NoAbout() { + chrome.tabs.executeScript(tabId, { + code: 'top === window', + allFrames: true, + }, chrome.test.callbackPass(function(results) { + // Child frame should not be matched because it's about:blank. + chrome.test.assertEq([true], results); + })); + }, + + function executeScript204About() { + chrome.tabs.executeScript(tabId, { + code: 'top === window', + allFrames: true, + matchAboutBlank: true, + }, chrome.test.callbackPass(function(results) { + // Child frame should not be matched because matchAboutBlank was true. + chrome.test.assertEq([true, false], results); + })); + }, + + // Now we have verified that (programmatic) content script injection works + // for a frame whose initial load resulted in a 204. + // Continue with testing navigation from a child frame to a 204 page, with + // a variety of origins for completeness. + + function loadSameOriginFrameAndWaitUntil204() { + // This is not a test, just preparing for the next test. + // All URLs are at the same origin. + navigateToFrameAndWaitUntil204Loaded(tabId, MAIN_HOST, MAIN_HOST); + }, + + function verifySameOriginManifestAfterSameOrigin204() { + checkManifestScriptsAfter204Navigation(tabId); + }, + + function loadSameOriginFrameAndWaitUntilCrossOrigin204() { + // This is not a test, just preparing for the next test. + // The frame is at the same origin as the top-level frame, but the 204 + // URL is at a different origin. + navigateToFrameAndWaitUntil204Loaded(tabId, MAIN_HOST, OTHER_HOST); + }, + + function verifySameOriginManifestAfterCrossOrigin204() { + checkManifestScriptsAfter204Navigation(tabId); + }, + + function loadCrossOriginFrameAndWaitUntil204() { + // This is not a test, just preparing for the next test. + // The frame's origin differs from the top-level frame, and the 204 URL is + // at the same origin as the frame. + navigateToFrameAndWaitUntil204Loaded(tabId, OTHER_HOST, OTHER_HOST); + }, + + function verifyCrossOriginManifestAfterSameOrigin204() { + checkManifestScriptsAfter204Navigation(tabId); + }, + + function loadCrossOriginFrameAndWaitUntilCrossOrigin204() { + // This is not a test, just preparing for the next test. + // The frame's origin differs from the top-level frame, and the origin of + // the 204 URL differs from the frame (it is incidentally the same as the + // main frame's origin). + navigateToFrameAndWaitUntil204Loaded(tabId, OTHER_HOST, MAIN_HOST); + }, + + function verifyCrossOriginManifestAfterCrossOrigin204() { + checkManifestScriptsAfter204Navigation(tabId); + }, + ]); +} + +// Navigates to a page that navigates to a 204 page via a script. +function navigateToFrameAndWaitUntil204Loaded(tabId, hostname, hostname204) { + var doneListening = chrome.test.listenForever( + chrome.webNavigation.onErrorOccurred, + function(details) { + if (details.tabId === tabId && details.frameId > 0) { + chrome.test.assertTrue(details.url.includes('page204.html'), + 'frame URL should be page204.html, but was ' + details.url); + doneListening(); + } + }); + + var url = 'http://' + hostname + ':' + config.testServer.port + + '/extensions/api_test/executescript/http204/navigate_to_204.html?' + + hostname204; + + chrome.tabs.executeScript(tabId, { + code: 'document.body.innerHTML = \'<iframe src="' + url + '"></iframe>\';', + }); +} + +// Checks whether the content scripts were run as expected in the frame that +// just received a failed provisional load (=received 204 reply). +function checkManifestScriptsAfter204Navigation(tabId) { + chrome.tabs.executeScript(tabId, { + allFrames: true, + code: '[' + + '[window.documentStart,' + + ' window.documentEnd],' + + '[window.didRunAtDocumentStartUnexpected,' + + ' window.didRunAtDocumentEndUnexpected],' + + ']', + }, chrome.test.callbackPass(function(results) { + chrome.test.assertEq(2, results.length); + // Main frame. Should not be affected by child frame navigations. + chrome.test.assertEq([[1, 1], [null, null]], results[0]); + + // Child frame. + chrome.test.assertEq([ + // Should run the content scripts even after a navigation to 204. + [1, 1], + // Should not inject non-matching scripts. + [null, null], + ], results[1]); + })); +} diff --git a/chrome/test/data/extensions/api_test/executescript/http204/manifest.json b/chrome/test/data/extensions/api_test/executescript/http204/manifest.json new file mode 100644 index 0000000..1060177 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/manifest.json @@ -0,0 +1,50 @@ +{ + "name": "executeScript and HTTP 204 in iframe", + "manifest_version": 2, + "version": "1", + "background": { + "scripts": ["background.js"] + }, + "content_scripts": [{ + "run_at": "document_start", + "js": ["start_test_when_ready.js"], + "matches": ["*://*/*page_with_204_frame.html*"] + }, { + "run_at": "document_start", + "js": ["at_document_start.js"], + "all_frames": true, + "match_about_blank": true, + "matches": ["*://*/*page_with_204_frame.html*", "*://*/*navigate_to_204.html*"] + }, { + "run_at": "document_end", + "js": ["at_document_end.js"], + "all_frames": true, + "match_about_blank": true, + "matches": ["*://*/*page_with_204_frame.html*", "*://*/*navigate_to_204.html*"] + }, { + "run_at": "document_idle", + "js": ["at_document_idle.js"], + "all_frames": true, + "match_about_blank": true, + "matches": ["*://*/*page_with_204_frame.html*", "*://*/*navigate_to_204.html*"] + }, { + "run_at": "document_start", + "js": ["at_document_start_unexpected.js"], + "all_frames": true, + "match_about_blank": true, + "matches": ["*://*/*page204.html*"] + }, { + "run_at": "document_end", + "js": ["at_document_end_unexpected.js"], + "all_frames": true, + "match_about_blank": true, + "matches": ["*://*/*page204.html*"] + }, { + "run_at": "document_idle", + "js": ["at_document_idle_unexpected.js"], + "all_frames": true, + "match_about_blank": true, + "matches": ["*://*/*page204.html*"] + }], + "permissions": ["*://*/*", "webNavigation"] +} diff --git a/chrome/test/data/extensions/api_test/executescript/http204/navigate_to_204.html b/chrome/test/data/extensions/api_test/executescript/http204/navigate_to_204.html new file mode 100644 index 0000000..0136294 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/navigate_to_204.html @@ -0,0 +1,14 @@ +<!-- html tag = document_start script --> +<html> +<body> +<script> +// Navigate to a page that replies with 204 No Content. +var a = document.createElement('a'); +a.href = '../../../../page204.html'; +if (location.search) { + a.hostname = location.search.slice(1); +} +location.href = a.href; +</script> +</body> +</html> diff --git a/chrome/test/data/extensions/api_test/executescript/http204/page_with_204_frame.html b/chrome/test/data/extensions/api_test/executescript/http204/page_with_204_frame.html new file mode 100644 index 0000000..1fca4e3 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/page_with_204_frame.html @@ -0,0 +1,3 @@ +<body style="font-family: 'unexpected font-family';"> +<iframe src="../../../../page204.html"></iframe> +</body> diff --git a/chrome/test/data/extensions/api_test/executescript/http204/start_test_when_ready.js b/chrome/test/data/extensions/api_test/executescript/http204/start_test_when_ready.js new file mode 100644 index 0000000..97cbd00 --- /dev/null +++ b/chrome/test/data/extensions/api_test/executescript/http204/start_test_when_ready.js @@ -0,0 +1,43 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Waits until the document_idle script has run in the child frame, because we +// test whether this happens. To prevent the test from stalling indefinitely, we +// start the test anyway even if the document_idle script was not detected in +// the child frame. + +// Wait for at most 2 seconds. +var kMaxDelayMs = 2000; + +var timeStart; +window.onload = function() { + window.onload = null; + timeStart = Date.now(); + tryStartTest(); +}; + +function tryStartTest() { + if (isChildFrameReady()) { + // If document_idle scripts run, then this happens within a few 100ms. + chrome.runtime.sendMessage('start the test'); + } else if (Date.now() - timeStart > kMaxDelayMs) { + // Start the test even if the child frame's document_idle script was not + // injected. This is expected (because we don't run document_end and + // document_idle scripts in frames with a failed provisional load). + console.error('Did not detect document_idle. Starting anyway!'); + chrome.runtime.sendMessage('start the test'); + } else { + setTimeout(tryStartTest, 200); + } +} + +function isChildFrameReady() { + try { + // documentIdle is set by at_document_idle.js + if (frames[0].documentIdle) { + return true; + } + } catch (e) {} + return false; +} diff --git a/extensions/renderer/script_injection_manager.cc b/extensions/renderer/script_injection_manager.cc index 8876707..aa0f5d3 100644 --- a/extensions/renderer/script_injection_manager.cc +++ b/extensions/renderer/script_injection_manager.cc @@ -71,6 +71,7 @@ class ScriptInjectionManager::RFOHelper : public content::RenderFrameObserver { bool OnMessageReceived(const IPC::Message& message) override; void DidCreateNewDocument() override; void DidCreateDocumentElement() override; + void DidFailProvisionalLoad(const blink::WebURLError& error) override; void DidFinishDocumentLoad() override; void DidFinishLoad() override; void FrameDetached() override; @@ -138,6 +139,27 @@ void ScriptInjectionManager::RFOHelper::DidCreateDocumentElement() { manager_->StartInjectScripts(render_frame(), UserScript::DOCUMENT_START); } +void ScriptInjectionManager::RFOHelper::DidFailProvisionalLoad( + const blink::WebURLError& error) { + FrameStatusMap::iterator it = manager_->frame_statuses_.find(render_frame()); + if (it != manager_->frame_statuses_.end() && + it->second == UserScript::DOCUMENT_START) { + // Since the provisional load failed, the frame stays at its previous loaded + // state and origin (or the parent's origin for new/about:blank frames). + // Reset the frame to DOCUMENT_IDLE in order to reflect that the frame is + // done loading, and avoid any deadlock in the system. + // + // We skip injection of DOCUMENT_END and DOCUMENT_IDLE scripts, because the + // injections closely follow the DOMContentLoaded (and onload) events, which + // are not triggered after a failed provisional load. + // This assumption is verified in the checkDOMContentLoadedEvent subtest of + // ExecuteScriptApiTest.FrameWithHttp204 (browser_tests). + InvalidateAndResetFrame(); + should_run_idle_ = false; + manager_->frame_statuses_[render_frame()] = UserScript::DOCUMENT_IDLE; + } +} + void ScriptInjectionManager::RFOHelper::DidFinishDocumentLoad() { DCHECK(content::RenderThread::Get()); manager_->StartInjectScripts(render_frame(), UserScript::DOCUMENT_END); @@ -461,8 +483,7 @@ void ScriptInjectionManager::HandlePermitScriptInjection(int64_t request_id) { // At this point, because the request is present in pending_injections_, we // know that this is the same page that issued the request (otherwise, - // RFOHelper's DidStartProvisionalLoad callback would have caused it to be - // cleared out). + // RFOHelper::InvalidateAndResetFrame would have caused it to be cleared out). scoped_ptr<ScriptInjection> injection(std::move(*iter)); pending_injections_.erase(iter); |