summaryrefslogtreecommitdiff
path: root/chromium/chrome/browser/extensions/api/tab_capture/offscreen_tab.cc
blob: 8dbdcf8a26505ef4ad8343481583bbe6ca71d2f0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
// 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 "base/bind.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "chrome/browser/extensions/api/tab_capture/tab_capture_registry.h"
#include "chrome/browser/media/router/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_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/web_preferences.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/process_manager.h"

using content::WebContents;

DEFINE_WEB_CONTENTS_USER_DATA_KEY(extensions::OffscreenTabsOwner);

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;
    }
  }
}

// Navigation policy for presentations, where top-level navigations are not
// allowed.
class OffscreenTab::PresentationNavigationPolicy
    : public OffscreenTab::NavigationPolicy {
 public:
  PresentationNavigationPolicy() : first_navigation_started_(false) {}
  ~PresentationNavigationPolicy() override = 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()),
      capture_poll_timer_(false, false),
      content_capture_was_detected_(false),
      navigation_policy_(new NavigationPolicy) {
  DCHECK(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.
  offscreen_tab_web_contents_.reset(
      WebContents::Create(WebContents::CreateParams(profile_.get())));
  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);

    if (auto* render_view_host =
            offscreen_tab_web_contents_->GetRenderViewHost()) {
      auto web_prefs = render_view_host->GetWebkitPreferences();
      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.
  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) {
  DCHECK_EQ(offscreen_tab_web_contents_.get(), contents);

  if (in_fullscreen_mode())
    return;

  non_fullscreen_size_ =
      contents->GetRenderWidgetHostView()->GetViewBounds().size();
  if (contents->GetCapturerCount() >= 0 &&
      !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,
      const 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_TAB_AUDIO_CAPTURE) {
      devices.push_back(content::MediaStreamDevice(
          content::MEDIA_TAB_AUDIO_CAPTURE, std::string(), std::string()));
    }
    if (request.video_type == content::MEDIA_TAB_VIDEO_CAPTURE) {
      devices.push_back(content::MediaStreamDevice(
          content::MEDIA_TAB_VIDEO_CAPTURE, std::string(), std::string()));
    }
  }

  DVLOG(2) << "Allowing " << devices.size()
           << " capture devices for OffscreenTab content.";

  callback.Run(devices, devices.empty() ? content::MEDIA_DEVICE_INVALID_STATE
                                        : content::MEDIA_DEVICE_OK,
               std::unique_ptr<content::MediaStreamUI>(nullptr));
}

bool OffscreenTab::CheckMediaAccessPermission(
    WebContents* contents,
    const GURL& security_origin,
    content::MediaStreamType type) {
  DCHECK_EQ(offscreen_tab_web_contents_.get(), contents);
  return type == content::MEDIA_TAB_AUDIO_CAPTURE ||
      type == content::MEDIA_TAB_VIDEO_CAPTURE;
}

void OffscreenTab::DidShowFullscreenWidget() {
  if (offscreen_tab_web_contents_->GetCapturerCount() == 0 ||
      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_->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());

  if (content_capture_was_detected_) {
    if (offscreen_tab_web_contents_->GetCapturerCount() == 0) {
      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_->GetCapturerCount() > 0) {
    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)));
}

}  // namespace extensions