blob: 94cd0e5d195d407847376fdae37ea87c92621903 [file] [log] [blame]
// Copyright 2014 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/external_install_ui.h"
#include <string>
#include "base/bind.h"
#include "base/lazy_instance.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/metrics/histogram.h"
#include "base/scoped_observer.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/extensions/extension_install_prompt.h"
#include "chrome/browser/extensions/extension_install_ui.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_uninstall_dialog.h"
#include "chrome/browser/extensions/webstore_data_fetcher.h"
#include "chrome/browser/extensions/webstore_data_fetcher_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/global_error/global_error.h"
#include "chrome/browser/ui/global_error/global_error_service.h"
#include "chrome/browser/ui/global_error/global_error_service_factory.h"
#include "chrome/common/extensions/manifest_url_handler.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_observer.h"
#include "content/public/browser/notification_registrar.h"
#include "content/public/browser/notification_source.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_registry_observer.h"
#include "extensions/common/constants.h"
#include "grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia_operations.h"
namespace extensions {
namespace {
// Whether the external extension can use the streamlined bubble install flow.
bool UseBubbleInstall(const Extension* extension, bool is_new_profile) {
return ManifestURL::UpdatesFromGallery(extension) && !is_new_profile;
}
} // namespace
static const int kMenuCommandId = IDC_EXTERNAL_EXTENSION_ALERT;
class ExternalInstallGlobalError;
namespace extensions {
class ExtensionRegistry;
}
// This class is refcounted to stay alive while we try and pull webstore data.
class ExternalInstallDialogDelegate
: public ExtensionInstallPrompt::Delegate,
public WebstoreDataFetcherDelegate,
public content::NotificationObserver,
public base::RefCountedThreadSafe<ExternalInstallDialogDelegate> {
public:
ExternalInstallDialogDelegate(Browser* browser,
ExtensionService* service,
const Extension* extension,
bool use_global_error);
Browser* browser() { return browser_; }
private:
friend class base::RefCountedThreadSafe<ExternalInstallDialogDelegate>;
friend class ExternalInstallGlobalError;
virtual ~ExternalInstallDialogDelegate();
// ExtensionInstallPrompt::Delegate:
virtual void InstallUIProceed() OVERRIDE;
virtual void InstallUIAbort(bool user_initiated) OVERRIDE;
// WebstoreDataFetcherDelegate:
virtual void OnWebstoreRequestFailure() OVERRIDE;
virtual void OnWebstoreResponseParseSuccess(
scoped_ptr<base::DictionaryValue> webstore_data) OVERRIDE;
virtual void OnWebstoreResponseParseFailure(
const std::string& error) OVERRIDE;
// content::NotificationObserver:
virtual void Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) OVERRIDE;
// Show the install dialog to the user.
void ShowInstallUI();
// The UI for showing the install dialog when enabling.
scoped_ptr<ExtensionInstallPrompt> install_ui_;
scoped_refptr<ExtensionInstallPrompt::Prompt> prompt_;
Browser* browser_;
base::WeakPtr<ExtensionService> service_weak_;
scoped_ptr<WebstoreDataFetcher> webstore_data_fetcher_;
content::NotificationRegistrar registrar_;
std::string extension_id_;
bool use_global_error_;
DISALLOW_COPY_AND_ASSIGN(ExternalInstallDialogDelegate);
};
// Only shows a menu item, no bubble. Clicking the menu item shows
// an external install dialog.
class ExternalInstallMenuAlert : public GlobalErrorWithStandardBubble,
public content::NotificationObserver,
public ExtensionRegistryObserver {
public:
ExternalInstallMenuAlert(ExtensionService* service,
const Extension* extension);
virtual ~ExternalInstallMenuAlert();
// GlobalError implementation.
virtual Severity GetSeverity() OVERRIDE;
virtual bool HasMenuItem() OVERRIDE;
virtual int MenuItemCommandID() OVERRIDE;
virtual base::string16 MenuItemLabel() OVERRIDE;
virtual void ExecuteMenuItem(Browser* browser) OVERRIDE;
virtual bool HasBubbleView() OVERRIDE;
virtual base::string16 GetBubbleViewTitle() OVERRIDE;
virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE;
virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE;
virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE;
virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE;
virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE;
virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE;
protected:
ExtensionService* service_;
const Extension* extension_;
private:
// Delete this instance after cleaning jobs.
void Clean();
// content::NotificationObserver implementation.
virtual void Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) OVERRIDE;
// ExtensionRegistryObserver implementation.
virtual void OnExtensionLoaded(content::BrowserContext* browser_context,
const Extension* extension) OVERRIDE;
content::NotificationRegistrar registrar_;
// Listen to extension load notifications.
ScopedObserver<ExtensionRegistry, ExtensionRegistryObserver>
extension_registry_observer_;
DISALLOW_COPY_AND_ASSIGN(ExternalInstallMenuAlert);
};
// Shows a menu item and a global error bubble, replacing the install dialog.
class ExternalInstallGlobalError : public ExternalInstallMenuAlert {
public:
ExternalInstallGlobalError(
ExtensionService* service,
const Extension* extension,
ExternalInstallDialogDelegate* delegate,
scoped_refptr<ExtensionInstallPrompt::Prompt> prompt);
virtual ~ExternalInstallGlobalError();
virtual void ExecuteMenuItem(Browser* browser) OVERRIDE;
virtual bool HasBubbleView() OVERRIDE;
virtual gfx::Image GetBubbleViewIcon() OVERRIDE;
virtual base::string16 GetBubbleViewTitle() OVERRIDE;
virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE;
virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE;
virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE;
virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE;
virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE;
virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE;
protected:
// Ref-counted, but needs to be disposed of if we are dismissed without
// having been clicked (perhaps because the user enabled the extension
// manually).
ExternalInstallDialogDelegate* delegate_;
scoped_refptr<ExtensionInstallPrompt::Prompt> prompt_;
private:
DISALLOW_COPY_AND_ASSIGN(ExternalInstallGlobalError);
};
static void CreateExternalInstallGlobalError(
base::WeakPtr<ExtensionService> service,
const std::string& extension_id,
const ExtensionInstallPrompt::ShowParams& show_params,
ExtensionInstallPrompt::Delegate* prompt_delegate,
scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) {
if (!service.get())
return;
const Extension* extension = service->GetInstalledExtension(extension_id);
if (!extension)
return;
GlobalErrorService* error_service =
GlobalErrorServiceFactory::GetForProfile(service->profile());
if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId))
return;
ExternalInstallDialogDelegate* delegate =
static_cast<ExternalInstallDialogDelegate*>(prompt_delegate);
ExternalInstallGlobalError* error_bubble = new ExternalInstallGlobalError(
service.get(), extension, delegate, prompt);
error_service->AddGlobalError(error_bubble);
// Show bubble immediately if possible.
if (delegate->browser())
error_bubble->ShowBubbleView(delegate->browser());
}
static void ShowExternalInstallDialog(
ExtensionService* service,
Browser* browser,
const Extension* extension) {
// This object manages its own lifetime.
new ExternalInstallDialogDelegate(browser, service, extension, false);
}
// ExternalInstallDialogDelegate --------------------------------------------
ExternalInstallDialogDelegate::ExternalInstallDialogDelegate(
Browser* browser,
ExtensionService* service,
const Extension* extension,
bool use_global_error)
: browser_(browser),
service_weak_(service->AsWeakPtr()),
extension_id_(extension->id()),
use_global_error_(use_global_error) {
AddRef(); // Balanced in Proceed or Abort.
prompt_ = new ExtensionInstallPrompt::Prompt(
ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT);
// If we don't have a browser, we can't go to the webstore to fetch data.
// This should only happen in tests.
if (!browser) {
ShowInstallUI();
return;
}
// Make sure to be notified if the owning profile is destroyed.
registrar_.Add(this,
chrome::NOTIFICATION_PROFILE_DESTROYED,
content::Source<Profile>(browser->profile()));
webstore_data_fetcher_.reset(new WebstoreDataFetcher(
this,
browser->profile()->GetRequestContext(),
GURL::EmptyGURL(),
extension->id()));
webstore_data_fetcher_->Start();
}
void ExternalInstallDialogDelegate::OnWebstoreRequestFailure() {
ShowInstallUI();
}
void ExternalInstallDialogDelegate::OnWebstoreResponseParseSuccess(
scoped_ptr<base::DictionaryValue> webstore_data) {
std::string localized_user_count;
double average_rating;
int rating_count;
if (!webstore_data->GetString(kUsersKey, &localized_user_count) ||
!webstore_data->GetDouble(kAverageRatingKey, &average_rating) ||
!webstore_data->GetInteger(kRatingCountKey, &rating_count)) {
// If we don't get a valid webstore response, short circuit, and continue
// to show a prompt without webstore data.
ShowInstallUI();
return;
}
bool show_user_count = true;
webstore_data->GetBoolean(kShowUserCountKey, &show_user_count);
prompt_->SetWebstoreData(localized_user_count,
show_user_count,
average_rating,
rating_count);
ShowInstallUI();
}
void ExternalInstallDialogDelegate::OnWebstoreResponseParseFailure(
const std::string& error) {
ShowInstallUI();
}
void ExternalInstallDialogDelegate::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED);
// If the owning profile is destroyed, we need to abort so that we don't leak.
InstallUIAbort(false); // Not user initiated.
}
void ExternalInstallDialogDelegate::ShowInstallUI() {
const Extension* extension = NULL;
if (!service_weak_.get() ||
!(extension = service_weak_->GetInstalledExtension(extension_id_))) {
return;
}
install_ui_.reset(
ExtensionInstallUI::CreateInstallPromptWithBrowser(browser_));
const ExtensionInstallPrompt::ShowDialogCallback callback =
use_global_error_ ?
base::Bind(&CreateExternalInstallGlobalError,
service_weak_,
extension_id_) :
ExtensionInstallPrompt::GetDefaultShowDialogCallback();
install_ui_->ConfirmExternalInstall(this, extension, callback, prompt_);
}
ExternalInstallDialogDelegate::~ExternalInstallDialogDelegate() {
}
void ExternalInstallDialogDelegate::InstallUIProceed() {
const Extension* extension = NULL;
if (service_weak_.get() &&
(extension = service_weak_->GetInstalledExtension(extension_id_))) {
service_weak_->GrantPermissionsAndEnableExtension(extension);
}
Release();
}
void ExternalInstallDialogDelegate::InstallUIAbort(bool user_initiated) {
const Extension* extension = NULL;
// Uninstall the extension if the abort was user initiated (and not, e.g., the
// result of the window closing).
// Otherwise, the extension will remain installed, but unacknowledged, so it
// will be prompted again.
if (user_initiated &&
service_weak_.get() &&
(extension = service_weak_->GetInstalledExtension(extension_id_))) {
service_weak_->UninstallExtension(extension_id_, false, NULL);
}
Release();
}
// ExternalInstallMenuAlert -------------------------------------------------
ExternalInstallMenuAlert::ExternalInstallMenuAlert(ExtensionService* service,
const Extension* extension)
: service_(service),
extension_(extension),
extension_registry_observer_(this) {
extension_registry_observer_.Add(ExtensionRegistry::Get(service->profile()));
registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_REMOVED,
content::Source<Profile>(service->profile()));
}
ExternalInstallMenuAlert::~ExternalInstallMenuAlert() {
}
GlobalError::Severity ExternalInstallMenuAlert::GetSeverity() {
return SEVERITY_LOW;
}
bool ExternalInstallMenuAlert::HasMenuItem() {
return true;
}
int ExternalInstallMenuAlert::MenuItemCommandID() {
return kMenuCommandId;
}
base::string16 ExternalInstallMenuAlert::MenuItemLabel() {
int id = -1;
if (extension_->is_app())
id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_APP;
else if (extension_->is_theme())
id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_THEME;
else
id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_EXTENSION;
return l10n_util::GetStringFUTF16(id, base::UTF8ToUTF16(extension_->name()));
}
void ExternalInstallMenuAlert::ExecuteMenuItem(Browser* browser) {
ShowExternalInstallDialog(service_, browser, extension_);
}
bool ExternalInstallMenuAlert::HasBubbleView() {
return false;
}
base::string16 ExternalInstallMenuAlert::GetBubbleViewTitle() {
return base::string16();
}
std::vector<base::string16> ExternalInstallMenuAlert::GetBubbleViewMessages() {
return std::vector<base::string16>();
}
base::string16 ExternalInstallMenuAlert::GetBubbleViewAcceptButtonLabel() {
return base::string16();
}
base::string16 ExternalInstallMenuAlert::GetBubbleViewCancelButtonLabel() {
return base::string16();
}
void ExternalInstallMenuAlert::OnBubbleViewDidClose(Browser* browser) {
NOTREACHED();
}
void ExternalInstallMenuAlert::BubbleViewAcceptButtonPressed(
Browser* browser) {
NOTREACHED();
}
void ExternalInstallMenuAlert::BubbleViewCancelButtonPressed(
Browser* browser) {
NOTREACHED();
}
void ExternalInstallMenuAlert::OnExtensionLoaded(
content::BrowserContext* browser_context,
const Extension* extension) {
if (extension == extension_)
Clean();
}
void ExternalInstallMenuAlert::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
// The error is invalidated if the extension has been loaded or removed.
DCHECK_EQ(type, chrome::NOTIFICATION_EXTENSION_REMOVED);
const Extension* extension = content::Details<const Extension>(details).ptr();
if (extension == extension_)
Clean();
}
void ExternalInstallMenuAlert::Clean() {
GlobalErrorService* error_service =
GlobalErrorServiceFactory::GetForProfile(service_->profile());
error_service->RemoveGlobalError(this);
service_->AcknowledgeExternalExtension(extension_->id());
delete this;
}
// ExternalInstallGlobalError -----------------------------------------------
ExternalInstallGlobalError::ExternalInstallGlobalError(
ExtensionService* service,
const Extension* extension,
ExternalInstallDialogDelegate* delegate,
scoped_refptr<ExtensionInstallPrompt::Prompt> prompt)
: ExternalInstallMenuAlert(service, extension),
delegate_(delegate),
prompt_(prompt) {
}
ExternalInstallGlobalError::~ExternalInstallGlobalError() {
if (delegate_)
delegate_->Release();
}
void ExternalInstallGlobalError::ExecuteMenuItem(Browser* browser) {
ShowBubbleView(browser);
}
bool ExternalInstallGlobalError::HasBubbleView() {
return true;
}
gfx::Image ExternalInstallGlobalError::GetBubbleViewIcon() {
if (prompt_->icon().IsEmpty())
return GlobalErrorWithStandardBubble::GetBubbleViewIcon();
// Scale icon to a reasonable size.
return gfx::Image(gfx::ImageSkiaOperations::CreateResizedImage(
*prompt_->icon().ToImageSkia(),
skia::ImageOperations::RESIZE_BEST,
gfx::Size(extension_misc::EXTENSION_ICON_SMALL,
extension_misc::EXTENSION_ICON_SMALL)));
}
base::string16 ExternalInstallGlobalError::GetBubbleViewTitle() {
return prompt_->GetDialogTitle();
}
std::vector<base::string16>
ExternalInstallGlobalError::GetBubbleViewMessages() {
std::vector<base::string16> messages;
messages.push_back(prompt_->GetHeading());
if (prompt_->GetPermissionCount()) {
messages.push_back(prompt_->GetPermissionsHeading());
for (size_t i = 0; i < prompt_->GetPermissionCount(); ++i) {
messages.push_back(l10n_util::GetStringFUTF16(
IDS_EXTENSION_PERMISSION_LINE,
prompt_->GetPermission(i)));
}
}
// TODO(yoz): OAuth issue advice?
return messages;
}
base::string16 ExternalInstallGlobalError::GetBubbleViewAcceptButtonLabel() {
return prompt_->GetAcceptButtonLabel();
}
base::string16 ExternalInstallGlobalError::GetBubbleViewCancelButtonLabel() {
return prompt_->GetAbortButtonLabel();
}
void ExternalInstallGlobalError::OnBubbleViewDidClose(Browser* browser) {
}
void ExternalInstallGlobalError::BubbleViewAcceptButtonPressed(
Browser* browser) {
ExternalInstallDialogDelegate* delegate = delegate_;
delegate_ = NULL;
delegate->InstallUIProceed();
}
void ExternalInstallGlobalError::BubbleViewCancelButtonPressed(
Browser* browser) {
ExternalInstallDialogDelegate* delegate = delegate_;
delegate_ = NULL;
delegate->InstallUIAbort(true);
}
// Public interface ---------------------------------------------------------
void AddExternalInstallError(ExtensionService* service,
const Extension* extension,
bool is_new_profile) {
GlobalErrorService* error_service =
GlobalErrorServiceFactory::GetForProfile(service->profile());
if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId))
return;
if (UseBubbleInstall(extension, is_new_profile)) {
Browser* browser = NULL;
#if !defined(OS_ANDROID)
browser = chrome::FindTabbedBrowser(service->profile(),
true,
chrome::GetActiveDesktop());
#endif
new ExternalInstallDialogDelegate(browser, service, extension, true);
} else {
error_service->AddGlobalError(
new ExternalInstallMenuAlert(service, extension));
}
}
void RemoveExternalInstallError(ExtensionService* service) {
GlobalErrorService* error_service =
GlobalErrorServiceFactory::GetForProfile(service->profile());
GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
kMenuCommandId);
if (error) {
error_service->RemoveGlobalError(error);
delete error;
}
}
bool HasExternalInstallError(ExtensionService* service) {
GlobalErrorService* error_service =
GlobalErrorServiceFactory::GetForProfile(service->profile());
GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
kMenuCommandId);
return !!error;
}
bool HasExternalInstallBubble(ExtensionService* service) {
GlobalErrorService* error_service =
GlobalErrorServiceFactory::GetForProfile(service->profile());
GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
kMenuCommandId);
return error && error->HasBubbleView();
}
} // namespace extensions