#include "extensions/common/extension_l10n_util.h"
#include <stddef.h>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <vector>
#include "base/auto_reset.h"
#include "base/containers/contains.h"
#include "base/containers/extend.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/json/json_file_value_serializer.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "extensions/common/constants.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension.h"
#include "extensions/common/extensions_client.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/message_bundle.h"
#include "extensions/common/utils/base_string.h"
#include "third_party/icu/source/common/unicode/uloc.h"
#include "third_party/zlib/google/compression_utils.h"
#include "ui/base/l10n/l10n_util.h"
#if BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
#include "extensions/common/extension_l10n_util_ext.cc"
#endif
namespace errors = extensions::manifest_errors;
namespace keys = extensions::manifest_keys;
namespace {
bool g_allow_gzipped_messages_for_test = false;
std::optional<base::Value::Dict> LoadMessageFile(
const base::FilePath& locale_path,
const std::string& locale,
std::string* error,
extension_l10n_util::GzippedMessagesPermission gzip_permission) {
base::FilePath file_path =
locale_path.AppendASCII(locale).Append(extensions::kMessagesFilename);
std::optional<base::Value::Dict> dictionary;
if (base::PathExists(file_path)) {
JSONFileValueDeserializer messages_deserializer(file_path);
std::unique_ptr<base::Value> value =
messages_deserializer.Deserialize(nullptr, error);
if (value) {
dictionary = std::move(*value).TakeDict();
}
} else if (gzip_permission == extension_l10n_util::GzippedMessagesPermission::
kAllowForTrustedSource ||
g_allow_gzipped_messages_for_test) {
base::FilePath compressed_file_path =
file_path.AddExtension(FILE_PATH_LITERAL(".gz"));
if (base::PathExists(compressed_file_path)) {
std::string compressed_data;
if (!base::ReadFileToString(compressed_file_path, &compressed_data)) {
*error = base::StringPrintf("Failed to read compressed locale %s.",
locale.c_str());
return dictionary;
}
std::string data;
if (!compression::GzipUncompress(compressed_data, &data)) {
*error = base::StringPrintf("Failed to decompress locale %s.",
locale.c_str());
return dictionary;
}
base::JSONReader::Result value =
base::JSONReader::ReadAndReturnValueWithError(
data, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
if (value.has_value()) {
dictionary = std::move(*value).TakeDict();
} else {
*error = value.error().message;
}
}
} else {
LOG(ERROR) << "Unable to load message file: " << locale_path.AsUTF8Unsafe();
}
if (!dictionary) {
if (error->empty()) {
*error = base::StringPrintf("Catalog file is missing for locale %s.",
locale.c_str());
} else {
*error = extensions::ErrorUtils::FormatErrorMessage(
errors::kLocalesInvalidLocale,
base::UTF16ToUTF8(file_path.LossyDisplayName()), *error);
}
}
return dictionary;
}
bool LocalizeManifestValue(const std::string& key,
const extensions::MessageBundle& messages,
base::Value::Dict* manifest,
std::string* error) {
std::string* result = manifest->FindStringByDottedPath(key);
if (!result)
return true;
if (!messages.ReplaceMessages(result, error))
return false;
manifest->SetByDottedPath(key, *result);
return true;
}
bool LocalizeManifestListValue(const std::string& key,
const extensions::MessageBundle& messages,
base::Value::Dict* manifest,
std::string* error) {
base::Value::List* list_value = manifest->FindListByDottedPath(key);
if (!list_value)
return true;
for (base::Value& item : *list_value) {
if (item.is_string()) {
std::string result = item.GetString();
if (!messages.ReplaceMessages(&result, error)) {
return false;
}
item = base::Value(result);
}
}
return true;
}
std::string& GetProcessLocale() {
static base::NoDestructor<std::string> process_locale;
return *process_locale;
}
std::string& GetPreferredLocale() {
static base::NoDestructor<std::string> preferred_locale;
return *preferred_locale;
}
std::string LocaleForLocalization() {
std::string preferred_locale =
l10n_util::NormalizeLocale(GetPreferredLocale());
if (!preferred_locale.empty())
return preferred_locale;
return extension_l10n_util::CurrentLocaleOrDefault();
}
}
namespace extension_l10n_util {
GzippedMessagesPermission GetGzippedMessagesPermissionForExtension(
const extensions::Extension* extension) {
return extension
? GetGzippedMessagesPermissionForLocation(extension->location())
: GzippedMessagesPermission::kDisallow;
}
GzippedMessagesPermission GetGzippedMessagesPermissionForLocation(
extensions::mojom::ManifestLocation location) {
return location == extensions::mojom::ManifestLocation::kComponent
? GzippedMessagesPermission::kAllowForTrustedSource
: GzippedMessagesPermission::kDisallow;
}
base::AutoReset<bool> AllowGzippedMessagesAllowedForTest() {
return base::AutoReset<bool>(&g_allow_gzipped_messages_for_test, true);
}
void SetProcessLocale(const std::string& locale) {
GetProcessLocale() = locale;
}
void SetPreferredLocale(const std::string& locale) {
GetPreferredLocale() = locale;
}
std::string GetDefaultLocaleFromManifest(const base::Value::Dict& manifest,
std::string* error) {
if (const std::string* default_locale =
manifest.FindString(keys::kDefaultLocale)) {
return *default_locale;
}
*error = errors::kInvalidDefaultLocale;
return std::string();
}
bool ShouldRelocalizeManifest(const base::Value::Dict& manifest) {
if (!manifest.Find(keys::kDefaultLocale))
return false;
std::string manifest_current_locale;
const std::string* manifest_current_locale_in =
manifest.FindString(keys::kCurrentLocale);
if (manifest_current_locale_in)
manifest_current_locale = *manifest_current_locale_in;
return manifest_current_locale != LocaleForLocalization();
}
bool LocalizeManifest(const extensions::MessageBundle& messages,
base::Value::Dict* manifest,
std::string* error) {
const std::string* result = manifest->FindString(keys::kName);
if (!result) {
*error = errors::kInvalidName;
return false;
}
if (!LocalizeManifestValue(keys::kName, messages, manifest, error)) {
return false;
}
if (!LocalizeManifestValue(keys::kShortName, messages, manifest, error))
return false;
if (!LocalizeManifestValue(keys::kDescription, messages, manifest, error))
return false;
auto get_title_key = [](const char* action_key) {
return base::StrCat({action_key, ".", keys::kActionDefaultTitle});
};
if (!LocalizeManifestValue(get_title_key(keys::kBrowserAction), messages,
manifest, error)) {
return false;
}
if (!LocalizeManifestValue(get_title_key(keys::kPageAction), messages,
manifest, error)) {
return false;
}
if (!LocalizeManifestValue(get_title_key(keys::kAction), messages, manifest,
error)) {
return false;
}
if (!LocalizeManifestValue(keys::kOmniboxKeyword, messages, manifest, error))
return false;
base::Value::List* file_handlers =
manifest->FindListByDottedPath(keys::kFileBrowserHandlers);
if (file_handlers) {
for (base::Value& handler : *file_handlers) {
base::Value::Dict* dict = handler.GetIfDict();
if (!dict) {
*error = errors::kInvalidFileBrowserHandler;
return false;
}
if (!LocalizeManifestValue(keys::kActionDefaultTitle, messages, dict,
error))
return false;
}
}
base::Value::List* input_components =
manifest->FindListByDottedPath(keys::kInputComponents);
if (input_components) {
for (base::Value& module : *input_components) {
base::Value::Dict* dict = module.GetIfDict();
if (!dict) {
*error = errors::kInvalidInputComponents;
return false;
}
if (!LocalizeManifestValue(keys::kName, messages, dict, error))
return false;
if (!LocalizeManifestValue(keys::kDescription, messages, dict, error))
return false;
}
}
if (!LocalizeManifestValue(keys::kLaunchLocalPath, messages, manifest, error))
return false;
if (!LocalizeManifestValue(keys::kLaunchWebURL, messages, manifest, error))
return false;
base::Value::Dict* commands_handler =
manifest->FindDictByDottedPath(keys::kCommands);
if (commands_handler) {
for (auto iter : *commands_handler) {
std::string key =
base::StringPrintf("commands.%s.description", iter.first.c_str());
if (!LocalizeManifestValue(key, messages, manifest, error))
return false;
}
}
base::Value::Dict* search_provider =
manifest->FindDictByDottedPath(keys::kOverrideSearchProvider);
if (search_provider) {
for (auto iter : *search_provider) {
std::string key = base::StrCat(
{keys::kOverrideSearchProvider, ".", iter.first.c_str()});
bool success =
(key == keys::kSettingsOverrideAlternateUrls)
? LocalizeManifestListValue(key, messages, manifest, error)
: LocalizeManifestValue(key, messages, manifest, error);
if (!success)
return false;
}
}
if (!LocalizeManifestValue(
keys::kOverrideHomepage, messages, manifest, error))
return false;
if (!LocalizeManifestListValue(
keys::kOverrideStartupPage, messages, manifest, error))
return false;
manifest->Set(keys::kCurrentLocale, LocaleForLocalization());
return true;
}
bool LocalizeExtension(const base::FilePath& extension_path,
base::Value::Dict* manifest,
GzippedMessagesPermission gzip_permission,
std::string* error) {
DCHECK(manifest);
std::string default_locale = GetDefaultLocaleFromManifest(*manifest, error);
std::unique_ptr<extensions::MessageBundle> message_bundle(
extensions::file_util::LoadMessageBundle(extension_path, default_locale,
gzip_permission, error));
if (!message_bundle && !error->empty())
return false;
if (message_bundle && !LocalizeManifest(*message_bundle, manifest, error))
return false;
return true;
}
bool AddLocale(const std::set<std::string>& chrome_locales,
const base::FilePath& locale_folder,
const std::string& locale_name,
std::set<std::string>* valid_locales,
std::string* error) {
if (base::StartsWith(locale_name, ".", base::CompareCase::SENSITIVE))
return true;
if (!base::Contains(chrome_locales, locale_name)) {
DLOG(WARNING) << base::StringPrintf("Supplied locale %s is not supported.",
locale_name.c_str());
return true;
}
if (!base::PathExists(locale_folder.Append(extensions::kMessagesFilename))) {
*error = base::StringPrintf("Catalog file is missing for locale %s.",
locale_name.c_str());
return false;
}
valid_locales->insert(locale_name);
return true;
}
#if !BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
std::string CurrentLocaleOrDefault() {
std::string current_locale = l10n_util::NormalizeLocale(GetProcessLocale());
if (current_locale.empty())
current_locale = "en";
return current_locale;
}
#endif
void GetAllLocales(std::set<std::string>* all_locales) {
const std::vector<std::string>& available_locales =
l10n_util::GetAvailableICULocales();
for (const auto& locale : available_locales) {
std::vector<std::string> result = l10n_util::GetParentLocales(locale);
all_locales->insert(result.begin(), result.end());
}
}
void GetAllFallbackLocales(const std::string& default_locale,
std::vector<std::string>* all_fallback_locales) {
DCHECK(all_fallback_locales);
std::string application_locale = CurrentLocaleOrDefault();
#if !BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
std::string preferred_locale =
l10n_util::NormalizeLocale(GetPreferredLocale());
if (!preferred_locale.empty() && preferred_locale != default_locale &&
preferred_locale != application_locale) {
all_fallback_locales->push_back(preferred_locale);
}
#endif
if (!application_locale.empty() && application_locale != default_locale) {
base::Extend(*all_fallback_locales,
l10n_util::GetParentLocales(application_locale));
}
all_fallback_locales->push_back(default_locale);
}
bool GetValidLocales(const base::FilePath& locale_path,
std::set<std::string>* valid_locales,
std::string* error) {
std::set<std::string> chrome_locales;
GetAllLocales(&chrome_locales);
base::FileEnumerator locales(
locale_path, false, base::FileEnumerator::DIRECTORIES);
base::FilePath locale_folder;
while (!(locale_folder = locales.Next()).empty()) {
std::string locale_name = locale_folder.BaseName().MaybeAsASCII();
if (locale_name.empty()) {
NOTREACHED();
}
if (!AddLocale(
chrome_locales, locale_folder, locale_name, valid_locales, error)) {
valid_locales->clear();
return false;
}
}
if (valid_locales->empty()) {
*error = errors::kLocalesNoValidLocaleNamesListed;
return false;
}
return true;
}
extensions::MessageBundle* LoadMessageCatalogs(
const base::FilePath& locale_path,
const std::string& default_locale,
GzippedMessagesPermission gzip_permission,
std::string* error) {
std::vector<std::string> all_fallback_locales;
GetAllFallbackLocales(default_locale, &all_fallback_locales);
extensions::MessageBundle::CatalogVector catalogs;
for (const auto& fallback_locale : all_fallback_locales) {
base::FilePath this_locale_path = locale_path.AppendASCII(fallback_locale);
if (!base::PathExists(this_locale_path))
continue;
std::optional<base::Value::Dict> catalog =
LoadMessageFile(locale_path, fallback_locale, error, gzip_permission);
if (!catalog.has_value()) {
return nullptr;
}
catalogs.push_back(std::move(*catalog));
}
return extensions::MessageBundle::Create(catalogs, error);
}
bool ValidateExtensionLocales(const base::FilePath& extension_path,
const base::Value::Dict& manifest,
std::u16string* error) {
std::string utf8_error;
std::string default_locale =
GetDefaultLocaleFromManifest(manifest, &utf8_error);
if (default_locale.empty()) {
*error = base::UTF8ToUTF16(utf8_error);
return true;
}
base::FilePath locale_path = extension_path.Append(extensions::kLocaleFolder);
std::set<std::string> valid_locales;
if (!GetValidLocales(locale_path, &valid_locales, &utf8_error)) {
*error = base::UTF8ToUTF16(utf8_error);
return false;
}
for (const auto& locale : valid_locales) {
std::string locale_error;
std::unique_ptr<extensions::MessageBundle> bundle(LoadMessageCatalogs(
locale_path, locale, GzippedMessagesPermission::kDisallow,
&locale_error));
if (locale_error.empty()) {
continue;
}
if (!utf8_error.empty()) {
utf8_error += '\n';
}
base::FilePath file_path =
locale_path.AppendASCII(locale).Append(extensions::kMessagesFilename);
utf8_error.append(extensions::ErrorUtils::FormatErrorMessage(
errors::kLocalesInvalidLocale,
base::UTF16ToUTF8(file_path.LossyDisplayName()), locale_error));
}
if (!utf8_error.empty()) {
*error = base::UTF8ToUTF16(utf8_error);
return false;
}
return true;
}
bool ShouldSkipValidation(const base::FilePath& locales_path,
const base::FilePath& locale_path,
const std::set<std::string>& all_locales) {
base::FilePath relative_path;
if (!locales_path.AppendRelativePath(locale_path, &relative_path)) {
NOTREACHED();
}
std::string subdir = relative_path.MaybeAsASCII();
if (subdir.empty())
return true;
if (base::Contains(subdir, '.'))
return true;
if (!extensions::ContainsStringIgnoreCaseASCII(all_locales, subdir))
return true;
return false;
}
ScopedLocaleForTest::ScopedLocaleForTest()
: process_locale_(GetProcessLocale()),
preferred_locale_(GetPreferredLocale()) {}
ScopedLocaleForTest::ScopedLocaleForTest(std::string_view locale)
: ScopedLocaleForTest(locale, locale) {}
ScopedLocaleForTest::ScopedLocaleForTest(std::string_view process_locale,
std::string_view preferred_locale)
: ScopedLocaleForTest() {
SetProcessLocale(std::string(process_locale));
SetPreferredLocale(std::string(preferred_locale));
}
ScopedLocaleForTest::~ScopedLocaleForTest() {
SetProcessLocale(std::string(process_locale_));
SetPreferredLocale(std::string(preferred_locale_));
}
const std::string& GetPreferredLocaleForTest() {
return GetPreferredLocale();
}
}