|  | // Copyright 2012 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/ash/customization/customization_document.h" | 
|  |  | 
|  | #include <algorithm> | 
|  | #include <memory> | 
|  | #include <optional> | 
|  | #include <string_view> | 
|  | #include <utility> | 
|  |  | 
|  | #include "ash/constants/ash_paths.h" | 
|  | #include "base/files/file_path.h" | 
|  | #include "base/files/file_util.h" | 
|  | #include "base/functional/bind.h" | 
|  | #include "base/functional/callback_helpers.h" | 
|  | #include "base/i18n/rtl.h" | 
|  | #include "base/json/json_reader.h" | 
|  | #include "base/logging.h" | 
|  | #include "base/memory/raw_ptr.h" | 
|  | #include "base/memory/weak_ptr.h" | 
|  | #include "base/metrics/histogram_macros.h" | 
|  | #include "base/path_service.h" | 
|  | #include "base/strings/pattern.h" | 
|  | #include "base/strings/string_split.h" | 
|  | #include "base/strings/string_util.h" | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "base/task/thread_pool.h" | 
|  | #include "base/threading/scoped_blocking_call.h" | 
|  | #include "base/time/time.h" | 
|  | #include "chrome/browser/ash/app_list/app_list_syncable_service.h" | 
|  | #include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h" | 
|  | #include "chrome/browser/ash/customization/customization_wallpaper_downloader.h" | 
|  | #include "chrome/browser/ash/customization/customization_wallpaper_util.h" | 
|  | #include "chrome/browser/ash/extensions/default_app_order.h" | 
|  | #include "chrome/browser/ash/login/wizard_controller.h" | 
|  | #include "chrome/browser/ash/net/delay_network_call.h" | 
|  | #include "chrome/browser/browser_process.h" | 
|  | #include "chrome/browser/extensions/external_loader.h" | 
|  | #include "chrome/browser/extensions/external_provider_impl.h" | 
|  | #include "chrome/browser/profiles/profile.h" | 
|  | #include "chrome/common/pref_names.h" | 
|  | #include "chromeos/ash/components/system/statistics_provider.h" | 
|  | #include "components/pref_registry/pref_registry_syncable.h" | 
|  | #include "components/prefs/pref_registry_simple.h" | 
|  | #include "components/prefs/pref_service.h" | 
|  | #include "content/public/browser/browser_task_traits.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "extensions/common/extension_urls.h" | 
|  | #include "net/base/load_flags.h" | 
|  | #include "net/http/http_response_headers.h" | 
|  | #include "net/http/http_status_code.h" | 
|  | #include "services/network/public/cpp/resource_request.h" | 
|  | #include "services/network/public/cpp/shared_url_loader_factory.h" | 
|  | #include "services/network/public/cpp/simple_url_loader.h" | 
|  | #include "services/network/public/mojom/url_response_head.mojom.h" | 
|  |  | 
|  | namespace ash { | 
|  | namespace { | 
|  |  | 
|  | // Manifest attributes names. | 
|  | const char kVersionAttr[] = "version"; | 
|  | const char kDefaultAttr[] = "default"; | 
|  | const char kInitialLocaleAttr[] = "initial_locale"; | 
|  | const char kInitialTimezoneAttr[] = "initial_timezone"; | 
|  | const char kKeyboardLayoutAttr[] = "keyboard_layout"; | 
|  | const char kHwidMapAttr[] = "hwid_map"; | 
|  | const char kHwidMaskAttr[] = "hwid_mask"; | 
|  | const char kSetupContentAttr[] = "setup_content"; | 
|  | const char kEulaPageAttr[] = "eula_page"; | 
|  | const char kDefaultWallpaperAttr[] = "default_wallpaper"; | 
|  | const char kDefaultAppsAttr[] = "default_apps"; | 
|  | const char kLocalizedContent[] = "localized_content"; | 
|  | const char kDefaultAppsFolderName[] = "default_apps_folder_name"; | 
|  | const char kIdAttr[] = "id"; | 
|  |  | 
|  | const char kAcceptedManifestVersion[] = "1.0"; | 
|  |  | 
|  | // This is subdirectory relative to PathService(DIR_CHROMEOS_CUSTOM_WALLPAPERS), | 
|  | // where downloaded (and resized) wallpaper is stored. | 
|  | const char kCustomizationDefaultWallpaperDir[] = "customization"; | 
|  |  | 
|  | // The original downloaded image file is stored under this name. | 
|  | const char kCustomizationDefaultWallpaperDownloadedFile[] = | 
|  | "default_downloaded_wallpaper.bin"; | 
|  |  | 
|  | // Name of local state option that tracks if services customization has been | 
|  | // applied. | 
|  | const char kServicesCustomizationAppliedPref[] = "ServicesCustomizationApplied"; | 
|  |  | 
|  | // Maximum number of retries to fetch file if network is not available. | 
|  | const int kMaxFetchRetries = 3; | 
|  |  | 
|  | // Delay between file fetch retries if network is not available. | 
|  | const int kRetriesDelayInSec = 2; | 
|  |  | 
|  | // Name of profile option that tracks cached version of service customization. | 
|  | const char kServicesCustomizationKey[] = "customization.manifest_cache"; | 
|  |  | 
|  | // Empty customization document that doesn't customize anything. | 
|  | const char kEmptyServicesCustomizationManifest[] = "{ \"version\": \"1.0\" }"; | 
|  |  | 
|  | constexpr net::NetworkTrafficAnnotationTag kCustomizationDocumentNetworkTag = | 
|  | net::DefineNetworkTrafficAnnotation("customization_document", | 
|  | R"( | 
|  | semantics { | 
|  | sender: "Customization document" | 
|  | description: | 
|  | "Get OEM customization manifest from OEM specific URLs that " | 
|  | "provide custom configuration locales, wallpaper etc." | 
|  | trigger: | 
|  | "Triggered on OOBE after user accepts EULA and everytime the " | 
|  | "device boots in. Expected to run only once at OOBE. If the " | 
|  | "network request fails, retried each boot until it succeeds." | 
|  | data: "None." | 
|  | destination: GOOGLE_OWNED_SERVICE | 
|  | } | 
|  | policy { | 
|  | cookies_allowed: NO | 
|  | setting: | 
|  | "This feature is set by OEMs and can be overridden by users." | 
|  | policy_exception_justification: | 
|  | "This request is made based on OEM customization and does not " | 
|  | "send/store any sensitive data." | 
|  | })"); | 
|  |  | 
|  | struct CustomizationDocumentTestOverride { | 
|  | raw_ptr<ServicesCustomizationDocument, DanglingUntriaged> | 
|  | customization_document = nullptr; | 
|  | scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory; | 
|  | }; | 
|  |  | 
|  | // Global overrider for ServicesCustomizationDocument for tests. | 
|  | CustomizationDocumentTestOverride* g_test_overrides = nullptr; | 
|  |  | 
|  |  | 
|  | std::string GetLocaleSpecificStringImpl(const base::Value::Dict& root, | 
|  | const std::string& locale, | 
|  | const std::string& dictionary_name, | 
|  | const std::string& entry_name) { | 
|  | const base::Value::Dict* dictionary_content = root.FindDict(dictionary_name); | 
|  | if (!dictionary_content) | 
|  | return std::string(); | 
|  |  | 
|  | const base::Value::Dict* locale_dictionary = | 
|  | dictionary_content->FindDict(locale); | 
|  | if (locale_dictionary) { | 
|  | const std::string* result = locale_dictionary->FindString(entry_name); | 
|  | if (result) | 
|  | return *result; | 
|  | } | 
|  |  | 
|  | const base::Value::Dict* default_dictionary = | 
|  | dictionary_content->FindDict(kDefaultAttr); | 
|  | if (default_dictionary) { | 
|  | const std::string* result = default_dictionary->FindString(entry_name); | 
|  | if (result) | 
|  | return *result; | 
|  | } | 
|  |  | 
|  | return std::string(); | 
|  | } | 
|  |  | 
|  | void CheckWallpaperCacheExists(const base::FilePath& path, bool* exists) { | 
|  | DCHECK(exists); | 
|  | *exists = base::PathExists(path); | 
|  | } | 
|  |  | 
|  | std::string ReadFileInBackground(const base::FilePath& file) { | 
|  | base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, | 
|  | base::BlockingType::MAY_BLOCK); | 
|  |  | 
|  | std::string manifest; | 
|  | if (!base::ReadFileToString(file, &manifest)) { | 
|  | manifest.clear(); | 
|  | LOG(ERROR) << "Failed to load services customization manifest from: " | 
|  | << file.value(); | 
|  | } | 
|  |  | 
|  | return manifest; | 
|  | } | 
|  |  | 
|  | }  // anonymous namespace | 
|  |  | 
|  | // A custom extensions::ExternalLoader that the ServicesCustomizationDocument | 
|  | // creates and uses to publish OEM default apps to the extensions system. | 
|  | class ServicesCustomizationExternalLoader : public extensions::ExternalLoader { | 
|  | public: | 
|  | explicit ServicesCustomizationExternalLoader(Profile* profile) | 
|  | : profile_(profile) {} | 
|  |  | 
|  | ServicesCustomizationExternalLoader( | 
|  | const ServicesCustomizationExternalLoader&) = delete; | 
|  | ServicesCustomizationExternalLoader& operator=( | 
|  | const ServicesCustomizationExternalLoader&) = delete; | 
|  |  | 
|  | Profile* profile() { return profile_; } | 
|  |  | 
|  | // Used by the ServicesCustomizationDocument to update the current apps. | 
|  | void SetCurrentApps(base::Value::Dict prefs) { | 
|  | apps_ = std::move(prefs); | 
|  | is_apps_set_ = true; | 
|  | StartLoading(); | 
|  | } | 
|  |  | 
|  | // Implementation of extensions::ExternalLoader: | 
|  | void StartLoading() override { | 
|  | if (!is_apps_set_) { | 
|  | ServicesCustomizationDocument::GetInstance()->StartFetching(); | 
|  | // In case of missing customization ID, SetCurrentApps will be called | 
|  | // synchronously from StartFetching and this function will be called | 
|  | // recursively so we need to return to avoid calling LoadFinished twice. | 
|  | // In case of async load it is safe to return empty list because this | 
|  | // provider didn't install any app yet so no app can be removed due to | 
|  | // returning empty list. | 
|  | if (is_apps_set_) | 
|  | return; | 
|  | } | 
|  |  | 
|  | VLOG(1) << "ServicesCustomization extension loader publishing " | 
|  | << apps_.size() << " apps."; | 
|  | LoadFinished(apps_.Clone()); | 
|  | } | 
|  |  | 
|  | base::WeakPtr<ServicesCustomizationExternalLoader> AsWeakPtr() { | 
|  | return weak_ptr_factory_.GetWeakPtr(); | 
|  | } | 
|  |  | 
|  | protected: | 
|  | ~ServicesCustomizationExternalLoader() override = default; | 
|  |  | 
|  | private: | 
|  | bool is_apps_set_ = false; | 
|  | base::Value::Dict apps_; | 
|  | raw_ptr<Profile> profile_; | 
|  | base::WeakPtrFactory<ServicesCustomizationExternalLoader> weak_ptr_factory_{ | 
|  | this}; | 
|  | }; | 
|  |  | 
|  | // CustomizationDocument implementation. --------------------------------------- | 
|  |  | 
|  | CustomizationDocument::CustomizationDocument( | 
|  | const std::string& accepted_version) | 
|  | : accepted_version_(accepted_version) {} | 
|  |  | 
|  | CustomizationDocument::~CustomizationDocument() = default; | 
|  |  | 
|  | bool CustomizationDocument::LoadManifestFromFile( | 
|  | const base::FilePath& manifest_path) { | 
|  | std::string manifest; | 
|  | if (!base::ReadFileToString(manifest_path, &manifest)) | 
|  | return false; | 
|  | return LoadManifestFromString(manifest); | 
|  | } | 
|  |  | 
|  | bool CustomizationDocument::LoadManifestFromString( | 
|  | const std::string& manifest) { | 
|  | auto parsed_json = base::JSONReader::ReadAndReturnValueWithError( | 
|  | manifest, | 
|  | base::JSON_ALLOW_TRAILING_COMMAS | base::JSON_PARSE_CHROMIUM_EXTENSIONS); | 
|  | if (!parsed_json.has_value()) { | 
|  | NOTREACHED() << parsed_json.error().message; | 
|  | } | 
|  |  | 
|  | if (!parsed_json->is_dict()) { | 
|  | NOTREACHED(); | 
|  | } | 
|  |  | 
|  | root_ = | 
|  | std::make_unique<base::Value::Dict>(std::move(*parsed_json).TakeDict()); | 
|  |  | 
|  | const std::string* result = root_->FindString(kVersionAttr); | 
|  | if (!result || *result != accepted_version_) { | 
|  | LOG(ERROR) << "Wrong customization manifest version"; | 
|  | root_.reset(); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | std::string CustomizationDocument::GetLocaleSpecificString( | 
|  | const std::string& locale, | 
|  | const std::string& dictionary_name, | 
|  | const std::string& entry_name) const { | 
|  | return GetLocaleSpecificStringImpl(*root_, locale, dictionary_name, | 
|  | entry_name); | 
|  | } | 
|  |  | 
|  | // StartupCustomizationDocument implementation. -------------------------------- | 
|  |  | 
|  | StartupCustomizationDocument::StartupCustomizationDocument() | 
|  | : CustomizationDocument(kAcceptedManifestVersion) { | 
|  | { | 
|  | // Loading manifest causes us to do blocking IO on UI thread. | 
|  | // Temporarily allow it until we fix http://crosbug.com/11103 | 
|  | base::ScopedAllowBlocking allow_blocking; | 
|  | base::FilePath startup_customization_manifest; | 
|  | base::PathService::Get(FILE_STARTUP_CUSTOMIZATION_MANIFEST, | 
|  | &startup_customization_manifest); | 
|  | LoadManifestFromFile(startup_customization_manifest); | 
|  | } | 
|  | Init(system::StatisticsProvider::GetInstance()); | 
|  | } | 
|  |  | 
|  | StartupCustomizationDocument::StartupCustomizationDocument( | 
|  | system::StatisticsProvider* statistics_provider, | 
|  | const std::string& manifest) | 
|  | : CustomizationDocument(kAcceptedManifestVersion) { | 
|  | LoadManifestFromString(manifest); | 
|  | Init(statistics_provider); | 
|  | } | 
|  |  | 
|  | StartupCustomizationDocument::~StartupCustomizationDocument() = default; | 
|  |  | 
|  | StartupCustomizationDocument* StartupCustomizationDocument::GetInstance() { | 
|  | return base::Singleton< | 
|  | StartupCustomizationDocument, | 
|  | base::DefaultSingletonTraits<StartupCustomizationDocument>>::get(); | 
|  | } | 
|  |  | 
|  | void StartupCustomizationDocument::Init( | 
|  | system::StatisticsProvider* statistics_provider) { | 
|  | if (IsReady()) { | 
|  | const std::string* initial_locale_ptr = | 
|  | root_->FindString(kInitialLocaleAttr); | 
|  | if (initial_locale_ptr) | 
|  | initial_locale_ = *initial_locale_ptr; | 
|  |  | 
|  | const std::string* initial_timezone_ptr = | 
|  | root_->FindString(kInitialTimezoneAttr); | 
|  | if (initial_timezone_ptr) | 
|  | initial_timezone_ = *initial_timezone_ptr; | 
|  |  | 
|  | const std::string* keyboard_layout_ptr = | 
|  | root_->FindString(kKeyboardLayoutAttr); | 
|  | if (keyboard_layout_ptr) | 
|  | keyboard_layout_ = *keyboard_layout_ptr; | 
|  |  | 
|  | if (const std::optional<std::string_view> hwid = | 
|  | statistics_provider->GetMachineStatistic( | 
|  | system::kHardwareClassKey)) { | 
|  | base::Value::List* hwid_list = root_->FindList(kHwidMapAttr); | 
|  | if (hwid_list) { | 
|  | for (const base::Value& hwid_value : *hwid_list) { | 
|  | const base::Value::Dict* hwid_dictionary = nullptr; | 
|  | if (hwid_value.is_dict()) | 
|  | hwid_dictionary = &hwid_value.GetDict(); | 
|  |  | 
|  | const std::string* hwid_mask = | 
|  | hwid_dictionary ? hwid_dictionary->FindString(kHwidMaskAttr) | 
|  | : nullptr; | 
|  | if (hwid_mask) { | 
|  | if (base::MatchPattern(hwid.value(), *hwid_mask)) { | 
|  | // If HWID for this machine matches some mask, use HWID specific | 
|  | // settings. | 
|  | const std::string* initial_locale = | 
|  | hwid_dictionary->FindString(kInitialLocaleAttr); | 
|  | if (initial_locale) | 
|  | initial_locale_ = *initial_locale; | 
|  |  | 
|  | const std::string* initial_timezone = | 
|  | hwid_dictionary->FindString(kInitialTimezoneAttr); | 
|  | if (initial_timezone) | 
|  | initial_timezone_ = *initial_timezone; | 
|  |  | 
|  | const std::string* keyboard_layout = | 
|  | hwid_dictionary->FindString(kKeyboardLayoutAttr); | 
|  | if (keyboard_layout) | 
|  | keyboard_layout_ = *keyboard_layout; | 
|  | } | 
|  | // Don't break here to allow other entires to be applied if match. | 
|  | } else { | 
|  | LOG(ERROR) << "Syntax error in customization manifest"; | 
|  | } | 
|  | } | 
|  | } | 
|  | } else { | 
|  | LOG(ERROR) << "HWID is missing in machine statistics"; | 
|  | } | 
|  | } | 
|  |  | 
|  | // If manifest doesn't exist still apply values from VPD. | 
|  | if (const std::optional<std::string_view> locale_statistic = | 
|  | statistics_provider->GetMachineStatistic(system::kInitialLocaleKey)) { | 
|  | initial_locale_ = std::string(locale_statistic.value()); | 
|  | } | 
|  | if (const std::optional<std::string_view> timezone_statistic = | 
|  | statistics_provider->GetMachineStatistic( | 
|  | system::kInitialTimezoneKey)) { | 
|  | initial_timezone_ = std::string(timezone_statistic.value()); | 
|  | } | 
|  | if (const std::optional<std::string_view> keyboard_statistic = | 
|  | statistics_provider->GetMachineStatistic( | 
|  | system::kKeyboardLayoutKey)) { | 
|  | keyboard_layout_ = std::string(keyboard_statistic.value()); | 
|  | } | 
|  | configured_locales_ = base::SplitString( | 
|  | initial_locale_, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); | 
|  |  | 
|  | // Convert ICU locale to chrome ("en_US" to "en-US", etc.). | 
|  | std::ranges::for_each(configured_locales_, base::i18n::GetCanonicalLocale); | 
|  |  | 
|  | // Let's always have configured_locales_.front() a valid entry. | 
|  | if (configured_locales_.size() == 0) | 
|  | configured_locales_.push_back(std::string()); | 
|  | } | 
|  |  | 
|  | const std::vector<std::string>& | 
|  | StartupCustomizationDocument::configured_locales() const { | 
|  | return configured_locales_; | 
|  | } | 
|  |  | 
|  | const std::string& StartupCustomizationDocument::initial_locale_default() | 
|  | const { | 
|  | DCHECK_GT(configured_locales_.size(), 0UL); | 
|  | return configured_locales_.front(); | 
|  | } | 
|  |  | 
|  | std::string StartupCustomizationDocument::GetEULAPage( | 
|  | const std::string& locale) const { | 
|  | return GetLocaleSpecificString(locale, kSetupContentAttr, kEulaPageAttr); | 
|  | } | 
|  |  | 
|  | // ServicesCustomizationDocument implementation. ------------------------------- | 
|  |  | 
|  | class ServicesCustomizationDocument::ApplyingTask { | 
|  | public: | 
|  | // Registers in ServicesCustomizationDocument; | 
|  | explicit ApplyingTask(ServicesCustomizationDocument* document); | 
|  |  | 
|  | // Do not automatically deregister as we might be called on invalid thread. | 
|  | ~ApplyingTask(); | 
|  |  | 
|  | // Mark task finished and check for customization applied. | 
|  | void Finished(bool success); | 
|  |  | 
|  | private: | 
|  | raw_ptr<ServicesCustomizationDocument> document_; | 
|  |  | 
|  | // This is error-checking flag to prevent destroying unfinished task | 
|  | // or double finish. | 
|  | bool engaged_; | 
|  | }; | 
|  |  | 
|  | ServicesCustomizationDocument::ApplyingTask::ApplyingTask( | 
|  | ServicesCustomizationDocument* document) | 
|  | : document_(document), engaged_(true) { | 
|  | document->ApplyingTaskStarted(); | 
|  | } | 
|  |  | 
|  | ServicesCustomizationDocument::ApplyingTask::~ApplyingTask() { | 
|  | DCHECK(!engaged_); | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::ApplyingTask::Finished(bool success) { | 
|  | DCHECK(engaged_); | 
|  | if (engaged_) { | 
|  | engaged_ = false; | 
|  | document_->ApplyingTaskFinished(success); | 
|  | } | 
|  | } | 
|  |  | 
|  | ServicesCustomizationDocument::ServicesCustomizationDocument() | 
|  | : CustomizationDocument(kAcceptedManifestVersion), | 
|  | num_retries_(0), | 
|  | load_started_(false), | 
|  | apply_tasks_started_(0), | 
|  | apply_tasks_finished_(0), | 
|  | apply_tasks_success_(0) {} | 
|  |  | 
|  | ServicesCustomizationDocument::ServicesCustomizationDocument( | 
|  | const std::string& manifest) | 
|  | : CustomizationDocument(kAcceptedManifestVersion), | 
|  | apply_tasks_started_(0), | 
|  | apply_tasks_finished_(0), | 
|  | apply_tasks_success_(0) { | 
|  | LoadManifestFromString(manifest); | 
|  | } | 
|  |  | 
|  | ServicesCustomizationDocument::~ServicesCustomizationDocument() = default; | 
|  |  | 
|  | // static | 
|  | ServicesCustomizationDocument* ServicesCustomizationDocument::GetInstance() { | 
|  | if (g_test_overrides) | 
|  | return g_test_overrides->customization_document; | 
|  |  | 
|  | return base::Singleton< | 
|  | ServicesCustomizationDocument, | 
|  | base::DefaultSingletonTraits<ServicesCustomizationDocument>>::get(); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void ServicesCustomizationDocument::RegisterPrefs( | 
|  | PrefRegistrySimple* registry) { | 
|  | registry->RegisterBooleanPref(kServicesCustomizationAppliedPref, false); | 
|  | registry->RegisterStringPref(prefs::kCustomizationDefaultWallpaperURL, | 
|  | std::string()); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void ServicesCustomizationDocument::RegisterProfilePrefs( | 
|  | user_prefs::PrefRegistrySyncable* registry) { | 
|  | registry->RegisterDictionaryPref(kServicesCustomizationKey); | 
|  | } | 
|  |  | 
|  | // static | 
|  | bool ServicesCustomizationDocument::WasOOBECustomizationApplied() { | 
|  | PrefService* prefs = g_browser_process->local_state(); | 
|  | // prefs can be NULL in some tests. | 
|  | if (prefs) | 
|  | return prefs->GetBoolean(kServicesCustomizationAppliedPref); | 
|  | else | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // static | 
|  | void ServicesCustomizationDocument::SetApplied(bool val) { | 
|  | PrefService* prefs = g_browser_process->local_state(); | 
|  | // prefs can be NULL in some tests. | 
|  | if (prefs) | 
|  | prefs->SetBoolean(kServicesCustomizationAppliedPref, val); | 
|  | } | 
|  |  | 
|  | // static | 
|  | base::FilePath ServicesCustomizationDocument::GetCustomizedWallpaperCacheDir() { | 
|  | base::FilePath custom_wallpaper_dir; | 
|  | if (!base::PathService::Get(ash::DIR_CUSTOM_WALLPAPERS, | 
|  | &custom_wallpaper_dir)) { | 
|  | LOG(DFATAL) << "Unable to get custom wallpaper dir."; | 
|  | return base::FilePath(); | 
|  | } | 
|  | return custom_wallpaper_dir.Append(kCustomizationDefaultWallpaperDir); | 
|  | } | 
|  |  | 
|  | // static | 
|  | base::FilePath | 
|  | ServicesCustomizationDocument::GetCustomizedWallpaperDownloadedFileName() { | 
|  | const base::FilePath dir = GetCustomizedWallpaperCacheDir(); | 
|  | if (dir.empty()) { | 
|  | NOTREACHED(); | 
|  | } | 
|  | return dir.Append(kCustomizationDefaultWallpaperDownloadedFile); | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::EnsureCustomizationApplied() { | 
|  | if (WasOOBECustomizationApplied()) | 
|  | return; | 
|  |  | 
|  | // When customization manifest is fetched, applying will start automatically. | 
|  | if (IsReady()) | 
|  | return; | 
|  |  | 
|  | StartFetching(); | 
|  | } | 
|  |  | 
|  | base::OnceClosure | 
|  | ServicesCustomizationDocument::EnsureCustomizationAppliedClosure() { | 
|  | return base::BindOnce( | 
|  | &ServicesCustomizationDocument::EnsureCustomizationApplied, | 
|  | weak_ptr_factory_.GetWeakPtr()); | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::StartFetching() { | 
|  | if (IsReady() || load_started_) | 
|  | return; | 
|  |  | 
|  | if (!url_.is_valid()) { | 
|  | system::StatisticsProvider* provider = | 
|  | system::StatisticsProvider::GetInstance(); | 
|  | const std::optional<std::string_view> customization_id = | 
|  | provider->GetMachineStatistic(system::kCustomizationIdKey); | 
|  | if (customization_id && !customization_id->empty()) { | 
|  | url_ = GURL(base::StringPrintf( | 
|  | kManifestUrl, base::ToLowerASCII(customization_id.value()).c_str())); | 
|  | } else { | 
|  | // Remember that there is no customization ID in VPD. | 
|  | OnCustomizationNotFound(); | 
|  | return; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (url_.is_valid()) { | 
|  | load_started_ = true; | 
|  | if (url_.SchemeIsFile()) { | 
|  | base::ThreadPool::PostTaskAndReplyWithResult( | 
|  | FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()}, | 
|  | base::BindOnce(&ReadFileInBackground, base::FilePath(url_.GetPath())), | 
|  | base::BindOnce(&ServicesCustomizationDocument::OnManifestRead, | 
|  | weak_ptr_factory_.GetWeakPtr())); | 
|  | } else { | 
|  | StartFileFetch(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::OnManifestRead( | 
|  | const std::string& manifest) { | 
|  | if (!manifest.empty()) | 
|  | LoadManifestFromString(manifest); | 
|  |  | 
|  | load_started_ = false; | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::StartFileFetch() { | 
|  | if (custom_network_delay_) { | 
|  | DelayNetworkCallWithCustomDelay( | 
|  | base::BindOnce(&ServicesCustomizationDocument::DoStartFileFetch, | 
|  | weak_ptr_factory_.GetWeakPtr()), | 
|  | custom_network_delay_.value()); | 
|  | } else { | 
|  | DelayNetworkCall( | 
|  | base::BindOnce(&ServicesCustomizationDocument::DoStartFileFetch, | 
|  | weak_ptr_factory_.GetWeakPtr())); | 
|  | } | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::DoStartFileFetch() { | 
|  | auto request = std::make_unique<network::ResourceRequest>(); | 
|  | request->url = url_; | 
|  | request->load_flags = net::LOAD_DISABLE_CACHE; | 
|  | request->credentials_mode = network::mojom::CredentialsMode::kOmit; | 
|  | request->headers.SetHeader("Accept", "application/json"); | 
|  |  | 
|  | url_loader_ = network::SimpleURLLoader::Create( | 
|  | std::move(request), kCustomizationDocumentNetworkTag); | 
|  |  | 
|  | url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( | 
|  | g_test_overrides ? g_test_overrides->url_loader_factory.get() | 
|  | : g_browser_process->shared_url_loader_factory().get(), | 
|  | base::BindOnce(&ServicesCustomizationDocument::OnSimpleLoaderComplete, | 
|  | base::Unretained(this))); | 
|  | } | 
|  |  | 
|  | bool ServicesCustomizationDocument::LoadManifestFromString( | 
|  | const std::string& manifest) { | 
|  | if (CustomizationDocument::LoadManifestFromString(manifest)) { | 
|  | OnManifestLoaded(); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | return false; | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::OnManifestLoaded() { | 
|  | if (!WasOOBECustomizationApplied()) | 
|  | ApplyOOBECustomization(); | 
|  |  | 
|  | auto prefs = GetDefaultAppsInProviderFormat(*root_); | 
|  | for (auto& external_loader : external_loaders_) { | 
|  | if (external_loader) { | 
|  | UpdateCachedManifest(external_loader->profile()); | 
|  | external_loader->SetCurrentApps(prefs.Clone()); | 
|  | SetOemFolderName(external_loader->profile(), *root_); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::OnSimpleLoaderComplete( | 
|  | std::unique_ptr<std::string> response_body) { | 
|  | int response_code = -1; | 
|  | std::string mime_type; | 
|  | if (url_loader_->ResponseInfo() && url_loader_->ResponseInfo()->headers) { | 
|  | response_code = url_loader_->ResponseInfo()->headers->response_code(); | 
|  | url_loader_->ResponseInfo()->headers->GetMimeType(&mime_type); | 
|  | } | 
|  |  | 
|  | if (response_body && mime_type == "application/json") { | 
|  | LoadManifestFromString(*response_body); | 
|  | } else if (response_code == net::HTTP_NOT_FOUND) { | 
|  | LOG(ERROR) << "Customization manifest is missing on server: " | 
|  | << url_.spec(); | 
|  | OnCustomizationNotFound(); | 
|  | } else { | 
|  | if (num_retries_ < kMaxFetchRetries) { | 
|  | num_retries_++; | 
|  | content::GetUIThreadTaskRunner({})->PostDelayedTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce(&ServicesCustomizationDocument::StartFileFetch, | 
|  | weak_ptr_factory_.GetWeakPtr()), | 
|  | base::Seconds(kRetriesDelayInSec)); | 
|  | return; | 
|  | } | 
|  | // This doesn't stop fetching manifest on next restart. | 
|  | LOG(ERROR) << "URL fetch for services customization failed:" | 
|  | << " response code = " << response_code | 
|  | << " URL = " << url_.spec(); | 
|  |  | 
|  | } | 
|  | load_started_ = false; | 
|  | } | 
|  |  | 
|  | bool ServicesCustomizationDocument::ApplyOOBECustomization() { | 
|  | if (apply_tasks_started_) | 
|  | return false; | 
|  |  | 
|  | CheckAndApplyWallpaper(); | 
|  | return false; | 
|  | } | 
|  |  | 
|  | bool ServicesCustomizationDocument::GetDefaultWallpaperUrl( | 
|  | GURL* out_url) const { | 
|  | if (!IsReady()) | 
|  | return false; | 
|  |  | 
|  | const std::string* url = root_->FindString(kDefaultWallpaperAttr); | 
|  | if (!url) | 
|  | return false; | 
|  |  | 
|  | *out_url = GURL(*url); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | std::optional<base::Value::Dict> ServicesCustomizationDocument::GetDefaultApps() | 
|  | const { | 
|  | if (!IsReady()) | 
|  | return std::nullopt; | 
|  |  | 
|  | return GetDefaultAppsInProviderFormat(*root_); | 
|  | } | 
|  |  | 
|  | std::string ServicesCustomizationDocument::GetOemAppsFolderName( | 
|  | const std::string& locale) const { | 
|  | if (!IsReady()) | 
|  | return std::string(); | 
|  |  | 
|  | return GetOemAppsFolderNameImpl(locale, *root_); | 
|  | } | 
|  |  | 
|  | base::Value::Dict ServicesCustomizationDocument::GetDefaultAppsInProviderFormat( | 
|  | const base::Value::Dict& root) { | 
|  | base::Value::Dict prefs; | 
|  | const base::Value::List* apps_list = root.FindList(kDefaultAppsAttr); | 
|  | if (apps_list) { | 
|  | for (const base::Value& app_entry_value : *apps_list) { | 
|  | std::string app_id; | 
|  | base::Value::Dict entry; | 
|  | if (app_entry_value.is_string()) { | 
|  | app_id = app_entry_value.GetString(); | 
|  | } else if (app_entry_value.is_dict()) { | 
|  | const base::Value::Dict& app_entry = app_entry_value.GetDict(); | 
|  | const std::string* app_id_ptr = app_entry.FindString(kIdAttr); | 
|  | if (!app_id_ptr) { | 
|  | LOG(ERROR) << "Wrong format of default application list"; | 
|  | prefs.clear(); | 
|  | break; | 
|  | } | 
|  | app_id = *app_id_ptr; | 
|  | entry = app_entry.Clone(); | 
|  | entry.Remove(kIdAttr); | 
|  | } else { | 
|  | LOG(ERROR) << "Wrong format of default application list"; | 
|  | prefs.clear(); | 
|  | break; | 
|  | } | 
|  | if (!entry.Find(extensions::ExternalProviderImpl::kExternalUpdateUrl)) { | 
|  | entry.Set(extensions::ExternalProviderImpl::kExternalUpdateUrl, | 
|  | extension_urls::GetWebstoreUpdateUrl().spec()); | 
|  | } | 
|  | prefs.SetByDottedPath(app_id, std::move(entry)); | 
|  | } | 
|  | } | 
|  |  | 
|  | return prefs; | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::UpdateCachedManifest(Profile* profile) { | 
|  | profile->GetPrefs()->SetDict(kServicesCustomizationKey, root_->Clone()); | 
|  | } | 
|  |  | 
|  | extensions::ExternalLoader* ServicesCustomizationDocument::CreateExternalLoader( | 
|  | Profile* profile) { | 
|  | ServicesCustomizationExternalLoader* loader = | 
|  | new ServicesCustomizationExternalLoader(profile); | 
|  | external_loaders_.push_back(loader->AsWeakPtr()); | 
|  |  | 
|  | if (IsReady()) { | 
|  | UpdateCachedManifest(profile); | 
|  | loader->SetCurrentApps(GetDefaultAppsInProviderFormat(*root_)); | 
|  | SetOemFolderName(profile, *root_); | 
|  | } else { | 
|  | const base::Value::Dict& root = | 
|  | profile->GetPrefs()->GetDict(kServicesCustomizationKey); | 
|  | if (root.FindString(kVersionAttr)) { | 
|  | // If version exists, profile has cached version of customization. | 
|  | loader->SetCurrentApps(GetDefaultAppsInProviderFormat(root)); | 
|  | SetOemFolderName(profile, root); | 
|  | } else { | 
|  | // StartFetching will be called from ServicesCustomizationExternalLoader | 
|  | // when StartLoading is called. We can't initiate manifest fetch here | 
|  | // because caller may never call StartLoading for the provider. | 
|  | } | 
|  | } | 
|  |  | 
|  | return loader; | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::OnCustomizationNotFound() { | 
|  | LoadManifestFromString(kEmptyServicesCustomizationManifest); | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::SetOemFolderName( | 
|  | Profile* profile, | 
|  | const base::Value::Dict& root) { | 
|  | std::string locale = g_browser_process->GetApplicationLocale(); | 
|  | std::string name = GetOemAppsFolderNameImpl(locale, root); | 
|  | if (name.empty()) | 
|  | name = chromeos::default_app_order::GetOemAppsFolderName(); | 
|  | if (!name.empty()) { | 
|  | app_list::AppListSyncableService* service = | 
|  | app_list::AppListSyncableServiceFactory::GetForProfile(profile); | 
|  | if (!service) { | 
|  | LOG(WARNING) << "AppListSyncableService is not ready for setting OEM " | 
|  | "folder name"; | 
|  | return; | 
|  | } | 
|  | service->SetOemFolderName(name); | 
|  | } | 
|  | } | 
|  |  | 
|  | std::string ServicesCustomizationDocument::GetOemAppsFolderNameImpl( | 
|  | const std::string& locale, | 
|  | const base::Value::Dict& root) const { | 
|  | return GetLocaleSpecificStringImpl(root, locale, kLocalizedContent, | 
|  | kDefaultAppsFolderName); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void ServicesCustomizationDocument::InitializeForTesting( | 
|  | scoped_refptr<network::SharedURLLoaderFactory> factory) { | 
|  | g_test_overrides = new CustomizationDocumentTestOverride; | 
|  | g_test_overrides->customization_document = new ServicesCustomizationDocument; | 
|  | // `base::TimeDelta()` means zero time delta - i.e. the request will be | 
|  | // started immediately. | 
|  | g_test_overrides->customization_document->custom_network_delay_ = | 
|  | std::make_optional(base::TimeDelta()); | 
|  | g_test_overrides->url_loader_factory = std::move(factory); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void ServicesCustomizationDocument::ShutdownForTesting() { | 
|  | delete g_test_overrides->customization_document; | 
|  | delete g_test_overrides; | 
|  | g_test_overrides = nullptr; | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::StartOEMWallpaperDownload( | 
|  | const GURL& wallpaper_url, | 
|  | std::unique_ptr<ServicesCustomizationDocument::ApplyingTask> applying) { | 
|  | DCHECK(wallpaper_url.is_valid()); | 
|  |  | 
|  | const base::FilePath dir = GetCustomizedWallpaperCacheDir(); | 
|  | const base::FilePath file = GetCustomizedWallpaperDownloadedFileName(); | 
|  | if (dir.empty() || file.empty()) { | 
|  | NOTREACHED(); | 
|  | } | 
|  |  | 
|  | wallpaper_downloader_ = std::make_unique<CustomizationWallpaperDownloader>( | 
|  | wallpaper_url, dir, file, | 
|  | base::BindOnce(&ServicesCustomizationDocument::OnOEMWallpaperDownloaded, | 
|  | weak_ptr_factory_.GetWeakPtr(), std::move(applying))); | 
|  |  | 
|  | wallpaper_downloader_->Start(); | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::CheckAndApplyWallpaper() { | 
|  | if (wallpaper_downloader_.get()) { | 
|  | VLOG(1) << "CheckAndApplyWallpaper(): download has already started."; | 
|  | return; | 
|  | } | 
|  | std::unique_ptr<ServicesCustomizationDocument::ApplyingTask> applying( | 
|  | new ServicesCustomizationDocument::ApplyingTask(this)); | 
|  |  | 
|  | GURL wallpaper_url; | 
|  | if (!GetDefaultWallpaperUrl(&wallpaper_url)) { | 
|  | PrefService* pref_service = g_browser_process->local_state(); | 
|  | std::string current_url = | 
|  | pref_service->GetString(prefs::kCustomizationDefaultWallpaperURL); | 
|  | if (!current_url.empty()) { | 
|  | VLOG(1) << "ServicesCustomizationDocument::CheckAndApplyWallpaper() : " | 
|  | << "No wallpaper URL attribute in customization document, " | 
|  | << "but current value is non-empty: '" << current_url | 
|  | << "'. Ignored."; | 
|  | } | 
|  | applying->Finished(true); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Should fail if this ever happens in tests. | 
|  | DCHECK(wallpaper_url.is_valid()); | 
|  | if (!wallpaper_url.is_valid()) { | 
|  | if (!wallpaper_url.is_empty()) { | 
|  | LOG(WARNING) << "Invalid Customized Wallpaper URL '" | 
|  | << wallpaper_url.spec() << "'."; | 
|  | } | 
|  | applying->Finished(false); | 
|  | return; | 
|  | } | 
|  |  | 
|  | std::unique_ptr<bool> exists(new bool(false)); | 
|  |  | 
|  | base::OnceClosure check_file_exists = base::BindOnce( | 
|  | &CheckWallpaperCacheExists, GetCustomizedWallpaperDownloadedFileName(), | 
|  | base::Unretained(exists.get())); | 
|  | base::OnceClosure on_checked_closure = base::BindOnce( | 
|  | &ServicesCustomizationDocument::OnCheckedWallpaperCacheExists, | 
|  | weak_ptr_factory_.GetWeakPtr(), std::move(exists), std::move(applying)); | 
|  | base::ThreadPool::PostTaskAndReply( | 
|  | FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT}, | 
|  | std::move(check_file_exists), std::move(on_checked_closure)); | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::OnCheckedWallpaperCacheExists( | 
|  | std::unique_ptr<bool> exists, | 
|  | std::unique_ptr<ServicesCustomizationDocument::ApplyingTask> applying) { | 
|  | DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | 
|  | DCHECK(exists); | 
|  | DCHECK(applying); | 
|  |  | 
|  | ApplyWallpaper(*exists, std::move(applying)); | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::ApplyWallpaper( | 
|  | bool default_wallpaper_file_exists, | 
|  | std::unique_ptr<ServicesCustomizationDocument::ApplyingTask> applying) { | 
|  | GURL wallpaper_url; | 
|  | const bool wallpaper_url_present = GetDefaultWallpaperUrl(&wallpaper_url); | 
|  |  | 
|  | PrefService* pref_service = g_browser_process->local_state(); | 
|  |  | 
|  | std::string current_url = | 
|  | pref_service->GetString(prefs::kCustomizationDefaultWallpaperURL); | 
|  | if (current_url != wallpaper_url.spec()) { | 
|  | if (wallpaper_url_present) { | 
|  | VLOG(1) << "ServicesCustomizationDocument::ApplyWallpaper() : " | 
|  | << "Wallpaper URL in customization document '" | 
|  | << wallpaper_url.spec() << "' differs from current '" | 
|  | << current_url << "'." | 
|  | << (GURL(current_url).is_valid() && default_wallpaper_file_exists | 
|  | ? " Ignored." | 
|  | : " Will refetch."); | 
|  | } else { | 
|  | VLOG(1) << "ServicesCustomizationDocument::ApplyWallpaper() : " | 
|  | << "No wallpaper URL attribute in customization document, " | 
|  | << "but current value is non-empty: '" << current_url | 
|  | << "'. Ignored."; | 
|  | } | 
|  | } | 
|  | if (!wallpaper_url_present) { | 
|  | applying->Finished(true); | 
|  | return; | 
|  | } | 
|  |  | 
|  | DCHECK(wallpaper_url.is_valid()); | 
|  |  | 
|  | // Never update system-wide wallpaper (i.e. do not check | 
|  | // current_url == wallpaper_url.spec() ) | 
|  | if (GURL(current_url).is_valid() && default_wallpaper_file_exists) { | 
|  | VLOG(1) | 
|  | << "ServicesCustomizationDocument::ApplyWallpaper() : reuse existing"; | 
|  | OnOEMWallpaperDownloaded(std::move(applying), true, GURL(current_url)); | 
|  | } else { | 
|  | VLOG(1) | 
|  | << "ServicesCustomizationDocument::ApplyWallpaper() : start download"; | 
|  | StartOEMWallpaperDownload(wallpaper_url, std::move(applying)); | 
|  | } | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::OnOEMWallpaperDownloaded( | 
|  | std::unique_ptr<ServicesCustomizationDocument::ApplyingTask> applying, | 
|  | bool success, | 
|  | const GURL& wallpaper_url) { | 
|  | if (success) { | 
|  | DCHECK(wallpaper_url.is_valid()); | 
|  |  | 
|  | VLOG(1) << "Setting default wallpaper to '" | 
|  | << GetCustomizedWallpaperDownloadedFileName().value() << "' ('" | 
|  | << wallpaper_url.spec() << "')"; | 
|  | customization_wallpaper_util::StartSettingCustomizedDefaultWallpaper( | 
|  | wallpaper_url, GetCustomizedWallpaperDownloadedFileName()); | 
|  | } | 
|  | wallpaper_downloader_.reset(); | 
|  | applying->Finished(success); | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::ApplyingTaskStarted() { | 
|  | ++apply_tasks_started_; | 
|  | } | 
|  |  | 
|  | void ServicesCustomizationDocument::ApplyingTaskFinished(bool success) { | 
|  | DCHECK_GT(apply_tasks_started_, apply_tasks_finished_); | 
|  | ++apply_tasks_finished_; | 
|  |  | 
|  | apply_tasks_success_ += success; | 
|  |  | 
|  | if (apply_tasks_started_ != apply_tasks_finished_) | 
|  | return; | 
|  |  | 
|  | if (apply_tasks_success_ == apply_tasks_finished_) | 
|  | SetApplied(true); | 
|  | } | 
|  |  | 
|  | }  // namespace ash |