// Copyright 2014 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/extension_user_script_loader.h"

#include <stddef.h>

#include <memory>
#include <optional>
#include <set>
#include <string>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/location.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/values_test_util.h"
#include "chrome/browser/extensions/chrome_extension_system_factory.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/component_extension_resource_manager.h"
#include "extensions/browser/content_verifier/content_verifier.h"
#include "extensions/browser/extension_pref_value_map.h"
#include "extensions/browser/extension_pref_value_map_factory.h"
#include "extensions/browser/test_extensions_browser_client.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_builder.h"
#include "extensions/grit/extensions_browser_resources.h"
#include "testing/gtest/include/gtest/gtest.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using extensions::URLPatternSet;

namespace {

static void AddPattern(URLPatternSet* extent, const std::string& pattern) {
  int schemes = URLPattern::SCHEME_ALL;
  extent->AddPattern(URLPattern(schemes, pattern));
}
}  // namespace

namespace extensions {

// Test bringing up a script loader on a specific directory, putting a script
// in there, etc.
class ExtensionUserScriptLoaderTest : public testing::Test {
 public:
  ExtensionUserScriptLoaderTest() = default;

  ExtensionUserScriptLoaderTest(const ExtensionUserScriptLoaderTest&) = delete;
  ExtensionUserScriptLoaderTest& operator=(
      const ExtensionUserScriptLoaderTest&) = delete;

  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
  }

  // Directory containing user scripts.
  base::ScopedTempDir temp_dir_;

 private:
  content::BrowserTaskEnvironment task_environment_;
};

// Test that a callback passed in will get called once scripts are loaded.
TEST_F(ExtensionUserScriptLoaderTest, NoScriptsWithCallbackAfterLoad) {
  TestingProfile profile;
  scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
  ExtensionUserScriptLoader loader(&profile, *extension,
                                   /*state_store=*/nullptr,
                                   /*content_verifier=*/nullptr);
  base::RunLoop run_loop;
  auto on_load_complete = [&run_loop](UserScriptLoader* loader,
                                      const std::optional<std::string>& error) {
    EXPECT_FALSE(error.has_value()) << *error;
    run_loop.Quit();
  };

  loader.StartLoadForTesting(base::BindLambdaForTesting(on_load_complete));
  run_loop.Run();
}

// Verifies that adding an empty set of scripts will trigger a callback
// immediately but will not trigger a load.
TEST_F(ExtensionUserScriptLoaderTest, NoScriptsAddedWithCallback) {
  TestingProfile profile;
  scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
  ExtensionUserScriptLoader loader(&profile, *extension,
                                   /*state_store=*/nullptr,
                                   /*content_verifier=*/nullptr);

  // Use a flag instead of a RunLoop to verify that the callback was called
  // synchronously.
  bool callback_called = false;
  auto callback = [&callback_called](UserScriptLoader* loader,
                                     const std::optional<std::string>& error) {
    // Check that there is at least an error message.
    EXPECT_TRUE(error.has_value());
    EXPECT_THAT(*error, testing::HasSubstr("No changes to loaded scripts"));
    callback_called = true;
  };

  loader.AddScripts({}, base::BindLambdaForTesting(callback));
  EXPECT_TRUE(callback_called);
}

// Test that all callbacks will be called when a load completes and no other
// load is queued.
TEST_F(ExtensionUserScriptLoaderTest, QueuedLoadWithCallback) {
  TestingProfile profile;
  scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
  ExtensionUserScriptLoader loader(&profile, *extension,
                                   /*state_store=*/nullptr,
                                   /*content_verifier=*/nullptr);
  base::RunLoop run_loop;

  // Record if one callback has already been called. The test succeeds if two
  // callbacks are called.
  bool first_callback_fired = false;

  // Creates a callback which:
  // 1) Checks that the loader has completed its initial load.
  // 2) Sets |first_callback_fired| to true if no callback has been called yet,
  // otherwise completes the test.
  auto on_load_complete = [&run_loop, &first_callback_fired](
                              UserScriptLoader* loader,
                              const std::optional<std::string>& error) {
    EXPECT_FALSE(error.has_value()) << *error;
    EXPECT_TRUE(loader->initial_load_complete());
    if (first_callback_fired)
      run_loop.Quit();
    else
      first_callback_fired = true;
  };

  loader.StartLoadForTesting(base::BindLambdaForTesting(on_load_complete));

  // The next load request should be queued, but both `on_load_complete`
  // callbacks should be released at the same time as the queued load will merge
  // with the current load.
  loader.StartLoadForTesting(base::BindLambdaForTesting(on_load_complete));
  run_loop.Run();
}

TEST_F(ExtensionUserScriptLoaderTest, Parse1) {
  const std::string text(
    "// This is my awesome script\n"
    "// It does stuff.\n"
    "// ==UserScript==   trailing garbage\n"
    "// @name foobar script\n"
    "// @namespace http://www.google.com/\n"
    "// @include *mail.google.com*\n"
    "// \n"
    "// @othergarbage\n"
    "// @include *mail.yahoo.com*\r\n"
    "// @include  \t *mail.msn.com*\n" // extra spaces after "@include" OK
    "//@include not-recognized\n" // must have one space after "//"
    "// ==/UserScript==  trailing garbage\n"
    "\n"
    "\n"
    "alert('hoo!');\n");

  UserScript script;
  EXPECT_TRUE(ExtensionUserScriptLoader::ParseMetadataHeader(text, &script));
  ASSERT_EQ(3U, script.globs().size());
  EXPECT_EQ("*mail.google.com*", script.globs()[0]);
  EXPECT_EQ("*mail.yahoo.com*", script.globs()[1]);
  EXPECT_EQ("*mail.msn.com*", script.globs()[2]);
}

TEST_F(ExtensionUserScriptLoaderTest, Parse2) {
  const std::string text("default to @include *");

  UserScript script;
  EXPECT_TRUE(ExtensionUserScriptLoader::ParseMetadataHeader(text, &script));
  ASSERT_EQ(1U, script.globs().size());
  EXPECT_EQ("*", script.globs()[0]);
}

TEST_F(ExtensionUserScriptLoaderTest, Parse3) {
  const std::string text(
    "// ==UserScript==\n"
    "// @include *foo*\n"
    "// ==/UserScript=="); // no trailing newline

  UserScript script;
  ExtensionUserScriptLoader::ParseMetadataHeader(text, &script);
  ASSERT_EQ(1U, script.globs().size());
  EXPECT_EQ("*foo*", script.globs()[0]);
}

TEST_F(ExtensionUserScriptLoaderTest, Parse4) {
  const std::string text(
    "// ==UserScript==\n"
    "// @match http://*.mail.google.com/*\n"
    "// @match  \t http://mail.yahoo.com/*\n"
    "// ==/UserScript==\n");

  URLPatternSet expected_patterns;
  AddPattern(&expected_patterns, "http://*.mail.google.com/*");
  AddPattern(&expected_patterns, "http://mail.yahoo.com/*");

  UserScript script;
  EXPECT_TRUE(ExtensionUserScriptLoader::ParseMetadataHeader(text, &script));
  EXPECT_EQ(0U, script.globs().size());
  EXPECT_EQ(expected_patterns, script.url_patterns());
}

TEST_F(ExtensionUserScriptLoaderTest, Parse5) {
  const std::string text(
    "// ==UserScript==\n"
    "// @match http://*mail.google.com/*\n"
    "// ==/UserScript==\n");

  // Invalid @match value.
  UserScript script;
  EXPECT_FALSE(ExtensionUserScriptLoader::ParseMetadataHeader(text, &script));
}

TEST_F(ExtensionUserScriptLoaderTest, Parse6) {
  const std::string text(
    "// ==UserScript==\n"
    "// @include http://*.mail.google.com/*\n"
    "// @match  \t http://mail.yahoo.com/*\n"
    "// ==/UserScript==\n");

  // Allowed to match @include and @match.
  UserScript script;
  EXPECT_TRUE(ExtensionUserScriptLoader::ParseMetadataHeader(text, &script));
}

TEST_F(ExtensionUserScriptLoaderTest, Parse7) {
  // Greasemonkey allows there to be any leading text before the comment marker.
  const std::string text(
    "// ==UserScript==\n"
    "adsasdfasf// @name hello\n"
    "  // @description\twiggity woo\n"
    "\t// @match  \t http://mail.yahoo.com/*\n"
    "// ==/UserScript==\n");

  UserScript script;
  EXPECT_TRUE(ExtensionUserScriptLoader::ParseMetadataHeader(text, &script));
  ASSERT_EQ("hello", script.name());
  ASSERT_EQ("wiggity woo", script.description());
  ASSERT_EQ(1U, script.url_patterns().patterns().size());
  EXPECT_EQ("http://mail.yahoo.com/*",
            script.url_patterns().begin()->GetAsString());
}

TEST_F(ExtensionUserScriptLoaderTest, Parse8) {
  const std::string text(
    "// ==UserScript==\n"
    "// @name myscript\n"
    "// @match http://www.google.com/*\n"
    "// @exclude_match http://www.google.com/foo*\n"
    "// ==/UserScript==\n");

  UserScript script;
  EXPECT_TRUE(ExtensionUserScriptLoader::ParseMetadataHeader(text, &script));
  ASSERT_EQ("myscript", script.name());
  ASSERT_EQ(1U, script.url_patterns().patterns().size());
  EXPECT_EQ("http://www.google.com/*",
            script.url_patterns().begin()->GetAsString());
  ASSERT_EQ(1U, script.exclude_url_patterns().patterns().size());
  EXPECT_EQ("http://www.google.com/foo*",
            script.exclude_url_patterns().begin()->GetAsString());
}

TEST_F(ExtensionUserScriptLoaderTest, SkipBOMAtTheBeginning) {
  base::FilePath path = temp_dir_.GetPath().AppendASCII("script.user.js");
  const std::string content("\xEF\xBB\xBF alert('hello');");
  ASSERT_TRUE(base::WriteFile(path, content));

  auto user_script = std::make_unique<UserScript>();
  user_script->set_id("_mc_generated");
  user_script->js_scripts().push_back(UserScript::Content::CreateFile(
      temp_dir_.GetPath(), path.BaseName(), GURL()));

  UserScriptList user_scripts;
  user_scripts.push_back(std::move(user_script));

  TestingProfile profile;
  base::HistogramTester histogram_tester;
  scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
  ExtensionUserScriptLoader loader(&profile, *extension,
                                   /*state_store=*/nullptr,
                                   /*content_verifier=*/nullptr);
  user_scripts = loader.LoadScriptsForTest(std::move(user_scripts));

  EXPECT_EQ(content.substr(3),
            std::string(user_scripts[0]->js_scripts()[0]->GetContent()));
  // Verify that an entry has been recorded for the appropriate histograms and
  // that the length of the script is 0 kb.
  histogram_tester.ExpectUniqueSample(
      "Extensions.ContentScripts.ContentScriptLength", 0, 1);
  histogram_tester.ExpectUniqueSample(
      "Extensions.ContentScripts.ManifestContentScriptsLengthPerLoad", 0, 1);
  histogram_tester.ExpectTotalCount(
      "Extensions.ContentScripts.DynamicContentScriptsLengthPerLoad", 0);
}

TEST_F(ExtensionUserScriptLoaderTest, LeaveBOMNotAtTheBeginning) {
  base::FilePath path = temp_dir_.GetPath().AppendASCII("script.user.js");
  const std::string content("alert('here's a BOOM: \xEF\xBB\xBF');");
  ASSERT_TRUE(base::WriteFile(path, content));

  auto user_script = std::make_unique<UserScript>();
  user_script->set_id("_mc_test");
  user_script->js_scripts().push_back(UserScript::Content::CreateFile(
      temp_dir_.GetPath(), path.BaseName(), GURL()));

  UserScriptList user_scripts;
  user_scripts.push_back(std::move(user_script));

  TestingProfile profile;
  base::HistogramTester histogram_tester;
  scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
  ExtensionUserScriptLoader loader(&profile, *extension,
                                   /*state_store=*/nullptr,
                                   /*content_verifier=*/nullptr);
  user_scripts = loader.LoadScriptsForTest(std::move(user_scripts));

  EXPECT_EQ(content,
            std::string(user_scripts[0]->js_scripts()[0]->GetContent()));
  // Verify that an entry has been recorded for the appropriate histograms and
  // that the length of the script is 0 kb.
  histogram_tester.ExpectUniqueSample(
      "Extensions.ContentScripts.ContentScriptLength", 0, 1);
  histogram_tester.ExpectUniqueSample(
      "Extensions.ContentScripts.ManifestContentScriptsLengthPerLoad", 0, 1);
  histogram_tester.ExpectTotalCount(
      "Extensions.ContentScripts.DynamicContentScriptsLengthPerLoad", 0);
}

TEST_F(ExtensionUserScriptLoaderTest, ComponentExtensionContentScriptIsLoaded) {
  class MockResourceManager : public ComponentExtensionResourceManager {
   public:
    bool IsComponentExtensionResource(const base::FilePath& extension_path,
                                      const base::FilePath& resource_path,
                                      int* resource_id) const override {
      if (extension_path.BaseName().value() == FILE_PATH_LITERAL("pdf") &&
          resource_path.value() == FILE_PATH_LITERAL("main.js")) {
        *resource_id = IDR_EXTENSION_DEFAULT_ICON;
        return true;
      }
      return false;
    }
    const ui::TemplateReplacements* GetTemplateReplacementsForExtension(
        const ExtensionId& extension_id,
        const content::BrowserContext* context) const override {
      return nullptr;
    }
    bool IsDynamicComponentExtensionResource(
        const ExtensionId& extension_id,
        const std::string& path,
        const content::BrowserContext* context) const override {
      return false;
    }
    std::string GetDynamicResourceContent(
        const ExtensionId& extension_id,
        const std::string& path,
        const content::BrowserContext* context) const override {
      return "";
    }
    [[nodiscard]] base::ScopedClosureRunner RegisterTemplateDataProvider(
        const ExtensionId& extension_id,
        const content::BrowserContext* context,
        TemplateDataProvider provider) const override {
      return base::ScopedClosureRunner();
    }
  };

  class TestClient : public TestExtensionsBrowserClient {
   public:
    TestClient() = default;
    const ComponentExtensionResourceManager*
    GetComponentExtensionResourceManager() override {
      return &mock_manager_;
    }
    content::BrowserContext* GetOriginalContext(
        content::BrowserContext* context) override {
      return context;
    }
    bool IsValidContext(void* context) override { return context != nullptr; }

   private:
    MockResourceManager mock_manager_;
  };

  TestClient client;
  client.set_extension_system_factory(
      ChromeExtensionSystemFactory::GetInstance());
  ExtensionsBrowserClient* old_client = ExtensionsBrowserClient::Get();
  ExtensionsBrowserClient::Set(&client);

  base::FilePath resources_dir;
  ASSERT_TRUE(base::PathService::Get(chrome::DIR_RESOURCES, &resources_dir));

  const base::FilePath extension_path = resources_dir.AppendASCII("pdf");
  const base::FilePath resource_path(FILE_PATH_LITERAL("main.js"));

  auto user_script = std::make_unique<UserScript>();
  user_script->set_id("_dc_test");
  user_script->js_scripts().push_back(
      UserScript::Content::CreateFile(extension_path, resource_path, GURL()));

  UserScriptList user_scripts;
  user_scripts.push_back(std::move(user_script));

  TestingProfile::Builder profile_builder;
  profile_builder.AddTestingFactory(
      ChromeExtensionSystemFactory::GetInstance(),
      base::BindRepeating(&TestExtensionSystem::Build));
  profile_builder.AddTestingFactory(
      ExtensionPrefValueMapFactory::GetInstance(),
      base::BindRepeating([](content::BrowserContext* context)
                              -> std::unique_ptr<KeyedService> {
        return std::make_unique<ExtensionPrefValueMap>();
      }));
  std::unique_ptr<TestingProfile> profile = profile_builder.Build();

  base::HistogramTester histogram_tester;
  scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
  ExtensionUserScriptLoader loader(profile.get(), *extension,
                                   /*state_store=*/nullptr,
                                   /*content_verifier=*/nullptr);
  user_scripts = loader.LoadScriptsForTest(std::move(user_scripts));

  EXPECT_FALSE(user_scripts[0]->js_scripts()[0]->GetContent().empty());
  // Verify that an entry has been recorded for the appropriate histograms and
  // that the length of the script is 0 kb.
  histogram_tester.ExpectTotalCount(
      "Extensions.ContentScripts.ContentScriptLength", 1);
  histogram_tester.ExpectTotalCount(
      "Extensions.ContentScripts.ManifestContentScriptsLengthPerLoad", 0);
  histogram_tester.ExpectTotalCount(
      "Extensions.ContentScripts.DynamicContentScriptsLengthPerLoad", 1);

  ExtensionsBrowserClient::Set(old_client);
}

TEST_F(ExtensionUserScriptLoaderTest, RecordScriptLengthUmas) {
  base::FilePath a_script_path = temp_dir_.GetPath().AppendASCII("a.script.js");
  const std::string a_string(3200, 'a');
  ASSERT_TRUE(base::WriteFile(a_script_path, a_string));

  base::FilePath b_script_path = temp_dir_.GetPath().AppendASCII("b.script.js");
  const std::string b_string(2200, 'b');
  ASSERT_TRUE(base::WriteFile(b_script_path, b_string));

  base::FilePath c_script_path = temp_dir_.GetPath().AppendASCII("c.script.js");
  const std::string c_string(1200, 'c');
  ASSERT_TRUE(base::WriteFile(c_script_path, c_string));

  // Create a dynamic user script which specifies a 3kb and 2kb file.
  auto user_script_1 = std::make_unique<UserScript>();
  user_script_1->set_id("_dc_dynamic");
  user_script_1->js_scripts().push_back(UserScript::Content::CreateFile(
      temp_dir_.GetPath(), a_script_path.BaseName(), GURL()));
  user_script_1->js_scripts().push_back(UserScript::Content::CreateFile(
      temp_dir_.GetPath(), b_script_path.BaseName(), GURL()));

  // Create a manifest user script which specifies a 1kb file.
  auto user_script_2 = std::make_unique<UserScript>();
  user_script_2->set_id("_mc_generated_manifest");
  user_script_2->js_scripts().push_back(UserScript::Content::CreateFile(
      temp_dir_.GetPath(), c_script_path.BaseName(), GURL()));

  UserScriptList user_scripts;
  user_scripts.push_back(std::move(user_script_1));
  user_scripts.push_back(std::move(user_script_2));

  TestingProfile profile;
  base::HistogramTester histogram_tester;
  scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
  ExtensionUserScriptLoader loader(&profile, *extension,
                                   /*state_store=*/nullptr,
                                   /*content_verifier=*/nullptr);
  user_scripts = loader.LoadScriptsForTest(std::move(user_scripts));

  // Verify that an entry has been recorded for the appropriate histograms.
  histogram_tester.ExpectBucketCount(
      "Extensions.ContentScripts.ContentScriptLength", 1, 1);
  histogram_tester.ExpectBucketCount(
      "Extensions.ContentScripts.ContentScriptLength", 2, 1);
  histogram_tester.ExpectBucketCount(
      "Extensions.ContentScripts.ContentScriptLength", 3, 1);

  histogram_tester.ExpectUniqueSample(
      "Extensions.ContentScripts.ManifestContentScriptsLengthPerLoad", 1, 1);
  histogram_tester.ExpectUniqueSample(
      "Extensions.ContentScripts.DynamicContentScriptsLengthPerLoad", 5, 1);
}

}  // namespace extensions
