// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/files/file_path.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/devtools/devtools_window_testing.h"
#include "chrome/browser/devtools/features.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "chrome/browser/policy/policy_test_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/url_constants.h"
#include "chrome/test/base/chrome_test_path_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/api/messaging/messaging_delegate.h"
#include "extensions/common/extension.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"

using extensions::mojom::ManifestLocation;

namespace policy {

namespace {

// Utility for waiting until the dev-mode controls are visible/hidden
// Uses a MutationObserver on the attributes of the DOM element.
void WaitForExtensionsDevModeControlsVisibility(
    content::WebContents* contents,
    const char* dev_controls_accessor_js,
    const char* dev_controls_visibility_check_js,
    bool expected_visible) {
  ASSERT_TRUE(content::ExecJs(
      contents,
      base::StringPrintf(
          "var screenElement = %s;"
          "new Promise(resolve => {"
          "  function SendReplyIfAsExpected() {"
          "    var is_visible = %s;"
          "    if (is_visible != %s)"
          "      return false;"
          "    observer.disconnect();"
          "    resolve(true);"
          "    return true;"
          "  }"
          "  var observer = new MutationObserver(SendReplyIfAsExpected);"
          "  if (!SendReplyIfAsExpected()) {"
          "    var options = { 'attributes': true };"
          "    observer.observe(screenElement, options);"
          "  }"
          "});",
          dev_controls_accessor_js, dev_controls_visibility_check_js,
          base::ToString(expected_visible))));
}

// Utility to get a PolicyMap for setting the DeveloperToolsAvailability policy
// to a given value.
PolicyMap MakeDeveloperToolsAvailabilityMap(int value) {
  PolicyMap policies;
  policies.Set(key::kDeveloperToolsAvailability, POLICY_LEVEL_MANDATORY,
               POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD, base::Value(value),
               nullptr);
  return policies;
}

// Navigates the current tab of the browser to the given URL without any
// waiting after the navigation is triggered. Note: the
// ui_test_utils::BROWSER_TEST_NO_WAIT flag passed in results this returning
// right after the Browser::OpenURL() call without waiting for any load
// events.
void NavigateToURLNoWait(BrowserWindowInterface* browser, const GURL& url) {
  ui_test_utils::NavigateToURLWithDisposition(
      browser, url, WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_NO_WAIT);
}

// Utility to navigate the current tab of the browser to the specified page and
// then kill it using chrome://kill, verifying that the page ends up crashed.
void VerifyPageAllowsKill(BrowserWindowInterface* browser, const GURL& url) {
  SCOPED_TRACE(base::StringPrintf("Verifying url allows kill: '%s'",
                                  url.spec().c_str()));
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser, url));
  {
    content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes;
    content::RenderProcessHostWatcher exit_observer(
        browser->tab_strip_model()
            ->GetActiveWebContents()
            ->GetPrimaryMainFrame()
            ->GetProcess(),
        content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
    ASSERT_TRUE(
        ui_test_utils::NavigateToURL(browser, GURL(blink::kChromeUIKillURL)));
    exit_observer.Wait();
    // The kill url will have left a hanging pending entry on the
    // NavigationController and the contents will be marked as having crashed.
    content::WebContents* web_contents =
        browser->tab_strip_model()->GetActiveWebContents();
    EXPECT_TRUE(web_contents->GetController().GetPendingEntry());
    EXPECT_TRUE(web_contents->IsCrashed());
    EXPECT_FALSE(exit_observer.did_exit_normally());
  }
}

// Utility to navigate the current tab of the browser to the specified page and
// then attempt to kill it using chrome://kill, verifying that the kill is
// blocked before any navigation is started.
void VerifyPageBlocksKill(BrowserWindowInterface* browser, const GURL& url) {
  SCOPED_TRACE(base::StringPrintf("Verifying url blocks kill: '%s'",
                                  url.spec().c_str()));
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser, url));
  // We expect the debug URL to be blocked synchronously, so we don't have to
  // wait for a load stop here. Afterwards we verify nothing has happened by
  // checking there is no pending entry on the NavigationController (indicating
  // no navigation was started) and that the contents has not crashed.
  NavigateToURLNoWait(browser, GURL(blink::kChromeUIKillURL));
  content::WebContents* web_contents =
      browser->tab_strip_model()->GetActiveWebContents();
  EXPECT_FALSE(web_contents->GetController().GetPendingEntry());
  EXPECT_FALSE(web_contents->IsCrashed());
}

// Utility to navigate the current tab of the browser to the specified page and
// return true if a javascript URL can be run on it, false otherwise.
bool PageAllowsJavascriptURL(BrowserWindowInterface* browser, const GURL& url) {
  SCOPED_TRACE(base::StringPrintf("Checking url allows javascript URLs: '%s'",
                                  url.spec().c_str()));
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser, url));

  content::WebContents* web_contents =
      browser->tab_strip_model()->GetActiveWebContents();
  const std::u16string original_title = web_contents->GetTitle();

  const GURL javascript_url("javascript:void(document.title='Modified Title')");
  NavigateToURLNoWait(browser, javascript_url);
  // Run another script we can wait on to ensure the javascript URL will have
  // processed if it was going to.
  EXPECT_EQ(true, content::EvalJs(web_contents, "true"));

  // Check if the title was changed and return true if so.
  if (web_contents->GetTitle() != original_title) {
    EXPECT_EQ(u"Modified Title", web_contents->GetTitle());
    return true;
  }
  EXPECT_NE(u"Modified Title", web_contents->GetTitle());
  return false;
}

}  // namespace

class DeveloperToolsPolicyWithDialogFeatureTest
    : public PolicyTest,
      public testing::WithParamInterface<bool> {
 public:
  DeveloperToolsPolicyWithDialogFeatureTest() {
    if (IsDialogFeatureEnabled()) {
      scoped_feature_list_.InitAndEnableFeature(
          features::kDevToolsShowPolicyDialog);
    } else {
      scoped_feature_list_.InitAndDisableFeature(
          features::kDevToolsShowPolicyDialog);
    }
  }

  bool IsDialogFeatureEnabled() const { return GetParam(); }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_P(DeveloperToolsPolicyWithDialogFeatureTest,
                       DeveloperToolsDisabledByLegacyPolicy) {
  // Verifies that access to the developer tools can be disabled by setting the
  // legacy DeveloperToolsDisabled policy.

  // Open devtools.
  EXPECT_TRUE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
  content::WebContents* contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  DevToolsWindow* devtools_window =
      DevToolsWindow::GetInstanceForInspectedWebContents(contents);
  EXPECT_TRUE(devtools_window);

  // Disable devtools via policy.
  PolicyMap policies;
  policies.Set(key::kDeveloperToolsDisabled, POLICY_LEVEL_MANDATORY,
               POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD, base::Value(true),
               nullptr);
  content::WebContentsDestroyedWatcher close_observer(
      DevToolsWindowTesting::Get(devtools_window)->main_web_contents());
  UpdateProviderPolicy(policies);
  // wait for devtools close
  close_observer.Wait();
  // The existing devtools window should have closed.
  EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
  if (IsDialogFeatureEnabled()) {
    // And it's not possible to open it again, but a dialog should be shown.
    EXPECT_TRUE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
    EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
    web_modal::WebContentsModalDialogManager* dialog_manager =
        web_modal::WebContentsModalDialogManager::FromWebContents(contents);
    EXPECT_TRUE(dialog_manager->IsDialogActive());
    dialog_manager->CloseAllDialogs();
  } else {
    // And it's not possible to open it again.
    EXPECT_FALSE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
    EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
  }
}

IN_PROC_BROWSER_TEST_P(DeveloperToolsPolicyWithDialogFeatureTest,
                       DeveloperToolsDisabledByDeveloperToolsAvailability) {
  // Verifies that access to the developer tools can be disabled by setting the
  // DeveloperToolsAvailability policy.

  // Open devtools.
  EXPECT_TRUE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
  content::WebContents* contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  DevToolsWindow* devtools_window =
      DevToolsWindow::GetInstanceForInspectedWebContents(contents);
  EXPECT_TRUE(devtools_window);

  content::WebContentsDestroyedWatcher close_observer(
      DevToolsWindowTesting::Get(devtools_window)->main_web_contents());
  // Disable devtools via policy.
  UpdateProviderPolicy(
      MakeDeveloperToolsAvailabilityMap(2 /* DeveloperToolsDisallowed */));
  // wait for devtools close
  close_observer.Wait();
  // The existing devtools window should have closed.
  EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
  if (IsDialogFeatureEnabled()) {
    // And it's not possible to open it again, but a dialog should be shown.
    EXPECT_TRUE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
    EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
    web_modal::WebContentsModalDialogManager* dialog_manager =
        web_modal::WebContentsModalDialogManager::FromWebContents(contents);
    EXPECT_TRUE(dialog_manager->IsDialogActive());
    dialog_manager->CloseAllDialogs();
  } else {
    // And it's not possible to open it again.
    EXPECT_FALSE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
    EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
  }
}

IN_PROC_BROWSER_TEST_F(PolicyTest,
                       DevToolsUrlDisabledByDeveloperToolsAvailability) {
  UpdateProviderPolicy(
      MakeDeveloperToolsAvailabilityMap(2 /* DeveloperToolsDisallowed */));

  GURL devtools_url("devtools://devtools/bundled/devtools_app.html");
  // Navigate to the extensions frame and enabled "Developer mode"
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), devtools_url));

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  // Loading fails so that the title is not set to "DevTools".
  EXPECT_EQ(u"devtools://devtools/bundled/devtools_app.html",
            web_contents->GetTitle());
}

IN_PROC_BROWSER_TEST_F(PolicyTest,
                       DevToolsUrlAllowedByDeveloperToolsAvailability) {
  UpdateProviderPolicy(
      MakeDeveloperToolsAvailabilityMap(0 /* DeveloperToolsDisallowed */));

  GURL devtools_url("devtools://devtools/bundled/devtools_app.html");
  // Navigate to the extensions frame and enabled "Developer mode"
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), devtools_url));

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  // Loading succeeds so that the title is not set to "DevTools".
  EXPECT_EQ(u"DevTools", web_contents->GetTitle());
}

// Test for https://b/263040629
IN_PROC_BROWSER_TEST_P(DeveloperToolsPolicyWithDialogFeatureTest,
                       AvailabilityWins) {
  // DeveloperToolsDisabled is true, but DeveloperToolsAvailability wins.
  PolicyMap policies;
  policies.Set(key::kDeveloperToolsAvailability, POLICY_LEVEL_MANDATORY,
               POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
               base::Value(1 /* DeveloperToolsAllowed */), nullptr);
  policies.Set(key::kDeveloperToolsDisabled, POLICY_LEVEL_MANDATORY,
               POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD, base::Value(true),
               nullptr);
  UpdateProviderPolicy(policies);

  EXPECT_TRUE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
  content::WebContents* contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  DevToolsWindow* devtools_window =
      DevToolsWindow::GetInstanceForInspectedWebContents(contents);
  EXPECT_TRUE(devtools_window);

  // Clearing DeveloperToolsAvailability leaves behind
  // DeveloperToolsDisabled, so the DevTools window gets closed.
  content::WebContentsDestroyedWatcher close_observer(
      DevToolsWindowTesting::Get(devtools_window)->main_web_contents());
  policies.Erase(key::kDeveloperToolsAvailability);
  UpdateProviderPolicy(policies);
  // wait for devtools close
  close_observer.Wait();
  // The existing devtools window should have closed.
  EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
  if (IsDialogFeatureEnabled()) {
    // And it's not possible to open it again, but a dialog should be shown.
    EXPECT_TRUE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
    EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
    web_modal::WebContentsModalDialogManager* dialog_manager =
        web_modal::WebContentsModalDialogManager::FromWebContents(contents);
    EXPECT_TRUE(dialog_manager->IsDialogActive());
    dialog_manager->CloseAllDialogs();
  } else {
    // And it's not possible to open it again.
    EXPECT_FALSE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
    EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
  }
}

IN_PROC_BROWSER_TEST_P(DeveloperToolsPolicyWithDialogFeatureTest,
                       ViewSourceDisabledByDeveloperToolsAvailability) {
  // Verifies that entry points to ViewSource can be disabled by setting the
  // DeveloperToolsAvailability policy.

  // Disable devtools via policy.
  UpdateProviderPolicy(
      MakeDeveloperToolsAvailabilityMap(2 /* DeveloperToolsDisallowed */));
  // Verify that it's possible to ViewSource.
  if (IsDialogFeatureEnabled()) {
    EXPECT_TRUE(chrome::ExecuteCommand(browser(), IDC_VIEW_SOURCE));
  } else {
    EXPECT_FALSE(chrome::ExecuteCommand(browser(), IDC_VIEW_SOURCE));
  }
}

INSTANTIATE_TEST_SUITE_P(All,
                         DeveloperToolsPolicyWithDialogFeatureTest,
                         testing::Bool());

IN_PROC_BROWSER_TEST_F(PolicyTest, DeveloperToolsDisabledExtensionsDevMode) {
  // Verifies that when DeveloperToolsDisabled policy is set, the "dev mode"
  // in chrome://extensions is actively turned off and the checkbox
  // is disabled.
  // Note: We don't test the indicator as it is tested in the policy pref test
  // for kDeveloperToolsDisabled and kDeveloperToolsAvailability.

  // This test depends on the following helper methods to locate the DOM
  // elements to be tested.
  const char define_helpers_js[] =
      R"(function getToolbar() {
           const manager = document.querySelector('extensions-manager');
           return manager.shadowRoot.querySelector('extensions-toolbar');
         }

         function getToggle() {
           return getToolbar().$.devMode;
         }

         function getControls() {
           return getToolbar().$.devDrawer;
         }
        )";

  const char toggle_dev_mode_accessor_js[] = "getToggle()";
  const char dev_controls_accessor_js[] = "getControls()";
  const char dev_controls_visibility_check_js[] =
      "getControls().hasAttribute('expanded')";

  // Navigate to the extensions frame and enabled "Developer mode"
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), GURL(chrome::kChromeUIExtensionsURL)));

  content::WebContents* contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  EXPECT_TRUE(content::ExecJs(contents, std::string(define_helpers_js)));

  EXPECT_TRUE(content::ExecJs(
      contents, base::StringPrintf("domAutomationController.send(%s.click());",
                                   toggle_dev_mode_accessor_js)));

  WaitForExtensionsDevModeControlsVisibility(contents, dev_controls_accessor_js,
                                             dev_controls_visibility_check_js,
                                             true);

  // Disable devtools via policy.
  UpdateProviderPolicy(
      MakeDeveloperToolsAvailabilityMap(2 /* DeveloperToolsDisallowed */));

  // Expect devcontrols to be hidden now...
  WaitForExtensionsDevModeControlsVisibility(contents, dev_controls_accessor_js,
                                             dev_controls_visibility_check_js,
                                             false);

  // ... and checkbox is disabled
  EXPECT_EQ(true, content::EvalJs(contents, base::StringPrintf(
                                                "%s.hasAttribute('disabled')",
                                                toggle_dev_mode_accessor_js)));
}

// Verifies debug URLs, specifically chrome://kill and javascript URLs, are
// blocked or allowed for different pages depending on the
// DeveloperToolsAvailability policy setting. Note: javascript URLs are always
// blocked on extension schemes, regardless of the policy setting.
// TODO(crbug.com/40064953): The loading of a force installed extension in this
// test runs into an issue on branded Windows builders.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING) && BUILDFLAG(IS_WIN)
#define MAYBE_DebugURLsDisabledByDeveloperToolsAvailability \
  DISABLED_DebugURLsDisabledByDeveloperToolsAvailability
#else
#define MAYBE_DebugURLsDisabledByDeveloperToolsAvailability \
  DebugURLsDisabledByDeveloperToolsAvailability
#endif
IN_PROC_BROWSER_TEST_F(PolicyTest,
                       MAYBE_DebugURLsDisabledByDeveloperToolsAvailability) {
  // TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
  extensions::ScopedTestMV2Enabler mv2_enabler;

  // Get a url for a standard web page.
  ASSERT_TRUE(embedded_test_server()->Start());
  GURL tab_url(embedded_test_server()->GetURL("/empty.html"));

  // Get a url for a force installed extension.
  base::FilePath crx_path(chrome_test_utils::GetTestFilePath(
      base::FilePath().AppendASCII("devtools").AppendASCII("extensions"),
      base::FilePath().AppendASCII("options.crx")));
  extensions::ChromeTestExtensionLoader loader(browser()->GetProfile());
  // TODO(crbug.com/40269105): We shouldn't need to ignore manifest warnings
  // here, but there's an issue related to the _metadata folder added for
  // content verification when force-installing an off-store crx in a branded
  // build, which produces an install warning.
  loader.set_ignore_manifest_warnings(true);
  loader.set_location(ManifestLocation::kExternalPolicyDownload);
  scoped_refptr<const extensions::Extension> extension =
      loader.LoadExtension(crx_path);
  ASSERT_TRUE(extension);
  GURL extension_url("chrome-extension://" + extension->id() + "/options.html");

  // The default for DeveloperToolsAvailability is to disallow for force
  // installed extensions. Even though that is already the value, we set it here
  // to be explicit about what the value currently is.
  // With this setting force installed extension should block the debug and
  // javascript URLs and but normal pages should allow them.
  UpdateProviderPolicy(MakeDeveloperToolsAvailabilityMap(
      0 /* DeveloperToolsDisallowedForForceInstalledExtensions */));
  {
    SCOPED_TRACE(
        "Testing DeveloperToolsDisallowedForForceInstalledExtensions policy "
        "setting");
    VerifyPageAllowsKill(browser(), tab_url);
    EXPECT_TRUE(PageAllowsJavascriptURL(browser(), tab_url));
    VerifyPageBlocksKill(browser(), extension_url);
    EXPECT_FALSE(PageAllowsJavascriptURL(browser(), extension_url));
  }

  // When the policy is set to always allow Devtools all the pages should allow
  // debug URLs to be used, but javascript URLs will still be blocked on any
  // extension schemes.
  UpdateProviderPolicy(
      MakeDeveloperToolsAvailabilityMap(1 /* DeveloperToolsAllowed */));
  {
    SCOPED_TRACE("Testing DeveloperToolsAllowed policy setting");
    VerifyPageAllowsKill(browser(), tab_url);
    EXPECT_TRUE(PageAllowsJavascriptURL(browser(), tab_url));
    VerifyPageAllowsKill(browser(), extension_url);
    EXPECT_FALSE(PageAllowsJavascriptURL(browser(), extension_url));
  }

  // When the policy is set to always disallow Devtools all the pages should
  // block debug and javascript URLs.
  UpdateProviderPolicy(
      MakeDeveloperToolsAvailabilityMap(2 /* DeveloperToolsDisallowed */));
  {
    SCOPED_TRACE("Testing DeveloperToolsDisallowed policy setting");
    VerifyPageBlocksKill(browser(), tab_url);
    EXPECT_FALSE(PageAllowsJavascriptURL(browser(), tab_url));
    VerifyPageBlocksKill(browser(), extension_url);
    EXPECT_FALSE(PageAllowsJavascriptURL(browser(), extension_url));
  }
}

}  // namespace policy
