1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/extensions/external_install_ui.h"
6
7#include <string>
8
9#include "base/bind.h"
10#include "base/lazy_instance.h"
11#include "base/memory/ref_counted.h"
12#include "base/memory/scoped_ptr.h"
13#include "base/message_loop/message_loop.h"
14#include "base/metrics/histogram.h"
15#include "base/strings/utf_string_conversions.h"
16#include "chrome/app/chrome_command_ids.h"
17#include "chrome/browser/chrome_notification_types.h"
18#include "chrome/browser/extensions/extension_install_prompt.h"
19#include "chrome/browser/extensions/extension_install_ui.h"
20#include "chrome/browser/extensions/extension_service.h"
21#include "chrome/browser/extensions/extension_uninstall_dialog.h"
22#include "chrome/browser/profiles/profile.h"
23#include "chrome/browser/ui/browser.h"
24#include "chrome/browser/ui/browser_finder.h"
25#include "chrome/browser/ui/global_error/global_error.h"
26#include "chrome/browser/ui/global_error/global_error_service.h"
27#include "chrome/browser/ui/global_error/global_error_service_factory.h"
28#include "chrome/browser/ui/host_desktop.h"
29#include "chrome/common/extensions/extension_constants.h"
30#include "chrome/common/extensions/manifest_url_handler.h"
31#include "content/public/browser/notification_details.h"
32#include "content/public/browser/notification_observer.h"
33#include "content/public/browser/notification_registrar.h"
34#include "content/public/browser/notification_source.h"
35#include "extensions/common/extension.h"
36#include "grit/chromium_strings.h"
37#include "grit/generated_resources.h"
38#include "grit/theme_resources.h"
39#include "ui/base/l10n/l10n_util.h"
40#include "ui/gfx/image/image.h"
41#include "ui/gfx/image/image_skia_operations.h"
42#include "ui/gfx/size.h"
43
44namespace extensions {
45
46namespace {
47
48// Whether the external extension can use the streamlined bubble install flow.
49bool UseBubbleInstall(const Extension* extension, bool is_new_profile) {
50  return ManifestURL::UpdatesFromGallery(extension) && !is_new_profile;
51}
52
53}  // namespace
54
55static const int kMenuCommandId = IDC_EXTERNAL_EXTENSION_ALERT;
56
57class ExternalInstallGlobalError;
58
59// TODO(mpcomplete): Get rid of the refcounting on this class, or document
60// why it's necessary. Will do after refactoring to merge back with
61// ExtensionDisabledDialogDelegate.
62class ExternalInstallDialogDelegate
63    : public ExtensionInstallPrompt::Delegate,
64      public base::RefCountedThreadSafe<ExternalInstallDialogDelegate> {
65 public:
66  ExternalInstallDialogDelegate(Browser* browser,
67                                ExtensionService* service,
68                                const Extension* extension,
69                                bool use_global_error);
70
71  Browser* browser() { return browser_; }
72
73 private:
74  friend class base::RefCountedThreadSafe<ExternalInstallDialogDelegate>;
75  friend class ExternalInstallGlobalError;
76
77  virtual ~ExternalInstallDialogDelegate();
78
79  // ExtensionInstallPrompt::Delegate:
80  virtual void InstallUIProceed() OVERRIDE;
81  virtual void InstallUIAbort(bool user_initiated) OVERRIDE;
82
83  // The UI for showing the install dialog when enabling.
84  scoped_ptr<ExtensionInstallPrompt> install_ui_;
85
86  Browser* browser_;
87  base::WeakPtr<ExtensionService> service_weak_;
88  const std::string extension_id_;
89};
90
91// Only shows a menu item, no bubble. Clicking the menu item shows
92// an external install dialog.
93class ExternalInstallMenuAlert : public GlobalErrorWithStandardBubble,
94                                 public content::NotificationObserver {
95 public:
96  ExternalInstallMenuAlert(ExtensionService* service,
97                           const Extension* extension);
98  virtual ~ExternalInstallMenuAlert();
99
100  const Extension* extension() const { return extension_; }
101
102  // GlobalError implementation.
103  virtual Severity GetSeverity() OVERRIDE;
104  virtual bool HasMenuItem() OVERRIDE;
105  virtual int MenuItemCommandID() OVERRIDE;
106  virtual base::string16 MenuItemLabel() OVERRIDE;
107  virtual void ExecuteMenuItem(Browser* browser) OVERRIDE;
108  virtual bool HasBubbleView() OVERRIDE;
109  virtual base::string16 GetBubbleViewTitle() OVERRIDE;
110  virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE;
111  virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE;
112  virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE;
113  virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE;
114  virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE;
115  virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE;
116
117  // content::NotificationObserver implementation.
118  virtual void Observe(int type,
119                       const content::NotificationSource& source,
120                       const content::NotificationDetails& details) OVERRIDE;
121
122 protected:
123  ExtensionService* service_;
124  const Extension* extension_;
125  content::NotificationRegistrar registrar_;
126};
127
128// Shows a menu item and a global error bubble, replacing the install dialog.
129class ExternalInstallGlobalError : public ExternalInstallMenuAlert {
130 public:
131  ExternalInstallGlobalError(ExtensionService* service,
132                             const Extension* extension,
133                             ExternalInstallDialogDelegate* delegate,
134                             const ExtensionInstallPrompt::Prompt& prompt);
135  virtual ~ExternalInstallGlobalError();
136
137  virtual void ExecuteMenuItem(Browser* browser) OVERRIDE;
138  virtual bool HasBubbleView() OVERRIDE;
139  virtual gfx::Image GetBubbleViewIcon() OVERRIDE;
140  virtual base::string16 GetBubbleViewTitle() OVERRIDE;
141  virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE;
142  virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE;
143  virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE;
144  virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE;
145  virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE;
146  virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE;
147
148 protected:
149  // Ref-counted, but needs to be disposed of if we are dismissed without
150  // having been clicked (perhaps because the user enabled the extension
151  // manually).
152  ExternalInstallDialogDelegate* delegate_;
153  const ExtensionInstallPrompt::Prompt* prompt_;
154};
155
156static void CreateExternalInstallGlobalError(
157    base::WeakPtr<ExtensionService> service,
158    const std::string& extension_id,
159    const ExtensionInstallPrompt::ShowParams& show_params,
160    ExtensionInstallPrompt::Delegate* prompt_delegate,
161    const ExtensionInstallPrompt::Prompt& prompt) {
162  if (!service.get())
163    return;
164  const Extension* extension = service->GetInstalledExtension(extension_id);
165  if (!extension)
166    return;
167  GlobalErrorService* error_service =
168      GlobalErrorServiceFactory::GetForProfile(service->profile());
169  if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId))
170    return;
171
172  ExternalInstallDialogDelegate* delegate =
173      static_cast<ExternalInstallDialogDelegate*>(prompt_delegate);
174  ExternalInstallGlobalError* error_bubble = new ExternalInstallGlobalError(
175      service.get(), extension, delegate, prompt);
176  error_service->AddGlobalError(error_bubble);
177  // Show bubble immediately if possible.
178  if (delegate->browser())
179    error_bubble->ShowBubbleView(delegate->browser());
180}
181
182static void ShowExternalInstallDialog(
183    ExtensionService* service,
184    Browser* browser,
185    const Extension* extension) {
186  // This object manages its own lifetime.
187  new ExternalInstallDialogDelegate(browser, service, extension, false);
188}
189
190// ExternalInstallDialogDelegate --------------------------------------------
191
192ExternalInstallDialogDelegate::ExternalInstallDialogDelegate(
193    Browser* browser,
194    ExtensionService* service,
195    const Extension* extension,
196    bool use_global_error)
197    : browser_(browser),
198      service_weak_(service->AsWeakPtr()),
199      extension_id_(extension->id()) {
200  AddRef();  // Balanced in Proceed or Abort.
201
202  install_ui_.reset(
203      ExtensionInstallUI::CreateInstallPromptWithBrowser(browser));
204
205  const ExtensionInstallPrompt::ShowDialogCallback callback =
206      use_global_error ?
207      base::Bind(&CreateExternalInstallGlobalError,
208                 service_weak_, extension_id_) :
209      ExtensionInstallPrompt::GetDefaultShowDialogCallback();
210  install_ui_->ConfirmExternalInstall(this, extension, callback);
211}
212
213ExternalInstallDialogDelegate::~ExternalInstallDialogDelegate() {
214}
215
216void ExternalInstallDialogDelegate::InstallUIProceed() {
217  if (!service_weak_.get())
218    return;
219  const Extension* extension =
220      service_weak_->GetInstalledExtension(extension_id_);
221  if (!extension)
222    return;
223  service_weak_->GrantPermissionsAndEnableExtension(extension);
224  Release();
225}
226
227void ExternalInstallDialogDelegate::InstallUIAbort(bool user_initiated) {
228  if (!service_weak_.get())
229    return;
230  const Extension* extension =
231      service_weak_->GetInstalledExtension(extension_id_);
232  if (!extension)
233    return;
234  service_weak_->UninstallExtension(extension_id_, false, NULL);
235  Release();
236}
237
238// ExternalInstallMenuAlert -------------------------------------------------
239
240ExternalInstallMenuAlert::ExternalInstallMenuAlert(
241    ExtensionService* service,
242    const Extension* extension)
243    : service_(service),
244      extension_(extension) {
245  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED,
246                 content::Source<Profile>(service->profile()));
247  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_REMOVED,
248                 content::Source<Profile>(service->profile()));
249}
250
251ExternalInstallMenuAlert::~ExternalInstallMenuAlert() {
252}
253
254GlobalError::Severity ExternalInstallMenuAlert::GetSeverity() {
255  return SEVERITY_LOW;
256}
257
258bool ExternalInstallMenuAlert::HasMenuItem() {
259  return true;
260}
261
262int ExternalInstallMenuAlert::MenuItemCommandID() {
263  return kMenuCommandId;
264}
265
266base::string16 ExternalInstallMenuAlert::MenuItemLabel() {
267  int id = -1;
268  if (extension_->is_app())
269    id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_APP;
270  else if (extension_->is_theme())
271    id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_THEME;
272  else
273    id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_EXTENSION;
274  return l10n_util::GetStringFUTF16(id, UTF8ToUTF16(extension_->name()));
275}
276
277void ExternalInstallMenuAlert::ExecuteMenuItem(Browser* browser) {
278  ShowExternalInstallDialog(service_, browser, extension_);
279}
280
281bool ExternalInstallMenuAlert::HasBubbleView() {
282  return false;
283}
284base::string16 ExternalInstallMenuAlert::GetBubbleViewTitle() {
285  return base::string16();
286}
287
288std::vector<base::string16> ExternalInstallMenuAlert::GetBubbleViewMessages() {
289  return std::vector<base::string16>();
290}
291
292base::string16 ExternalInstallMenuAlert::GetBubbleViewAcceptButtonLabel() {
293  return base::string16();
294}
295
296base::string16 ExternalInstallMenuAlert::GetBubbleViewCancelButtonLabel() {
297  return base::string16();
298}
299
300void ExternalInstallMenuAlert::OnBubbleViewDidClose(Browser* browser) {
301  NOTREACHED();
302}
303
304void ExternalInstallMenuAlert::BubbleViewAcceptButtonPressed(
305    Browser* browser) {
306  NOTREACHED();
307}
308
309void ExternalInstallMenuAlert::BubbleViewCancelButtonPressed(
310    Browser* browser) {
311  NOTREACHED();
312}
313
314void ExternalInstallMenuAlert::Observe(
315    int type,
316    const content::NotificationSource& source,
317    const content::NotificationDetails& details) {
318  // The error is invalidated if the extension has been loaded or removed.
319  DCHECK(type == chrome::NOTIFICATION_EXTENSION_LOADED ||
320         type == chrome::NOTIFICATION_EXTENSION_REMOVED);
321  const Extension* extension = content::Details<const Extension>(details).ptr();
322  if (extension != extension_)
323    return;
324  GlobalErrorService* error_service =
325      GlobalErrorServiceFactory::GetForProfile(service_->profile());
326  error_service->RemoveGlobalError(this);
327  service_->AcknowledgeExternalExtension(extension_->id());
328  delete this;
329}
330
331// ExternalInstallGlobalError -----------------------------------------------
332
333ExternalInstallGlobalError::ExternalInstallGlobalError(
334    ExtensionService* service,
335    const Extension* extension,
336    ExternalInstallDialogDelegate* delegate,
337    const ExtensionInstallPrompt::Prompt& prompt)
338    : ExternalInstallMenuAlert(service, extension),
339      delegate_(delegate),
340      prompt_(&prompt) {
341}
342
343ExternalInstallGlobalError::~ExternalInstallGlobalError() {
344  if (delegate_)
345    delegate_->Release();
346}
347
348void ExternalInstallGlobalError::ExecuteMenuItem(Browser* browser) {
349  ShowBubbleView(browser);
350}
351
352bool ExternalInstallGlobalError::HasBubbleView() {
353  return true;
354}
355
356gfx::Image ExternalInstallGlobalError::GetBubbleViewIcon() {
357  if (prompt_->icon().IsEmpty())
358    return GlobalErrorWithStandardBubble::GetBubbleViewIcon();
359  // Scale icon to a reasonable size.
360  return gfx::Image(gfx::ImageSkiaOperations::CreateResizedImage(
361      *prompt_->icon().ToImageSkia(),
362      skia::ImageOperations::RESIZE_BEST,
363      gfx::Size(extension_misc::EXTENSION_ICON_SMALL,
364                extension_misc::EXTENSION_ICON_SMALL)));
365}
366
367base::string16 ExternalInstallGlobalError::GetBubbleViewTitle() {
368  return prompt_->GetDialogTitle();
369}
370
371std::vector<base::string16>
372ExternalInstallGlobalError::GetBubbleViewMessages() {
373  std::vector<base::string16> messages;
374  messages.push_back(prompt_->GetHeading());
375  if (prompt_->GetPermissionCount()) {
376    messages.push_back(prompt_->GetPermissionsHeading());
377    for (size_t i = 0; i < prompt_->GetPermissionCount(); ++i) {
378      messages.push_back(l10n_util::GetStringFUTF16(
379          IDS_EXTENSION_PERMISSION_LINE,
380          prompt_->GetPermission(i)));
381    }
382  }
383  // TODO(yoz): OAuth issue advice?
384  return messages;
385}
386
387base::string16 ExternalInstallGlobalError::GetBubbleViewAcceptButtonLabel() {
388  return prompt_->GetAcceptButtonLabel();
389}
390
391base::string16 ExternalInstallGlobalError::GetBubbleViewCancelButtonLabel() {
392  return prompt_->GetAbortButtonLabel();
393}
394
395void ExternalInstallGlobalError::OnBubbleViewDidClose(Browser* browser) {
396}
397
398void ExternalInstallGlobalError::BubbleViewAcceptButtonPressed(
399    Browser* browser) {
400  ExternalInstallDialogDelegate* delegate = delegate_;
401  delegate_ = NULL;
402  delegate->InstallUIProceed();
403}
404
405void ExternalInstallGlobalError::BubbleViewCancelButtonPressed(
406    Browser* browser) {
407  ExternalInstallDialogDelegate* delegate = delegate_;
408  delegate_ = NULL;
409  delegate->InstallUIAbort(true);
410}
411
412// Public interface ---------------------------------------------------------
413
414void AddExternalInstallError(ExtensionService* service,
415                             const Extension* extension,
416                             bool is_new_profile) {
417  GlobalErrorService* error_service =
418      GlobalErrorServiceFactory::GetForProfile(service->profile());
419  if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId))
420    return;
421
422  if (UseBubbleInstall(extension, is_new_profile)) {
423    Browser* browser = NULL;
424#if !defined(OS_ANDROID)
425    browser = chrome::FindTabbedBrowser(service->profile(),
426                                        true,
427                                        chrome::GetActiveDesktop());
428#endif
429    new ExternalInstallDialogDelegate(browser, service, extension, true);
430  } else {
431    error_service->AddGlobalError(
432        new ExternalInstallMenuAlert(service, extension));
433  }
434}
435
436void RemoveExternalInstallError(ExtensionService* service) {
437  GlobalErrorService* error_service =
438      GlobalErrorServiceFactory::GetForProfile(service->profile());
439  GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
440      kMenuCommandId);
441  if (error) {
442    error_service->RemoveGlobalError(error);
443    delete error;
444  }
445}
446
447bool HasExternalInstallError(ExtensionService* service) {
448  GlobalErrorService* error_service =
449      GlobalErrorServiceFactory::GetForProfile(service->profile());
450  GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
451      kMenuCommandId);
452  return !!error;
453}
454
455bool HasExternalInstallBubble(ExtensionService* service) {
456  GlobalErrorService* error_service =
457      GlobalErrorServiceFactory::GetForProfile(service->profile());
458  GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
459      kMenuCommandId);
460  return error && error->HasBubbleView();
461}
462
463}  // namespace extensions
464