// 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 <array>
#include <string>

#include "base/functional/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/policy/url_blocking_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/test/base/ui_test_utils.h"
#include "components/policy/core/browser/url_list/url_list_policy_pref_names.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/policy/core/common/policy_types.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_launch_params.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "url/gurl.h"

#if !BUILDFLAG(IS_MAC)
#include "extensions/browser/app_window/app_window.h"
#include "ui/base/window_open_disposition.h"
#endif

using content::BrowserThread;

namespace policy {

namespace {

// Verifies that the given url |spec| can be opened. This assumes that |spec|
// points at empty.html in the test data dir.
void CheckCanOpenURL(BrowserWindowInterface* browser, const std::string& spec) {
  GURL url(spec);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser, url));
  content::WebContents* contents =
      browser->tab_strip_model()->GetActiveWebContents();
  EXPECT_EQ(url, contents->GetLastCommittedURL());

  std::u16string blocked_page_title;
  if (url.has_host()) {
    blocked_page_title = base::UTF8ToUTF16(url.GetHost());
  } else {
    // Local file paths show the full URL.
    blocked_page_title = base::UTF8ToUTF16(url.spec());
  }
  EXPECT_NE(blocked_page_title, contents->GetTitle());
}

void CheckCanOpenViewSourceURL(BrowserWindowInterface* browser,
                               const std::string& spec) {
  GURL view_source_url("view-source:" + spec);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser, view_source_url));
  content::WebContents* contents =
      browser->tab_strip_model()->GetActiveWebContents();
  EXPECT_EQ(view_source_url, contents->GetLastCommittedURL());
}

// Handler for embedded http-server, returns a small page with javascript
// variable and a link to increment it. It's for JavascriptBlocklistable test.
std::unique_ptr<net::test_server::HttpResponse> JSIncrementerPageHandler(
    const net::test_server::HttpRequest& request) {
  if (request.relative_url != "/test.html") {
    return nullptr;
  }

  std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
      new net::test_server::BasicHttpResponse());
  http_response->set_code(net::HTTP_OK);
  http_response->set_content(
      "<head><script type=\"text/javascript\">\n"
      "<!--\n"
      "var value = 1;"
      "var increment = function() {"
      "  value = value + 1;"
      "};\n"
      "//-->\n"
      "</script></head><body>"
      "<a id='link' href=\"javascript:increment();\">click</a>"
      "</body>");
  http_response->set_content_type("text/html");
  return http_response;
}

// Fetch value from page generated by JSIncrementerPageHandler.
int JSIncrementerFetch(content::WebContents* contents) {
  return content::EvalJs(contents, "value;").ExtractInt();
}

}  //  namespace

IN_PROC_BROWSER_TEST_F(UrlBlockingPolicyTest, URLBlocklist) {
  // Checks that URLs can be blocklisted, and that exceptions can be made to
  // the blocklist.

  ASSERT_TRUE(embedded_test_server()->Start());

  const auto kURLS = std::to_array<std::string>({
      embedded_test_server()->GetURL("aaa.com", "/empty.html").spec(),
      embedded_test_server()->GetURL("bbb.com", "/empty.html").spec(),
      embedded_test_server()->GetURL("sub.bbb.com", "/empty.html").spec(),
      embedded_test_server()->GetURL("bbb.com", "/policy/blank.html").spec(),
      embedded_test_server()->GetURL("bbb.com.", "/policy/blank.html").spec(),
  });

  // Verify that "bbb.com" opens before applying the blocklist.
  CheckCanOpenURL(browser(), kURLS[1]);

  // Set a blocklist.
  base::ListValue blocklist;
  blocklist.Append("bbb.com");
  PolicyMap policies;
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(std::move(blocklist)), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();
  // All bbb.com URLs are blocked, and "aaa.com" is still unblocked.
  CheckCanOpenURL(browser(), kURLS[0]);
  for (size_t i = 1; i < std::size(kURLS); ++i)
    CheckURLIsBlocked(browser(), kURLS[i]);

  // Allowlist some sites of bbb.com.
  base::ListValue allowlist;
  allowlist.Append("sub.bbb.com");
  allowlist.Append("bbb.com/policy");
  policies.Set(key::kURLAllowlist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(allowlist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();
  CheckURLIsBlocked(browser(), kURLS[1]);
  CheckCanOpenURL(browser(), kURLS[2]);
  CheckCanOpenURL(browser(), kURLS[3]);
  CheckCanOpenURL(browser(), kURLS[4]);
}

IN_PROC_BROWSER_TEST_F(UrlBlockingPolicyTest, URLBlocklistViewSource) {
  // Checks that blocklisted urls are blocked when accessed by via view-source:,
  // and that blocklisting view-source:* blocks all view-source urls.

  ASSERT_TRUE(embedded_test_server()->Start());

  const std::string kURL_A =
      embedded_test_server()->GetURL("aaa.com", "/empty.html").spec();
  const std::string kURL_B =
      embedded_test_server()->GetURL("bbb.com", "/empty.html").spec();

  // Ensure that no urls are blocked by default.
  CheckCanOpenURL(browser(), kURL_A);
  CheckCanOpenURL(browser(), kURL_B);
  CheckCanOpenViewSourceURL(browser(), kURL_A);
  CheckCanOpenViewSourceURL(browser(), kURL_B);

  // Block bbb.com urls.
  base::ListValue blocklist;
  blocklist.Append("bbb.com");
  PolicyMap policies;
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  // Verify that blocking bbb.com also blocks view-source:bbb.com.
  CheckURLIsBlocked(browser(), kURL_B);
  CheckViewSourceURLIsBlocked(browser(), kURL_B);

  // Block all view-source urls.
  blocklist.Append("view-source:*");
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  // Verify that blocking view-source:* blocks view-source:aaa.com but does not
  // block http://aaa.com.
  CheckViewSourceURLIsBlocked(browser(), kURL_A);
  CheckCanOpenURL(browser(), kURL_A);
}

IN_PROC_BROWSER_TEST_F(UrlBlockingPolicyTest, URLBlocklistNonStandardScheme) {
  // Checks that non-standard schemes can be blocklisted, and that the blocking
  // page mentions the URL's scheme.
  const std::string kURL = "mailto:nobody";

  // Block mailto: urls.
  base::ListValue blocklist;
  blocklist.Append("mailto:*");
  PolicyMap policies;
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  // Ensure the URL is blocked.
  CheckURLIsBlocked(browser(), kURL);

  // Ensure the blocking page mentions the scheme.
  content::WebContents* contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  std::string result =
      content::EvalJs(contents, "document.body.textContent;").ExtractString();
  EXPECT_THAT(result, testing::HasSubstr("mailto"));
}

IN_PROC_BROWSER_TEST_F(UrlBlockingPolicyTest, URLBlocklistIncognito) {
  // Checks that URLs can be blocklisted, and that exceptions can be made to
  // the blocklist.

  BrowserWindowInterface* incognito_browser =
      OpenURLOffTheRecord(browser()->GetProfile(), GURL("about:blank"));

  ASSERT_TRUE(embedded_test_server()->Start());

  const auto kURLS = std::to_array<std::string>({
      embedded_test_server()->GetURL("aaa.com", "/empty.html").spec(),
      embedded_test_server()->GetURL("bbb.com", "/empty.html").spec(),
      embedded_test_server()->GetURL("sub.bbb.com", "/empty.html").spec(),
      embedded_test_server()->GetURL("bbb.com", "/policy/blank.html").spec(),
      embedded_test_server()->GetURL("bbb.com.", "/policy/blank.html").spec(),
  });

  // Verify that "bbb.com" opens before applying the blocklist.
  CheckCanOpenURL(incognito_browser, kURLS[1]);

  // Set a blocklist.
  base::ListValue blocklist;
  blocklist.Append("bbb.com");
  PolicyMap policies;
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();
  // All bbb.com URLs are blocked, and "aaa.com" is still unblocked.
  CheckCanOpenURL(incognito_browser, kURLS[0]);
  for (size_t i = 1; i < std::size(kURLS); ++i)
    CheckURLIsBlocked(incognito_browser, kURLS[i]);

  // Allowlist some sites of bbb.com.
  base::ListValue allowlist;
  allowlist.Append("sub.bbb.com");
  allowlist.Append("bbb.com/policy");
  policies.Set(key::kURLAllowlist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(allowlist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();
  CheckURLIsBlocked(incognito_browser, kURLS[1]);
  CheckCanOpenURL(incognito_browser, kURLS[2]);
  CheckCanOpenURL(incognito_browser, kURLS[3]);
  CheckCanOpenURL(incognito_browser, kURLS[4]);
}

IN_PROC_BROWSER_TEST_F(UrlBlockingPolicyTest, URLBlocklistAndAllowlist) {
  // Regression test for http://crbug.com/40535044. Blocklisting * and
  // allowlisting an origin should work.

  ASSERT_TRUE(embedded_test_server()->Start());

  base::ListValue blocklist;
  blocklist.Append("*");
  PolicyMap policies;
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);

  base::ListValue allowlist;
  allowlist.Append("aaa.com");
  policies.Set(key::kURLAllowlist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(allowlist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();
  CheckCanOpenURL(
      browser(),
      embedded_test_server()->GetURL("aaa.com", "/empty.html").spec());
}

IN_PROC_BROWSER_TEST_F(UrlBlockingPolicyTest, URLBlocklistSubresources) {
  // Checks that an image with a blocklisted URL is loaded, but an iframe with a
  // blocklisted URL is not.

  ASSERT_TRUE(embedded_test_server()->Start());

  GURL main_url =
      embedded_test_server()->GetURL("/policy/denylist-subresources.html");
  GURL image_url = embedded_test_server()->GetURL("/policy/pixel.png");
  GURL subframe_url = embedded_test_server()->GetURL("/policy/blank.html");

  // Set a blocklist containing the image and the iframe which are used by the
  // main document.
  base::ListValue blocklist;
  blocklist.Append(image_url.spec().c_str());
  blocklist.Append(subframe_url.spec().c_str());
  PolicyMap policies;
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));
  EXPECT_EQ("success", content::EvalJs(
                           browser()->tab_strip_model()->GetActiveWebContents(),
                           "imageLoadResult"));

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));
  EXPECT_EQ("error", content::EvalJs(
                         browser()->tab_strip_model()->GetActiveWebContents(),
                         "iframeLoadResult"));
}

IN_PROC_BROWSER_TEST_F(UrlBlockingPolicyTest, URLBlocklistClientRedirect) {
  // Checks that a client side redirect to a blocklisted URL is blocked.
  ASSERT_TRUE(embedded_test_server()->Start());

  GURL redirected_url =
      embedded_test_server()->GetURL("/policy/denylist-redirect.html");
  GURL first_url = embedded_test_server()->GetURL("/client-redirect?" +
                                                  redirected_url.spec());

  // There are two navigations: one when loading client-redirect.html and
  // another when the document redirects using http-equiv="refresh".
  ui_test_utils::NavigateToURLBlockUntilNavigationsComplete(browser(),
                                                            first_url, 2);
  EXPECT_EQ(u"Redirected!",
            browser()->tab_strip_model()->GetActiveWebContents()->GetTitle());

  base::ListValue blocklist;
  blocklist.Append(redirected_url.spec().c_str());
  PolicyMap policies;
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), first_url));
  content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents());
  EXPECT_NE(u"Redirected!",
            browser()->tab_strip_model()->GetActiveWebContents()->GetTitle());
}

IN_PROC_BROWSER_TEST_F(UrlBlockingPolicyTest, URLBlocklistServerRedirect) {
  // Checks that a server side redirect to a blocklisted URL is blocked.
  ASSERT_TRUE(embedded_test_server()->Start());

  GURL redirected_url =
      embedded_test_server()->GetURL("/policy/denylist-redirect.html");
  GURL first_url = embedded_test_server()->GetURL("/server-redirect?" +
                                                  redirected_url.spec());

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), first_url));
  content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents());
  EXPECT_EQ(u"Redirected!",
            browser()->tab_strip_model()->GetActiveWebContents()->GetTitle());

  base::ListValue blocklist;
  blocklist.Append(redirected_url.spec().c_str());
  PolicyMap policies;
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), first_url));
  content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents());
  EXPECT_NE(u"Redirected!",
            browser()->tab_strip_model()->GetActiveWebContents()->GetTitle());
}

IN_PROC_BROWSER_TEST_F(UrlBlockingPolicyTest, FileURLBlocklist) {
  // Check that FileURLs can be blocklisted and DisabledSchemes works together
  // with URLblocklisting and URLallowlisting.

  base::FilePath test_path;
  GetTestDataDirectory(&test_path);
  const std::string base_path = "file://" + test_path.AsUTF8Unsafe() + "/";
  const std::string folder_path = base_path + "apptest/";
  const std::string file_path1 = base_path + "title1.html";
  const std::string file_path2 = folder_path + "basic.html";

  CheckCanOpenURL(browser(), file_path1);
  CheckCanOpenURL(browser(), file_path2);

  // Set a blocklist for all the files.
  base::ListValue blocklist;
  blocklist.Append("file://*");
  PolicyMap policies;
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  CheckURLIsBlocked(browser(), file_path1);
  CheckURLIsBlocked(browser(), file_path2);

  // Replace the URLblocklist with disabling the file scheme.
  blocklist.EraseValue(base::Value("file://*"));
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  PrefService* prefs = browser()->GetProfile()->GetPrefs();

  EXPECT_FALSE(
      prefs->GetList(policy_prefs::kUrlBlocklist).contains("file://*"));

  base::ListValue disabledscheme;
  disabledscheme.Append("file");
  policies.Set(key::kDisabledSchemes, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(disabledscheme.Clone()),
               nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  EXPECT_TRUE(prefs->GetList(policy_prefs::kUrlBlocklist).contains("file://*"));

  // Allowlist one folder and blocklist an another just inside.
  base::ListValue allowlist;
  allowlist.Append(base_path);
  policies.Set(key::kURLAllowlist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(allowlist.Clone()), nullptr);
  blocklist.Append(folder_path);
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  CheckCanOpenURL(browser(), file_path1);
  CheckURLIsBlocked(browser(), file_path2);
}

// Tests that javascript-links are handled properly according to blocklist
// settings, bug crbug.com/40605746.
IN_PROC_BROWSER_TEST_F(UrlBlockingPolicyTest, JavascriptBlocklistable) {
  embedded_test_server()->RegisterRequestHandler(
      base::BindRepeating(&JSIncrementerPageHandler));
  ASSERT_TRUE(embedded_test_server()->Start());
  content::WebContents* contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL("/test.html")));

  EXPECT_EQ(JSIncrementerFetch(contents), 1);

  // Without blocklist policy value is incremented properly.
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), GURL("javascript:increment()"),
      WindowOpenDisposition::CURRENT_TAB, ui_test_utils::BROWSER_TEST_NO_WAIT);

  EXPECT_EQ(JSIncrementerFetch(contents), 2);

  // Create and apply a policy.
  base::ListValue blocklist;
  blocklist.Append("javascript://*");
  PolicyMap policies;
  policies.Set(key::kURLBlocklist, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
               POLICY_SOURCE_CLOUD, base::Value(blocklist.Clone()), nullptr);
  UpdateProviderPolicy(policies);
  FlushBlocklistPolicy();

  // After applying policy javascript URLs don't work any more, value leaves
  // unchanged.
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), GURL("javascript:increment()"),
      WindowOpenDisposition::CURRENT_TAB, ui_test_utils::BROWSER_TEST_NO_WAIT);
  EXPECT_EQ(JSIncrementerFetch(contents), 2);

  // But in-page links still work even if they are javascript-links.
  EXPECT_TRUE(
      content::ExecJs(contents, "document.getElementById('link').click();"));
  EXPECT_EQ(JSIncrementerFetch(contents), 3);
}

}  // namespace policy
