#include "extensions/common/utils/content_script_utils.h"
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <string_view>
#include "base/files/file_util.h"
#include "base/strings/escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "content/public/common/url_constants.h"
#include "extensions/common/api/content_scripts.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/permissions_parser.h"
#include "extensions/common/mojom/match_origin_as_fallback.mojom-shared.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/url_pattern.h"
#include "extensions/common/url_pattern_set.h"
#include "extensions/common/user_script.h"
#include "extensions/strings/grit/extensions_strings.h"
#include "net/base/mime_util.h"
#include "third_party/blink/public/common/mime_util/mime_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
namespace extensions {
namespace errors = manifest_errors;
namespace script_parsing {
namespace {
size_t g_max_script_length_in_bytes = 1024u * 1024u * 500u;
size_t g_max_scripts_length_per_extension_in_bytes =
1024u * 1024u * 1024u;
const char kEmptyFilesFynamicScriptError[] =
"Script with ID '*' must specify at least one js or css file.";
constexpr char kEmptyMatchesDynamicScriptError[] =
"Script with ID '*' must specify at least one match.";
constexpr char kInvalidExcludeMatchDynamicScriptError[] =
"Script with ID '*' has invalid value for exclude_matches[*]: *";
constexpr char kInvalidMatchDynamicScriptError[] =
"Script with ID '*' has invalid value for matches[*]: *";
constexpr char kForbiddenInlineCodeScriptError[] =
"Script with ID '*' has forbidden inline code source";
bool IsMimeTypeValid(const base::FilePath& relative_path,
ContentScriptType content_script_type) {
auto file_extension = relative_path.Extension();
if (file_extension.empty()) {
return false;
}
file_extension = file_extension.substr(1);
if (content_script_type == ContentScriptType::kJs &&
base::EqualsCaseInsensitiveASCII(file_extension,
UserScript::kFileExtension)) {
return true;
}
if (content_script_type == ContentScriptType::kCss &&
base::EqualsCaseInsensitiveASCII(file_extension, "scss")) {
return true;
}
std::string mime_type;
if (!net::GetWellKnownMimeTypeFromExtension(file_extension, &mime_type)) {
return false;
}
switch (content_script_type) {
case ContentScriptType::kJs:
return blink::IsSupportedJavascriptMimeType(mime_type);
case ContentScriptType::kCss:
return mime_type == "text/css";
}
}
bool IsScriptValid(const base::FilePath& path,
const base::FilePath& relative_path,
size_t max_script_length,
int file_not_read_error_id,
std::string* error,
std::vector<InstallWarning>* warnings,
size_t& remaining_length) {
InstallWarning script_file_too_large_warning(
l10n_util::GetStringFUTF8(IDS_EXTENSION_CONTENT_SCRIPT_FILE_TOO_LARGE,
relative_path.LossyDisplayName()),
api::content_scripts::ManifestKeys::kContentScripts,
base::UTF16ToUTF8(relative_path.LossyDisplayName()));
if (remaining_length == 0u) {
warnings->push_back(std::move(script_file_too_large_warning));
return true;
}
if (!base::PathExists(path)) {
*error = l10n_util::GetStringFUTF8(file_not_read_error_id,
relative_path.LossyDisplayName());
return false;
}
std::string content;
bool read_successful =
base::ReadFileToStringWithMaxSize(path, &content, max_script_length);
if (!read_successful && content.length() != max_script_length) {
*error = l10n_util::GetStringFUTF8(file_not_read_error_id,
relative_path.LossyDisplayName());
return false;
}
if (!base::IsStringUTF8(content)) {
*error = l10n_util::GetStringFUTF8(IDS_EXTENSION_BAD_FILE_ENCODING,
relative_path.LossyDisplayName());
return false;
}
if (read_successful) {
remaining_length -= content.size();
} else {
warnings->push_back(std::move(script_file_too_large_warning));
}
return true;
}
std::u16string GetEmptyFieldError(std::string_view static_error,
std::string_view dynamic_error,
const std::string& script_id,
std::optional<int> definition_index) {
if (definition_index) {
return ErrorUtils::FormatErrorMessageUTF16(
static_error, base::NumberToString(*definition_index));
}
return ErrorUtils::FormatErrorMessageUTF16(
dynamic_error, UserScript::TrimPrefixFromScriptID(script_id));
}
std::u16string GetInvalidMatchError(std::string_view static_error,
std::string_view dynamic_error,
const std::string& script_id,
std::optional<int> definition_index,
URLPattern::ParseResult parse_result,
int match_index) {
std::string match_index_string = base::NumberToString(match_index);
std::string parse_result_string =
URLPattern::GetParseResultString(parse_result);
if (definition_index) {
return ErrorUtils::FormatErrorMessageUTF16(
static_error, base::NumberToString(*definition_index),
match_index_string, parse_result_string);
}
return ErrorUtils::FormatErrorMessageUTF16(
dynamic_error, UserScript::TrimPrefixFromScriptID(script_id),
match_index_string, parse_result_string);
}
std::u16string GetEmptyMatchesError(const std::string& script_id,
std::optional<int> definition_index) {
return GetEmptyFieldError(errors::kInvalidMatchCount,
kEmptyMatchesDynamicScriptError, script_id,
definition_index);
}
std::u16string GetEmptyFilesError(const std::string& script_id,
std::optional<int> definition_index) {
return GetEmptyFieldError(errors::kMissingFile, kEmptyFilesFynamicScriptError,
script_id, definition_index);
}
std::u16string GetInvalidExcludeMatchError(const std::string& script_id,
std::optional<int> definition_index,
URLPattern::ParseResult parse_result,
int match_index) {
return GetInvalidMatchError(errors::kInvalidExcludeMatch,
kInvalidExcludeMatchDynamicScriptError, script_id,
definition_index, parse_result, match_index);
}
std::u16string GetInvalidMatchError(const std::string& script_id,
std::optional<int> definition_index,
URLPattern::ParseResult parse_result,
int match_index) {
return GetInvalidMatchError(errors::kInvalidMatch,
kInvalidMatchDynamicScriptError, script_id,
definition_index, parse_result, match_index);
}
}
size_t GetMaxScriptLength() {
return g_max_script_length_in_bytes;
}
size_t GetMaxScriptsLengthPerExtension() {
return g_max_scripts_length_per_extension_in_bytes;
}
ScopedMaxScriptLengthOverride CreateScopedMaxScriptLengthForTesting(
size_t max) {
return ScopedMaxScriptLengthOverride(&g_max_script_length_in_bytes, max);
}
ScopedMaxScriptLengthOverride
CreateScopedMaxScriptsLengthPerExtensionForTesting(size_t max) {
return ScopedMaxScriptLengthOverride(
&g_max_scripts_length_per_extension_in_bytes, max);
}
bool ParseMatchPatterns(const std::vector<std::string>& matches,
const std::vector<std::string>* exclude_matches,
int creation_flags,
bool can_execute_script_everywhere,
bool all_urls_includes_chrome_urls,
std::optional<int> definition_index,
UserScript* result,
std::u16string* error,
bool* wants_file_access) {
if (matches.empty()) {
*error = GetEmptyMatchesError(result->id(), definition_index);
return false;
}
const int valid_schemes =
UserScript::ValidUserScriptSchemes(can_execute_script_everywhere);
for (size_t i = 0; i < matches.size(); ++i) {
URLPattern pattern(valid_schemes);
const std::string& match_str = matches[i];
URLPattern::ParseResult parse_result = pattern.Parse(match_str);
if (parse_result != URLPattern::ParseResult::kSuccess) {
*error =
GetInvalidMatchError(result->id(), definition_index, parse_result, i);
return false;
}
if (!all_urls_includes_chrome_urls &&
pattern.scheme() != content::kChromeUIScheme) {
pattern.SetValidSchemes(pattern.valid_schemes() &
~URLPattern::SCHEME_CHROMEUI);
}
if (pattern.MatchesScheme(url::kFileScheme) &&
!can_execute_script_everywhere) {
if (wants_file_access)
*wants_file_access = true;
if (!(creation_flags & Extension::ALLOW_FILE_ACCESS)) {
pattern.SetValidSchemes(pattern.valid_schemes() &
~URLPattern::SCHEME_FILE);
}
}
result->add_url_pattern(pattern);
}
if (exclude_matches) {
for (size_t i = 0; i < exclude_matches->size(); ++i) {
const std::string& match_str = exclude_matches->at(i);
URLPattern pattern(valid_schemes);
URLPattern::ParseResult parse_result = pattern.Parse(match_str);
if (parse_result != URLPattern::ParseResult::kSuccess) {
*error = GetInvalidExcludeMatchError(result->id(), definition_index,
parse_result, i);
return false;
}
result->add_exclude_url_pattern(pattern);
}
}
return true;
}
bool ParseFileSources(
const Extension* extension,
const std::vector<api::scripts_internal::ScriptSource>* js,
const std::vector<api::scripts_internal::ScriptSource>* css,
std::optional<int> definition_index,
UserScript* result,
std::u16string* error) {
if (js) {
result->js_scripts().reserve(js->size());
for (const auto& source : *js) {
if (source.file) {
GURL url = extension->GetResourceURL(base::EscapePath(*source.file));
ExtensionResource resource = extension->GetResource(*source.file);
result->js_scripts().push_back(UserScript::Content::CreateFile(
resource.extension_root(), resource.relative_path(), url));
} else if (source.code) {
if (result->GetSource() != UserScript::Source::kDynamicUserScript) {
*error = ErrorUtils::FormatErrorMessageUTF16(
kForbiddenInlineCodeScriptError,
UserScript::TrimPrefixFromScriptID(result->id()));
return false;
}
GURL url = extension->GetResourceURL(
base::Uuid::GenerateRandomV4().AsLowercaseString());
std::unique_ptr<UserScript::Content> content =
UserScript::Content::CreateInlineCode(url);
content->set_content(*source.code);
result->js_scripts().push_back(std::move(content));
}
}
}
if (css) {
result->css_scripts().reserve(css->size());
for (const auto& source : *css) {
if (source.file) {
GURL url = extension->GetResourceURL(base::EscapePath(*source.file));
ExtensionResource resource = extension->GetResource(*source.file);
result->css_scripts().push_back(UserScript::Content::CreateFile(
resource.extension_root(), resource.relative_path(), url));
}
}
}
if (result->js_scripts().empty() && result->css_scripts().empty()) {
*error = GetEmptyFilesError(result->id(), definition_index);
return false;
}
return true;
}
void ParseGlobs(const std::vector<std::string>* include_globs,
const std::vector<std::string>* exclude_globs,
UserScript* result) {
if (include_globs) {
for (const std::string& glob : *include_globs) {
result->add_glob(glob);
}
}
if (exclude_globs) {
for (const std::string& glob : *exclude_globs) {
result->add_exclude_glob(glob);
}
}
}
bool ValidateMimeTypeFromFileExtension(const base::FilePath& relative_path,
ContentScriptType content_script_type,
std::string* error) {
if (!IsMimeTypeValid(relative_path, content_script_type)) {
switch (content_script_type) {
case ContentScriptType::kJs:
*error = l10n_util::GetStringFUTF8(
IDS_EXTENSION_CONTENT_SCRIPT_FILE_BAD_JS_MIME_TYPE,
relative_path.LossyDisplayName());
return false;
case ContentScriptType::kCss:
*error = l10n_util::GetStringFUTF8(
IDS_EXTENSION_CONTENT_SCRIPT_FILE_BAD_CSS_MIME_TYPE,
relative_path.LossyDisplayName());
return false;
}
}
return true;
}
bool ValidateFileSources(const UserScriptList& scripts,
ExtensionResource::SymlinkPolicy symlink_policy,
std::string* error,
std::vector<InstallWarning>* warnings) {
size_t remaining_scripts_length = GetMaxScriptsLengthPerExtension();
for (const std::unique_ptr<UserScript>& script : scripts) {
for (const std::unique_ptr<UserScript::Content>& js_script :
script->js_scripts()) {
if (js_script->source() == UserScript::Content::Source::kInlineCode) {
continue;
}
if (!ValidateMimeTypeFromFileExtension(js_script->relative_path(),
ContentScriptType::kJs, error)) {
warnings->emplace_back(
base::StringPrintf(errors::kInvalidUserScriptMimeType,
error->c_str()),
api::content_scripts::ManifestKeys::kContentScripts);
continue;
}
const base::FilePath& path = ExtensionResource::GetFilePath(
js_script->extension_root(), js_script->relative_path(),
symlink_policy);
size_t max_script_length =
std::min(remaining_scripts_length, GetMaxScriptLength());
if (!IsScriptValid(path, js_script->relative_path(), max_script_length,
IDS_EXTENSION_LOAD_JAVASCRIPT_FAILED, error, warnings,
remaining_scripts_length)) {
return false;
}
}
for (const std::unique_ptr<UserScript::Content>& css_script :
script->css_scripts()) {
if (!ValidateMimeTypeFromFileExtension(css_script->relative_path(),
ContentScriptType::kCss, error)) {
warnings->emplace_back(
base::StringPrintf(errors::kInvalidUserScriptMimeType,
error->c_str()),
api::content_scripts::ManifestKeys::kContentScripts);
continue;
}
const base::FilePath& path = ExtensionResource::GetFilePath(
css_script->extension_root(), css_script->relative_path(),
symlink_policy);
size_t max_script_length =
std::min(remaining_scripts_length, GetMaxScriptLength());
if (!IsScriptValid(path, css_script->relative_path(), max_script_length,
IDS_EXTENSION_LOAD_CSS_FAILED, error, warnings,
remaining_scripts_length)) {
return false;
}
}
}
return true;
}
bool ValidateUserScriptMimeTypesFromFileExtensions(const UserScript& script,
std::string* error) {
for (const std::unique_ptr<UserScript::Content>& js_script :
script.js_scripts()) {
if (js_script->source() == UserScript::Content::Source::kInlineCode) {
continue;
}
if (!ValidateMimeTypeFromFileExtension(js_script->relative_path(),
ContentScriptType::kJs, error)) {
return false;
}
}
for (const std::unique_ptr<UserScript::Content>& css_script :
script.css_scripts()) {
if (!ValidateMimeTypeFromFileExtension(css_script->relative_path(),
ContentScriptType::kCss, error)) {
return false;
}
}
return true;
}
bool ValidateMatchOriginAsFallback(
mojom::MatchOriginAsFallbackBehavior match_origin_as_fallback,
const URLPatternSet& url_patterns,
std::u16string* error_out) {
if (match_origin_as_fallback ==
mojom::MatchOriginAsFallbackBehavior::kAlways) {
for (const auto& pattern : url_patterns) {
if (pattern.path() != "/*") {
*error_out = errors::kMatchOriginAsFallbackCantHavePaths;
return false;
}
}
}
return true;
}
ExtensionResource::SymlinkPolicy GetSymlinkPolicy(const Extension* extension) {
if ((extension->creation_flags() & Extension::FOLLOW_SYMLINKS_ANYWHERE) !=
0) {
return ExtensionResource::FOLLOW_SYMLINKS_ANYWHERE;
}
return ExtensionResource::SYMLINKS_MUST_RESOLVE_WITHIN_ROOT;
}
}
}