| // 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/bundle_installer.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/i18n/rtl.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/values.h" |
| #include "chrome/browser/extensions/crx_installer.h" |
| #include "chrome/browser/extensions/permissions_updater.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/permissions/permission_set.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/image/image_skia.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| enum AutoApproveForTest { |
| DO_NOT_SKIP = 0, |
| PROCEED, |
| ABORT |
| }; |
| |
| AutoApproveForTest g_auto_approve_for_test = DO_NOT_SKIP; |
| |
| scoped_refptr<Extension> CreateDummyExtension( |
| const BundleInstaller::Item& item, |
| const base::DictionaryValue& manifest, |
| content::BrowserContext* browser_context) { |
| // We require localized names so we can have nice error messages when we can't |
| // parse an extension manifest. |
| CHECK(!item.localized_name.empty()); |
| |
| std::string error; |
| scoped_refptr<Extension> extension = Extension::Create(base::FilePath(), |
| Manifest::INTERNAL, |
| manifest, |
| Extension::NO_FLAGS, |
| item.id, |
| &error); |
| // Initialize permissions so that withheld permissions are displayed properly |
| // in the install prompt. |
| PermissionsUpdater(browser_context, PermissionsUpdater::INIT_FLAG_TRANSIENT) |
| .InitializePermissions(extension.get()); |
| return extension; |
| } |
| |
| const int kHeadingIdsInstallPrompt[] = { |
| IDS_EXTENSION_BUNDLE_INSTALL_PROMPT_TITLE_EXTENSIONS, |
| IDS_EXTENSION_BUNDLE_INSTALL_PROMPT_TITLE_APPS, |
| IDS_EXTENSION_BUNDLE_INSTALL_PROMPT_TITLE_EXTENSION_APPS |
| }; |
| |
| const int kHeadingIdsDelegatedInstallPrompt[] = { |
| IDS_EXTENSION_BUNDLE_DELEGATED_INSTALL_PROMPT_TITLE_EXTENSIONS, |
| IDS_EXTENSION_BUNDLE_DELEGATED_INSTALL_PROMPT_TITLE_APPS, |
| IDS_EXTENSION_BUNDLE_DELEGATED_INSTALL_PROMPT_TITLE_EXTENSION_APPS |
| }; |
| |
| const int kHeadingIdsInstalledBubble[] = { |
| IDS_EXTENSION_BUNDLE_INSTALLED_HEADING_EXTENSIONS, |
| IDS_EXTENSION_BUNDLE_INSTALLED_HEADING_APPS, |
| IDS_EXTENSION_BUNDLE_INSTALLED_HEADING_EXTENSION_APPS |
| }; |
| |
| } // namespace |
| |
| // static |
| void BundleInstaller::SetAutoApproveForTesting(bool auto_approve) { |
| CHECK(base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType)); |
| g_auto_approve_for_test = auto_approve ? PROCEED : ABORT; |
| } |
| |
| BundleInstaller::Item::Item() : state(STATE_PENDING) {} |
| |
| BundleInstaller::Item::~Item() {} |
| |
| base::string16 BundleInstaller::Item::GetNameForDisplay() const { |
| base::string16 name = base::UTF8ToUTF16(localized_name); |
| base::i18n::AdjustStringForLocaleDirection(&name); |
| return name; |
| } |
| |
| BundleInstaller::BundleInstaller(Browser* browser, |
| const std::string& name, |
| const SkBitmap& icon, |
| const std::string& authuser, |
| const std::string& delegated_username, |
| const BundleInstaller::ItemList& items) |
| : approved_(false), |
| browser_(browser), |
| name_(name), |
| icon_(icon), |
| authuser_(authuser), |
| delegated_username_(delegated_username), |
| host_desktop_type_(browser->host_desktop_type()), |
| profile_(browser->profile()) { |
| BrowserList::AddObserver(this); |
| for (size_t i = 0; i < items.size(); ++i) { |
| items_[items[i].id] = items[i]; |
| items_[items[i].id].state = Item::STATE_PENDING; |
| } |
| } |
| |
| BundleInstaller::~BundleInstaller() { |
| BrowserList::RemoveObserver(this); |
| } |
| |
| BundleInstaller::ItemList BundleInstaller::GetItemsWithState( |
| Item::State state) const { |
| ItemList list; |
| |
| for (const std::pair<std::string, Item>& entry : items_) { |
| if (entry.second.state == state) |
| list.push_back(entry.second); |
| } |
| |
| return list; |
| } |
| |
| bool BundleInstaller::HasItemWithState(Item::State state) const { |
| return CountItemsWithState(state) > 0; |
| } |
| |
| size_t BundleInstaller::CountItemsWithState(Item::State state) const { |
| return std::count_if(items_.begin(), items_.end(), |
| [state] (const std::pair<std::string, Item>& entry) { |
| return entry.second.state == state; |
| }); |
| } |
| |
| void BundleInstaller::PromptForApproval(const ApprovalCallback& callback) { |
| approval_callback_ = callback; |
| |
| ParseManifests(); |
| } |
| |
| void BundleInstaller::CompleteInstall(content::WebContents* web_contents, |
| const base::Closure& callback) { |
| DCHECK(web_contents); |
| CHECK(approved_); |
| |
| install_callback_ = callback; |
| |
| if (!HasItemWithState(Item::STATE_PENDING)) { |
| install_callback_.Run(); |
| return; |
| } |
| |
| // Start each WebstoreInstaller. |
| for (const std::pair<std::string, Item>& entry : items_) { |
| if (entry.second.state != Item::STATE_PENDING) |
| continue; |
| |
| // Since we've already confirmed the permissions, create an approval that |
| // lets CrxInstaller bypass the prompt. |
| scoped_ptr<WebstoreInstaller::Approval> approval( |
| WebstoreInstaller::Approval::CreateWithNoInstallPrompt( |
| profile_, |
| entry.first, |
| make_scoped_ptr(parsed_manifests_[entry.first]->DeepCopy()), |
| true)); |
| approval->use_app_installed_bubble = false; |
| approval->skip_post_install_ui = true; |
| approval->authuser = authuser_; |
| approval->installing_icon = |
| gfx::ImageSkia::CreateFrom1xBitmap(entry.second.icon); |
| |
| scoped_refptr<WebstoreInstaller> installer = new WebstoreInstaller( |
| profile_, |
| this, |
| web_contents, |
| entry.first, |
| approval.Pass(), |
| WebstoreInstaller::INSTALL_SOURCE_OTHER); |
| installer->Start(); |
| } |
| } |
| |
| base::string16 BundleInstaller::GetHeadingTextFor(Item::State state) const { |
| // For STATE_FAILED, we can't tell if the items were apps or extensions |
| // so we always show the same message. |
| if (state == Item::STATE_FAILED) { |
| if (HasItemWithState(state)) |
| return l10n_util::GetStringUTF16(IDS_EXTENSION_BUNDLE_ERROR_HEADING); |
| return base::string16(); |
| } |
| |
| size_t total = CountItemsWithState(state); |
| if (total == 0) |
| return base::string16(); |
| |
| size_t apps = std::count_if( |
| dummy_extensions_.begin(), dummy_extensions_.end(), |
| [] (const scoped_refptr<const Extension>& ext) { return ext->is_app(); }); |
| |
| bool has_apps = apps > 0; |
| bool has_extensions = apps < total; |
| size_t index = (has_extensions << 0) + (has_apps << 1) - 1; |
| |
| if (state == Item::STATE_PENDING) { |
| if (!delegated_username_.empty()) { |
| return l10n_util::GetStringFUTF16( |
| kHeadingIdsDelegatedInstallPrompt[index], base::UTF8ToUTF16(name_), |
| base::UTF8ToUTF16(delegated_username_)); |
| } else { |
| return l10n_util::GetStringFUTF16(kHeadingIdsInstallPrompt[index], |
| base::UTF8ToUTF16(name_)); |
| } |
| } else { |
| return l10n_util::GetStringUTF16(kHeadingIdsInstalledBubble[index]); |
| } |
| } |
| |
| void BundleInstaller::ParseManifests() { |
| if (items_.empty()) { |
| approval_callback_.Run(APPROVAL_ERROR); |
| return; |
| } |
| |
| net::URLRequestContextGetter* context_getter = |
| browser_ ? browser_->profile()->GetRequestContext() : nullptr; |
| |
| for (const std::pair<std::string, Item>& entry : items_) { |
| scoped_refptr<WebstoreInstallHelper> helper = new WebstoreInstallHelper( |
| this, entry.first, entry.second.manifest, entry.second.icon_url, |
| context_getter); |
| helper->Start(); |
| } |
| } |
| |
| void BundleInstaller::ShowPromptIfDoneParsing() { |
| // We don't prompt until all the manifests have been parsed. |
| if (CountItemsWithState(Item::STATE_PENDING) != dummy_extensions_.size()) |
| return; |
| |
| ShowPrompt(); |
| } |
| |
| void BundleInstaller::ShowPrompt() { |
| // Abort if we couldn't create any Extensions out of the manifests. |
| if (dummy_extensions_.empty()) { |
| approval_callback_.Run(APPROVAL_ERROR); |
| return; |
| } |
| |
| scoped_refptr<PermissionSet> permissions; |
| for (size_t i = 0; i < dummy_extensions_.size(); ++i) { |
| permissions = PermissionSet::CreateUnion( |
| permissions.get(), |
| dummy_extensions_[i]->permissions_data()->active_permissions().get()); |
| } |
| |
| if (g_auto_approve_for_test == PROCEED) { |
| InstallUIProceed(); |
| } else if (g_auto_approve_for_test == ABORT) { |
| InstallUIAbort(true); |
| } else { |
| Browser* browser = browser_; |
| if (!browser) { |
| // The browser that we got initially could have gone away during our |
| // thread hopping. |
| browser = chrome::FindLastActiveWithProfile(profile_, host_desktop_type_); |
| } |
| content::WebContents* web_contents = NULL; |
| if (browser) |
| web_contents = browser->tab_strip_model()->GetActiveWebContents(); |
| install_ui_.reset(new ExtensionInstallPrompt(web_contents)); |
| if (delegated_username_.empty()) { |
| install_ui_->ConfirmBundleInstall(this, &icon_, permissions.get()); |
| } else { |
| install_ui_->ConfirmPermissionsForDelegatedBundleInstall( |
| this, delegated_username_, &icon_, permissions.get()); |
| } |
| } |
| } |
| |
| void BundleInstaller::ShowInstalledBubbleIfDone() { |
| // We're ready to show the installed bubble when no items are pending. |
| if (HasItemWithState(Item::STATE_PENDING)) |
| return; |
| |
| if (browser_) |
| ShowInstalledBubble(this, browser_); |
| |
| install_callback_.Run(); |
| } |
| |
| void BundleInstaller::OnWebstoreParseSuccess( |
| const std::string& id, |
| const SkBitmap& icon, |
| base::DictionaryValue* manifest) { |
| items_[id].icon = icon; |
| dummy_extensions_.push_back( |
| CreateDummyExtension(items_[id], *manifest, profile_)); |
| parsed_manifests_[id] = linked_ptr<base::DictionaryValue>(manifest); |
| |
| ShowPromptIfDoneParsing(); |
| } |
| |
| void BundleInstaller::OnWebstoreParseFailure( |
| const std::string& id, |
| WebstoreInstallHelper::Delegate::InstallHelperResultCode result_code, |
| const std::string& error_message) { |
| items_[id].state = Item::STATE_FAILED; |
| |
| ShowPromptIfDoneParsing(); |
| } |
| |
| void BundleInstaller::InstallUIProceed() { |
| approved_ = true; |
| approval_callback_.Run(APPROVED); |
| } |
| |
| void BundleInstaller::InstallUIAbort(bool user_initiated) { |
| for (std::pair<const std::string, Item>& entry : items_) |
| entry.second.state = Item::STATE_FAILED; |
| |
| approval_callback_.Run(user_initiated ? USER_CANCELED : APPROVAL_ERROR); |
| } |
| |
| void BundleInstaller::OnExtensionInstallSuccess(const std::string& id) { |
| items_[id].state = Item::STATE_INSTALLED; |
| |
| ShowInstalledBubbleIfDone(); |
| } |
| |
| void BundleInstaller::OnExtensionInstallFailure( |
| const std::string& id, |
| const std::string& error, |
| WebstoreInstaller::FailureReason reason) { |
| items_[id].state = Item::STATE_FAILED; |
| |
| ExtensionList::iterator i = std::find_if( |
| dummy_extensions_.begin(), dummy_extensions_.end(), |
| [&id] (const scoped_refptr<const Extension>& ext) { |
| return ext->id() == id; |
| }); |
| CHECK(dummy_extensions_.end() != i); |
| dummy_extensions_.erase(i); |
| |
| ShowInstalledBubbleIfDone(); |
| } |
| |
| void BundleInstaller::OnBrowserRemoved(Browser* browser) { |
| if (browser_ == browser) |
| browser_ = nullptr; |
| } |
| |
| } // namespace extensions |