| // Copyright 2018 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/web_applications/extensions/pending_bookmark_app_manager.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/strings/string16.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/web_applications/components/app_registrar.h" |
| #include "chrome/browser/web_applications/components/web_app_constants.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/uninstall_reason.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| const int kSecondsToWaitForWebContentsLoad = 30; |
| |
| std::unique_ptr<content::WebContents> WebContentsCreateWrapper( |
| Profile* profile) { |
| return content::WebContents::Create( |
| content::WebContents::CreateParams(profile)); |
| } |
| |
| std::unique_ptr<BookmarkAppInstallationTask> InstallationTaskCreateWrapper( |
| Profile* profile, |
| web_app::PendingAppManager::AppInfo app_info) { |
| return std::make_unique<BookmarkAppInstallationTask>(profile, |
| std::move(app_info)); |
| } |
| |
| } // namespace |
| |
| struct PendingBookmarkAppManager::TaskAndCallback { |
| TaskAndCallback(std::unique_ptr<BookmarkAppInstallationTask> task, |
| OnceInstallCallback callback) |
| : task(std::move(task)), callback(std::move(callback)) {} |
| ~TaskAndCallback() = default; |
| |
| std::unique_ptr<BookmarkAppInstallationTask> task; |
| OnceInstallCallback callback; |
| }; |
| |
| PendingBookmarkAppManager::PendingBookmarkAppManager( |
| Profile* profile, |
| web_app::AppRegistrar* registrar) |
| : profile_(profile), |
| registrar_(registrar), |
| extension_ids_map_(profile->GetPrefs()), |
| web_contents_factory_(base::BindRepeating(&WebContentsCreateWrapper)), |
| task_factory_(base::BindRepeating(&InstallationTaskCreateWrapper)), |
| timer_(std::make_unique<base::OneShotTimer>()) {} |
| |
| PendingBookmarkAppManager::~PendingBookmarkAppManager() = default; |
| |
| void PendingBookmarkAppManager::Install(AppInfo app_to_install, |
| OnceInstallCallback callback) { |
| pending_tasks_and_callbacks_.push_front(std::make_unique<TaskAndCallback>( |
| task_factory_.Run(profile_, std::move(app_to_install)), |
| std::move(callback))); |
| |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&PendingBookmarkAppManager::MaybeStartNextInstallation, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PendingBookmarkAppManager::InstallApps( |
| std::vector<AppInfo> apps_to_install, |
| const RepeatingInstallCallback& callback) { |
| for (auto& app_to_install : apps_to_install) { |
| pending_tasks_and_callbacks_.push_back(std::make_unique<TaskAndCallback>( |
| task_factory_.Run(profile_, std::move(app_to_install)), callback)); |
| } |
| |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&PendingBookmarkAppManager::MaybeStartNextInstallation, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PendingBookmarkAppManager::UninstallApps( |
| std::vector<GURL> apps_to_uninstall, |
| const UninstallCallback& callback) { |
| for (auto& app_to_uninstall : apps_to_uninstall) { |
| base::Optional<std::string> extension_id = |
| extension_ids_map_.LookupExtensionId(app_to_uninstall); |
| if (!extension_id) { |
| callback.Run(app_to_uninstall, false); |
| continue; |
| } |
| |
| base::Optional<bool> opt = |
| IsExtensionPresentAndInstalled(extension_id.value()); |
| if (!opt.has_value() || !opt.value()) { |
| LOG(WARNING) << "Couldn't uninstall app with url " << app_to_uninstall |
| << "; App doesn't exist"; |
| callback.Run(app_to_uninstall, false); |
| continue; |
| } |
| |
| DCHECK(opt.value()); |
| base::string16 error; |
| bool uninstalled = |
| ExtensionSystem::Get(profile_)->extension_service()->UninstallExtension( |
| extension_id.value(), UNINSTALL_REASON_ORPHANED_EXTERNAL_EXTENSION, |
| &error); |
| |
| if (!uninstalled) { |
| LOG(WARNING) << "Couldn't uninstall app with url " << app_to_uninstall |
| << ". " << error; |
| } |
| |
| callback.Run(app_to_uninstall, uninstalled); |
| } |
| } |
| |
| std::vector<GURL> PendingBookmarkAppManager::GetInstalledAppUrls( |
| web_app::InstallSource install_source) const { |
| return web_app::ExtensionIdsMap::GetInstalledAppUrls(profile_, |
| install_source); |
| } |
| |
| base::Optional<std::string> PendingBookmarkAppManager::LookupAppId( |
| const GURL& url) const { |
| return extension_ids_map_.LookupExtensionId(url); |
| } |
| |
| void PendingBookmarkAppManager::SetFactoriesForTesting( |
| WebContentsFactory web_contents_factory, |
| TaskFactory task_factory) { |
| web_contents_factory_ = std::move(web_contents_factory); |
| task_factory_ = std::move(task_factory); |
| } |
| |
| void PendingBookmarkAppManager::SetTimerForTesting( |
| std::unique_ptr<base::OneShotTimer> timer) { |
| timer_ = std::move(timer); |
| } |
| |
| base::Optional<bool> PendingBookmarkAppManager::IsExtensionPresentAndInstalled( |
| const std::string& extension_id) { |
| if (registrar_->IsInstalled(extension_id)) { |
| return base::Optional<bool>(true); |
| } |
| |
| if (registrar_->WasExternalAppUninstalledByUser(extension_id)) { |
| return base::Optional<bool>(false); |
| } |
| |
| return base::nullopt; |
| } |
| |
| void PendingBookmarkAppManager::MaybeStartNextInstallation() { |
| if (current_task_and_callback_) |
| return; |
| |
| while (!pending_tasks_and_callbacks_.empty()) { |
| std::unique_ptr<TaskAndCallback> front = |
| std::move(pending_tasks_and_callbacks_.front()); |
| pending_tasks_and_callbacks_.pop_front(); |
| |
| const web_app::PendingAppManager::AppInfo& app_info = |
| front->task->app_info(); |
| |
| if (app_info.always_update) { |
| StartInstallationTask(std::move(front)); |
| return; |
| } |
| |
| base::Optional<std::string> extension_id = |
| extension_ids_map_.LookupExtensionId(app_info.url); |
| |
| if (extension_id) { |
| base::Optional<bool> opt = |
| IsExtensionPresentAndInstalled(extension_id.value()); |
| if (opt.has_value()) { |
| bool installed = opt.value(); |
| if (installed || !app_info.override_previous_user_uninstall) { |
| // TODO(crbug.com/878262): Handle the case where the app is already |
| // installed but from a different source. |
| std::move(front->callback) |
| .Run(app_info.url, |
| installed |
| ? web_app::InstallResultCode::kAlreadyInstalled |
| : web_app::InstallResultCode::kPreviouslyUninstalled); |
| continue; |
| } |
| } |
| } |
| StartInstallationTask(std::move(front)); |
| return; |
| } |
| |
| web_contents_.reset(); |
| } |
| |
| void PendingBookmarkAppManager::StartInstallationTask( |
| std::unique_ptr<TaskAndCallback> task) { |
| DCHECK(!current_task_and_callback_); |
| current_task_and_callback_ = std::move(task); |
| |
| CreateWebContentsIfNecessary(); |
| Observe(web_contents_.get()); |
| |
| content::NavigationController::LoadURLParams load_params( |
| current_task_and_callback_->task->app_info().url); |
| load_params.transition_type = ui::PAGE_TRANSITION_GENERATED; |
| web_contents_->GetController().LoadURLWithParams(load_params); |
| timer_->Start( |
| FROM_HERE, base::TimeDelta::FromSeconds(kSecondsToWaitForWebContentsLoad), |
| base::BindOnce(&PendingBookmarkAppManager::OnWebContentsLoadTimedOut, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PendingBookmarkAppManager::CreateWebContentsIfNecessary() { |
| if (web_contents_) |
| return; |
| |
| web_contents_ = web_contents_factory_.Run(profile_); |
| BookmarkAppInstallationTask::CreateTabHelpers(web_contents_.get()); |
| } |
| |
| void PendingBookmarkAppManager::OnInstalled( |
| BookmarkAppInstallationTask::Result result) { |
| CurrentInstallationFinished(result.app_id); |
| } |
| |
| void PendingBookmarkAppManager::OnWebContentsLoadTimedOut() { |
| web_contents_->Stop(); |
| LOG(ERROR) << "Error installing " |
| << current_task_and_callback_->task->app_info().url.spec(); |
| LOG(ERROR) << " page took too long to load."; |
| Observe(nullptr); |
| CurrentInstallationFinished(base::nullopt); |
| } |
| |
| void PendingBookmarkAppManager::CurrentInstallationFinished( |
| const base::Optional<std::string>& app_id) { |
| // Post a task to avoid reentrancy issues e.g. adding a WebContentsObserver |
| // while a previous observer call is being executed. Post a task before |
| // running the callback in case the callback tries to install another |
| // app. |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&PendingBookmarkAppManager::MaybeStartNextInstallation, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| auto install_result_code = |
| app_id ? web_app::InstallResultCode::kSuccess |
| : web_app::InstallResultCode::kFailedUnknownReason; |
| |
| std::unique_ptr<TaskAndCallback> task_and_callback; |
| task_and_callback.swap(current_task_and_callback_); |
| std::move(task_and_callback->callback) |
| .Run(task_and_callback->task->app_info().url, install_result_code); |
| } |
| |
| void PendingBookmarkAppManager::DidFinishLoad( |
| content::RenderFrameHost* render_frame_host, |
| const GURL& validated_url) { |
| timer_->Stop(); |
| if (web_contents_->GetMainFrame() != render_frame_host) { |
| return; |
| } |
| |
| if (validated_url != current_task_and_callback_->task->app_info().url) { |
| LOG(ERROR) << "Error installing " |
| << current_task_and_callback_->task->app_info().url.spec(); |
| LOG(ERROR) << " page redirected to " << validated_url.spec(); |
| CurrentInstallationFinished(base::nullopt); |
| return; |
| } |
| |
| Observe(nullptr); |
| current_task_and_callback_->task->Install( |
| web_contents_.get(), |
| base::BindOnce(&PendingBookmarkAppManager::OnInstalled, |
| // Safe because the installation task will not run its |
| // callback after being deleted and this class owns the |
| // task. |
| base::Unretained(this))); |
| } |
| |
| void PendingBookmarkAppManager::DidFailLoad( |
| content::RenderFrameHost* render_frame_host, |
| const GURL& validated_url, |
| int error_code, |
| const base::string16& error_description) { |
| timer_->Stop(); |
| if (web_contents_->GetMainFrame() != render_frame_host) { |
| return; |
| } |
| |
| LOG(ERROR) << "Error installing " |
| << current_task_and_callback_->task->app_info().url.spec(); |
| LOG(ERROR) << " page failed to load."; |
| Observe(nullptr); |
| CurrentInstallationFinished(base::nullopt); |
| } |
| |
| } // namespace extensions |