blob: 9e1ac78b4ecb49d8959ebf690a4bb489678b5be9 [file] [log] [blame]
// Copyright (c) 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/chromeos/extensions/external_cache.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/file_util.h"
#include "base/files/file_enumerator.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "base/version.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/extensions/crx_installer.h"
#include "chrome/browser/extensions/external_provider_impl.h"
#include "chrome/browser/extensions/updater/extension_downloader.h"
#include "chrome/common/extensions/extension_constants.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
#include "extensions/common/extension.h"
namespace chromeos {
namespace {
// File name extension for CRX files (not case sensitive).
const char kCRXFileExtension[] = ".crx";
// Delay between checks for flag file presence when waiting for the cache to
// become ready.
const int64_t kCacheStatusPollingDelayMs = 1000;
} // namespace
const char ExternalCache::kCacheReadyFlagFileName[] = ".initialized";
ExternalCache::ExternalCache(const base::FilePath& cache_dir,
net::URLRequestContextGetter* request_context,
const scoped_refptr<base::SequencedTaskRunner>&
backend_task_runner,
Delegate* delegate,
bool always_check_updates,
bool wait_for_cache_initialization)
: cache_dir_(cache_dir),
request_context_(request_context),
delegate_(delegate),
shutdown_(false),
always_check_updates_(always_check_updates),
wait_for_cache_initialization_(wait_for_cache_initialization),
cached_extensions_(new base::DictionaryValue()),
backend_task_runner_(backend_task_runner),
weak_ptr_factory_(this) {
notification_registrar_.Add(
this,
chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR,
content::NotificationService::AllBrowserContextsAndSources());
}
ExternalCache::~ExternalCache() {
}
void ExternalCache::Shutdown(const base::Closure& callback) {
DCHECK(!shutdown_);
shutdown_ = true;
backend_task_runner_->PostTask(FROM_HERE,
base::Bind(&ExternalCache::BackendShudown,
callback));
}
void ExternalCache::UpdateExtensionsList(
scoped_ptr<base::DictionaryValue> prefs) {
if (shutdown_)
return;
extensions_ = prefs.Pass();
if (extensions_->empty()) {
// Don't check cache and clear it if there are no extensions in the list.
// It is important case because login to supervised user shouldn't clear
// cache for normal users.
// TODO(dpolukhin): introduce reference counting to preserve cache elements
// when they are not needed for current user but needed for other users.
cached_extensions_->Clear();
UpdateExtensionLoader();
} else {
CheckCache();
}
}
void ExternalCache::OnDamagedFileDetected(const base::FilePath& path) {
if (shutdown_)
return;
for (base::DictionaryValue::Iterator it(*cached_extensions_.get());
!it.IsAtEnd(); it.Advance()) {
const base::DictionaryValue* entry = NULL;
if (!it.value().GetAsDictionary(&entry)) {
NOTREACHED() << "ExternalCache found bad entry with type "
<< it.value().GetType();
continue;
}
std::string external_crx;
if (entry->GetString(extensions::ExternalProviderImpl::kExternalCrx,
&external_crx) &&
external_crx == path.value()) {
LOG(ERROR) << "ExternalCache extension at " << path.value()
<< " failed to install, deleting it.";
cached_extensions_->Remove(it.key(), NULL);
UpdateExtensionLoader();
// The file will be downloaded again on the next restart.
if (cache_dir_.IsParent(path)) {
backend_task_runner_->PostTask(
FROM_HERE,
base::Bind(base::IgnoreResult(base::DeleteFile),
path,
true));
}
// Don't try to DownloadMissingExtensions() from here,
// since it can cause a fail/retry loop.
return;
}
}
LOG(ERROR) << "ExternalCache cannot find external_crx " << path.value();
}
void ExternalCache::Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
if (shutdown_)
return;
switch (type) {
case chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR: {
extensions::CrxInstaller* installer =
content::Source<extensions::CrxInstaller>(source).ptr();
OnDamagedFileDetected(installer->source_file());
break;
}
default:
NOTREACHED();
}
}
void ExternalCache::OnExtensionDownloadFailed(
const std::string& id,
extensions::ExtensionDownloaderDelegate::Error error,
const extensions::ExtensionDownloaderDelegate::PingResult& ping_result,
const std::set<int>& request_ids) {
if (shutdown_)
return;
if (error == NO_UPDATE_AVAILABLE) {
if (!cached_extensions_->HasKey(id)) {
LOG(ERROR) << "ExternalCache extension " << id
<< " not found on update server";
}
} else {
LOG(ERROR) << "ExternalCache failed to download extension " << id
<< ", error " << error;
}
}
void ExternalCache::OnExtensionDownloadFinished(
const std::string& id,
const base::FilePath& path,
const GURL& download_url,
const std::string& version,
const extensions::ExtensionDownloaderDelegate::PingResult& ping_result,
const std::set<int>& request_ids) {
if (shutdown_)
return;
backend_task_runner_->PostTask(
FROM_HERE,
base::Bind(&ExternalCache::BackendInstallCacheEntry,
weak_ptr_factory_.GetWeakPtr(),
cache_dir_,
id,
path,
version));
}
bool ExternalCache::IsExtensionPending(const std::string& id) {
// Pending means that there is no installed version yet.
return extensions_->HasKey(id) && !cached_extensions_->HasKey(id);
}
bool ExternalCache::GetExtensionExistingVersion(const std::string& id,
std::string* version) {
DictionaryValue* extension_dictionary = NULL;
if (cached_extensions_->GetDictionary(id, &extension_dictionary)) {
if (extension_dictionary->GetString(
extensions::ExternalProviderImpl::kExternalVersion, version)) {
return true;
}
*version = delegate_->GetInstalledExtensionVersion(id);
return !version->empty();
}
return false;
}
void ExternalCache::UpdateExtensionLoader() {
if (shutdown_)
return;
VLOG(1) << "Notify ExternalCache delegate about cache update";
if (delegate_)
delegate_->OnExtensionListsUpdated(cached_extensions_.get());
}
void ExternalCache::CheckCache() {
if (wait_for_cache_initialization_) {
backend_task_runner_->PostTask(
FROM_HERE,
base::Bind(&ExternalCache::BackendCheckCacheStatus,
weak_ptr_factory_.GetWeakPtr(),
cache_dir_));
} else {
CheckCacheContents();
}
}
// static
void ExternalCache::BackendCheckCacheStatus(
base::WeakPtr<ExternalCache> external_cache,
const base::FilePath& cache_dir) {
content::BrowserThread::PostTask(
content::BrowserThread::UI,
FROM_HERE,
base::Bind(&ExternalCache::OnCacheStatusChecked,
external_cache,
base::PathExists(cache_dir.AppendASCII(kCacheReadyFlagFileName))));
}
void ExternalCache::OnCacheStatusChecked(bool ready) {
if (shutdown_)
return;
if (ready) {
CheckCacheContents();
} else {
content::BrowserThread::PostDelayedTask(
content::BrowserThread::UI,
FROM_HERE,
base::Bind(&ExternalCache::CheckCache,
weak_ptr_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(kCacheStatusPollingDelayMs));
}
}
void ExternalCache::CheckCacheContents() {
if (shutdown_)
return;
backend_task_runner_->PostTask(
FROM_HERE,
base::Bind(&ExternalCache::BackendCheckCacheContents,
weak_ptr_factory_.GetWeakPtr(),
cache_dir_,
base::Passed(make_scoped_ptr(extensions_->DeepCopy()))));
}
// static
void ExternalCache::BackendCheckCacheContents(
base::WeakPtr<ExternalCache> external_cache,
const base::FilePath& cache_dir,
scoped_ptr<base::DictionaryValue> prefs) {
BackendCheckCacheContentsInternal(cache_dir, prefs.get());
content::BrowserThread::PostTask(content::BrowserThread::UI,
FROM_HERE,
base::Bind(&ExternalCache::OnCacheUpdated,
external_cache,
base::Passed(&prefs)));
}
// static
void ExternalCache::BackendCheckCacheContentsInternal(
const base::FilePath& cache_dir,
base::DictionaryValue* prefs) {
// Start by verifying that the cache_dir exists.
if (!base::DirectoryExists(cache_dir)) {
// Create it now.
if (!base::CreateDirectory(cache_dir)) {
LOG(ERROR) << "Failed to create ExternalCache directory at "
<< cache_dir.value();
}
// Nothing else to do. Cache won't be used.
return;
}
// Enumerate all the files in the cache |cache_dir|, including directories
// and symlinks. Each unrecognized file will be erased.
int types = base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES |
base::FileEnumerator::SHOW_SYM_LINKS;
base::FileEnumerator enumerator(cache_dir, false /* recursive */, types);
for (base::FilePath path = enumerator.Next();
!path.empty(); path = enumerator.Next()) {
base::FileEnumerator::FileInfo info = enumerator.GetInfo();
std::string basename = path.BaseName().value();
if (info.IsDirectory() || base::IsLink(info.GetName())) {
LOG(ERROR) << "Erasing bad file in ExternalCache directory: " << basename;
base::DeleteFile(path, true /* recursive */);
continue;
}
// Skip flag file that indicates that cache is ready.
if (basename == kCacheReadyFlagFileName)
continue;
// crx files in the cache are named <extension-id>-<version>.crx.
std::string id;
std::string version;
if (EndsWith(basename, kCRXFileExtension, false /* case-sensitive */)) {
size_t n = basename.find('-');
if (n != std::string::npos && n + 1 < basename.size() - 4) {
id = basename.substr(0, n);
// Size of |version| = total size - "<id>" - "-" - ".crx"
version = basename.substr(n + 1, basename.size() - 5 - id.size());
}
}
base::DictionaryValue* entry = NULL;
if (!extensions::Extension::IdIsValid(id)) {
LOG(ERROR) << "Bad extension id in ExternalCache: " << id;
id.clear();
} else if (!prefs->GetDictionary(id, &entry)) {
LOG(WARNING) << basename << " is in the cache but is not configured by "
<< "the ExternalCache source, and will be erased.";
id.clear();
}
if (!Version(version).IsValid()) {
LOG(ERROR) << "Bad extension version in ExternalCache: " << version;
version.clear();
}
if (id.empty() || version.empty()) {
LOG(ERROR) << "Invalid file in ExternalCache, erasing: " << basename;
base::DeleteFile(path, true /* recursive */);
continue;
}
// Enforce a lower-case id.
id = StringToLowerASCII(id);
std::string update_url;
std::string prev_version_string;
std::string prev_crx;
if (entry->GetString(extensions::ExternalProviderImpl::kExternalUpdateUrl,
&update_url)) {
VLOG(1) << "ExternalCache found cached version " << version
<< " for extension id: " << id;
entry->Remove(extensions::ExternalProviderImpl::kExternalUpdateUrl, NULL);
entry->SetString(extensions::ExternalProviderImpl::kExternalVersion,
version);
entry->SetString(extensions::ExternalProviderImpl::kExternalCrx,
path.value());
if (extension_urls::IsWebstoreUpdateUrl(GURL(update_url))) {
entry->SetBoolean(extensions::ExternalProviderImpl::kIsFromWebstore,
true);
}
} else if (
entry->GetString(extensions::ExternalProviderImpl::kExternalVersion,
&prev_version_string) &&
entry->GetString(extensions::ExternalProviderImpl::kExternalCrx,
&prev_crx)) {
Version prev_version(prev_version_string);
Version curr_version(version);
DCHECK(prev_version.IsValid());
DCHECK(curr_version.IsValid());
if (prev_version.CompareTo(curr_version) <= 0) {
VLOG(1) << "ExternalCache found old cached version "
<< prev_version_string << " path: " << prev_crx;
base::FilePath prev_crx_file(prev_crx);
if (cache_dir.IsParent(prev_crx_file)) {
// Only delete old cached files under cache_dir_ folder.
base::DeleteFile(base::FilePath(prev_crx), true /* recursive */);
}
entry->SetString(extensions::ExternalProviderImpl::kExternalVersion,
version);
entry->SetString(extensions::ExternalProviderImpl::kExternalCrx,
path.value());
} else {
VLOG(1) << "ExternalCache found old cached version "
<< version << " path: " << path.value();
base::DeleteFile(path, true /* recursive */);
}
} else {
NOTREACHED() << "ExternalCache found bad entry for extension id: " << id
<< " file path: " << path.value();
}
}
}
void ExternalCache::OnCacheUpdated(scoped_ptr<base::DictionaryValue> prefs) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
if (shutdown_)
return;
// If request_context_ is missing we can't download anything.
if (!downloader_ && request_context_) {
downloader_.reset(
new extensions::ExtensionDownloader(this, request_context_));
}
cached_extensions_->Clear();
for (base::DictionaryValue::Iterator it(*extensions_.get());
!it.IsAtEnd(); it.Advance()) {
const base::DictionaryValue* entry = NULL;
if (!it.value().GetAsDictionary(&entry)) {
LOG(ERROR) << "ExternalCache found bad entry with type "
<< it.value().GetType();
continue;
}
bool keep_if_present =
entry->HasKey(extensions::ExternalProviderImpl::kKeepIfPresent);
// Check for updates for all extensions configured except for extensions
// marked as keep_if_present.
if (downloader_ && !keep_if_present) {
GURL update_url;
std::string external_update_url;
if (entry->GetString(extensions::ExternalProviderImpl::kExternalUpdateUrl,
&external_update_url)) {
update_url = GURL(external_update_url);
} else if (always_check_updates_) {
update_url = extension_urls::GetWebstoreUpdateUrl();
}
if (update_url.is_valid())
downloader_->AddPendingExtension(it.key(), update_url, 0);
}
base::DictionaryValue* cached_entry = NULL;
if (prefs->GetDictionary(it.key(), &cached_entry)) {
bool has_external_crx = cached_entry->HasKey(
extensions::ExternalProviderImpl::kExternalCrx);
bool is_already_installed =
!delegate_->GetInstalledExtensionVersion(it.key()).empty();
if (!downloader_ || keep_if_present || has_external_crx ||
is_already_installed) {
scoped_ptr<base::Value> value;
prefs->Remove(it.key(), &value);
cached_extensions_->Set(it.key(), value.release());
}
}
}
if (downloader_)
downloader_->StartAllPending();
VLOG(1) << "Updated ExternalCache, there are "
<< cached_extensions_->size() << " extensions cached";
UpdateExtensionLoader();
}
// static
void ExternalCache::BackendInstallCacheEntry(
base::WeakPtr<ExternalCache> external_cache,
const base::FilePath& cache_dir,
const std::string& id,
const base::FilePath& path,
const std::string& version) {
Version version_validator(version);
if (!version_validator.IsValid()) {
LOG(ERROR) << "ExternalCache downloaded extension " << id << " but got bad "
<< "version: " << version;
base::DeleteFile(path, true /* recursive */);
return;
}
std::string basename = id + "-" + version + kCRXFileExtension;
base::FilePath cached_crx_path = cache_dir.Append(basename);
if (base::PathExists(cached_crx_path)) {
LOG(WARNING) << "AppPack downloaded a crx whose filename will overwrite "
<< "an existing cached crx.";
base::DeleteFile(cached_crx_path, true /* recursive */);
}
if (!base::DirectoryExists(cache_dir)) {
LOG(ERROR) << "AppPack cache directory does not exist, creating now: "
<< cache_dir.value();
if (!base::CreateDirectory(cache_dir)) {
LOG(ERROR) << "Failed to create the AppPack cache dir!";
base::DeleteFile(path, true /* recursive */);
return;
}
}
if (!base::Move(path, cached_crx_path)) {
LOG(ERROR) << "Failed to move AppPack crx from " << path.value()
<< " to " << cached_crx_path.value();
base::DeleteFile(path, true /* recursive */);
return;
}
content::BrowserThread::PostTask(
content::BrowserThread::UI,
FROM_HERE,
base::Bind(&ExternalCache::OnCacheEntryInstalled,
external_cache,
id,
cached_crx_path,
version));
}
void ExternalCache::OnCacheEntryInstalled(const std::string& id,
const base::FilePath& path,
const std::string& version) {
if (shutdown_)
return;
VLOG(1) << "AppPack installed a new extension in the cache: " << path.value();
base::DictionaryValue* entry = NULL;
if (!extensions_->GetDictionary(id, &entry)) {
LOG(ERROR) << "ExternalCache cannot find entry for extension " << id;
return;
}
// Copy entry to don't modify it inside extensions_.
entry = entry->DeepCopy();
std::string update_url;
if (entry->GetString(extensions::ExternalProviderImpl::kExternalUpdateUrl,
&update_url) &&
extension_urls::IsWebstoreUpdateUrl(GURL(update_url))) {
entry->SetBoolean(extensions::ExternalProviderImpl::kIsFromWebstore, true);
}
entry->Remove(extensions::ExternalProviderImpl::kExternalUpdateUrl, NULL);
entry->SetString(extensions::ExternalProviderImpl::kExternalVersion, version);
entry->SetString(extensions::ExternalProviderImpl::kExternalCrx,
path.value());
cached_extensions_->Set(id, entry);
UpdateExtensionLoader();
}
// static
void ExternalCache::BackendShudown(const base::Closure& callback) {
content::BrowserThread::PostTask(content::BrowserThread::UI,
FROM_HERE,
callback);
}
std::string ExternalCache::Delegate::GetInstalledExtensionVersion(
const std::string& id) {
return std::string();
}
} // namespace chromeos