| // 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/extensions/webstore_installer.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <limits> |
| #include <set> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/files/file_util.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/path_service.h" |
| #include "base/rand_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/download/download_crx_util.h" |
| #include "chrome/browser/download/download_prefs.h" |
| #include "chrome/browser/download/download_stats.h" |
| #include "chrome/browser/extensions/crx_installer.h" |
| #include "chrome/browser/extensions/install_tracker.h" |
| #include "chrome/browser/extensions/install_tracker_factory.h" |
| #include "chrome/browser/extensions/install_verifier.h" |
| #include "chrome/browser/extensions/shared_module_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/crx_file/id_util.h" |
| #include "components/download/public/common/download_url_parameters.h" |
| #include "components/update_client/update_query_params.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/download_manager.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/notification_details.h" |
| #include "content/public/browser/notification_service.h" |
| #include "content/public/browser/notification_source.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/extension_file_task_runner.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/install/crx_install_error.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_urls.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/common/manifest_handlers/shared_module_info.h" |
| #include "net/base/escape.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/gurl.h" |
| |
| #if defined(OS_CHROMEOS) |
| #include "chrome/browser/chromeos/drive/file_system_util.h" |
| #endif |
| |
| using content::BrowserContext; |
| using content::BrowserThread; |
| using content::DownloadManager; |
| using content::NavigationController; |
| using download::DownloadItem; |
| using download::DownloadUrlParameters; |
| |
| namespace { |
| |
| // Key used to attach the Approval to the DownloadItem. |
| const char kApprovalKey[] = "extensions.webstore_installer"; |
| |
| const char kInvalidIdError[] = "Invalid id"; |
| const char kDownloadDirectoryError[] = "Could not create download directory"; |
| const char kDownloadCanceledError[] = "Download canceled"; |
| const char kDownloadInterruptedError[] = "Download interrupted"; |
| const char kInvalidDownloadError[] = |
| "Download was not a valid extension or user script"; |
| const char kDependencyNotFoundError[] = "Dependency not found"; |
| const char kDependencyNotSharedModuleError[] = |
| "Dependency is not shared module"; |
| const char kInlineInstallSource[] = "inline"; |
| const char kDefaultInstallSource[] = "ondemand"; |
| const char kAppLauncherInstallSource[] = "applauncher"; |
| |
| // TODO(rockot): Share this duplicated constant with the extension updater. |
| // See http://crbug.com/371398. |
| const char kAuthUserQueryKey[] = "authuser"; |
| |
| constexpr base::TimeDelta kTimeRemainingThreshold = |
| base::TimeDelta::FromSeconds(1); |
| |
| // Folder for downloading crx files from the webstore. This is used so that the |
| // crx files don't go via the usual downloads folder. |
| const base::FilePath::CharType kWebstoreDownloadFolder[] = |
| FILE_PATH_LITERAL("Webstore Downloads"); |
| |
| base::FilePath* g_download_directory_for_tests = NULL; |
| |
| base::FilePath GetDownloadFilePath(const base::FilePath& download_directory, |
| const std::string& id) { |
| // Ensure the download directory exists. TODO(asargent) - make this use |
| // common code from the downloads system. |
| if (!base::DirectoryExists(download_directory) && |
| !base::CreateDirectory(download_directory)) { |
| return base::FilePath(); |
| } |
| |
| // This is to help avoid a race condition between when we generate this |
| // filename and when the download starts writing to it (think concurrently |
| // running sharded browser tests installing the same test file, for |
| // instance). |
| std::string random_number = base::NumberToString( |
| base::RandGenerator(std::numeric_limits<uint16_t>::max())); |
| |
| base::FilePath file = |
| download_directory.AppendASCII(id + "_" + random_number + ".crx"); |
| |
| return base::GetUniquePath(file); |
| } |
| |
| void MaybeAppendAuthUserParameter(const std::string& authuser, GURL* url) { |
| if (authuser.empty()) |
| return; |
| std::string old_query = url->query(); |
| url::Component query(0, old_query.length()); |
| url::Component key, value; |
| // Ensure that the URL doesn't already specify an authuser parameter. |
| while (url::ExtractQueryKeyValue( |
| old_query.c_str(), &query, &key, &value)) { |
| std::string key_string = old_query.substr(key.begin, key.len); |
| if (key_string == kAuthUserQueryKey) { |
| return; |
| } |
| } |
| if (!old_query.empty()) { |
| old_query += "&"; |
| } |
| std::string authuser_param = base::StringPrintf( |
| "%s=%s", |
| kAuthUserQueryKey, |
| authuser.c_str()); |
| |
| // TODO(rockot): Share this duplicated code with the extension updater. |
| // See http://crbug.com/371398. |
| std::string new_query_string = old_query + authuser_param; |
| url::Component new_query(0, new_query_string.length()); |
| url::Replacements<char> replacements; |
| replacements.SetQuery(new_query_string.c_str(), new_query); |
| *url = url->ReplaceComponents(replacements); |
| } |
| |
| std::string GetErrorMessageForDownloadInterrupt( |
| download::DownloadInterruptReason reason) { |
| switch (reason) { |
| case download::DOWNLOAD_INTERRUPT_REASON_SERVER_UNAUTHORIZED: |
| case download::DOWNLOAD_INTERRUPT_REASON_SERVER_FORBIDDEN: |
| return l10n_util::GetStringUTF8(IDS_WEBSTORE_DOWNLOAD_ACCESS_DENIED); |
| default: |
| break; |
| } |
| return kDownloadInterruptedError; |
| } |
| |
| } // namespace |
| |
| namespace extensions { |
| |
| // static |
| GURL WebstoreInstaller::GetWebstoreInstallURL( |
| const std::string& extension_id, |
| InstallSource source) { |
| std::string install_source; |
| switch (source) { |
| case INSTALL_SOURCE_INLINE: |
| install_source = kInlineInstallSource; |
| break; |
| case INSTALL_SOURCE_APP_LAUNCHER: |
| install_source = kAppLauncherInstallSource; |
| break; |
| case INSTALL_SOURCE_OTHER: |
| install_source = kDefaultInstallSource; |
| } |
| |
| base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess(); |
| if (cmd_line->HasSwitch(::switches::kAppsGalleryDownloadURL)) { |
| std::string download_url = |
| cmd_line->GetSwitchValueASCII(::switches::kAppsGalleryDownloadURL); |
| return GURL(base::StringPrintf(download_url.c_str(), |
| extension_id.c_str())); |
| } |
| std::vector<base::StringPiece> params; |
| std::string extension_param = "id=" + extension_id; |
| std::string installsource_param = "installsource=" + install_source; |
| params.push_back(extension_param); |
| if (!install_source.empty()) |
| params.push_back(installsource_param); |
| params.push_back("uc"); |
| std::string url_string = extension_urls::GetWebstoreUpdateUrl().spec(); |
| |
| GURL url(url_string + "?response=redirect&" + |
| update_client::UpdateQueryParams::Get( |
| update_client::UpdateQueryParams::CRX) + |
| "&x=" + net::EscapeQueryParamValue(base::JoinString(params, "&"), |
| true)); |
| DCHECK(url.is_valid()); |
| |
| return url; |
| } |
| |
| void WebstoreInstaller::Delegate::OnExtensionDownloadStarted( |
| const std::string& id, |
| download::DownloadItem* item) {} |
| |
| void WebstoreInstaller::Delegate::OnExtensionDownloadProgress( |
| const std::string& id, |
| download::DownloadItem* item) {} |
| |
| WebstoreInstaller::Approval::Approval() |
| : profile(NULL), |
| use_app_installed_bubble(false), |
| skip_post_install_ui(false), |
| skip_install_dialog(false), |
| manifest_check_level(MANIFEST_CHECK_LEVEL_STRICT) { |
| } |
| |
| std::unique_ptr<WebstoreInstaller::Approval> |
| WebstoreInstaller::Approval::CreateWithInstallPrompt(Profile* profile) { |
| std::unique_ptr<Approval> result(new Approval()); |
| result->profile = profile; |
| return result; |
| } |
| |
| std::unique_ptr<WebstoreInstaller::Approval> |
| WebstoreInstaller::Approval::CreateForSharedModule(Profile* profile) { |
| std::unique_ptr<Approval> result(new Approval()); |
| result->profile = profile; |
| result->skip_install_dialog = true; |
| result->skip_post_install_ui = true; |
| result->manifest_check_level = MANIFEST_CHECK_LEVEL_NONE; |
| return result; |
| } |
| |
| std::unique_ptr<WebstoreInstaller::Approval> |
| WebstoreInstaller::Approval::CreateWithNoInstallPrompt( |
| Profile* profile, |
| const std::string& extension_id, |
| std::unique_ptr<base::DictionaryValue> parsed_manifest, |
| bool strict_manifest_check) { |
| std::unique_ptr<Approval> result(new Approval()); |
| result->extension_id = extension_id; |
| result->profile = profile; |
| result->manifest = std::unique_ptr<Manifest>(new Manifest( |
| Manifest::INVALID_LOCATION, |
| std::unique_ptr<base::DictionaryValue>(parsed_manifest->DeepCopy()))); |
| result->skip_install_dialog = true; |
| result->manifest_check_level = strict_manifest_check ? |
| MANIFEST_CHECK_LEVEL_STRICT : MANIFEST_CHECK_LEVEL_LOOSE; |
| return result; |
| } |
| |
| WebstoreInstaller::Approval::~Approval() {} |
| |
| const WebstoreInstaller::Approval* WebstoreInstaller::GetAssociatedApproval( |
| const DownloadItem& download) { |
| return static_cast<const Approval*>(download.GetUserData(kApprovalKey)); |
| } |
| |
| WebstoreInstaller::WebstoreInstaller(Profile* profile, |
| Delegate* delegate, |
| content::WebContents* web_contents, |
| const std::string& id, |
| std::unique_ptr<Approval> approval, |
| InstallSource source) |
| : content::WebContentsObserver(web_contents), |
| extension_registry_observer_(this), |
| profile_(profile), |
| delegate_(delegate), |
| id_(id), |
| install_source_(source), |
| download_item_(NULL), |
| approval_(approval.release()), |
| total_modules_(0), |
| download_started_(false) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(web_contents); |
| |
| registrar_.Add(this, |
| extensions::NOTIFICATION_EXTENSION_INSTALL_ERROR, |
| content::Source<CrxInstaller>(NULL)); |
| extension_registry_observer_.Add(ExtensionRegistry::Get(profile)); |
| } |
| |
| void WebstoreInstaller::Start() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| AddRef(); // Balanced in ReportSuccess and ReportFailure. |
| |
| if (!crx_file::id_util::IdIsValid(id_)) { |
| ReportFailure(kInvalidIdError, FAILURE_REASON_OTHER); |
| return; |
| } |
| |
| ExtensionService* extension_service = |
| ExtensionSystem::Get(profile_)->extension_service(); |
| if (approval_.get() && approval_->dummy_extension.get()) { |
| extension_service->shared_module_service()->CheckImports( |
| approval_->dummy_extension.get(), &pending_modules_, &pending_modules_); |
| // Do not check the return value of CheckImports, the CRX installer |
| // will report appropriate error messages and fail to install if there |
| // is an import error. |
| } |
| |
| // Add the extension main module into the list. |
| SharedModuleInfo::ImportInfo info; |
| info.extension_id = id_; |
| pending_modules_.push_back(info); |
| |
| total_modules_ = pending_modules_.size(); |
| |
| std::set<std::string> ids; |
| std::list<SharedModuleInfo::ImportInfo>::const_iterator i; |
| for (i = pending_modules_.begin(); i != pending_modules_.end(); ++i) { |
| ids.insert(i->extension_id); |
| } |
| InstallVerifier::Get(profile_)->AddProvisional(ids); |
| |
| std::string name; |
| if (!approval_->manifest->value()->GetString(manifest_keys::kName, &name)) { |
| NOTREACHED(); |
| } |
| extensions::InstallTracker* tracker = |
| extensions::InstallTrackerFactory::GetForBrowserContext(profile_); |
| extensions::InstallObserver::ExtensionInstallParams params( |
| id_, |
| name, |
| approval_->installing_icon, |
| approval_->manifest->is_app(), |
| approval_->manifest->is_platform_app()); |
| tracker->OnBeginExtensionInstall(params); |
| |
| tracker->OnBeginExtensionDownload(id_); |
| |
| // TODO(crbug.com/305343): Query manifest of dependencies before |
| // downloading & installing those dependencies. |
| DownloadNextPendingModule(); |
| } |
| |
| void WebstoreInstaller::Observe(int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) { |
| DCHECK_EQ(extensions::NOTIFICATION_EXTENSION_INSTALL_ERROR, type); |
| |
| CrxInstaller* crx_installer = content::Source<CrxInstaller>(source).ptr(); |
| CHECK(crx_installer); |
| if (crx_installer != crx_installer_.get()) |
| return; |
| |
| // TODO(rdevlin.cronin): Continue removing std::string errors and |
| // replacing with base::string16. See crbug.com/71980. |
| const extensions::CrxInstallError* error = |
| content::Details<const extensions::CrxInstallError>(details).ptr(); |
| const std::string utf8_error = base::UTF16ToUTF8(error->message()); |
| crx_installer_ = nullptr; |
| // ReportFailure releases a reference to this object so it must be the |
| // last operation in this method. |
| ReportFailure(utf8_error, FAILURE_REASON_OTHER); |
| } |
| |
| void WebstoreInstaller::OnExtensionInstalled( |
| content::BrowserContext* browser_context, |
| const Extension* extension, |
| bool is_update) { |
| CHECK(profile_->IsSameProfile(Profile::FromBrowserContext(browser_context))); |
| if (pending_modules_.empty()) |
| return; |
| SharedModuleInfo::ImportInfo info = pending_modules_.front(); |
| if (extension->id() != info.extension_id) |
| return; |
| pending_modules_.pop_front(); |
| |
| // Clean up local state from the current download. |
| if (download_item_) { |
| download_item_->RemoveObserver(this); |
| download_item_->Remove(); |
| download_item_ = NULL; |
| } |
| crx_installer_ = NULL; |
| |
| if (pending_modules_.empty()) { |
| CHECK_EQ(extension->id(), id_); |
| ReportSuccess(); |
| } else { |
| const base::Version version_required(info.minimum_version); |
| if (version_required.IsValid() && |
| extension->version().CompareTo(version_required) < 0) { |
| // It should not happen, CrxInstaller will make sure the version is |
| // equal or newer than version_required. |
| ReportFailure(kDependencyNotFoundError, |
| FAILURE_REASON_DEPENDENCY_NOT_FOUND); |
| } else if (!SharedModuleInfo::IsSharedModule(extension)) { |
| // It should not happen, CrxInstaller will make sure it is a shared |
| // module. |
| ReportFailure(kDependencyNotSharedModuleError, |
| FAILURE_REASON_DEPENDENCY_NOT_SHARED_MODULE); |
| } else { |
| DownloadNextPendingModule(); |
| } |
| } |
| } |
| |
| void WebstoreInstaller::InvalidateDelegate() { |
| delegate_ = NULL; |
| } |
| |
| void WebstoreInstaller::SetDownloadDirectoryForTests( |
| base::FilePath* directory) { |
| g_download_directory_for_tests = directory; |
| } |
| |
| WebstoreInstaller::~WebstoreInstaller() { |
| if (download_item_) { |
| download_item_->RemoveObserver(this); |
| download_item_ = NULL; |
| } |
| } |
| |
| void WebstoreInstaller::OnDownloadStarted( |
| const std::string& extension_id, |
| DownloadItem* item, |
| download::DownloadInterruptReason interrupt_reason) { |
| if (!item || interrupt_reason != download::DOWNLOAD_INTERRUPT_REASON_NONE) { |
| if (item) |
| item->Remove(); |
| ReportFailure(download::DownloadInterruptReasonToString(interrupt_reason), |
| FAILURE_REASON_OTHER); |
| return; |
| } |
| |
| bool found = false; |
| for (const auto& module : pending_modules_) { |
| if (extension_id == module.extension_id) { |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| // If this extension is not pending, it means another installer has |
| // installed this extension and triggered OnExtensionInstalled(). In this |
| // case, either it was the main module and success has already been |
| // reported, or it was a dependency and either failed (ie. wrong version) or |
| // the next download was triggered. In any case, the only thing that needs |
| // to be done is to stop this download. |
| item->Remove(); |
| return; |
| } |
| |
| DCHECK_EQ(download::DOWNLOAD_INTERRUPT_REASON_NONE, interrupt_reason); |
| DCHECK(!pending_modules_.empty()); |
| download_item_ = item; |
| download_item_->AddObserver(this); |
| if (pending_modules_.size() > 1) { |
| // We are downloading a shared module. We need create an approval for it. |
| std::unique_ptr<Approval> approval = |
| Approval::CreateForSharedModule(profile_); |
| const SharedModuleInfo::ImportInfo& info = pending_modules_.front(); |
| approval->extension_id = info.extension_id; |
| const base::Version version_required(info.minimum_version); |
| |
| if (version_required.IsValid()) { |
| approval->minimum_version.reset(new base::Version(version_required)); |
| } |
| download_item_->SetUserData(kApprovalKey, std::move(approval)); |
| } else { |
| // It is for the main module of the extension. We should use the provided |
| // |approval_|. |
| if (approval_) |
| download_item_->SetUserData(kApprovalKey, std::move(approval_)); |
| } |
| |
| if (!download_started_) { |
| if (delegate_) |
| delegate_->OnExtensionDownloadStarted(id_, download_item_); |
| download_started_ = true; |
| } |
| } |
| |
| void WebstoreInstaller::OnDownloadUpdated(DownloadItem* download) { |
| CHECK_EQ(download_item_, download); |
| |
| switch (download->GetState()) { |
| case DownloadItem::CANCELLED: |
| ReportFailure(kDownloadCanceledError, FAILURE_REASON_CANCELLED); |
| break; |
| case DownloadItem::INTERRUPTED: |
| RecordInterrupt(download); |
| ReportFailure( |
| GetErrorMessageForDownloadInterrupt(download->GetLastReason()), |
| FAILURE_REASON_OTHER); |
| break; |
| case DownloadItem::COMPLETE: |
| // Stop the progress timer if it's running. |
| download_progress_timer_.Stop(); |
| |
| // Only wait for other notifications if the download is really |
| // an extension. |
| if (!download_crx_util::IsExtensionDownload(*download)) { |
| ReportFailure(kInvalidDownloadError, FAILURE_REASON_OTHER); |
| return; |
| } |
| |
| if (crx_installer_.get()) |
| return; // DownloadItemImpl calls the observer twice, ignore it. |
| |
| StartCrxInstaller(*download); |
| |
| if (pending_modules_.size() == 1) { |
| // The download is the last module - the extension main module. |
| if (delegate_) |
| delegate_->OnExtensionDownloadProgress(id_, download); |
| extensions::InstallTracker* tracker = |
| extensions::InstallTrackerFactory::GetForBrowserContext(profile_); |
| tracker->OnDownloadProgress(id_, 100); |
| } |
| break; |
| case DownloadItem::IN_PROGRESS: { |
| if (delegate_ && pending_modules_.size() == 1) { |
| // Only report download progress for the main module to |delegrate_|. |
| delegate_->OnExtensionDownloadProgress(id_, download); |
| } |
| UpdateDownloadProgress(); |
| break; |
| } |
| default: |
| // Continue listening if the download is not in one of the above states. |
| break; |
| } |
| } |
| |
| void WebstoreInstaller::OnDownloadDestroyed(DownloadItem* download) { |
| CHECK_EQ(download_item_, download); |
| download_item_->RemoveObserver(this); |
| download_item_ = NULL; |
| } |
| |
| void WebstoreInstaller::DownloadNextPendingModule() { |
| CHECK(!pending_modules_.empty()); |
| if (pending_modules_.size() == 1) { |
| DCHECK_EQ(id_, pending_modules_.front().extension_id); |
| DownloadCrx(id_, install_source_); |
| } else { |
| DownloadCrx(pending_modules_.front().extension_id, INSTALL_SOURCE_OTHER); |
| } |
| } |
| |
| void WebstoreInstaller::DownloadCrx( |
| const std::string& extension_id, |
| InstallSource source) { |
| download_url_ = GetWebstoreInstallURL(extension_id, source); |
| MaybeAppendAuthUserParameter(approval_->authuser, &download_url_); |
| |
| base::FilePath user_data_dir; |
| base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir); |
| base::FilePath download_path = user_data_dir.Append(kWebstoreDownloadFolder); |
| |
| base::FilePath download_directory(g_download_directory_for_tests ? |
| *g_download_directory_for_tests : download_path); |
| |
| #if defined(OS_CHROMEOS) |
| // Do not use drive for extension downloads. |
| if (drive::util::IsUnderDriveMountPoint(download_directory)) { |
| download_directory = DownloadPrefs::FromBrowserContext( |
| profile_)->GetDefaultDownloadDirectoryForProfile(); |
| } |
| #endif |
| |
| base::PostTaskAndReplyWithResult( |
| GetExtensionFileTaskRunner().get(), FROM_HERE, |
| base::BindOnce(&GetDownloadFilePath, download_directory, extension_id), |
| base::BindOnce(&WebstoreInstaller::StartDownload, this, extension_id)); |
| } |
| |
| // http://crbug.com/165634 |
| // http://crbug.com/126013 |
| // The current working theory is that one of the many pointers dereferenced in |
| // here is occasionally deleted before all of its referers are nullified, |
| // probably in a callback race. After this comment is released, the crash |
| // reports should narrow down exactly which pointer it is. Collapsing all the |
| // early-returns into a single branch makes it hard to see exactly which pointer |
| // it is. |
| void WebstoreInstaller::StartDownload(const std::string& extension_id, |
| const base::FilePath& file) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (file.empty()) { |
| ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER); |
| return; |
| } |
| |
| DownloadManager* download_manager = |
| BrowserContext::GetDownloadManager(profile_); |
| if (!download_manager) { |
| ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER); |
| return; |
| } |
| |
| content::WebContents* contents = web_contents(); |
| if (!contents) { |
| ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER); |
| return; |
| } |
| if (!contents->GetRenderViewHost()) { |
| ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER); |
| return; |
| } |
| if (!contents->GetRenderViewHost()->GetProcess()) { |
| ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER); |
| return; |
| } |
| |
| content::NavigationController& controller = contents->GetController(); |
| if (!controller.GetBrowserContext()) { |
| ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER); |
| return; |
| } |
| if (!controller.GetBrowserContext()->GetResourceContext()) { |
| ReportFailure(kDownloadDirectoryError, FAILURE_REASON_OTHER); |
| return; |
| } |
| |
| // The download url for the given extension is contained in |download_url_|. |
| // We will navigate the current tab to this url to start the download. The |
| // download system will then pass the crx to the CrxInstaller. |
| int render_process_host_id = |
| contents->GetRenderViewHost()->GetProcess()->GetID(); |
| int render_view_host_routing_id = |
| contents->GetRenderViewHost()->GetRoutingID(); |
| |
| content::RenderFrameHost* render_frame_host = contents->GetMainFrame(); |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("webstore_installer", R"( |
| semantics { |
| sender: "Webstore Installer" |
| description: "Downloads an extension for installation." |
| trigger: |
| "User initiates a webstore extension installation flow, including " |
| "installing from the webstore, inline installation from a site, " |
| "re-installing a corrupted extension, and others." |
| data: |
| "The id of the extension to be installed and information about the " |
| "user's installation, including version, language, distribution " |
| "(Chrome vs Chromium), NaCl architecture, installation source (as " |
| "an enum), and accepted crx formats." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: |
| "This feature cannot be disabled. It is only activated if the user " |
| "triggers an extension installation." |
| chrome_policy { |
| ExtensionInstallBlacklist { |
| ExtensionInstallBlacklist: { |
| entries: '*' |
| } |
| } |
| } |
| })"); |
| std::unique_ptr<DownloadUrlParameters> params(new DownloadUrlParameters( |
| download_url_, render_process_host_id, render_view_host_routing_id, |
| render_frame_host->GetRoutingID(), traffic_annotation)); |
| params->set_file_path(file); |
| if (controller.GetVisibleEntry()) { |
| content::Referrer referrer = content::Referrer::SanitizeForRequest( |
| download_url_, |
| content::Referrer(controller.GetVisibleEntry()->GetURL(), |
| network::mojom::ReferrerPolicy::kDefault)); |
| params->set_referrer(referrer.url); |
| params->set_referrer_policy( |
| content::Referrer::ReferrerPolicyForUrlRequest(referrer.policy)); |
| } |
| params->set_callback(base::Bind(&WebstoreInstaller::OnDownloadStarted, |
| this, |
| extension_id)); |
| params->set_download_source(download::DownloadSource::EXTENSION_INSTALLER); |
| download_manager->DownloadUrl(std::move(params)); |
| } |
| |
| void WebstoreInstaller::UpdateDownloadProgress() { |
| // If the download has gone away, or isn't in progress (in which case we can't |
| // give a good progress estimate), stop any running timers and return. |
| if (!download_item_ || |
| download_item_->GetState() != DownloadItem::IN_PROGRESS) { |
| download_progress_timer_.Stop(); |
| return; |
| } |
| |
| int percent = download_item_->PercentComplete(); |
| // Only report progress if percent is more than 0 or we have finished |
| // downloading at least one of the pending modules. |
| int finished_modules = total_modules_ - pending_modules_.size(); |
| if (finished_modules > 0 && percent < 0) |
| percent = 0; |
| if (percent >= 0) { |
| percent = (percent + (finished_modules * 100)) / total_modules_; |
| extensions::InstallTracker* tracker = |
| extensions::InstallTrackerFactory::GetForBrowserContext(profile_); |
| tracker->OnDownloadProgress(id_, percent); |
| } |
| |
| // If there's enough time remaining on the download to warrant an update, |
| // set the timer (overwriting any current timers). Otherwise, stop the |
| // timer. |
| base::TimeDelta time_remaining; |
| if (download_item_->TimeRemaining(&time_remaining) && |
| time_remaining > kTimeRemainingThreshold) { |
| download_progress_timer_.Start(FROM_HERE, kTimeRemainingThreshold, this, |
| &WebstoreInstaller::UpdateDownloadProgress); |
| } else { |
| download_progress_timer_.Stop(); |
| } |
| } |
| |
| void WebstoreInstaller::StartCrxInstaller(const DownloadItem& download) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(!crx_installer_.get()); |
| |
| // The clock may be backward, e.g. daylight savings time just happenned. |
| if (download.GetEndTime() >= download.GetStartTime()) { |
| UMA_HISTOGRAM_TIMES("Extensions.WebstoreDownload.FileDownload", |
| download.GetEndTime() - download.GetStartTime()); |
| } |
| ExtensionService* service = ExtensionSystem::Get(profile_)-> |
| extension_service(); |
| CHECK(service); |
| |
| const Approval* approval = GetAssociatedApproval(download); |
| DCHECK(approval); |
| |
| crx_installer_ = download_crx_util::CreateCrxInstaller(profile_, download); |
| |
| crx_installer_->set_expected_id(approval->extension_id); |
| crx_installer_->set_is_gallery_install(true); |
| crx_installer_->set_allow_silent_install(true); |
| |
| crx_installer_->InstallCrx(download.GetFullPath()); |
| } |
| |
| void WebstoreInstaller::ReportFailure(const std::string& error, |
| FailureReason reason) { |
| if (delegate_) { |
| delegate_->OnExtensionInstallFailure(id_, error, reason); |
| delegate_ = NULL; |
| } |
| |
| extensions::InstallTracker* tracker = |
| extensions::InstallTrackerFactory::GetForBrowserContext(profile_); |
| tracker->OnInstallFailure(id_); |
| |
| Release(); // Balanced in Start(). |
| } |
| |
| void WebstoreInstaller::ReportSuccess() { |
| if (delegate_) { |
| delegate_->OnExtensionInstallSuccess(id_); |
| delegate_ = NULL; |
| } |
| |
| Release(); // Balanced in Start(). |
| } |
| |
| void WebstoreInstaller::RecordInterrupt(const DownloadItem* download) const { |
| base::UmaHistogramSparse("Extensions.WebstoreDownload.InterruptReason", |
| download->GetLastReason()); |
| |
| // Use logarithmic bin sizes up to 1 TB. |
| const int kNumBuckets = 30; |
| const int64_t kMaxSizeKb = 1 << kNumBuckets; |
| UMA_HISTOGRAM_CUSTOM_COUNTS( |
| "Extensions.WebstoreDownload.InterruptReceivedKBytes", |
| download->GetReceivedBytes() / 1024, |
| 1, |
| kMaxSizeKb, |
| kNumBuckets); |
| int64_t total_bytes = download->GetTotalBytes(); |
| if (total_bytes >= 0) { |
| UMA_HISTOGRAM_CUSTOM_COUNTS( |
| "Extensions.WebstoreDownload.InterruptTotalKBytes", |
| total_bytes / 1024, |
| 1, |
| kMaxSizeKb, |
| kNumBuckets); |
| } |
| UMA_HISTOGRAM_BOOLEAN( |
| "Extensions.WebstoreDownload.InterruptTotalSizeUnknown", |
| total_bytes <= 0); |
| } |
| |
| } // namespace extensions |