Commit 7b73a6fa authored by mark a. foltz's avatar mark a. foltz Committed by Commit Bot

[chrome.tabCapture] Close presentations that attempt to navigate.

Presentations have the sandboxed-top-level-navigation-browsing-context-flag set [1],
so they should not be able to start top-level navigations that cross documents.

This patch allows this navigation policy to be enforced for offscreen tabs that
are started as presentations.

A companion change in Blink, https://chromium-review.googlesource.com/c/540498,
sets the appropriate flags on the Document for the presentation; this adds
additional enforcement in the WebContents layer.

[1] https://www.w3.org/TR/html51/browsers.html#sandboxed-top-level-navigation-browsing-context-flag

Bug: 697526
Change-Id: Ic1f797b408df82c226d6023966afe2f972d6d159
Reviewed-on: https://chromium-review.googlesource.com/538975
Commit-Queue: mark a. foltz <mfoltz@chromium.org>
Reviewed-by: default avatarYuri Wiitala <miu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#486547}
parent 8d8ee780
......@@ -14,6 +14,7 @@
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/web_contents_sizer.h"
#include "content/public/browser/keyboard_event_processing_result.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
......@@ -29,7 +30,7 @@ namespace {
// Upper limit on the number of simultaneous off-screen tabs per extension
// instance.
const int kMaxOffscreenTabsPerExtension = 3;
const int kMaxOffscreenTabsPerExtension = 4;
// Time intervals used by the logic that detects when the capture of an
// offscreen tab has stopped, to automatically tear it down and free resources.
......@@ -82,13 +83,42 @@ void OffscreenTabsOwner::DestroyTab(OffscreenTab* tab) {
}
}
// Navigation policy for presentations, where top-level navigations are not
// allowed.
class OffscreenTab::PresentationNavigationPolicy
: public OffscreenTab::NavigationPolicy {
public:
PresentationNavigationPolicy() : first_navigation_started_(false) {}
~PresentationNavigationPolicy() = default;
private:
// OffscreenTab::NavigationPolicy overrides
bool DidStartNavigation(content::NavigationHandle* navigation_handle) final {
// We only care about top-level navigations that are cross-document.
if (!navigation_handle->IsInMainFrame() ||
navigation_handle->IsSameDocument()) {
return true;
}
// The initial navigation had already begun.
if (first_navigation_started_)
return false;
first_navigation_started_ = true;
return true;
}
bool first_navigation_started_;
};
OffscreenTab::OffscreenTab(OffscreenTabsOwner* owner)
: owner_(owner),
profile_(Profile::FromBrowserContext(
owner->extension_web_contents()->GetBrowserContext())
->CreateOffTheRecordProfile()),
->CreateOffTheRecordProfile()),
capture_poll_timer_(false, false),
content_capture_was_detected_(false) {
content_capture_was_detected_(false),
navigation_policy_(new NavigationPolicy) {
DCHECK(profile_);
}
......@@ -120,11 +150,15 @@ void OffscreenTab::Start(const GURL& start_url,
offscreen_tab_web_contents_->SetAudioMuted(true);
if (!optional_presentation_id.empty()) {
DVLOG(1) << " Register with ReceiverPresentationServiceDelegateImpl, "
<< "[presentation_id]: " << optional_presentation_id;
// Create a ReceiverPSDImpl associated with the offscreen tab's WebContents.
// The new instance will set up the necessary infrastructure to allow
// controlling peers the ability to connect to the offscreen tab.
// This offscreen tab is a presentation created through the Presentation
// API. https://www.w3.org/TR/presentation-api/
//
// Create a ReceiverPresentationServiceDelegateImpl associated with the
// offscreen tab's WebContents. The new instance will set up the necessary
// infrastructure to allow controlling pages the ability to connect to the
// offscreen tab.
DVLOG(1) << "Register with ReceiverPresentationServiceDelegateImpl, "
<< "presentation_id=" << optional_presentation_id;
media_router::ReceiverPresentationServiceDelegateImpl::CreateForWebContents(
offscreen_tab_web_contents_.get(), optional_presentation_id);
......@@ -134,6 +168,11 @@ void OffscreenTab::Start(const GURL& start_url,
web_prefs.presentation_receiver = true;
render_view_host->UpdateWebkitPreferences(web_prefs);
}
// Presentations are not allowed to perform top-level navigations after
// initial load. This is enforced through sandboxing flags, but we also
// enforce it here.
navigation_policy_.reset(new PresentationNavigationPolicy);
}
// Navigate to the initial URL.
......@@ -146,11 +185,15 @@ void OffscreenTab::Start(const GURL& start_url,
DieIfContentCaptureEnded();
}
void OffscreenTab::Close() {
if (offscreen_tab_web_contents_)
offscreen_tab_web_contents_->ClosePage();
}
void OffscreenTab::CloseContents(WebContents* source) {
DCHECK_EQ(offscreen_tab_web_contents_.get(), source);
// Javascript in the page called window.close().
DVLOG(1) << "OffscreenTab will die at renderer's request for start_url="
<< start_url_.spec();
DVLOG(1) << "OffscreenTab for start_url=" << start_url_.spec() << " will die";
owner_->DestroyTab(this);
// |this| is no longer valid.
}
......@@ -159,6 +202,7 @@ bool OffscreenTab::ShouldSuppressDialogs(WebContents* source) {
DCHECK_EQ(offscreen_tab_web_contents_.get(), source);
// Suppress all because there is no possible direct user interaction with
// dialogs.
// TODO(crbug.com/734191): This does not suppress window.print().
return true;
}
......@@ -346,6 +390,25 @@ void OffscreenTab::DidShowFullscreenWidget() {
current_fs_view->SetSize(offscreen_tab_web_contents_->GetPreferredSize());
}
void OffscreenTab::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK(offscreen_tab_web_contents_.get());
if (!navigation_policy_->DidStartNavigation(navigation_handle)) {
DVLOG(2) << "Closing because NavigationPolicy disallowed "
<< "StartNavigation to " << navigation_handle->GetURL().spec();
Close();
}
}
// Default navigation policy.
OffscreenTab::NavigationPolicy::NavigationPolicy() = default;
OffscreenTab::NavigationPolicy::~NavigationPolicy() = default;
bool OffscreenTab::NavigationPolicy::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
return true;
}
void OffscreenTab::DieIfContentCaptureEnded() {
DCHECK(offscreen_tab_web_contents_.get());
......
......@@ -122,6 +122,9 @@ class OffscreenTab : protected content::WebContentsDelegate,
const gfx::Size& initial_size,
const std::string& optional_presentation_id);
// Closes the underlying WebContents.
void Close();
// content::WebContentsDelegate overrides to provide the desired behaviors.
void CloseContents(content::WebContents* source) final;
bool ShouldSuppressDialogs(content::WebContents* source) final;
......@@ -170,12 +173,27 @@ class OffscreenTab : protected content::WebContentsDelegate,
// content::WebContentsObserver overrides
void DidShowFullscreenWidget() final;
void DidStartNavigation(content::NavigationHandle* navigation_handle) final;
private:
bool in_fullscreen_mode() const {
return !non_fullscreen_size_.IsEmpty();
}
// Selected calls to the navigation methods in WebContentsObserver are
// delegated to this object to determine a navigation is allowed. If any
// call returns false, the offscreen tab is destroyed. The default policy
// allows all navigations.
class NavigationPolicy {
public:
NavigationPolicy();
~NavigationPolicy();
virtual bool DidStartNavigation(
content::NavigationHandle* navigation_handle);
};
class PresentationNavigationPolicy; // Forward declaration
// Called by |capture_poll_timer_| to automatically destroy this OffscreenTab
// when the capturer count returns to zero.
void DieIfContentCaptureEnded();
......@@ -213,6 +231,9 @@ class OffscreenTab : protected content::WebContentsDelegate,
// |offscreen_tab_web_contents_| is first detected.
bool content_capture_was_detected_;
// Object consulted to determine which offscreen tab navigations are allowed.
std::unique_ptr<NavigationPolicy> navigation_policy_;
DISALLOW_COPY_AND_ASSIGN(OffscreenTab);
};
......
......@@ -20,8 +20,8 @@ chrome.test.runTests([
helloWorldPageUri,
{video: true},
function(stream) {
if (streamsSoFar.length == 3) {
// 4th off-screen tab capture should fail.
if (streamsSoFar.length == 4) {
// 5th off-screen tab capture should fail.
chrome.test.assertLastError(
'Extension has already started too many off-screen tabs.');
chrome.test.assertFalse(!!stream);
......
......@@ -19,6 +19,18 @@ function waitForGreenOrRedTestResultAndEndTest(stream) {
);
}
function waitForTabToCloseAndEndTest(stream) {
let check = () => {
if (!stream.getTracks().find(track => track.readyState != 'ended')) {
chrome.test.succeed();
}
};
check();
stream.getTracks().forEach(track => {
track.onended = check;
});
}
chrome.test.runTests([
function cannotAccessLocalResources() {
chrome.tabCapture.captureOffscreenTab(
......@@ -77,9 +89,27 @@ chrome.test.runTests([
waitForGreenOrRedTestResultAndEndTest);
},
function cannotNavigateWhenPresenting() {
const captureOptions = getCaptureOptions();
captureOptions.presentationId = 'presentation_id';
chrome.tabCapture.captureOffscreenTab(
makeDataUriFromDocument(
'<html><script>' +
'window.location = "http://example.com/some_url.html";' +
'</script></html>'),
captureOptions,
waitForTabToCloseAndEndTest);
// NOTE: If this test times out, it means that one of the following did not
// happen:
// 1. page loaded
// 2. tab capture began
// 3. page attempted to navigate
// 4. page was closed by offscreen_tab.cc
// 5. MediaStreamTracks were ended
},
// NOTE: Before adding any more tests, the maximum off-screen tab limit would
// have to be increased (or a design issue resolved). This is because
// off-screen tabs are not closed the instant the LocalMediaStream is stopped,
// but approximately 1 second later.
]);
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment