blob: 2445347918e5cf3b87ac03daa6c09da9afa90c5c [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/app_mode/kiosk_app_data.h"
#include <memory>
#include <vector>
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/ref_counted_memory.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/ash/app_mode/kiosk_app_data_delegate.h"
#include "chrome/browser/ash/app_mode/kiosk_app_manager.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/webstore_data_fetcher.h"
#include "chrome/browser/extensions/webstore_install_helper.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/image_loader.h"
#include "extensions/browser/sandboxed_unpacker.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/common/manifest_handlers/kiosk_mode_info.h"
#include "extensions/common/verifier_formats.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/image/image.h"
using content::BrowserThread;
namespace ash {
namespace {
// Keys for local state data. See sample layout in KioskAppManager.
constexpr char kKeyRequiredPlatformVersion[] = "required_platform_version";
bool ignore_kiosk_app_data_load_failures_for_testing = false;
// Returns true for valid kiosk app manifest.
bool IsValidKioskAppManifest(const extensions::Manifest& manifest) {
bool kiosk_enabled;
if (manifest.GetBoolean(extensions::manifest_keys::kKioskEnabled,
&kiosk_enabled)) {
return kiosk_enabled;
}
return false;
}
std::string ValueToString(const base::Value& value) {
std::string json;
base::JSONWriter::Write(value, &json);
return json;
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// KioskAppData::CrxLoader
// Loads meta data from crx file.
class KioskAppData::CrxLoader : public extensions::SandboxedUnpackerClient {
public:
CrxLoader(const base::WeakPtr<KioskAppData>& client,
const base::FilePath& crx_file)
: client_(client),
crx_file_(crx_file),
success_(false),
task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {}
void Start() {
task_runner_->PostTask(FROM_HERE,
base::BindOnce(&CrxLoader::StartInThreadPool, this));
}
bool success() const { return success_; }
const base::FilePath& crx_file() const { return crx_file_; }
const std::string& name() const { return name_; }
const SkBitmap& icon() const { return icon_; }
const std::string& required_platform_version() const {
return required_platform_version_;
}
private:
~CrxLoader() override = default;
// extensions::SandboxedUnpackerClient
void OnUnpackSuccess(const base::FilePath& temp_dir,
const base::FilePath& extension_root,
std::unique_ptr<base::DictionaryValue> original_manifest,
const extensions::Extension* extension,
const SkBitmap& install_icon,
extensions::declarative_net_request::RulesetInstallPrefs
ruleset_install_prefs) override {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
const extensions::KioskModeInfo* info =
extensions::KioskModeInfo::Get(extension);
if (info == nullptr) {
LOG(ERROR) << extension->id() << " is not a valid kiosk app.";
success_ = false;
} else {
success_ = true;
name_ = extension->name();
icon_ = install_icon;
required_platform_version_ = info->required_platform_version;
}
NotifyFinishedInThreadPool();
}
void OnUnpackFailure(const extensions::CrxInstallError& error) override {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
success_ = false;
NotifyFinishedInThreadPool();
}
void StartInThreadPool() {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
if (!temp_dir_.CreateUniqueTempDir()) {
success_ = false;
NotifyFinishedInThreadPool();
return;
}
auto unpacker = base::MakeRefCounted<extensions::SandboxedUnpacker>(
extensions::mojom::ManifestLocation::kInternal,
extensions::Extension::NO_FLAGS, temp_dir_.GetPath(),
task_runner_.get(), this);
unpacker->StartWithCrx(extensions::CRXFileInfo(
crx_file_, extensions::GetPolicyVerifierFormat()));
}
void NotifyFinishedInThreadPool() {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
if (!temp_dir_.Delete()) {
LOG(WARNING) << "Can not delete temp directory at "
<< temp_dir_.GetPath().value();
}
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&CrxLoader::NotifyFinishedOnUIThread, this));
}
void NotifyFinishedOnUIThread() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (client_)
client_->OnCrxLoadFinished(this);
}
base::WeakPtr<KioskAppData> client_;
base::FilePath crx_file_;
bool success_;
const scoped_refptr<base::SequencedTaskRunner> task_runner_;
base::ScopedTempDir temp_dir_;
// Extracted meta data.
std::string name_;
SkBitmap icon_;
std::string required_platform_version_;
DISALLOW_COPY_AND_ASSIGN(CrxLoader);
};
////////////////////////////////////////////////////////////////////////////////
// KioskAppData::WebstoreDataParser
// Use WebstoreInstallHelper to parse the manifest and decode the icon.
class KioskAppData::WebstoreDataParser
: public extensions::WebstoreInstallHelper::Delegate {
public:
explicit WebstoreDataParser(const base::WeakPtr<KioskAppData>& client)
: client_(client) {}
void Start(const std::string& app_id,
const std::string& manifest,
const GURL& icon_url,
network::mojom::URLLoaderFactory* loader_factory) {
scoped_refptr<extensions::WebstoreInstallHelper> webstore_helper =
new extensions::WebstoreInstallHelper(this, app_id, manifest, icon_url);
webstore_helper->Start(loader_factory);
}
private:
friend class base::RefCounted<WebstoreDataParser>;
~WebstoreDataParser() override = default;
void ReportFailure() {
if (client_)
client_->OnWebstoreParseFailure();
delete this;
}
// WebstoreInstallHelper::Delegate overrides:
void OnWebstoreParseSuccess(
const std::string& id,
const SkBitmap& icon,
std::unique_ptr<base::DictionaryValue> parsed_manifest) override {
extensions::Manifest manifest(
extensions::mojom::ManifestLocation::kInvalidLocation,
std::move(parsed_manifest), id);
if (!IsValidKioskAppManifest(manifest)) {
ReportFailure();
return;
}
std::string required_platform_version;
if (manifest.HasPath(
extensions::manifest_keys::kKioskRequiredPlatformVersion) &&
(!manifest.GetString(
extensions::manifest_keys::kKioskRequiredPlatformVersion,
&required_platform_version) ||
!extensions::KioskModeInfo::IsValidPlatformVersion(
required_platform_version))) {
ReportFailure();
return;
}
if (client_)
client_->OnWebstoreParseSuccess(icon, required_platform_version);
delete this;
}
void OnWebstoreParseFailure(const std::string& id,
InstallHelperResultCode result_code,
const std::string& error_message) override {
ReportFailure();
}
base::WeakPtr<KioskAppData> client_;
DISALLOW_COPY_AND_ASSIGN(WebstoreDataParser);
};
////////////////////////////////////////////////////////////////////////////////
// KioskAppData
KioskAppData::KioskAppData(KioskAppDataDelegate* delegate,
const std::string& app_id,
const AccountId& account_id,
const GURL& update_url,
const base::FilePath& cached_crx)
: KioskAppDataBase(KioskAppManager::kKioskDictionaryName,
app_id,
account_id),
delegate_(delegate),
status_(Status::kInit),
update_url_(update_url),
crx_file_(cached_crx) {
if (ignore_kiosk_app_data_load_failures_for_testing) {
LOG(WARNING) << "Force KioskAppData loaded for testing.";
SetStatus(Status::kLoaded);
}
}
KioskAppData::~KioskAppData() = default;
void KioskAppData::Load() {
SetStatus(Status::kLoading);
if (LoadFromCache())
return;
StartFetch();
}
void KioskAppData::LoadFromInstalledApp(Profile* profile,
const extensions::Extension* app) {
SetStatus(Status::kLoading);
if (!app) {
app = extensions::ExtensionRegistry::Get(profile)->GetInstalledExtension(
app_id());
}
DCHECK_EQ(app_id(), app->id());
name_ = app->name();
required_platform_version_ =
extensions::KioskModeInfo::Get(app)->required_platform_version;
const int kIconSize = extension_misc::EXTENSION_ICON_LARGE;
extensions::ExtensionResource image = extensions::IconsInfo::GetIconResource(
app, kIconSize, ExtensionIconSet::MATCH_BIGGER);
extensions::ImageLoader::Get(profile)->LoadImageAsync(
app, image, gfx::Size(kIconSize, kIconSize),
base::BindOnce(&KioskAppData::OnExtensionIconLoaded,
weak_factory_.GetWeakPtr()));
}
void KioskAppData::SetCachedCrx(const base::FilePath& crx_file) {
if (crx_file_ == crx_file)
return;
crx_file_ = crx_file;
LoadFromCrx();
}
bool KioskAppData::IsLoading() const {
return status_ == Status::kLoading;
}
bool KioskAppData::IsFromWebStore() const {
return update_url_.is_empty() ||
extension_urls::IsWebstoreUpdateUrl(update_url_);
}
void KioskAppData::SetStatusForTest(Status status) {
SetStatus(status);
}
// static
std::unique_ptr<KioskAppData> KioskAppData::CreateForTest(
KioskAppDataDelegate* delegate,
const std::string& app_id,
const AccountId& account_id,
const GURL& update_url,
const std::string& required_platform_version) {
std::unique_ptr<KioskAppData> data(new KioskAppData(
delegate, app_id, account_id, update_url, base::FilePath()));
data->status_ = Status::kLoaded;
data->required_platform_version_ = required_platform_version;
return data;
}
void KioskAppData::SetStatus(Status status) {
if (status == Status::kError &&
ignore_kiosk_app_data_load_failures_for_testing) {
LOG(WARNING) << "Ignoring KioskAppData error for testing. Force OK.";
status = Status::kLoaded;
}
if (status_ == status)
return;
status_ = status;
if (!delegate_)
return;
switch (status_) {
case Status::kInit:
case Status::kLoading:
case Status::kLoaded:
delegate_->OnKioskAppDataChanged(app_id());
break;
case Status::kError:
delegate_->OnKioskAppDataLoadFailure(app_id());
break;
}
}
network::mojom::URLLoaderFactory* KioskAppData::GetURLLoaderFactory() {
return g_browser_process->system_network_context_manager()
->GetURLLoaderFactory();
}
bool KioskAppData::LoadFromCache() {
PrefService* local_state = g_browser_process->local_state();
const base::DictionaryValue* dict =
local_state->GetDictionary(dictionary_name());
if (!LoadFromDictionary(*dict))
return false;
const std::string app_key = std::string(kKeyApps) + '.' + app_id();
const std::string required_platform_version_key =
app_key + '.' + kKeyRequiredPlatformVersion;
return dict->GetString(required_platform_version_key,
&required_platform_version_);
}
void KioskAppData::SetCache(const std::string& name,
const SkBitmap& icon,
const std::string& required_platform_version) {
name_ = name;
required_platform_version_ = required_platform_version;
icon_ = gfx::ImageSkia::CreateFrom1xBitmap(icon);
icon_.MakeThreadSafe();
base::FilePath cache_dir;
if (delegate_)
delegate_->GetKioskAppIconCacheDir(&cache_dir);
SaveIcon(icon, cache_dir);
PrefService* local_state = g_browser_process->local_state();
DictionaryPrefUpdate dict_update(local_state, dictionary_name());
SaveToDictionary(dict_update);
const std::string app_key = std::string(kKeyApps) + '.' + app_id();
const std::string required_platform_version_key =
app_key + '.' + kKeyRequiredPlatformVersion;
dict_update->SetString(required_platform_version_key,
required_platform_version);
}
void KioskAppData::OnExtensionIconLoaded(const gfx::Image& icon) {
if (icon.IsEmpty()) {
LOG(WARNING) << "Failed to load icon from installed app"
<< ", id=" << app_id();
SetCache(name_, *extensions::util::GetDefaultAppIcon().bitmap(),
required_platform_version_);
} else {
SetCache(name_, icon.AsBitmap(), required_platform_version_);
}
SetStatus(Status::kLoaded);
}
void KioskAppData::OnIconLoadSuccess(const gfx::ImageSkia& icon) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
kiosk_app_icon_loader_.reset();
icon_ = icon;
SetStatus(Status::kLoaded);
}
void KioskAppData::OnIconLoadFailure() {
kiosk_app_icon_loader_.reset();
// Re-fetch data from web store when failed to load cached data.
StartFetch();
}
// static
void KioskAppData::SetIgnoreKioskAppDataLoadFailuresForTesting(bool value) {
ignore_kiosk_app_data_load_failures_for_testing = value;
}
void KioskAppData::OnWebstoreParseSuccess(
const SkBitmap& icon,
const std::string& required_platform_version) {
SetCache(name_, icon, required_platform_version);
SetStatus(Status::kLoaded);
}
void KioskAppData::OnWebstoreParseFailure() {
LOG(WARNING) << "Webstore request parse failure for app_id=" << app_id();
SetStatus(Status::kError);
}
void KioskAppData::StartFetch() {
if (!IsFromWebStore()) {
LoadFromCrx();
return;
}
webstore_fetcher_ =
std::make_unique<extensions::WebstoreDataFetcher>(this, GURL(), app_id());
webstore_fetcher_->set_max_auto_retries(3);
webstore_fetcher_->Start(g_browser_process->system_network_context_manager()
->GetURLLoaderFactory());
}
void KioskAppData::OnWebstoreRequestFailure(const std::string& extension_id) {
LOG(WARNING) << "Webstore request failure for app_id=" << extension_id;
SetStatus(Status::kError);
}
void KioskAppData::OnWebstoreResponseParseSuccess(
const std::string& extension_id,
std::unique_ptr<base::DictionaryValue> webstore_data) {
std::string id;
if (!webstore_data.get()->GetString(kIdKey, &id)) {
LOG(ERROR) << "Webstore response error (" << kIdKey
<< "): " << ValueToString(*webstore_data.get());
OnWebstoreResponseParseFailure(extension_id, kInvalidWebstoreResponseError);
return;
}
if (extension_id != id) {
LOG(ERROR) << "Webstore response error (" << kIdKey
<< "): " << ValueToString(*webstore_data.get());
LOG(ERROR) << "Received extension id " << id
<< " does not equal expected extension id " << extension_id;
OnWebstoreResponseParseFailure(extension_id, kInvalidWebstoreResponseError);
return;
}
// Takes ownership of |webstore_data|.
webstore_fetcher_.reset();
std::string manifest;
if (!CheckResponseKeyValue(id, webstore_data.get(), kManifestKey, &manifest))
return;
if (!CheckResponseKeyValue(id, webstore_data.get(), kLocalizedNameKey,
&name_))
return;
std::string icon_url_string;
if (!CheckResponseKeyValue(id, webstore_data.get(), kIconUrlKey,
&icon_url_string))
return;
GURL icon_url =
extension_urls::GetWebstoreLaunchURL().Resolve(icon_url_string);
if (!icon_url.is_valid()) {
LOG(ERROR) << "Webstore response error (icon url): "
<< ValueToString(*webstore_data);
OnWebstoreResponseParseFailure(extension_id, kInvalidWebstoreResponseError);
return;
}
// WebstoreDataParser deletes itself when done.
(new WebstoreDataParser(weak_factory_.GetWeakPtr()))
->Start(app_id(), manifest, icon_url, GetURLLoaderFactory());
}
void KioskAppData::OnWebstoreResponseParseFailure(
const std::string& extension_id,
const std::string& error) {
LOG(ERROR) << "Webstore failed for kiosk app " << app_id() << ", " << error;
webstore_fetcher_.reset();
SetStatus(Status::kError);
}
bool KioskAppData::CheckResponseKeyValue(const std::string& extension_id,
const base::DictionaryValue* response,
const char* key,
std::string* value) {
if (!response->GetString(key, value)) {
LOG(ERROR) << "Webstore response error (" << key
<< "): " << ValueToString(*response);
OnWebstoreResponseParseFailure(extension_id, kInvalidWebstoreResponseError);
return false;
}
return true;
}
void KioskAppData::LoadFromCrx() {
if (crx_file_.empty())
return;
scoped_refptr<CrxLoader> crx_loader(
new CrxLoader(weak_factory_.GetWeakPtr(), crx_file_));
crx_loader->Start();
}
void KioskAppData::OnCrxLoadFinished(const CrxLoader* crx_loader) {
DCHECK(crx_loader);
if (crx_loader->crx_file() != crx_file_)
return;
if (!crx_loader->success()) {
LOG(ERROR) << "Failed to load cached extension data for app_id="
<< app_id();
// If after unpacking the cached extension we received an error, schedule a
// redownload upon next session start(kiosk or login).
if (delegate_)
delegate_->OnExternalCacheDamaged(app_id());
SetStatus(Status::kInit);
return;
}
SkBitmap icon = crx_loader->icon();
if (icon.empty())
icon = *extensions::util::GetDefaultAppIcon().bitmap();
SetCache(crx_loader->name(), icon, crx_loader->required_platform_version());
SetStatus(Status::kLoaded);
}
} // namespace ash