blob: 61c7a291521d345193d9f0bab12f829106fd8849 [file] [log] [blame]
// 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 <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_piece.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/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/content_verifier.h"
#include "extensions/common/extension_builder.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
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,
/*listen_for_extension_system_loaded=*/true,
/*content_verifier=*/nullptr);
base::RunLoop run_loop;
auto on_load_complete = [&run_loop](
UserScriptLoader* loader,
const absl::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,
/*listen_for_extension_system_loaded=*/true,
/*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 absl::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(std::make_unique<UserScriptList>(),
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,
/*listen_for_extension_system_loaded=*/true,
/*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 absl::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');");
size_t written = base::WriteFile(path, content.c_str(), content.size());
ASSERT_EQ(written, content.size());
auto user_script = std::make_unique<UserScript>();
user_script->set_id("_generated");
user_script->js_scripts().push_back(std::make_unique<UserScript::File>(
temp_dir_.GetPath(), path.BaseName(), GURL()));
auto user_scripts = std::make_unique<UserScriptList>();
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,
/*listen_for_extension_system_loaded=*/true,
/*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');");
size_t written = base::WriteFile(path, content.c_str(), content.size());
ASSERT_EQ(written, content.size());
auto user_script = std::make_unique<UserScript>();
user_script->set_id("test");
user_script->js_scripts().push_back(std::make_unique<UserScript::File>(
temp_dir_.GetPath(), path.BaseName(), GURL()));
auto user_scripts = std::make_unique<UserScriptList>();
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,
/*listen_for_extension_system_loaded=*/true,
/*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.ExpectTotalCount(
"Extensions.ContentScripts.ManifestContentScriptsLengthPerLoad", 0);
histogram_tester.ExpectUniqueSample(
"Extensions.ContentScripts.DynamicContentScriptsLengthPerLoad", 0, 1);
}
TEST_F(ExtensionUserScriptLoaderTest, ComponentExtensionContentScriptIsLoaded) {
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("test");
user_script->js_scripts().push_back(std::make_unique<UserScript::File>(
extension_path, resource_path, GURL()));
auto user_scripts = std::make_unique<UserScriptList>();
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,
/*listen_for_extension_system_loaded=*/true,
/*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);
}
TEST_F(ExtensionUserScriptLoaderTest, RecordScriptLengthUmas) {
base::FilePath a_script_path = temp_dir_.GetPath().AppendASCII("a.script.js");
const std::string a_string(3200, 'a');
size_t written =
base::WriteFile(a_script_path, a_string.c_str(), a_string.size());
ASSERT_EQ(written, a_string.size());
base::FilePath b_script_path = temp_dir_.GetPath().AppendASCII("b.script.js");
const std::string b_string(2200, 'b');
written = base::WriteFile(b_script_path, b_string.c_str(), b_string.size());
ASSERT_EQ(written, b_string.size());
base::FilePath c_script_path = temp_dir_.GetPath().AppendASCII("c.script.js");
const std::string c_string(1200, 'c');
written = base::WriteFile(c_script_path, c_string.c_str(), c_string.size());
ASSERT_EQ(written, c_string.size());
// 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("dynamic");
user_script_1->js_scripts().push_back(std::make_unique<UserScript::File>(
temp_dir_.GetPath(), a_script_path.BaseName(), GURL()));
user_script_1->js_scripts().push_back(std::make_unique<UserScript::File>(
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("_generated_manifest");
user_script_2->js_scripts().push_back(std::make_unique<UserScript::File>(
temp_dir_.GetPath(), c_script_path.BaseName(), GURL()));
auto user_scripts = std::make_unique<UserScriptList>();
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,
/*listen_for_extension_system_loaded=*/true,
/*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