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/extension_disabled_ui.h"
6
7#include <bitset>
8#include <string>
9
10#include "base/bind.h"
11#include "base/lazy_instance.h"
12#include "base/memory/ref_counted.h"
13#include "base/memory/scoped_ptr.h"
14#include "base/message_loop/message_loop.h"
15#include "base/metrics/histogram.h"
16#include "base/strings/string_util.h"
17#include "base/strings/utf_string_conversions.h"
18#include "chrome/app/chrome_command_ids.h"
19#include "chrome/browser/extensions/extension_install_prompt.h"
20#include "chrome/browser/extensions/extension_install_ui.h"
21#include "chrome/browser/extensions/extension_service.h"
22#include "chrome/browser/extensions/extension_uninstall_dialog.h"
23#include "chrome/browser/extensions/extension_util.h"
24#include "chrome/browser/profiles/profile.h"
25#include "chrome/browser/ui/browser.h"
26#include "chrome/browser/ui/browser_window.h"
27#include "chrome/browser/ui/global_error/global_error.h"
28#include "chrome/browser/ui/global_error/global_error_service.h"
29#include "chrome/browser/ui/global_error/global_error_service_factory.h"
30#include "chrome/browser/ui/tabs/tab_strip_model.h"
31#include "chrome/grit/chromium_strings.h"
32#include "chrome/grit/generated_resources.h"
33#include "content/public/browser/notification_details.h"
34#include "content/public/browser/notification_observer.h"
35#include "content/public/browser/notification_registrar.h"
36#include "content/public/browser/notification_source.h"
37#include "extensions/browser/extension_util.h"
38#include "extensions/browser/image_loader.h"
39#include "extensions/browser/notification_types.h"
40#include "extensions/browser/uninstall_reason.h"
41#include "extensions/common/constants.h"
42#include "extensions/common/extension.h"
43#include "extensions/common/extension_icon_set.h"
44#include "extensions/common/manifest_handlers/icons_handler.h"
45#include "extensions/common/permissions/permission_message_provider.h"
46#include "extensions/common/permissions/permission_set.h"
47#include "extensions/common/permissions/permissions_data.h"
48#include "ui/base/l10n/l10n_util.h"
49#include "ui/gfx/image/image.h"
50#include "ui/gfx/image/image_skia_operations.h"
51#include "ui/gfx/size.h"
52
53using extensions::Extension;
54
55namespace {
56
57static const int kIconSize = extension_misc::EXTENSION_ICON_SMALL;
58
59static base::LazyInstance<
60    std::bitset<IDC_EXTENSION_DISABLED_LAST -
61                IDC_EXTENSION_DISABLED_FIRST + 1> >
62    menu_command_ids = LAZY_INSTANCE_INITIALIZER;
63
64// Get an available menu ID.
65int GetMenuCommandID() {
66  int id;
67  for (id = IDC_EXTENSION_DISABLED_FIRST;
68       id <= IDC_EXTENSION_DISABLED_LAST; ++id) {
69    if (!menu_command_ids.Get()[id - IDC_EXTENSION_DISABLED_FIRST]) {
70      menu_command_ids.Get().set(id - IDC_EXTENSION_DISABLED_FIRST);
71      return id;
72    }
73  }
74  // This should not happen.
75  DCHECK(id <= IDC_EXTENSION_DISABLED_LAST) <<
76      "No available menu command IDs for ExtensionDisabledGlobalError";
77  return IDC_EXTENSION_DISABLED_LAST;
78}
79
80// Make a menu ID available when it is no longer used.
81void ReleaseMenuCommandID(int id) {
82  menu_command_ids.Get().reset(id - IDC_EXTENSION_DISABLED_FIRST);
83}
84
85}  // namespace
86
87// ExtensionDisabledDialogDelegate --------------------------------------------
88
89class ExtensionDisabledDialogDelegate
90    : public ExtensionInstallPrompt::Delegate,
91      public base::RefCountedThreadSafe<ExtensionDisabledDialogDelegate> {
92 public:
93  ExtensionDisabledDialogDelegate(ExtensionService* service,
94                                  scoped_ptr<ExtensionInstallPrompt> install_ui,
95                                  const Extension* extension);
96
97 private:
98  friend class base::RefCountedThreadSafe<ExtensionDisabledDialogDelegate>;
99
100  virtual ~ExtensionDisabledDialogDelegate();
101
102  // ExtensionInstallPrompt::Delegate:
103  virtual void InstallUIProceed() OVERRIDE;
104  virtual void InstallUIAbort(bool user_initiated) OVERRIDE;
105
106  // The UI for showing the install dialog when enabling.
107  scoped_ptr<ExtensionInstallPrompt> install_ui_;
108
109  ExtensionService* service_;
110  const Extension* extension_;
111};
112
113ExtensionDisabledDialogDelegate::ExtensionDisabledDialogDelegate(
114    ExtensionService* service,
115    scoped_ptr<ExtensionInstallPrompt> install_ui,
116    const Extension* extension)
117    : install_ui_(install_ui.Pass()),
118      service_(service),
119      extension_(extension) {
120  AddRef();  // Balanced in Proceed or Abort.
121  install_ui_->ConfirmReEnable(this, extension_);
122}
123
124ExtensionDisabledDialogDelegate::~ExtensionDisabledDialogDelegate() {
125}
126
127void ExtensionDisabledDialogDelegate::InstallUIProceed() {
128  service_->GrantPermissionsAndEnableExtension(extension_);
129  Release();
130}
131
132void ExtensionDisabledDialogDelegate::InstallUIAbort(bool user_initiated) {
133  std::string histogram_name = user_initiated
134                                   ? "Extensions.Permissions_ReEnableCancel2"
135                                   : "Extensions.Permissions_ReEnableAbort2";
136  ExtensionService::RecordPermissionMessagesHistogram(
137      extension_, histogram_name.c_str());
138
139  // Do nothing. The extension will remain disabled.
140  Release();
141}
142
143// ExtensionDisabledGlobalError -----------------------------------------------
144
145class ExtensionDisabledGlobalError
146    : public GlobalErrorWithStandardBubble,
147      public content::NotificationObserver,
148      public extensions::ExtensionUninstallDialog::Delegate {
149 public:
150  ExtensionDisabledGlobalError(ExtensionService* service,
151                               const Extension* extension,
152                               bool is_remote_install,
153                               const gfx::Image& icon);
154  virtual ~ExtensionDisabledGlobalError();
155
156  // GlobalError implementation.
157  virtual Severity GetSeverity() OVERRIDE;
158  virtual bool HasMenuItem() OVERRIDE;
159  virtual int MenuItemCommandID() OVERRIDE;
160  virtual base::string16 MenuItemLabel() OVERRIDE;
161  virtual void ExecuteMenuItem(Browser* browser) OVERRIDE;
162  virtual gfx::Image GetBubbleViewIcon() OVERRIDE;
163  virtual base::string16 GetBubbleViewTitle() OVERRIDE;
164  virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE;
165  virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE;
166  virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE;
167  virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE;
168  virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE;
169  virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE;
170  virtual bool ShouldCloseOnDeactivate() const OVERRIDE;
171
172  // ExtensionUninstallDialog::Delegate implementation.
173  virtual void ExtensionUninstallAccepted() OVERRIDE;
174  virtual void ExtensionUninstallCanceled() OVERRIDE;
175
176  // content::NotificationObserver implementation.
177  virtual void Observe(int type,
178                       const content::NotificationSource& source,
179                       const content::NotificationDetails& details) OVERRIDE;
180
181 private:
182  ExtensionService* service_;
183  const Extension* extension_;
184  bool is_remote_install_;
185  gfx::Image icon_;
186
187  // How the user responded to the error; used for metrics.
188  enum UserResponse {
189    IGNORED,
190    REENABLE,
191    UNINSTALL,
192    EXTENSION_DISABLED_UI_BUCKET_BOUNDARY
193  };
194  UserResponse user_response_;
195
196  scoped_ptr<extensions::ExtensionUninstallDialog> uninstall_dialog_;
197
198  // Menu command ID assigned for this extension's error.
199  int menu_command_id_;
200
201  content::NotificationRegistrar registrar_;
202};
203
204// TODO(yoz): create error at startup for disabled extensions.
205ExtensionDisabledGlobalError::ExtensionDisabledGlobalError(
206    ExtensionService* service,
207    const Extension* extension,
208    bool is_remote_install,
209    const gfx::Image& icon)
210    : service_(service),
211      extension_(extension),
212      is_remote_install_(is_remote_install),
213      icon_(icon),
214      user_response_(IGNORED),
215      menu_command_id_(GetMenuCommandID()) {
216  if (icon_.IsEmpty()) {
217    icon_ = gfx::Image(
218        gfx::ImageSkiaOperations::CreateResizedImage(
219            extension_->is_app() ?
220                extensions::util::GetDefaultAppIcon() :
221                extensions::util::GetDefaultExtensionIcon(),
222            skia::ImageOperations::RESIZE_BEST,
223            gfx::Size(kIconSize, kIconSize)));
224  }
225  registrar_.Add(this,
226                 extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED,
227                 content::Source<Profile>(service->profile()));
228  registrar_.Add(this,
229                 extensions::NOTIFICATION_EXTENSION_REMOVED,
230                 content::Source<Profile>(service->profile()));
231}
232
233ExtensionDisabledGlobalError::~ExtensionDisabledGlobalError() {
234  ReleaseMenuCommandID(menu_command_id_);
235  if (is_remote_install_) {
236    UMA_HISTOGRAM_ENUMERATION("Extensions.DisabledUIUserResponseRemoteInstall",
237                              user_response_,
238                              EXTENSION_DISABLED_UI_BUCKET_BOUNDARY);
239  } else {
240    UMA_HISTOGRAM_ENUMERATION("Extensions.DisabledUIUserResponse",
241                              user_response_,
242                              EXTENSION_DISABLED_UI_BUCKET_BOUNDARY);
243  }
244}
245
246GlobalError::Severity ExtensionDisabledGlobalError::GetSeverity() {
247  return SEVERITY_LOW;
248}
249
250bool ExtensionDisabledGlobalError::HasMenuItem() {
251  return true;
252}
253
254int ExtensionDisabledGlobalError::MenuItemCommandID() {
255  return menu_command_id_;
256}
257
258base::string16 ExtensionDisabledGlobalError::MenuItemLabel() {
259  std::string extension_name = extension_->name();
260  // Ampersands need to be escaped to avoid being treated like
261  // mnemonics in the menu.
262  base::ReplaceChars(extension_name, "&", "&&", &extension_name);
263
264  if (is_remote_install_) {
265    return l10n_util::GetStringFUTF16(
266        IDS_EXTENSION_DISABLED_REMOTE_INSTALL_ERROR_TITLE,
267        base::UTF8ToUTF16(extension_name));
268  } else {
269    return l10n_util::GetStringFUTF16(IDS_EXTENSION_DISABLED_ERROR_TITLE,
270                                      base::UTF8ToUTF16(extension_name));
271  }
272}
273
274void ExtensionDisabledGlobalError::ExecuteMenuItem(Browser* browser) {
275  ShowBubbleView(browser);
276}
277
278gfx::Image ExtensionDisabledGlobalError::GetBubbleViewIcon() {
279  return icon_;
280}
281
282base::string16 ExtensionDisabledGlobalError::GetBubbleViewTitle() {
283  if (is_remote_install_) {
284    return l10n_util::GetStringFUTF16(
285        IDS_EXTENSION_DISABLED_REMOTE_INSTALL_ERROR_TITLE,
286        base::UTF8ToUTF16(extension_->name()));
287  } else {
288    return l10n_util::GetStringFUTF16(IDS_EXTENSION_DISABLED_ERROR_TITLE,
289                                      base::UTF8ToUTF16(extension_->name()));
290  }
291}
292
293std::vector<base::string16>
294ExtensionDisabledGlobalError::GetBubbleViewMessages() {
295  std::vector<base::string16> messages;
296  std::vector<base::string16> permission_warnings =
297      extensions::PermissionMessageProvider::Get()->GetWarningMessages(
298          extension_->permissions_data()->active_permissions().get(),
299          extension_->GetType());
300  if (is_remote_install_) {
301    messages.push_back(l10n_util::GetStringFUTF16(
302        extension_->is_app()
303            ? IDS_APP_DISABLED_REMOTE_INSTALL_ERROR_LABEL
304            : IDS_EXTENSION_DISABLED_REMOTE_INSTALL_ERROR_LABEL,
305        base::UTF8ToUTF16(extension_->name())));
306    if (!permission_warnings.empty())
307      messages.push_back(
308          l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_WILL_HAVE_ACCESS_TO));
309  } else {
310    messages.push_back(l10n_util::GetStringFUTF16(
311        extension_->is_app() ? IDS_APP_DISABLED_ERROR_LABEL
312                             : IDS_EXTENSION_DISABLED_ERROR_LABEL,
313        base::UTF8ToUTF16(extension_->name())));
314    messages.push_back(l10n_util::GetStringUTF16(
315        IDS_EXTENSION_PROMPT_WILL_NOW_HAVE_ACCESS_TO));
316  }
317  for (size_t i = 0; i < permission_warnings.size(); ++i) {
318    messages.push_back(l10n_util::GetStringFUTF16(
319        IDS_EXTENSION_PERMISSION_LINE, permission_warnings[i]));
320  }
321  return messages;
322}
323
324base::string16 ExtensionDisabledGlobalError::GetBubbleViewAcceptButtonLabel() {
325  if (is_remote_install_) {
326    return l10n_util::GetStringUTF16(
327        IDS_EXTENSION_PROMPT_REMOTE_INSTALL_BUTTON);
328  } else {
329    return l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_RE_ENABLE_BUTTON);
330  }
331}
332
333base::string16 ExtensionDisabledGlobalError::GetBubbleViewCancelButtonLabel() {
334  return l10n_util::GetStringUTF16(IDS_EXTENSIONS_UNINSTALL);
335}
336
337void ExtensionDisabledGlobalError::OnBubbleViewDidClose(Browser* browser) {
338}
339
340void ExtensionDisabledGlobalError::BubbleViewAcceptButtonPressed(
341    Browser* browser) {
342  // Delay extension reenabling so this bubble closes properly.
343  base::MessageLoop::current()->PostTask(FROM_HERE,
344      base::Bind(&ExtensionService::GrantPermissionsAndEnableExtension,
345                 service_->AsWeakPtr(), extension_));
346}
347
348void ExtensionDisabledGlobalError::BubbleViewCancelButtonPressed(
349    Browser* browser) {
350  uninstall_dialog_.reset(extensions::ExtensionUninstallDialog::Create(
351      service_->profile(), browser->window()->GetNativeWindow(), this));
352  // Delay showing the uninstall dialog, so that this function returns
353  // immediately, to close the bubble properly. See crbug.com/121544.
354  base::MessageLoop::current()->PostTask(
355      FROM_HERE,
356      base::Bind(&extensions::ExtensionUninstallDialog::ConfirmUninstall,
357                 uninstall_dialog_->AsWeakPtr(),
358                 extension_));
359}
360
361bool ExtensionDisabledGlobalError::ShouldCloseOnDeactivate() const {
362  // Since this indicates that an extension was disabled, we should definitely
363  // have the user acknowledge it, rather than having the bubble disappear when
364  // a new window pops up.
365  return false;
366}
367
368void ExtensionDisabledGlobalError::ExtensionUninstallAccepted() {
369  service_->UninstallExtension(extension_->id(),
370                               extensions::UNINSTALL_REASON_EXTENSION_DISABLED,
371                               base::Bind(&base::DoNothing),
372                               NULL);
373}
374
375void ExtensionDisabledGlobalError::ExtensionUninstallCanceled() {
376  // Nothing happens, and the error is still there.
377}
378
379void ExtensionDisabledGlobalError::Observe(
380    int type,
381    const content::NotificationSource& source,
382    const content::NotificationDetails& details) {
383  // The error is invalidated if the extension has been loaded or removed.
384  DCHECK(type == extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED ||
385         type == extensions::NOTIFICATION_EXTENSION_REMOVED);
386  const Extension* extension = content::Details<const Extension>(details).ptr();
387  if (extension != extension_)
388    return;
389  GlobalErrorServiceFactory::GetForProfile(service_->profile())->
390      RemoveGlobalError(this);
391
392  if (type == extensions::NOTIFICATION_EXTENSION_LOADED_DEPRECATED)
393    user_response_ = REENABLE;
394  else if (type == extensions::NOTIFICATION_EXTENSION_REMOVED)
395    user_response_ = UNINSTALL;
396  delete this;
397}
398
399// Globals --------------------------------------------------------------------
400
401namespace extensions {
402
403void AddExtensionDisabledErrorWithIcon(base::WeakPtr<ExtensionService> service,
404                                       const std::string& extension_id,
405                                       bool is_remote_install,
406                                       const gfx::Image& icon) {
407  if (!service.get())
408    return;
409  const Extension* extension = service->GetInstalledExtension(extension_id);
410  if (extension) {
411    GlobalErrorServiceFactory::GetForProfile(service->profile())
412        ->AddGlobalError(new ExtensionDisabledGlobalError(
413            service.get(), extension, is_remote_install, icon));
414  }
415}
416
417void AddExtensionDisabledError(ExtensionService* service,
418                               const Extension* extension,
419                               bool is_remote_install) {
420  // Do not display notifications for ephemeral apps that have been disabled.
421  // Instead, a prompt will be shown the next time the app is launched.
422  if (util::IsEphemeralApp(extension->id(), service->profile()))
423    return;
424
425  extensions::ExtensionResource image = extensions::IconsInfo::GetIconResource(
426      extension, kIconSize, ExtensionIconSet::MATCH_BIGGER);
427  gfx::Size size(kIconSize, kIconSize);
428  ImageLoader::Get(service->profile())
429      ->LoadImageAsync(extension,
430                       image,
431                       size,
432                       base::Bind(&AddExtensionDisabledErrorWithIcon,
433                                  service->AsWeakPtr(),
434                                  extension->id(),
435                                  is_remote_install));
436}
437
438void ShowExtensionDisabledDialog(ExtensionService* service,
439                                 content::WebContents* web_contents,
440                                 const Extension* extension) {
441  scoped_ptr<ExtensionInstallPrompt> install_ui(
442      new ExtensionInstallPrompt(web_contents));
443  // This object manages its own lifetime.
444  new ExtensionDisabledDialogDelegate(service, install_ui.Pass(), extension);
445}
446
447}  // namespace extensions
448