910e62b5创建于 1月15日历史提交
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/browser/api/offscreen/offscreen_document_manager.h"

#include "base/test/bind.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_destroyer.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/api/offscreen/lifetime_enforcer_factories.h"
#include "extensions/browser/api/offscreen/offscreen_document_lifetime_enforcer.h"
#include "extensions/browser/disable_reason.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_host_test_helper.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/offscreen_document_host.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/api/offscreen.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "extensions/common/switches.h"
#include "extensions/test/test_extension_dir.h"

#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/ui/browser.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

// A programmable lifetime enforcer.
class TestLifetimeEnforcer : public OffscreenDocumentLifetimeEnforcer {
 public:
  TestLifetimeEnforcer(OffscreenDocumentHost* offscreen_document,
                       TerminationCallback termination_callback,
                       NotifyInactiveCallback notify_inactive_callback)
      : OffscreenDocumentLifetimeEnforcer(offscreen_document,
                                          std::move(termination_callback),
                                          std::move(notify_inactive_callback)) {
  }
  ~TestLifetimeEnforcer() override = default;

  void CallTerminate() { TerminateDocument(); }
  void CallNotifyInactive() {
    DCHECK(!is_active_);
    NotifyInactive();
  }

  void SetActive(bool is_active) { is_active_ = is_active; }

 private:
  bool IsActive() override { return is_active_; }

  bool is_active_ = true;
};

// A test-only factory method to create and populate a test-only lifetime
// enforcer.
std::unique_ptr<OffscreenDocumentLifetimeEnforcer> CreateTestLifetimeEnforcer(
    TestLifetimeEnforcer** lifetime_enforcer_out,
    OffscreenDocumentHost* offscreen_document,
    OffscreenDocumentLifetimeEnforcer::TerminationCallback termination_callback,
    OffscreenDocumentLifetimeEnforcer::NotifyInactiveCallback
        notify_inactive_callback) {
  auto enforcer = std::make_unique<TestLifetimeEnforcer>(
      offscreen_document, std::move(termination_callback),
      std::move(notify_inactive_callback));
  *lifetime_enforcer_out = enforcer.get();
  return enforcer;
}

}  // namespace

class OffscreenDocumentManagerBrowserTest : public ExtensionApiTest {
 public:
  OffscreenDocumentManagerBrowserTest() = default;
  ~OffscreenDocumentManagerBrowserTest() override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ExtensionApiTest::SetUpCommandLine(command_line);
    // Add the kOffscreenDocumentTesting switch to allow the use of the
    // `TESTING` reason in offscreen document creation.
    command_line->AppendSwitch(switches::kOffscreenDocumentTesting);
  }

  // Creates a new offscreen document with the given `extension`, `url`,
  // `reasons`, and `profile`, and waits for it to load.
  OffscreenDocumentHost* CreateDocumentAndWaitForLoad(
      const Extension& extension,
      const GURL& url,
      std::set<api::offscreen::Reason> reasons,
      Profile& profile) {
    ExtensionHostTestHelper host_waiter(&profile);
    host_waiter.RestrictToType(mojom::ViewType::kOffscreenDocument);
    OffscreenDocumentHost* offscreen_document =
        OffscreenDocumentManager::Get(&profile)->CreateOffscreenDocument(
            extension, url, reasons);
    host_waiter.WaitForHostCompletedFirstLoad();

    return offscreen_document;
  }

  // Same as above, defaulting to a single reason of Reason::kTesting.
  OffscreenDocumentHost* CreateDocumentAndWaitForLoad(
      const Extension& extension,
      const GURL& url,
      Profile& profile) {
    return CreateDocumentAndWaitForLoad(
        extension, url, {api::offscreen::Reason::kTesting}, profile);
  }

  // Same as the above, defaulting to the on-the-record profile.
  OffscreenDocumentHost* CreateDocumentAndWaitForLoad(
      const Extension& extension,
      const GURL& url) {
    return CreateDocumentAndWaitForLoad(extension, url, *profile());
  }

  OffscreenDocumentManager* offscreen_document_manager() {
    return OffscreenDocumentManager::Get(profile());
  }
};

// Tests the flow of the OffscreenDocumentManager creating a new offscreen
// document for an extension.
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
                       CreateOffscreenDocument) {
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1"
         })";
  static constexpr char kOffscreenDocumentHtml[] =
      R"(<html>
           <body>
             <div id="signal">Hello, World</div>
           </body>
         </html>)";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                     kOffscreenDocumentHtml);

  // Note: We wrap `extension` in a refptr because we'll unload it later in the
  // test and need to make sure the object isn't deleted.
  scoped_refptr<const Extension> extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // To start, the manager should not have any offscreen documents registered.
  EXPECT_EQ(nullptr,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));

  OffscreenDocumentHost* offscreen_document = CreateDocumentAndWaitForLoad(
      *extension, extension->GetResourceURL("offscreen.html"));

  {
    // Check the document loaded properly. Note: general capabilities of
    // offscreen documents are exercised more in the OffscreenDocumentHost
    // tests, but this helps sanity check that the manager created it properly.
    static constexpr char kScript[] =
        R"({
             let div = document.getElementById('signal');
             div ? div.innerText : '<no div>';
           })";
    EXPECT_EQ("Hello, World",
              content::EvalJs(offscreen_document->host_contents(), kScript));
  }

  // The manager should now have a record of a document for the extension.
  EXPECT_EQ(offscreen_document,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));

  {
    // Disable the extension. This causes it to unload, and the offscreen
    // document should be closed.
    ExtensionHostTestHelper host_waiter(profile());
    host_waiter.RestrictToHost(offscreen_document);
    DisableExtension(extension->id(), {disable_reason::DISABLE_USER_ACTION});
    host_waiter.WaitForHostDestroyed();
    // Note: `offscreen_document` is destroyed at this point.
  }

  // There should no longer be a document for the extension.
  EXPECT_EQ(nullptr,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));
}

// Tests the flow of closing an existing offscreen document through the
// manager.
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
                       ClosingDocumentThroughTheManager) {
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1"
         })";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                     "<html>offscreen</html>");

  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  const GURL offscreen_url = extension->GetResourceURL("offscreen.html");

  OffscreenDocumentHost* offscreen_document =
      CreateDocumentAndWaitForLoad(*extension, offscreen_url);
  ASSERT_TRUE(offscreen_document);

  {
    ExtensionHostTestHelper host_waiter(profile());
    host_waiter.RestrictToHost(offscreen_document);
    offscreen_document_manager()->CloseOffscreenDocumentForExtension(
        *extension);
  }

  EXPECT_EQ(nullptr,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));
}

// Tests calling window.close() in an offscreen document closes it (through the
// manager).
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
                       CallingWindowCloseInAnOffscreenDocumentClosesIt) {
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1"
         })";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                     "<html>offscreen</html>");

  scoped_refptr<const Extension> extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  OffscreenDocumentHost* offscreen_document = CreateDocumentAndWaitForLoad(
      *extension, extension->GetResourceURL("offscreen.html"));
  ASSERT_TRUE(offscreen_document);
  EXPECT_EQ(offscreen_document,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));

  {
    // Call window.close() from the offscreen document. This should cause the
    // manager to close the document, destroying the host.
    ExtensionHostTestHelper host_waiter(profile());
    host_waiter.RestrictToHost(offscreen_document);
    ASSERT_TRUE(content::ExecJs(offscreen_document->host_contents(),
                                "window.close();"));
    host_waiter.WaitForHostDestroyed();
  }

  EXPECT_EQ(nullptr,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));
}

// Tests that lifetime enforcers can terminate an offscreen document (such as if
// a hard limit is reached).
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
                       LifetimeEnforcement_Terminate) {
  // Override the factory method for the testing reason to use our own
  // TestLifetimeEnforcer.
  TestLifetimeEnforcer* lifetime_enforcer = nullptr;
  LifetimeEnforcerFactories::TestingOverride factory_override;
  factory_override.map().emplace(
      api::offscreen::Reason::kTesting,
      base::BindRepeating(&CreateTestLifetimeEnforcer, &lifetime_enforcer));

  // Load an extension and create an offscreen document.
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1"
         })";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                     "<html>offscreen</html>");

  scoped_refptr<const Extension> extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  OffscreenDocumentHost* offscreen_document = CreateDocumentAndWaitForLoad(
      *extension, extension->GetResourceURL("offscreen.html"));
  ASSERT_TRUE(offscreen_document);
  EXPECT_EQ(offscreen_document,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));

  // The lifetime enforcer should have been created. Call the termination
  // callback; the offscreen document should be closed.
  ASSERT_TRUE(lifetime_enforcer);
  lifetime_enforcer->CallTerminate();

  // Note: `offscreen_document` is now unsafe to use.

  EXPECT_EQ(nullptr,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));
}

// Tests that the offscreen document is terminated when all the lifetime
// enforcers (currently only ever one) notify that the document is inactive.
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
                       LifetimeEnforcement_NotifyInactive) {
  // Override the factory method for the testing reason to use our own
  // TestLifetimeEnforcer.
  TestLifetimeEnforcer* lifetime_enforcer = nullptr;
  LifetimeEnforcerFactories::TestingOverride factory_override;
  factory_override.map().emplace(
      api::offscreen::Reason::kTesting,
      base::BindRepeating(&CreateTestLifetimeEnforcer, &lifetime_enforcer));

  // Load an extension and create an offscreen document.
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1"
         })";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                     "<html>offscreen</html>");

  scoped_refptr<const Extension> extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  OffscreenDocumentHost* offscreen_document = CreateDocumentAndWaitForLoad(
      *extension, extension->GetResourceURL("offscreen.html"));
  ASSERT_TRUE(offscreen_document);
  EXPECT_EQ(offscreen_document,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));

  // The lifetime enforcer should have been created.
  ASSERT_TRUE(lifetime_enforcer);

  // Set the document to be inactive and notify. The document should be closed.
  lifetime_enforcer->SetActive(false);
  lifetime_enforcer->CallNotifyInactive();

  // Note: `offscreen_document` and `lifetime_enforcer` are now unsafe to use.

  EXPECT_EQ(nullptr,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));
}

// Tests that when multiple reasons are provided, a lifetime enforcer is
// created for each, and the offscreen document is only terminated once all
// lifetime enforcers indicate the document is inactive.
IN_PROC_BROWSER_TEST_F(
    OffscreenDocumentManagerBrowserTest,
    LifetimeEnforcement_DocumentIsNotTerminatedUntilAllInactive) {
  // Override the factory method for both the dom parsing and blobs reasons to
  // use our own TestLifetimeEnforcer.
  TestLifetimeEnforcer* dom_parser_enforcer = nullptr;
  LifetimeEnforcerFactories::TestingOverride factory_override;
  factory_override.map().emplace(
      api::offscreen::Reason::kDomParser,
      base::BindRepeating(&CreateTestLifetimeEnforcer, &dom_parser_enforcer));
  TestLifetimeEnforcer* blobs_enforcer = nullptr;
  factory_override.map().emplace(
      api::offscreen::Reason::kBlobs,
      base::BindRepeating(&CreateTestLifetimeEnforcer, &blobs_enforcer));

  // Load an extension an create an offscreen document.
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1"
         })";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                     "<html>offscreen</html>");

  scoped_refptr<const Extension> extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Create a new document for both the blob and dom parser reasons.
  OffscreenDocumentHost* offscreen_document = CreateDocumentAndWaitForLoad(
      *extension, extension->GetResourceURL("offscreen.html"),
      {api::offscreen::Reason::kBlobs, api::offscreen::Reason::kDomParser},
      *profile());
  ASSERT_TRUE(offscreen_document);
  EXPECT_EQ(offscreen_document,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));

  // Each lifetime enforcer should have been created.
  ASSERT_TRUE(dom_parser_enforcer);
  ASSERT_TRUE(blobs_enforcer);

  // Set the dom parser enforcer to be inactive. Note that the blob enforcer is
  // still active.
  dom_parser_enforcer->SetActive(false);
  dom_parser_enforcer->CallNotifyInactive();

  // The document should still be around, since the blob enforcer is still
  // active.
  EXPECT_EQ(offscreen_document,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));

  // Re-activate and deactivate the dom parser enforcer (to verify it's okay for
  // it to cycle between states multiple times).
  // Note: Technically, the SetActive() calls here aren't necessary, but it
  // better indicates the real scenario.
  dom_parser_enforcer->SetActive(true);
  dom_parser_enforcer->SetActive(false);
  dom_parser_enforcer->CallNotifyInactive();

  // The document should still be active.
  EXPECT_EQ(offscreen_document,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));

  // Switch the active enforcers, first making the dom parser enforcer active,
  // then making the blob enforcer inactive.
  dom_parser_enforcer->SetActive(true);
  blobs_enforcer->SetActive(false);
  blobs_enforcer->CallNotifyInactive();

  // As above, the document should still be around, since a lifetime enforcer
  // is still active (this time, the dom parser enforcer).
  EXPECT_EQ(offscreen_document,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));

  // Finally, re-mark the dom parser as inactive.
  dom_parser_enforcer->SetActive(false);
  dom_parser_enforcer->CallNotifyInactive();

  // Note: `offscreen_document`, `dom_parser_enforcer`, and `blobs_enforcer`
  // are all now unsafe to use!

  // Now, the document should be closed.
  EXPECT_EQ(nullptr,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));
}

// Tests creating offscreen documents for an incognito split-mode extension.
IN_PROC_BROWSER_TEST_F(OffscreenDocumentManagerBrowserTest,
                       IncognitoOffscreenDocuments) {
  // `split` incognito mode is required in order to allow the extension to
  // have a separate process in incognito.
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1",
           "incognito": "split"
         })";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                     "<html>offscreen</html>");

  scoped_refptr<const Extension> extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  {
    // Enable the extension in incognito. This results in an extension reload;
    // wait for that to finish and update the `extension` pointer.
    TestExtensionRegistryObserver registry_observer(
        ExtensionRegistry::Get(profile()), extension->id());
    util::SetIsIncognitoEnabled(extension->id(), profile(),
                                /*enabled=*/true);
    extension = registry_observer.WaitForExtensionLoaded();
  }

  ASSERT_TRUE(extension);
  ASSERT_TRUE(util::IsIncognitoEnabled(extension->id(), profile()));

  const GURL offscreen_url = extension->GetResourceURL("offscreen.html");

  // Create an on-the-record offscreen document.
  OffscreenDocumentHost* on_the_record_host =
      CreateDocumentAndWaitForLoad(*extension, offscreen_url);
  ASSERT_TRUE(on_the_record_host);
  // Ensure the on-the-record context is used.
  // Note: Throughout this test, we use
  // `OffscreenDocumentHost::host_contents()` to access the BrowserContext
  // instead of `OffscreenDocumentHost::browser_context()`; this is to ensure
  // that the WebContents is hosted properly.
  EXPECT_FALSE(on_the_record_host->host_contents()
                   ->GetBrowserContext()
                   ->IsOffTheRecord());

#if BUILDFLAG(ENABLE_EXTENSIONS)
  // Create an incognito browser and an incognito offscreen document, and
  // validate that the proper context is used.
  Browser* incognito_browser = CreateIncognitoBrowser();
  ASSERT_TRUE(incognito_browser);
  Profile* incognito_profile = incognito_browser->profile();
#else
  Profile* incognito_profile =
      profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
#endif

  OffscreenDocumentHost* incognito_host = CreateDocumentAndWaitForLoad(
      *extension, offscreen_url, *incognito_profile);
  ASSERT_TRUE(incognito_host);
  EXPECT_TRUE(
      incognito_host->host_contents()->GetBrowserContext()->IsOffTheRecord());

  // These should be separate offscreen documents and have separate profiles,
  // but the same original profile.
  EXPECT_NE(incognito_host, on_the_record_host);
  EXPECT_EQ(Profile::FromBrowserContext(
                on_the_record_host->host_contents()->GetBrowserContext()),
            Profile::FromBrowserContext(
                incognito_host->host_contents()->GetBrowserContext())
                ->GetOriginalProfile());

  // Ensure the offscreen documents are registered with the appropriate
  // context.
  EXPECT_EQ(on_the_record_host,
            OffscreenDocumentManager::Get(profile())
                ->GetOffscreenDocumentForExtension(*extension));
  EXPECT_EQ(incognito_host, OffscreenDocumentManager::Get(incognito_profile)
                ->GetOffscreenDocumentForExtension(*extension));

  {
    ExtensionHostTestHelper host_waiter(incognito_profile);
    host_waiter.RestrictToHost(incognito_host);
#if BUILDFLAG(IS_ANDROID)
    // Destroy OTR profile. Consequently, the `incognito_host` should be
    // destroyed.
    ProfileDestroyer::DestroyOTRProfileWhenAppropriate(incognito_profile);
#else
    // Shut down the incognito browser, OTR profile will be destroyed.
    // Consequently, the `incognito_host` should be destroyed.
    CloseBrowserSynchronously(incognito_browser);
#endif
    host_waiter.WaitForHostDestroyed();
    // Note: `incognito_host` is destroyed at this point.
  }

  // The on-the-record document should remain.
  EXPECT_EQ(on_the_record_host,
            offscreen_document_manager()->GetOffscreenDocumentForExtension(
                *extension));
}

}  // namespace extensions