blob: 185deae24f2e8d94090da9e78b4c0b81521a2927 [file] [log] [blame]
// Copyright 2015 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.
#include "chrome/browser/extensions/api/tab_capture/offscreen_tab.h"
#include <algorithm>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/macros.h"
#include "base/stl_util.h"
#include "chrome/browser/extensions/api/tab_capture/tab_capture_registry.h"
#include "chrome/browser/media/router/presentation/presentation_navigation_policy.h"
#include "chrome/browser/media/router/presentation/receiver_presentation_service_delegate_impl.h" // nogncheck
#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_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/process_manager.h"
#include "third_party/blink/public/web/web_presentation_receiver_flags.h"
using content::WebContents;
namespace {
// Upper limit on the number of simultaneous off-screen tabs per extension
// instance.
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.
const int kMaxSecondsToWaitForCapture = 60;
const int kPollIntervalInSeconds = 1;
} // namespace
namespace extensions {
OffscreenTabsOwner::OffscreenTabsOwner(WebContents* extension_web_contents)
: extension_web_contents_(extension_web_contents) {
DCHECK(extension_web_contents_);
}
OffscreenTabsOwner::~OffscreenTabsOwner() {}
// static
OffscreenTabsOwner* OffscreenTabsOwner::Get(
content::WebContents* extension_web_contents) {
// CreateForWebContents() really means "create if not exists."
CreateForWebContents(extension_web_contents);
return FromWebContents(extension_web_contents);
}
OffscreenTab* OffscreenTabsOwner::OpenNewTab(
const GURL& start_url,
const gfx::Size& initial_size,
const std::string& optional_presentation_id) {
if (tabs_.size() >= kMaxOffscreenTabsPerExtension)
return nullptr; // Maximum number of offscreen tabs reached.
// OffscreenTab cannot be created with MakeUnique<OffscreenTab> since the
// constructor is protected. So create it separately, and then move it to
// |tabs_| below.
std::unique_ptr<OffscreenTab> offscreen_tab(new OffscreenTab(this));
tabs_.push_back(std::move(offscreen_tab));
tabs_.back()->Start(start_url, initial_size, optional_presentation_id);
return tabs_.back().get();
}
void OffscreenTabsOwner::DestroyTab(OffscreenTab* tab) {
for (std::vector<std::unique_ptr<OffscreenTab>>::iterator iter =
tabs_.begin();
iter != tabs_.end(); ++iter) {
if (iter->get() == tab) {
tabs_.erase(iter);
break;
}
}
}
OffscreenTab::OffscreenTab(OffscreenTabsOwner* owner)
: owner_(owner),
otr_profile_registration_(
IndependentOTRProfileManager::GetInstance()
->CreateFromOriginalProfile(
Profile::FromBrowserContext(
owner->extension_web_contents()->GetBrowserContext()),
base::BindOnce(&OffscreenTab::DieIfOriginalProfileDestroyed,
base::Unretained(this)))),
content_capture_was_detected_(false),
navigation_policy_(
std::make_unique<media_router::DefaultNavigationPolicy>()) {
DCHECK(otr_profile_registration_->profile());
}
OffscreenTab::~OffscreenTab() {
DVLOG(1) << "Destroying OffscreenTab for start_url=" << start_url_.spec();
}
void OffscreenTab::Start(const GURL& start_url,
const gfx::Size& initial_size,
const std::string& optional_presentation_id) {
DCHECK(start_time_.is_null());
start_url_ = start_url;
DVLOG(1) << "Starting OffscreenTab with initial size of "
<< initial_size.ToString() << " for start_url=" << start_url_.spec();
// Create the WebContents to contain the off-screen tab's page.
WebContents::CreateParams params(otr_profile_registration_->profile());
if (!optional_presentation_id.empty())
params.starting_sandbox_flags = blink::kPresentationReceiverSandboxFlags;
offscreen_tab_web_contents_ = WebContents::Create(params);
offscreen_tab_web_contents_->SetDelegate(this);
WebContentsObserver::Observe(offscreen_tab_web_contents_.get());
// Set initial size, if specified.
if (!initial_size.IsEmpty())
ResizeWebContents(offscreen_tab_web_contents_.get(),
gfx::Rect(initial_size));
// Mute audio output. When tab capture starts, the audio will be
// automatically unmuted, but will be captured into the MediaStream.
offscreen_tab_web_contents_->SetAudioMuted(true);
if (!optional_presentation_id.empty()) {
// 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);
// 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_ =
std::make_unique<media_router::PresentationNavigationPolicy>();
}
// Navigate to the initial URL.
content::NavigationController::LoadURLParams load_params(start_url_);
load_params.should_replace_current_entry = true;
load_params.should_clear_history_list = true;
offscreen_tab_web_contents_->GetController().LoadURLWithParams(load_params);
start_time_ = base::TimeTicks::Now();
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 for start_url=" << start_url_.spec() << " will die";
owner_->DestroyTab(this);
// |this| is no longer valid.
}
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;
}
bool OffscreenTab::ShouldFocusLocationBarByDefault(WebContents* source) {
DCHECK_EQ(offscreen_tab_web_contents_.get(), source);
// Indicate the location bar should be focused instead of the page, even
// though there is no location bar. This will prevent the page from
// automatically receiving input focus, which should never occur since there
// is not supposed to be any direct user interaction.
return true;
}
bool OffscreenTab::ShouldFocusPageAfterCrash() {
// Never focus the page. Not even after a crash.
return false;
}
void OffscreenTab::CanDownload(const GURL& url,
const std::string& request_method,
const base::Callback<void(bool)>& callback) {
// Offscreen tab pages are not allowed to download files.
callback.Run(false);
}
bool OffscreenTab::HandleContextMenu(const content::ContextMenuParams& params) {
// Context menus should never be shown. Do nothing, but indicate the context
// menu was shown so that default implementation in libcontent does not
// attempt to do so on its own.
return true;
}
content::KeyboardEventProcessingResult OffscreenTab::PreHandleKeyboardEvent(
WebContents* source,
const content::NativeWebKeyboardEvent& event) {
DCHECK_EQ(offscreen_tab_web_contents_.get(), source);
// Intercept and silence all keyboard events before they can be sent to the
// renderer.
return content::KeyboardEventProcessingResult::HANDLED;
}
bool OffscreenTab::PreHandleGestureEvent(WebContents* source,
const blink::WebGestureEvent& event) {
DCHECK_EQ(offscreen_tab_web_contents_.get(), source);
// Intercept and silence all gesture events before they can be sent to the
// renderer.
return true;
}
bool OffscreenTab::CanDragEnter(
WebContents* source,
const content::DropData& data,
blink::WebDragOperationsMask operations_allowed) {
DCHECK_EQ(offscreen_tab_web_contents_.get(), source);
// Halt all drag attempts onto the page since there should be no direct user
// interaction with it.
return false;
}
bool OffscreenTab::ShouldCreateWebContents(
content::WebContents* web_contents,
content::RenderFrameHost* opener,
content::SiteInstance* source_site_instance,
int32_t route_id,
int32_t main_frame_route_id,
int32_t main_frame_widget_route_id,
content::mojom::WindowContainerType window_container_type,
const GURL& opener_url,
const std::string& frame_name,
const GURL& target_url,
const std::string& partition_id,
content::SessionStorageNamespace* session_storage_namespace) {
DCHECK_EQ(offscreen_tab_web_contents_.get(), web_contents);
// Disallow creating separate WebContentses. The WebContents implementation
// uses this to spawn new windows/tabs, which is also not allowed for
// offscreen tabs.
return false;
}
bool OffscreenTab::EmbedsFullscreenWidget() const {
// OffscreenTab will manage fullscreen widgets.
return true;
}
void OffscreenTab::EnterFullscreenModeForTab(
WebContents* contents,
const GURL& origin,
const blink::WebFullscreenOptions& options) {
DCHECK_EQ(offscreen_tab_web_contents_.get(), contents);
if (in_fullscreen_mode())
return;
non_fullscreen_size_ =
contents->GetRenderWidgetHostView()->GetViewBounds().size();
if (contents->IsBeingCaptured() && !contents->GetPreferredSize().IsEmpty())
ResizeWebContents(contents, gfx::Rect(contents->GetPreferredSize()));
}
void OffscreenTab::ExitFullscreenModeForTab(WebContents* contents) {
DCHECK_EQ(offscreen_tab_web_contents_.get(), contents);
if (!in_fullscreen_mode())
return;
ResizeWebContents(contents, gfx::Rect(non_fullscreen_size_));
non_fullscreen_size_ = gfx::Size();
}
bool OffscreenTab::IsFullscreenForTabOrPending(
const WebContents* contents) const {
DCHECK_EQ(offscreen_tab_web_contents_.get(), contents);
return in_fullscreen_mode();
}
blink::WebDisplayMode OffscreenTab::GetDisplayMode(
const WebContents* contents) const {
DCHECK_EQ(offscreen_tab_web_contents_.get(), contents);
return in_fullscreen_mode() ? blink::kWebDisplayModeFullscreen
: blink::kWebDisplayModeBrowser;
}
void OffscreenTab::RequestMediaAccessPermission(
WebContents* contents,
const content::MediaStreamRequest& request,
content::MediaResponseCallback callback) {
DCHECK_EQ(offscreen_tab_web_contents_.get(), contents);
// This method is being called to check whether an extension is permitted to
// capture the page. Verify that the request is being made by the extension
// that spawned this OffscreenTab.
// Find the extension ID associated with the extension background page's
// WebContents.
content::BrowserContext* const extension_browser_context =
owner_->extension_web_contents()->GetBrowserContext();
const extensions::Extension* const extension =
ProcessManager::Get(extension_browser_context)->
GetExtensionForWebContents(owner_->extension_web_contents());
const std::string extension_id = extension ? extension->id() : "";
LOG_IF(DFATAL, extension_id.empty())
<< "Extension that started this OffscreenTab was not found.";
// If verified, allow any tab capture audio/video devices that were requested.
extensions::TabCaptureRegistry* const tab_capture_registry =
extensions::TabCaptureRegistry::Get(extension_browser_context);
content::MediaStreamDevices devices;
if (tab_capture_registry && tab_capture_registry->VerifyRequest(
request.render_process_id,
request.render_frame_id,
extension_id)) {
if (request.audio_type == content::MEDIA_GUM_TAB_AUDIO_CAPTURE) {
devices.push_back(content::MediaStreamDevice(
content::MEDIA_GUM_TAB_AUDIO_CAPTURE, std::string(), std::string()));
}
if (request.video_type == content::MEDIA_GUM_TAB_VIDEO_CAPTURE) {
devices.push_back(content::MediaStreamDevice(
content::MEDIA_GUM_TAB_VIDEO_CAPTURE, std::string(), std::string()));
}
}
DVLOG(2) << "Allowing " << devices.size()
<< " capture devices for OffscreenTab content.";
std::move(callback).Run(devices,
devices.empty() ? content::MEDIA_DEVICE_INVALID_STATE
: content::MEDIA_DEVICE_OK,
std::unique_ptr<content::MediaStreamUI>(nullptr));
}
bool OffscreenTab::CheckMediaAccessPermission(
content::RenderFrameHost* render_frame_host,
const GURL& security_origin,
content::MediaStreamType type) {
DCHECK_EQ(offscreen_tab_web_contents_.get(),
content::WebContents::FromRenderFrameHost(render_frame_host));
return type == content::MEDIA_GUM_TAB_AUDIO_CAPTURE ||
type == content::MEDIA_GUM_TAB_VIDEO_CAPTURE;
}
void OffscreenTab::DidShowFullscreenWidget() {
if (!offscreen_tab_web_contents_->IsBeingCaptured() ||
offscreen_tab_web_contents_->GetPreferredSize().IsEmpty())
return; // Do nothing, since no preferred size is specified.
content::RenderWidgetHostView* const current_fs_view =
offscreen_tab_web_contents_->GetFullscreenRenderWidgetHostView();
if (current_fs_view)
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_->AllowNavigation(navigation_handle)) {
DVLOG(2) << "Closing because NavigationPolicy disallowed "
<< "StartNavigation to " << navigation_handle->GetURL().spec();
Close();
}
}
void OffscreenTab::DieIfContentCaptureEnded() {
DCHECK(offscreen_tab_web_contents_.get());
if (content_capture_was_detected_) {
if (!offscreen_tab_web_contents_->IsBeingCaptured()) {
DVLOG(2) << "Capture of OffscreenTab content has stopped for start_url="
<< start_url_.spec();
owner_->DestroyTab(this);
return; // |this| is no longer valid.
} else {
DVLOG(3) << "Capture of OffscreenTab content continues for start_url="
<< start_url_.spec();
}
} else if (offscreen_tab_web_contents_->IsBeingCaptured()) {
DVLOG(2) << "Capture of OffscreenTab content has started for start_url="
<< start_url_.spec();
content_capture_was_detected_ = true;
} else if (base::TimeTicks::Now() - start_time_ >
base::TimeDelta::FromSeconds(kMaxSecondsToWaitForCapture)) {
// More than a minute has elapsed since this OffscreenTab was started and
// content capture still hasn't started. As a safety precaution, assume
// that content capture is never going to start and die to free up
// resources.
LOG(WARNING) << "Capture of OffscreenTab content did not start "
"within timeout for start_url=" << start_url_.spec();
owner_->DestroyTab(this);
return; // |this| is no longer valid.
}
// Schedule the timer to check again in a second.
capture_poll_timer_.Start(
FROM_HERE,
base::TimeDelta::FromSeconds(kPollIntervalInSeconds),
base::Bind(&OffscreenTab::DieIfContentCaptureEnded,
base::Unretained(this)));
}
void OffscreenTab::DieIfOriginalProfileDestroyed(Profile* profile) {
DCHECK(profile == otr_profile_registration_->profile());
owner_->DestroyTab(this);
}
} // namespace extensions