| // Copyright (c) 2012 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/policy/app_pack_updater.h" |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/file_util.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/location.h" |
| #include "base/stl_util.h" |
| #include "base/string_util.h" |
| #include "base/values.h" |
| #include "base/version.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/chromeos/policy/enterprise_install_attributes.h" |
| #include "chrome/browser/chromeos/settings/cros_settings.h" |
| #include "chrome/browser/chromeos/settings/cros_settings_names.h" |
| #include "chrome/browser/extensions/crx_installer.h" |
| #include "chrome/browser/extensions/external_loader.h" |
| #include "chrome/browser/extensions/external_provider_impl.h" |
| #include "chrome/browser/extensions/updater/extension_downloader.h" |
| #include "chrome/common/chrome_notification_types.h" |
| #include "chrome/common/extensions/extension.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" |
| |
| using content::BrowserThread; |
| |
| namespace policy { |
| |
| namespace { |
| |
| // Directory where the AppPack extensions are cached. |
| const char kAppPackCacheDir[] = "/var/cache/app_pack"; |
| |
| // File name extension for CRX files (not case sensitive). |
| const char kCRXFileExtension[] = ".crx"; |
| |
| } // namespace |
| |
| // A custom extensions::ExternalLoader that the AppPackUpdater creates and uses |
| // to publish AppPack updates to the extensions system. |
| class AppPackExternalLoader |
| : public extensions::ExternalLoader, |
| public base::SupportsWeakPtr<AppPackExternalLoader> { |
| public: |
| AppPackExternalLoader() {} |
| |
| // Used by the AppPackUpdater to update the current list of extensions. |
| // The format of |prefs| is detailed in the extensions::ExternalLoader/ |
| // Provider headers. |
| void SetCurrentAppPackExtensions(scoped_ptr<base::DictionaryValue> prefs) { |
| app_pack_prefs_.Swap(prefs.get()); |
| StartLoading(); |
| } |
| |
| // Implementation of extensions::ExternalLoader: |
| virtual void StartLoading() OVERRIDE { |
| prefs_.reset(app_pack_prefs_.DeepCopy()); |
| VLOG(1) << "AppPack extension loader publishing " |
| << app_pack_prefs_.size() << " crx files."; |
| LoadFinished(); |
| } |
| |
| protected: |
| virtual ~AppPackExternalLoader() {} |
| |
| private: |
| base::DictionaryValue app_pack_prefs_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AppPackExternalLoader); |
| }; |
| |
| AppPackUpdater::AppPackUpdater(net::URLRequestContextGetter* request_context, |
| EnterpriseInstallAttributes* install_attributes) |
| : weak_ptr_factory_(this), |
| initialized_(false), |
| created_extension_loader_(false), |
| request_context_(request_context), |
| install_attributes_(install_attributes) { |
| chromeos::CrosSettings::Get()->AddSettingsObserver(chromeos::kAppPack, this); |
| |
| if (install_attributes_->GetMode() == DEVICE_MODE_KIOSK) { |
| // Already in Kiosk mode, start loading. |
| BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| base::Bind(&AppPackUpdater::Init, |
| weak_ptr_factory_.GetWeakPtr())); |
| } else { |
| // Linger until the device switches to DEVICE_MODE_KIOSK and the app pack |
| // device setting appears. |
| } |
| } |
| |
| AppPackUpdater::~AppPackUpdater() { |
| chromeos::CrosSettings::Get()->RemoveSettingsObserver( |
| chromeos::kAppPack, this); |
| } |
| |
| extensions::ExternalLoader* AppPackUpdater::CreateExternalLoader() { |
| if (created_extension_loader_) { |
| NOTREACHED(); |
| return NULL; |
| } |
| created_extension_loader_ = true; |
| AppPackExternalLoader* loader = new AppPackExternalLoader(); |
| extension_loader_ = loader->AsWeakPtr(); |
| |
| // The cache may have been already checked. In that case, load the current |
| // extensions into the loader immediately. |
| UpdateExtensionLoader(); |
| |
| return loader; |
| } |
| |
| void AppPackUpdater::SetScreenSaverUpdateCallback( |
| const AppPackUpdater::ScreenSaverUpdateCallback& callback) { |
| screen_saver_update_callback_ = callback; |
| if (!screen_saver_update_callback_.is_null() && !screen_saver_path_.empty()) { |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(screen_saver_update_callback_, screen_saver_path_)); |
| } |
| } |
| |
| void AppPackUpdater::Init() { |
| if (initialized_) |
| return; |
| |
| initialized_ = true; |
| worker_pool_token_ = BrowserThread::GetBlockingPool()->GetSequenceToken(); |
| notification_registrar_.Add( |
| this, |
| chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR, |
| content::NotificationService::AllBrowserContextsAndSources()); |
| LoadPolicy(); |
| } |
| |
| void AppPackUpdater::Observe(int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) { |
| switch (type) { |
| case chrome::NOTIFICATION_SYSTEM_SETTING_CHANGED: |
| DCHECK_EQ(chromeos::kAppPack, |
| *content::Details<const std::string>(details).ptr()); |
| if (install_attributes_->GetMode() == DEVICE_MODE_KIOSK) { |
| if (!initialized_) |
| Init(); |
| else |
| LoadPolicy(); |
| } |
| break; |
| |
| case chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR: { |
| extensions::CrxInstaller* installer = |
| content::Source<extensions::CrxInstaller>(source).ptr(); |
| OnDamagedFileDetected(installer->source_file()); |
| break; |
| } |
| |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void AppPackUpdater::LoadPolicy() { |
| chromeos::CrosSettings* settings = chromeos::CrosSettings::Get(); |
| if (chromeos::CrosSettingsProvider::TRUSTED != settings->PrepareTrustedValues( |
| base::Bind(&AppPackUpdater::LoadPolicy, |
| weak_ptr_factory_.GetWeakPtr()))) { |
| return; |
| } |
| |
| app_pack_extensions_.clear(); |
| const base::Value* value = settings->GetPref(chromeos::kAppPack); |
| const base::ListValue* list = NULL; |
| if (value && value->GetAsList(&list)) { |
| for (base::ListValue::const_iterator it = list->begin(); |
| it != list->end(); ++it) { |
| base::DictionaryValue* dict = NULL; |
| if (!(*it)->GetAsDictionary(&dict)) { |
| LOG(WARNING) << "AppPack entry is not a dictionary, ignoring."; |
| continue; |
| } |
| std::string id; |
| std::string update_url; |
| if (dict->GetString(chromeos::kAppPackKeyExtensionId, &id) && |
| dict->GetString(chromeos::kAppPackKeyUpdateUrl, &update_url)) { |
| app_pack_extensions_[id] = update_url; |
| } else { |
| LOG(WARNING) << "Failed to read required fields for an AppPack entry, " |
| << "ignoring."; |
| } |
| } |
| } |
| |
| VLOG(1) << "Refreshed AppPack policy, got " << app_pack_extensions_.size() |
| << " entries."; |
| |
| value = settings->GetPref(chromeos::kScreenSaverExtensionId); |
| if (!value || !value->GetAsString(&screen_saver_id_)) { |
| screen_saver_id_.clear(); |
| SetScreenSaverPath(base::FilePath()); |
| } |
| |
| CheckCacheNow(); |
| } |
| |
| void AppPackUpdater::CheckCacheNow() { |
| std::set<std::string>* valid_ids = new std::set<std::string>(); |
| for (PolicyEntryMap::iterator it = app_pack_extensions_.begin(); |
| it != app_pack_extensions_.end(); ++it) { |
| valid_ids->insert(it->first); |
| } |
| PostBlockingTask(FROM_HERE, |
| base::Bind(&AppPackUpdater::BlockingCheckCache, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Owned(valid_ids))); |
| } |
| |
| // static |
| void AppPackUpdater::BlockingCheckCache( |
| base::WeakPtr<AppPackUpdater> app_pack_updater, |
| const std::set<std::string>* valid_ids) { |
| CacheEntryMap* entries = new CacheEntryMap(); |
| BlockingCheckCacheInternal(valid_ids, entries); |
| BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| base::Bind(&AppPackUpdater::OnCacheUpdated, |
| app_pack_updater, |
| base::Owned(entries))); |
| } |
| |
| // static |
| void AppPackUpdater::BlockingCheckCacheInternal( |
| const std::set<std::string>* valid_ids, |
| CacheEntryMap* entries) { |
| // Start by verifying that the cache dir exists. |
| base::FilePath dir(kAppPackCacheDir); |
| if (!file_util::DirectoryExists(dir)) { |
| // Create it now. |
| if (!file_util::CreateDirectory(dir)) |
| LOG(ERROR) << "Failed to create AppPack directory at " << dir.value(); |
| // Nothing else to do. |
| return; |
| } |
| |
| // Enumerate all the files in the 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(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() || file_util::IsLink(info.GetName())) { |
| LOG(ERROR) << "Erasing bad file in AppPack directory: " << basename; |
| file_util::Delete(path, true /* recursive */); |
| 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()); |
| } |
| } |
| |
| if (!extensions::Extension::IdIsValid(id)) { |
| LOG(ERROR) << "Bad AppPack extension id in cache: " << id; |
| id.clear(); |
| } else if (!ContainsKey(*valid_ids, id)) { |
| LOG(WARNING) << basename << " is in the cache but is not configured by " |
| << "the AppPack policy, and will be erased."; |
| id.clear(); |
| } |
| |
| if (!Version(version).IsValid()) { |
| LOG(ERROR) << "Bad AppPack extension version in cache: " << version; |
| version.clear(); |
| } |
| |
| if (id.empty() || version.empty()) { |
| LOG(ERROR) << "Invalid file in AppPack cache, erasing: " << basename; |
| file_util::Delete(path, true /* recursive */); |
| continue; |
| } |
| |
| // Enforce a lower-case id. |
| id = StringToLowerASCII(id); |
| |
| // File seems good so far. Make sure there isn't another entry with the |
| // same id but a different version. |
| |
| if (ContainsKey(*entries, id)) { |
| LOG(ERROR) << "Found two AppPack files for the same extension, will " |
| "erase the oldest version"; |
| CacheEntry& entry = (*entries)[id]; |
| Version vEntry(entry.cached_version); |
| Version vCurrent(version); |
| DCHECK(vEntry.IsValid()); |
| DCHECK(vCurrent.IsValid()); |
| if (vEntry.CompareTo(vCurrent) < 0) { |
| file_util::Delete(base::FilePath(entry.path), true /* recursive */); |
| entry.path = path.value(); |
| } else { |
| file_util::Delete(path, true /* recursive */); |
| } |
| continue; |
| } |
| |
| // This is the only file for this |id| so far, add it. |
| |
| CacheEntry& entry = (*entries)[id]; |
| entry.path = path.value(); |
| entry.cached_version = version; |
| } |
| } |
| |
| void AppPackUpdater::OnCacheUpdated(CacheEntryMap* cache_entries) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| cached_extensions_.swap(*cache_entries); |
| |
| CacheEntryMap::iterator it = cached_extensions_.find(screen_saver_id_); |
| if (it != cached_extensions_.end()) |
| SetScreenSaverPath(base::FilePath(it->second.path)); |
| else |
| SetScreenSaverPath(base::FilePath()); |
| |
| VLOG(1) << "Updated AppPack cache, there are " << cached_extensions_.size() |
| << " extensions cached and " |
| << (screen_saver_path_.empty() ? "no" : "the") << " screensaver"; |
| UpdateExtensionLoader(); |
| DownloadMissingExtensions(); |
| } |
| |
| void AppPackUpdater::UpdateExtensionLoader() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| if (!extension_loader_) { |
| VLOG(1) << "No AppPack loader created yet, not pushing extensions."; |
| return; |
| } |
| |
| // Build a DictionaryValue with the format that |
| // extensions::ExternalProviderImpl expects, containing info about the locally |
| // cached extensions. |
| |
| scoped_ptr<base::DictionaryValue> prefs(new base::DictionaryValue()); |
| for (CacheEntryMap::iterator it = cached_extensions_.begin(); |
| it != cached_extensions_.end(); ++it) { |
| const std::string& id = it->first; |
| // The screensaver isn't installed into the Profile. |
| if (id == screen_saver_id_) |
| continue; |
| |
| base::DictionaryValue* dict = new base::DictionaryValue(); |
| dict->SetString(extensions::ExternalProviderImpl::kExternalCrx, |
| it->second.path); |
| dict->SetString(extensions::ExternalProviderImpl::kExternalVersion, |
| it->second.cached_version); |
| |
| // Include this optional flag if the extension's update url is the Webstore. |
| PolicyEntryMap::iterator policy_entry = app_pack_extensions_.find(id); |
| if (policy_entry != app_pack_extensions_.end() && |
| extension_urls::IsWebstoreUpdateUrl( |
| GURL(policy_entry->second))) { |
| dict->SetBoolean(extensions::ExternalProviderImpl::kIsFromWebstore, true); |
| } |
| |
| prefs->Set(it->first, dict); |
| |
| VLOG(1) << "Updating AppPack extension loader, added " << it->second.path; |
| } |
| |
| extension_loader_->SetCurrentAppPackExtensions(prefs.Pass()); |
| } |
| |
| void AppPackUpdater::DownloadMissingExtensions() { |
| // Check for updates for all extensions configured by the policy. Some of |
| // them may already be in the cache; only those with updated version will be |
| // downloaded, in that case. |
| if (!downloader_.get()) { |
| downloader_.reset(new extensions::ExtensionDownloader(this, |
| request_context_)); |
| } |
| for (PolicyEntryMap::iterator it = app_pack_extensions_.begin(); |
| it != app_pack_extensions_.end(); ++it) { |
| downloader_->AddPendingExtension(it->first, GURL(it->second), 0); |
| } |
| VLOG(1) << "Downloading AppPack update manifest now"; |
| downloader_->StartAllPending(); |
| } |
| |
| void AppPackUpdater::OnExtensionDownloadFailed( |
| const std::string& id, |
| extensions::ExtensionDownloaderDelegate::Error error, |
| const extensions::ExtensionDownloaderDelegate::PingResult& ping_result, |
| const std::set<int>& request_ids) { |
| if (error == NO_UPDATE_AVAILABLE) { |
| if (!ContainsKey(cached_extensions_, id)) |
| LOG(ERROR) << "AppPack extension " << id << " not found on update server"; |
| } else { |
| LOG(ERROR) << "AppPack failed to download extension " << id |
| << ", error " << error; |
| } |
| } |
| |
| void AppPackUpdater::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) { |
| // The explicit copy ctors are to make sure that Bind() binds a copy and not |
| // a reference to the arguments. |
| PostBlockingTask(FROM_HERE, |
| base::Bind(&AppPackUpdater::BlockingInstallCacheEntry, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::string(id), |
| base::FilePath(path), |
| std::string(version))); |
| } |
| |
| void AppPackUpdater::OnBlacklistDownloadFinished( |
| const std::string& data, |
| const std::string& package_hash, |
| const std::string& version, |
| const extensions::ExtensionDownloaderDelegate::PingResult& ping_result, |
| const std::set<int>& request_ids) { |
| NOTREACHED(); |
| } |
| |
| bool AppPackUpdater::IsExtensionPending(const std::string& id) { |
| // Pending means that there is no installed version yet. |
| return ContainsKey(app_pack_extensions_, id) && |
| !ContainsKey(cached_extensions_, id); |
| } |
| |
| bool AppPackUpdater::GetExtensionExistingVersion(const std::string& id, |
| std::string* version) { |
| if (!ContainsKey(app_pack_extensions_, id) || |
| !ContainsKey(cached_extensions_, id)) { |
| return false; |
| } |
| |
| *version = cached_extensions_[id].cached_version; |
| return true; |
| } |
| |
| // static |
| void AppPackUpdater::BlockingInstallCacheEntry( |
| base::WeakPtr<AppPackUpdater> app_pack_updater, |
| const std::string& id, |
| const base::FilePath& path, |
| const std::string& version) { |
| Version version_validator(version); |
| if (!version_validator.IsValid()) { |
| LOG(ERROR) << "AppPack downloaded extension " << id << " but got bad " |
| << "version: " << version; |
| file_util::Delete(path, true /* recursive */); |
| return; |
| } |
| |
| std::string basename = id + "-" + version + kCRXFileExtension; |
| base::FilePath cache_dir(kAppPackCacheDir); |
| base::FilePath cached_crx_path = cache_dir.Append(basename); |
| |
| if (file_util::PathExists(cached_crx_path)) { |
| LOG(WARNING) << "AppPack downloaded a crx whose filename will overwrite " |
| << "an existing cached crx."; |
| file_util::Delete(cached_crx_path, true /* recursive */); |
| } |
| |
| if (!file_util::DirectoryExists(cache_dir)) { |
| LOG(ERROR) << "AppPack cache directory does not exist, creating now: " |
| << cache_dir.value(); |
| if (!file_util::CreateDirectory(cache_dir)) { |
| LOG(ERROR) << "Failed to create the AppPack cache dir!"; |
| file_util::Delete(path, true /* recursive */); |
| return; |
| } |
| } |
| |
| if (!file_util::Move(path, cached_crx_path)) { |
| LOG(ERROR) << "Failed to move AppPack crx from " << path.value() |
| << " to " << cached_crx_path.value(); |
| file_util::Delete(path, true /* recursive */); |
| return; |
| } |
| |
| BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| base::Bind(&AppPackUpdater::OnCacheEntryInstalled, |
| app_pack_updater, |
| std::string(id), |
| cached_crx_path.value(), |
| std::string(version))); |
| } |
| |
| void AppPackUpdater::OnCacheEntryInstalled(const std::string& id, |
| const std::string& path, |
| const std::string& version) { |
| VLOG(1) << "AppPack installed a new extension in the cache: " << path; |
| // Add to the list of cached extensions. |
| CacheEntry& entry = cached_extensions_[id]; |
| entry.path = path; |
| entry.cached_version = version; |
| |
| if (id == screen_saver_id_) { |
| VLOG(1) << "AppPack got the screen saver extension at " << path; |
| SetScreenSaverPath(base::FilePath(path)); |
| } else { |
| UpdateExtensionLoader(); |
| } |
| } |
| |
| void AppPackUpdater::OnDamagedFileDetected(const base::FilePath& path) { |
| // Search for |path| in |cached_extensions_|, and delete it if found. |
| for (CacheEntryMap::iterator it = cached_extensions_.begin(); |
| it != cached_extensions_.end(); ++it) { |
| if (it->second.path == path.value()) { |
| LOG(ERROR) << "AppPack extension at " << path.value() << " failed to " |
| << "install, deleting it."; |
| cached_extensions_.erase(it); |
| UpdateExtensionLoader(); |
| |
| // The file will be downloaded again on the next restart. |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| base::Bind(base::IgnoreResult(file_util::Delete), path, true)); |
| |
| // Don't try to DownloadMissingExtensions() from here, |
| // since it can cause a fail/retry loop. |
| break; |
| } |
| } |
| } |
| |
| void AppPackUpdater::PostBlockingTask(const tracked_objects::Location& location, |
| const base::Closure& task) { |
| BrowserThread::GetBlockingPool()->PostSequencedWorkerTaskWithShutdownBehavior( |
| worker_pool_token_, location, task, |
| base::SequencedWorkerPool::SKIP_ON_SHUTDOWN); |
| } |
| |
| void AppPackUpdater::SetScreenSaverPath(const base::FilePath& path) { |
| // Don't invoke the callback if the path isn't changing. |
| if (path != screen_saver_path_) { |
| screen_saver_path_ = path; |
| if (!screen_saver_update_callback_.is_null()) |
| screen_saver_update_callback_.Run(screen_saver_path_); |
| } |
| } |
| |
| } // namespace policy |