| // Copyright (c) 2011 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/extensions/crx_installer.h" |
| |
| #include <map> |
| #include <set> |
| |
| #include "base/file_util.h" |
| #include "base/lazy_instance.h" |
| #include "base/memory/scoped_temp_dir.h" |
| #include "base/metrics/histogram.h" |
| #include "base/path_service.h" |
| #include "base/stl_util-inl.h" |
| #include "base/stringprintf.h" |
| #include "base/task.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/time.h" |
| #include "base/utf_string_conversions.h" |
| #include "base/version.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/extensions/convert_user_script.h" |
| #include "chrome/browser/extensions/convert_web_app.h" |
| #include "chrome/browser/extensions/extension_error_reporter.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/shell_integration.h" |
| #include "chrome/browser/web_applications/web_app.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "chrome/common/extensions/extension_file_util.h" |
| #include "content/browser/browser_thread.h" |
| #include "content/common/notification_service.h" |
| #include "content/common/notification_type.h" |
| #include "grit/chromium_strings.h" |
| #include "grit/generated_resources.h" |
| #include "grit/theme_resources.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| |
| namespace { |
| |
| struct WhitelistedInstallData { |
| WhitelistedInstallData() {} |
| std::set<std::string> ids; |
| std::map<std::string, linked_ptr<DictionaryValue> > manifests; |
| }; |
| |
| static base::LazyInstance<WhitelistedInstallData> |
| g_whitelisted_install_data(base::LINKER_INITIALIZED); |
| |
| } // namespace |
| |
| // static |
| void CrxInstaller::SetWhitelistedInstallId(const std::string& id) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| g_whitelisted_install_data.Get().ids.insert(id); |
| } |
| |
| // static |
| void CrxInstaller::SetWhitelistedManifest(const std::string& id, |
| DictionaryValue* parsed_manifest) { |
| CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| WhitelistedInstallData& data = g_whitelisted_install_data.Get(); |
| data.manifests[id] = linked_ptr<DictionaryValue>(parsed_manifest); |
| } |
| |
| // static |
| const DictionaryValue* CrxInstaller::GetWhitelistedManifest( |
| const std::string& id) { |
| WhitelistedInstallData& data = g_whitelisted_install_data.Get(); |
| if (ContainsKey(data.manifests, id)) |
| return data.manifests[id].get(); |
| else |
| return NULL; |
| } |
| |
| // static |
| DictionaryValue* CrxInstaller::RemoveWhitelistedManifest( |
| const std::string& id) { |
| WhitelistedInstallData& data = g_whitelisted_install_data.Get(); |
| if (ContainsKey(data.manifests, id)) { |
| DictionaryValue* manifest = data.manifests[id].release(); |
| data.manifests.erase(id); |
| return manifest; |
| } |
| return NULL; |
| } |
| |
| // static |
| bool CrxInstaller::IsIdWhitelisted(const std::string& id) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| std::set<std::string>& ids = g_whitelisted_install_data.Get().ids; |
| return ContainsKey(ids, id); |
| } |
| |
| // static |
| bool CrxInstaller::ClearWhitelistedInstallId(const std::string& id) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| std::set<std::string>& ids = g_whitelisted_install_data.Get().ids; |
| if (ContainsKey(ids, id)) { |
| ids.erase(id); |
| return true; |
| } |
| return false; |
| } |
| |
| CrxInstaller::CrxInstaller(ExtensionService* frontend, |
| ExtensionInstallUI* client) |
| : install_directory_(frontend->install_directory()), |
| install_source_(Extension::INTERNAL), |
| extensions_enabled_(frontend->extensions_enabled()), |
| delete_source_(false), |
| is_gallery_install_(false), |
| create_app_shortcut_(false), |
| frontend_(frontend), |
| client_(client), |
| apps_require_extension_mime_type_(false), |
| allow_silent_install_(false) { |
| } |
| |
| CrxInstaller::~CrxInstaller() { |
| // Delete the temp directory and crx file as necessary. Note that the |
| // destructor might be called on any thread, so we post a task to the file |
| // thread to make sure the delete happens there. |
| if (!temp_dir_.value().empty()) { |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableFunction( |
| &extension_file_util::DeleteFile, temp_dir_, true)); |
| } |
| |
| if (delete_source_) { |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableFunction( |
| &extension_file_util::DeleteFile, source_file_, false)); |
| } |
| |
| // Make sure the UI is deleted on the ui thread. |
| BrowserThread::DeleteSoon(BrowserThread::UI, FROM_HERE, client_); |
| client_ = NULL; |
| } |
| |
| void CrxInstaller::InstallCrx(const FilePath& source_file) { |
| source_file_ = source_file; |
| |
| scoped_refptr<SandboxedExtensionUnpacker> unpacker( |
| new SandboxedExtensionUnpacker( |
| source_file, |
| g_browser_process->resource_dispatcher_host(), |
| this)); |
| |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod( |
| unpacker.get(), &SandboxedExtensionUnpacker::Start)); |
| } |
| |
| void CrxInstaller::InstallUserScript(const FilePath& source_file, |
| const GURL& original_url) { |
| DCHECK(!original_url.is_empty()); |
| |
| source_file_ = source_file; |
| original_url_ = original_url; |
| |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(this, &CrxInstaller::ConvertUserScriptOnFileThread)); |
| } |
| |
| void CrxInstaller::ConvertUserScriptOnFileThread() { |
| std::string error; |
| scoped_refptr<Extension> extension = |
| ConvertUserScriptToExtension(source_file_, original_url_, &error); |
| if (!extension) { |
| ReportFailureFromFileThread(error); |
| return; |
| } |
| |
| OnUnpackSuccess(extension->path(), extension->path(), extension); |
| } |
| |
| void CrxInstaller::InstallWebApp(const WebApplicationInfo& web_app) { |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(this, &CrxInstaller::ConvertWebAppOnFileThread, |
| web_app)); |
| } |
| |
| void CrxInstaller::ConvertWebAppOnFileThread( |
| const WebApplicationInfo& web_app) { |
| std::string error; |
| scoped_refptr<Extension> extension( |
| ConvertWebAppToExtension(web_app, base::Time::Now())); |
| if (!extension) { |
| // Validation should have stopped any potential errors before getting here. |
| NOTREACHED() << "Could not convert web app to extension."; |
| return; |
| } |
| |
| // TODO(aa): conversion data gets lost here :( |
| |
| OnUnpackSuccess(extension->path(), extension->path(), extension); |
| } |
| |
| bool CrxInstaller::AllowInstall(const Extension* extension, |
| std::string* error) { |
| DCHECK(error); |
| |
| // Make sure the expected id matches. |
| if (!expected_id_.empty() && expected_id_ != extension->id()) { |
| *error = base::StringPrintf( |
| "ID in new CRX manifest (%s) does not match expected id (%s)", |
| extension->id().c_str(), |
| expected_id_.c_str()); |
| return false; |
| } |
| |
| if (expected_version_.get() && |
| !expected_version_->Equals(*extension->version())) { |
| *error = base::StringPrintf( |
| "Version in new CRX %s manifest (%s) does not match expected " |
| "version (%s)", |
| extension->id().c_str(), |
| expected_version_->GetString().c_str(), |
| extension->version()->GetString().c_str()); |
| return false; |
| } |
| |
| // The checks below are skipped for themes and external installs. |
| if (extension->is_theme() || Extension::IsExternalLocation(install_source_)) |
| return true; |
| |
| if (!extensions_enabled_) { |
| *error = "Extensions are not enabled."; |
| return false; |
| } |
| |
| if (extension_->is_app()) { |
| // If the app was downloaded, apps_require_extension_mime_type_ |
| // will be set. In this case, check that it was served with the |
| // right mime type. Make an exception for file URLs, which come |
| // from the users computer and have no headers. |
| if (!original_url_.SchemeIsFile() && |
| apps_require_extension_mime_type_ && |
| original_mime_type_ != Extension::kMimeType) { |
| *error = base::StringPrintf( |
| "Apps must be served with content type %s.", |
| Extension::kMimeType); |
| return false; |
| } |
| |
| // If the client_ is NULL, then the app is either being installed via |
| // an internal mechanism like sync, external_extensions, or default apps. |
| // In that case, we don't want to enforce things like the install origin. |
| if (!is_gallery_install_ && client_) { |
| // For apps with a gallery update URL, require that they be installed |
| // from the gallery. |
| // TODO(erikkay) Apply this rule for paid extensions and themes as well. |
| if (extension->UpdatesFromGallery()) { |
| *error = l10n_util::GetStringFUTF8( |
| IDS_EXTENSION_DISALLOW_NON_DOWNLOADED_GALLERY_INSTALLS, |
| l10n_util::GetStringUTF16(IDS_EXTENSION_WEB_STORE_TITLE)); |
| return false; |
| } |
| |
| // For self-hosted apps, verify that the entire extent is on the same |
| // host (or a subdomain of the host) the download happened from. There's |
| // no way for us to verify that the app controls any other hosts. |
| URLPattern pattern(UserScript::kValidUserScriptSchemes); |
| pattern.set_host(original_url_.host()); |
| pattern.set_match_subdomains(true); |
| |
| ExtensionExtent::PatternList patterns = |
| extension_->web_extent().patterns(); |
| for (size_t i = 0; i < patterns.size(); ++i) { |
| if (!pattern.MatchesHost(patterns[i].host())) { |
| *error = base::StringPrintf( |
| "Apps must be served from the host that they affect."); |
| return false; |
| } |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| void CrxInstaller::OnUnpackFailure(const std::string& error_message) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
| ReportFailureFromFileThread(error_message); |
| } |
| |
| void CrxInstaller::OnUnpackSuccess(const FilePath& temp_dir, |
| const FilePath& extension_dir, |
| const Extension* extension) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
| |
| // Note: We take ownership of |extension| and |temp_dir|. |
| extension_ = extension; |
| temp_dir_ = temp_dir; |
| |
| // We don't have to delete the unpack dir explicity since it is a child of |
| // the temp dir. |
| unpacked_extension_root_ = extension_dir; |
| |
| std::string error; |
| if (!AllowInstall(extension, &error)) { |
| ReportFailureFromFileThread(error); |
| return; |
| } |
| |
| if (client_) { |
| Extension::DecodeIcon(extension_.get(), Extension::EXTENSION_ICON_LARGE, |
| &install_icon_); |
| } |
| |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| NewRunnableMethod(this, &CrxInstaller::ConfirmInstall)); |
| } |
| |
| // Helper method to let us compare a whitelisted manifest with the actual |
| // downloaded extension's manifest, but ignoring the kPublicKey since the |
| // whitelisted manifest doesn't have that value. |
| static bool EqualsIgnoringPublicKey( |
| const DictionaryValue& extension_manifest, |
| const DictionaryValue& whitelisted_manifest) { |
| scoped_ptr<DictionaryValue> manifest_copy(extension_manifest.DeepCopy()); |
| manifest_copy->Remove(extension_manifest_keys::kPublicKey, NULL); |
| return manifest_copy->Equals(&whitelisted_manifest); |
| } |
| |
| void CrxInstaller::ConfirmInstall() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| if (frontend_->extension_prefs()->IsExtensionBlacklisted(extension_->id())) { |
| VLOG(1) << "This extension: " << extension_->id() |
| << " is blacklisted. Install failed."; |
| ReportFailureFromUIThread("This extension is blacklisted."); |
| return; |
| } |
| |
| if (!frontend_->extension_prefs()->IsExtensionAllowedByPolicy( |
| extension_->id())) { |
| ReportFailureFromUIThread("This extension is blacklisted by admin policy."); |
| return; |
| } |
| |
| GURL overlapping_url; |
| const Extension* overlapping_extension = |
| frontend_->GetExtensionByOverlappingWebExtent(extension_->web_extent()); |
| if (overlapping_extension && |
| overlapping_extension->id() != extension_->id()) { |
| ReportFailureFromUIThread(l10n_util::GetStringFUTF8( |
| IDS_EXTENSION_OVERLAPPING_WEB_EXTENT, |
| UTF8ToUTF16(overlapping_extension->name()))); |
| return; |
| } |
| |
| current_version_ = |
| frontend_->extension_prefs()->GetVersionString(extension_->id()); |
| |
| // First see if it's whitelisted by id (the old mechanism). |
| bool whitelisted = ClearWhitelistedInstallId(extension_->id()) && |
| extension_->plugins().empty() && is_gallery_install_; |
| |
| // Now check if it's whitelisted by manifest. |
| scoped_ptr<DictionaryValue> whitelisted_manifest( |
| RemoveWhitelistedManifest(extension_->id())); |
| if (is_gallery_install_ && whitelisted_manifest.get()) { |
| if (!EqualsIgnoringPublicKey(*extension_->manifest_value(), |
| *whitelisted_manifest)) { |
| ReportFailureFromUIThread( |
| l10n_util::GetStringUTF8(IDS_EXTENSION_MANIFEST_INVALID)); |
| return; |
| } |
| whitelisted = true; |
| } |
| |
| if (client_ && |
| (!allow_silent_install_ || !whitelisted)) { |
| AddRef(); // Balanced in Proceed() and Abort(). |
| client_->ConfirmInstall(this, extension_.get()); |
| } else { |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(this, &CrxInstaller::CompleteInstall)); |
| } |
| return; |
| } |
| |
| void CrxInstaller::InstallUIProceed() { |
| BrowserThread::PostTask( |
| BrowserThread::FILE, FROM_HERE, |
| NewRunnableMethod(this, &CrxInstaller::CompleteInstall)); |
| |
| Release(); // balanced in ConfirmInstall(). |
| } |
| |
| void CrxInstaller::InstallUIAbort() { |
| // Technically, this can be called for other reasons than the user hitting |
| // cancel, but they're rare. |
| ExtensionService::RecordPermissionMessagesHistogram( |
| extension_, "Extensions.Permissions_InstallCancel"); |
| |
| // Kill the theme loading bubble. |
| NotificationService* service = NotificationService::current(); |
| service->Notify(NotificationType::NO_THEME_DETECTED, |
| Source<CrxInstaller>(this), |
| NotificationService::NoDetails()); |
| Release(); // balanced in ConfirmInstall(). |
| |
| // We're done. Since we don't post any more tasks to ourself, our ref count |
| // should go to zero and we die. The destructor will clean up the temp dir. |
| } |
| |
| void CrxInstaller::CompleteInstall() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
| |
| if (!current_version_.empty()) { |
| scoped_ptr<Version> current_version( |
| Version::GetVersionFromString(current_version_)); |
| if (current_version->CompareTo(*(extension_->version())) > 0) { |
| ReportFailureFromFileThread("Attempted to downgrade extension."); |
| return; |
| } |
| } |
| |
| // See how long extension install paths are. This is important on |
| // windows, because file operations may fail if the path to a file |
| // exceeds a small constant. See crbug.com/69693 . |
| UMA_HISTOGRAM_CUSTOM_COUNTS( |
| "Extensions.CrxInstallDirPathLength", |
| install_directory_.value().length(), 0, 500, 100); |
| |
| FilePath version_dir = extension_file_util::InstallExtension( |
| unpacked_extension_root_, |
| extension_->id(), |
| extension_->VersionString(), |
| install_directory_); |
| if (version_dir.empty()) { |
| ReportFailureFromFileThread( |
| l10n_util::GetStringUTF8( |
| IDS_EXTENSION_MOVE_DIRECTORY_TO_PROFILE_FAILED)); |
| return; |
| } |
| |
| // This is lame, but we must reload the extension because absolute paths |
| // inside the content scripts are established inside InitFromValue() and we |
| // just moved the extension. |
| // TODO(aa): All paths to resources inside extensions should be created |
| // lazily and based on the Extension's root path at that moment. |
| std::string error; |
| extension_ = extension_file_util::LoadExtension( |
| version_dir, |
| install_source_, |
| Extension::REQUIRE_KEY, |
| &error); |
| CHECK(error.empty()) << error; |
| |
| ReportSuccessFromFileThread(); |
| } |
| |
| void CrxInstaller::ReportFailureFromFileThread(const std::string& error) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| NewRunnableMethod(this, &CrxInstaller::ReportFailureFromUIThread, error)); |
| } |
| |
| void CrxInstaller::ReportFailureFromUIThread(const std::string& error) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| |
| NotificationService* service = NotificationService::current(); |
| service->Notify(NotificationType::EXTENSION_INSTALL_ERROR, |
| Source<CrxInstaller>(this), |
| Details<const std::string>(&error)); |
| |
| // This isn't really necessary, it is only used because unit tests expect to |
| // see errors get reported via this interface. |
| // |
| // TODO(aa): Need to go through unit tests and clean them up too, probably get |
| // rid of this line. |
| ExtensionErrorReporter::GetInstance()->ReportError(error, false); // quiet |
| |
| if (client_) |
| client_->OnInstallFailure(error); |
| } |
| |
| void CrxInstaller::ReportSuccessFromFileThread() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| NewRunnableMethod(this, &CrxInstaller::ReportSuccessFromUIThread)); |
| } |
| |
| void CrxInstaller::ReportSuccessFromUIThread() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| |
| // If there is a client, tell the client about installation. |
| if (client_) |
| client_->OnInstallSuccess(extension_.get(), install_icon_.get()); |
| |
| // Tell the frontend about the installation and hand off ownership of |
| // extension_ to it. |
| frontend_->OnExtensionInstalled(extension_); |
| extension_ = NULL; |
| |
| // We're done. We don't post any more tasks to ourselves so we are deleted |
| // soon. |
| } |