blob: efbc9275a1ca5f8d75d62db4a29f88157ce6ed16 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/extensions/extension_browser_test_util.h"
#include "base/files/file_util.h"
#include "base/json/json_file_value_serializer.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "extensions/common/api/web_accessible_resources.h"
#include "extensions/common/api/web_accessible_resources_mv2.h"
#include "extensions/common/constants.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace extensions::browser_test_util {
namespace {
// Creates a copy of `source` within `temp_dir` and populates `out` with the
// destination path. Returns true on success.
bool CreateTempDirectoryCopy(const base::FilePath& temp_dir,
const base::FilePath& source,
base::FilePath* out) {
base::ScopedAllowBlockingForTesting allow_blocking;
base::FilePath temp_subdir;
if (!base::CreateTemporaryDirInDir(temp_dir, base::FilePath::StringType(),
&temp_subdir)) {
ADD_FAILURE() << "Could not create temporary dir for test under "
<< temp_dir;
return false;
}
// Copy all files from `source` to `temp_subdir`.
if (!base::CopyDirectory(source, temp_subdir, true /* recursive */)) {
ADD_FAILURE() << source.value() << " could not be copied to "
<< temp_subdir.value();
return false;
}
*out = temp_subdir.Append(source.BaseName());
return true;
}
// Moves match patterns from the `permissions_key` list to the
// `host_permissions_key` list.
void DoMoveHostPermissions(base::Value::Dict& manifest_dict,
const char* permissions_key,
const char* host_permissions_key) {
base::Value::List* const permissions =
manifest_dict.FindList(permissions_key);
if (!permissions) {
return;
}
// Add the permissions to the appropriate destinations, then update/add
// the target lists as appropriate.
base::Value::List permissions_list;
base::Value::List host_permissions_list;
for (auto& value : *permissions) {
CHECK(value.is_string());
const std::string& str_value = value.GetString();
if (str_value == "<all_urls>" ||
str_value.find("://") != std::string::npos) {
host_permissions_list.Append(std::move(value));
} else {
permissions_list.Append(std::move(value));
}
}
if (permissions_list.empty()) {
manifest_dict.Remove(permissions_key);
} else {
*permissions = std::move(permissions_list);
}
if (!host_permissions_list.empty()) {
manifest_dict.Set(host_permissions_key, std::move(host_permissions_list));
}
}
// Moves match patterns from permissions/optional_permissions to the
// host_permissions/optional_host_permissions.
void MoveHostPermissions(base::Value::Dict& manifest_dict) {
DoMoveHostPermissions(manifest_dict, manifest_keys::kPermissions,
manifest_keys::kHostPermissions);
DoMoveHostPermissions(manifest_dict, manifest_keys::kOptionalPermissions,
manifest_keys::kOptionalHostPermissions);
}
using web_accessible_resource =
api::web_accessible_resources::WebAccessibleResource;
// Upgrades MV2 format to MV3 format.
void UpgradeWebAccessibleResources(base::Value::Dict& manifest_dict) {
base::Value::List* const web_accessible_resources = manifest_dict.FindList(
api::web_accessible_resources::ManifestKeys::kWebAccessibleResources);
if (!web_accessible_resources) {
return;
}
// Copy all of the entries to a single dictionary entry that matches all
// URLs.
auto war_dict = base::Value::Dict()
.Set(web_accessible_resource::kResources,
web_accessible_resources->Clone())
.Set(web_accessible_resource::kMatches,
base::Value::List().Append("<all_urls>"));
// Clear the list and append the dictionary.
web_accessible_resources->clear();
web_accessible_resources->Append(std::move(war_dict));
}
// Modifies `manifest_dict` changing its manifest version to 3.
bool ModifyManifestForManifestVersion3(base::Value::Dict& manifest_dict) {
// This should only be used for manifest v2 extension.
std::optional<int> current_manifest_version =
manifest_dict.FindInt(manifest_keys::kManifestVersion);
if (!current_manifest_version || *current_manifest_version != 2) {
ADD_FAILURE() << manifest_dict << " should have a manifest version of 2.";
return false;
}
UpgradeWebAccessibleResources(manifest_dict);
MoveHostPermissions(manifest_dict);
manifest_dict.Set(manifest_keys::kManifestVersion, 3);
return true;
}
// Modifies extension at `extension_root` and its `manifest_dict` converting it
// to a service worker based extension.
// NOTE: The conversion works only for extensions with background.scripts and
// requires the background.persistent key. The background.page key is not
// supported.
bool ModifyExtensionForServiceWorker(const base::FilePath& extension_root,
base::Value::Dict& manifest_dict) {
base::ScopedAllowBlockingForTesting allow_blocking;
// Retrieve the value of the `background` key and verify that it has
// the `persistent` key and specifies JS files.
// Background pages that specify HTML files are not supported.
base::Value::Dict* background_dict = manifest_dict.FindDict("background");
if (!background_dict) {
ADD_FAILURE() << extension_root.value()
<< " 'background' key not found in manifest.json";
return false;
}
{
std::optional<bool> background_persistent =
background_dict->FindBool("persistent");
if (!background_persistent.has_value()) {
ADD_FAILURE() << extension_root.value()
<< ": The \"persistent\" key must be specified to run as a "
"Service Worker-based extension.";
return false;
}
}
base::Value::List* background_scripts_list =
background_dict->FindList("scripts");
// Number of JS scripts must be >= 1.
if (!background_scripts_list || background_scripts_list->empty()) {
ADD_FAILURE() << extension_root.value()
<< ": Only extensions with JS script(s) can be loaded "
"as a sw-based extension.";
return false;
}
std::string service_worker_script;
// If there's just one script, use it directly in the "service_worker" key.
if (background_scripts_list->size() == 1) {
service_worker_script = (*background_scripts_list)[0].GetString();
} else {
// Otherwise, generate a script that uses importScripts() to import
// all of the scripts.
service_worker_script = "generated_service_worker__.js";
std::vector<std::string> script_filenames;
for (const base::Value& script : *background_scripts_list) {
script_filenames.push_back(base::StrCat({"'", script.GetString(), "'"}));
}
base::FilePath combined_script_filepath =
extension_root.AppendASCII(service_worker_script);
// Collision with generated script filename.
if (base::PathExists(combined_script_filepath)) {
ADD_FAILURE() << combined_script_filepath.value()
<< " already exists, make sure " << extension_root.value()
<< " does not contained file named "
<< service_worker_script;
return false;
}
std::string generated_sw_script_content = base::StringPrintf(
"importScripts(%s);", base::JoinString(script_filenames, ",").c_str());
if (!base::WriteFile(combined_script_filepath,
generated_sw_script_content)) {
ADD_FAILURE() << "Could not write combined Service Worker script to: "
<< combined_script_filepath.value();
return false;
}
}
// Remove the existing background specification and replace it with a service
// worker.
background_dict->Remove("persistent");
background_dict->Remove("scripts");
background_dict->Set("service_worker", std::move(service_worker_script));
return true;
}
} // namespace
bool IsServiceWorkerContext(ContextType context_type) {
return context_type == ContextType::kServiceWorker ||
context_type == ContextType::kServiceWorkerMV2;
}
bool ModifyExtensionIfNeeded(const LoadOptions& options,
ContextType context_type,
size_t test_pre_count,
const base::FilePath& temp_dir_path,
const base::FilePath& input_path,
base::FilePath* out_path) {
base::ScopedAllowBlockingForTesting scoped_allow_blocking;
const ContextType context_type_to_use =
options.context_type == ContextType::kNone ? context_type
: options.context_type;
// Use context_type_ if LoadOptions.context_type is unspecified.
// Otherwise, use LoadOptions.context_type.
const bool load_as_service_worker =
IsServiceWorkerContext(context_type_to_use);
// Early return if no modification is needed.
if (!load_as_service_worker && !options.load_as_manifest_version_3) {
*out_path = input_path;
return true;
}
// Tests that have a PRE_ stage need to exist in a temporary directory that
// persists after the test fixture is destroyed. The test bots are configured
// to use a unique temp directory that's cleaned up after the tests run, so
// this won't pollute the system tmp directory.
base::FilePath temp_dir;
if (test_pre_count == 0) {
temp_dir = temp_dir_path;
} else if (!base::GetTempDir(&temp_dir)) {
ADD_FAILURE() << "Could not get temporary dir for test.";
return false;
}
base::FilePath extension_root;
if (!CreateTempDirectoryCopy(temp_dir, input_path, &extension_root)) {
return false;
}
std::string error;
std::optional<base::Value::Dict> manifest_dict =
extensions::file_util::LoadManifest(extension_root, &error);
if (!manifest_dict) {
ADD_FAILURE() << extension_root.value()
<< " could not load manifest: " << error;
return false;
}
if (load_as_service_worker &&
!ModifyExtensionForServiceWorker(extension_root, *manifest_dict)) {
return false;
}
const bool is_service_worker_mv3 =
context_type_to_use == ContextType::kServiceWorker;
// Update the manifest if converting to a service worker MV3-based
// extension or if requested in `options`.
if ((is_service_worker_mv3 || options.load_as_manifest_version_3) &&
!ModifyManifestForManifestVersion3(*manifest_dict)) {
return false;
}
// Write out manifest.json.
base::FilePath manifest_path = extension_root.Append(kManifestFilename);
if (!JSONFileValueSerializer(manifest_path).Serialize(*manifest_dict)) {
ADD_FAILURE() << "Could not write manifest file to "
<< manifest_path.value();
return false;
}
*out_path = extension_root;
return true;
}
} // namespace extensions::browser_test_util