// Copyright 2018 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/common/manifest_handlers/permissions_parser.h"

#include <algorithm>

#include "base/strings/stringprintf.h"
#include "base/test/values_test_util.h"
#include "chrome/common/extensions/manifest_tests/chrome_manifest_test.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/mojom/api_permission_id.mojom.h"
#include "extensions/common/permissions/permissions_data.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

using PermissionsParserTest = ChromeManifestTest;

TEST_F(PermissionsParserTest, RemoveOverlappingAPIPermissions) {
  scoped_refptr<Extension> extension(LoadAndExpectWarning(
      "permissions_overlapping_api_permissions.json",
      ErrorUtils::FormatErrorMessage(
          manifest_errors::kPermissionMarkedOptionalAndRequired, "tabs")));

  std::set<std::string> required_api_names =
      PermissionsParser::GetRequiredPermissions(extension.get())
          .GetAPIsAsStrings();

  std::set<std::string> optional_api_names =
      PermissionsParser::GetOptionalPermissions(extension.get())
          .GetAPIsAsStrings();

  // Check that required api permissions have not changed while "tabs" is
  // removed from optional permissions as it is specified as required.
  EXPECT_THAT(required_api_names,
              testing::UnorderedElementsAre("tabs", "storage"));
  EXPECT_THAT(optional_api_names, testing::UnorderedElementsAre("bookmarks"));
}

TEST_F(PermissionsParserTest, RemoveOverlappingHostPermissions) {
  scoped_refptr<Extension> extension(LoadAndExpectWarning(
      "permissions_overlapping_host_permissions.json",
      ErrorUtils::FormatErrorMessage(
          manifest_errors::kPermissionMarkedOptionalAndRequired,
          "https://google.com/*")));

  const URLPatternSet& required_hosts =
      PermissionsParser::GetRequiredPermissions(extension.get())
          .explicit_hosts();

  const URLPatternSet& optional_hosts =
      PermissionsParser::GetOptionalPermissions(extension.get())
          .explicit_hosts();

  // Check that required hosts have not changed at all while
  // "https://google.com/maps" is removed from optional hosts as it is a strict
  // subset of hosts specified as required.
  EXPECT_THAT(required_hosts.ToStringVector(),
              testing::UnorderedElementsAre("https://example.com/*",
                                            "https://*.google.com/*"));
  EXPECT_THAT(optional_hosts.ToStringVector(),
              testing::UnorderedElementsAre("*://chromium.org/*"));
}

// Same as the above test, except host permissions are specified in
// `host_permissions` and `optional_host_permissions` as the extension is
// running Manifest V3.
TEST_F(PermissionsParserTest, RemoveOverlappingHostPermissions_ManifestV3) {
  scoped_refptr<Extension> extension(LoadAndExpectWarning(
      "permissions_overlapping_host_permissions_mv3.json",
      ErrorUtils::FormatErrorMessage(
          manifest_errors::kPermissionMarkedOptionalAndRequired,
          "https://google.com/*")));

  const URLPatternSet& required_hosts =
      PermissionsParser::GetRequiredPermissions(extension.get())
          .explicit_hosts();

  const URLPatternSet& optional_hosts =
      PermissionsParser::GetOptionalPermissions(extension.get())
          .explicit_hosts();

  // Check that required hosts have not changed at all while
  // "https://google.com/maps" is removed from optional hosts as it is a strict
  // subset of hosts specified as required.
  EXPECT_THAT(required_hosts.ToStringVector(),
              testing::UnorderedElementsAre("https://example.com/*",
                                            "https://*.google.com/*"));
  EXPECT_THAT(optional_hosts.ToStringVector(),
              testing::UnorderedElementsAre("*://chromium.org/*"));
}

TEST_F(PermissionsParserTest, RequiredHostPermissionsAllURLs) {
  scoped_refptr<Extension> extension(LoadAndExpectWarning(
      "permissions_all_urls_host_permissions.json",
      ErrorUtils::FormatErrorMessage(
          manifest_errors::kPermissionMarkedOptionalAndRequired,
          "http://*/*")));

  const URLPatternSet& optional_hosts =
      PermissionsParser::GetOptionalPermissions(extension.get())
          .explicit_hosts();

  // Since we specified <all_urls> as a required host permission,
  // there should be no optional host permissions.
  EXPECT_THAT(optional_hosts.ToStringVector(), testing::IsEmpty());
}

TEST_F(PermissionsParserTest, OptionalHostPermissionsAllURLs) {
  scoped_refptr<Extension> extension(
      LoadAndExpectSuccess("permissions_optional_all_urls_permissions.json"));

  const URLPatternSet& required_hosts =
      PermissionsParser::GetRequiredPermissions(extension.get())
          .explicit_hosts();

  const URLPatternSet& optional_hosts =
      PermissionsParser::GetOptionalPermissions(extension.get())
          .explicit_hosts();

  // This time, required permissions are a subset of optional permissions
  // so we make sure that permissions remain the same
  // as what's specified in the manifest.
  EXPECT_THAT(required_hosts.ToStringVector(),
              testing::UnorderedElementsAre("https://*.google.com/*"));

  EXPECT_THAT(optional_hosts.ToStringVector(),
              testing::UnorderedElementsAre("*://*/*"));
}

TEST_F(PermissionsParserTest, OptionalHostPermissionsInvalidScheme) {
  std::vector<std::string> expected_warnings;
  expected_warnings.push_back(ErrorUtils::FormatErrorMessage(
      manifest_errors::kInvalidPermissionScheme,
      manifest_keys::kOptionalPermissions, "chrome://extensions/"));

  scoped_refptr<Extension> extension(LoadAndExpectWarnings(
      "optional_permissions_invalid_scheme.json", expected_warnings));
}

TEST_F(PermissionsParserTest, HostPermissionsKey) {
  std::vector<std::string> expected_warnings;
  expected_warnings.push_back(ErrorUtils::FormatErrorMessage(
      manifest_errors::kPermissionUnknown, "https://google.com/*"));
  expected_warnings.push_back(ErrorUtils::FormatErrorMessage(
      manifest_errors::kPermissionUnknown, "http://chromium.org/*"));

  scoped_refptr<Extension> extension(
      LoadAndExpectWarnings("host_permissions_key.json", expected_warnings));

  // Expect that the host specified in |host_permissions| is parsed.
  const URLPatternSet& required_hosts =
      PermissionsParser::GetRequiredPermissions(extension.get())
          .explicit_hosts();

  EXPECT_THAT(required_hosts.ToStringVector(),
              testing::UnorderedElementsAre("https://example.com/*"));

  // Expect that the host specified in |optional_host_permissions| is parsed.
  const URLPatternSet& optional_hosts =
      PermissionsParser::GetOptionalPermissions(extension.get())
          .explicit_hosts();

  EXPECT_THAT(optional_hosts.ToStringVector(),
              testing::UnorderedElementsAre("https://optional.com/*"));
}

TEST_F(PermissionsParserTest, HostPermissionsKeyInvalidHosts) {
  std::vector<std::string> expected_warnings;
  expected_warnings.push_back(ErrorUtils::FormatErrorMessage(
      manifest_errors::kPatternMalformed, "malformed_host"));
  expected_warnings.push_back(ErrorUtils::FormatErrorMessage(
      manifest_errors::kPatternMalformed, "optional_malformed_host"));

  scoped_refptr<Extension> extension(LoadAndExpectWarnings(
      "host_permissions_key_invalid_hosts.json", expected_warnings));
}

TEST_F(PermissionsParserTest, HostPermissionsKeyInvalidScheme) {
  std::vector<std::string> expected_warnings;
  expected_warnings.push_back(ErrorUtils::FormatErrorMessage(
      manifest_errors::kInvalidPermissionScheme,
      manifest_keys::kHostPermissions, "chrome://extensions/"));
  expected_warnings.push_back(ErrorUtils::FormatErrorMessage(
      manifest_errors::kInvalidPermissionScheme,
      manifest_keys::kOptionalHostPermissions, "chrome://settings/"));

  scoped_refptr<Extension> extension(LoadAndExpectWarnings(
      "host_permissions_key_invalid_scheme.json", expected_warnings));
}

// Tests that listing a permissions as optional when that permission cannot be
// optional produces a warning and doesn't add the permission.
TEST_F(PermissionsParserTest, UnsupportedOptionalPermissionWarning) {
  scoped_refptr<Extension> extension(LoadAndExpectWarning(
      "unsupported_optional_api_permission.json",
      ErrorUtils::FormatErrorMessage(
          manifest_errors::kPermissionCannotBeOptional, "geolocation")));

  // Check that the geolocation was not included in the optional permissions as
  // it is not allowed to be optional.
  std::set<std::string> optional_api_names =
      PermissionsParser::GetOptionalPermissions(extension.get())
          .GetAPIsAsStrings();
  EXPECT_THAT(optional_api_names, testing::UnorderedElementsAre("tabs"));
}

// Test that chrome://favicon is a supported permission in MV2, but not MV3.
TEST_F(PermissionsParserTest, ChromeFavicon) {
  auto get_manifest_data = [](int manifest_version, const char* permission) {
    constexpr char kManifestStub[] =
        R"({
             "name": "Test",
             "version": "0.1",
             "manifest_version": %d,
             "%s": ["%s"]
           })";

    const char* permissions_key = manifest_version > 2
                                      ? manifest_keys::kHostPermissions
                                      : manifest_keys::kPermissions;
    base::Value manifest_value = base::test::ParseJson(base::StringPrintf(
        kManifestStub, manifest_version, permissions_key, permission));
    EXPECT_TRUE(manifest_value.is_dict());
    return ManifestData(std::move(manifest_value).TakeDict(), permission);
  };

  static constexpr char kFaviconPattern[] = "chrome://favicon/*";
  // <all_urls> implicitly includes chrome://favicon, if it's supported.
  constexpr char kAllUrls[] = "<all_urls>";

  auto has_favicon_access = [](const Extension& extension) {
    const GURL favicon_url("chrome://favicon");
    return extension.permissions_data()->HasHostPermission(favicon_url);
  };

  auto has_install_warning = [](const Extension& extension) {
    const char* permissions_key = extension.manifest_version() > 2
                                      ? manifest_keys::kHostPermissions
                                      : manifest_keys::kPermissions;

    InstallWarning expected_warning(ErrorUtils::FormatErrorMessage(
        manifest_errors::kInvalidPermissionScheme, permissions_key,
        kFaviconPattern));
    return std::ranges::contains(extension.install_warnings(),
                                 expected_warning);
  };

  {
    scoped_refptr<const Extension> extension =
        LoadAndExpectSuccess(get_manifest_data(2, kFaviconPattern));
    ASSERT_TRUE(extension);
    EXPECT_TRUE(has_favicon_access(*extension));
    EXPECT_FALSE(has_install_warning(*extension));
  }

  {
    scoped_refptr<const Extension> extension =
        LoadAndExpectSuccess(get_manifest_data(2, kAllUrls));
    ASSERT_TRUE(extension);
    EXPECT_TRUE(has_favicon_access(*extension));
    EXPECT_FALSE(has_install_warning(*extension));
  }
  {
    scoped_refptr<const Extension> extension =
        LoadAndExpectSuccess(get_manifest_data(3, kFaviconPattern));
    ASSERT_TRUE(extension);
    EXPECT_FALSE(has_favicon_access(*extension));
    // Since chrome://favicon is not a valid permission in MV3, we expect a
    // warning to be thrown.
    EXPECT_TRUE(has_install_warning(*extension));
  }
  {
    scoped_refptr<const Extension> extension =
        LoadAndExpectSuccess(get_manifest_data(3, kAllUrls));
    ASSERT_TRUE(extension);
    EXPECT_FALSE(has_favicon_access(*extension));
    // NOTE: We don't expect an install warning here, because the <all_urls>
    // permission is still supported. It just doesn't grant favicon access.
    EXPECT_FALSE(has_install_warning(*extension));
  }
}

TEST_F(PermissionsParserTest, InternalPermissionsAreNotAllowedInTheManifest) {
  static constexpr char kManifest[] =
      R"({
           "name": "My Extension",
           "manifest_version": 3,
           "version": "0.1",
           "permissions": ["searchProvider"]
         })";
  scoped_refptr<const Extension> extension =
      LoadAndExpectWarning(ManifestData::FromJSON(kManifest),
                           "Permission 'searchProvider' is unknown.");

  ASSERT_TRUE(extension);
  EXPECT_FALSE(extension->permissions_data()->HasAPIPermission(
      mojom::APIPermissionID::kSearchProvider));
}

}  // namespace extensions
