1 // Copyright 2014 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/scoped_observer.h" 16 #include "base/strings/utf_string_conversions.h" 17 #include "chrome/app/chrome_command_ids.h" 18 #include "chrome/browser/chrome_notification_types.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/webstore_data_fetcher.h" 24 #include "chrome/browser/extensions/webstore_data_fetcher_delegate.h" 25 #include "chrome/browser/profiles/profile.h" 26 #include "chrome/browser/ui/browser.h" 27 #include "chrome/browser/ui/browser_finder.h" 28 #include "chrome/browser/ui/global_error/global_error.h" 29 #include "chrome/browser/ui/global_error/global_error_service.h" 30 #include "chrome/browser/ui/global_error/global_error_service_factory.h" 31 #include "chrome/common/extensions/manifest_url_handler.h" 32 #include "content/public/browser/notification_details.h" 33 #include "content/public/browser/notification_observer.h" 34 #include "content/public/browser/notification_registrar.h" 35 #include "content/public/browser/notification_source.h" 36 #include "extensions/browser/extension_registry.h" 37 #include "extensions/browser/extension_registry_observer.h" 38 #include "extensions/common/constants.h" 39 #include "grit/generated_resources.h" 40 #include "ui/base/l10n/l10n_util.h" 41 #include "ui/gfx/image/image.h" 42 #include "ui/gfx/image/image_skia_operations.h" 43 44 namespace extensions { 45 46 namespace { 47 48 // Whether the external extension can use the streamlined bubble install flow. 49 bool UseBubbleInstall(const Extension* extension, bool is_new_profile) { 50 return ManifestURL::UpdatesFromGallery(extension) && !is_new_profile; 51 } 52 53 } // namespace 54 55 static const int kMenuCommandId = IDC_EXTERNAL_EXTENSION_ALERT; 56 57 class ExternalInstallGlobalError; 58 59 namespace extensions { 60 class ExtensionRegistry; 61 } 62 63 // This class is refcounted to stay alive while we try and pull webstore data. 64 class ExternalInstallDialogDelegate 65 : public ExtensionInstallPrompt::Delegate, 66 public WebstoreDataFetcherDelegate, 67 public content::NotificationObserver, 68 public base::RefCountedThreadSafe<ExternalInstallDialogDelegate> { 69 public: 70 ExternalInstallDialogDelegate(Browser* browser, 71 ExtensionService* service, 72 const Extension* extension, 73 bool use_global_error); 74 75 Browser* browser() { return browser_; } 76 77 private: 78 friend class base::RefCountedThreadSafe<ExternalInstallDialogDelegate>; 79 friend class ExternalInstallGlobalError; 80 81 virtual ~ExternalInstallDialogDelegate(); 82 83 // ExtensionInstallPrompt::Delegate: 84 virtual void InstallUIProceed() OVERRIDE; 85 virtual void InstallUIAbort(bool user_initiated) OVERRIDE; 86 87 // WebstoreDataFetcherDelegate: 88 virtual void OnWebstoreRequestFailure() OVERRIDE; 89 virtual void OnWebstoreResponseParseSuccess( 90 scoped_ptr<base::DictionaryValue> webstore_data) OVERRIDE; 91 virtual void OnWebstoreResponseParseFailure( 92 const std::string& error) OVERRIDE; 93 94 // content::NotificationObserver: 95 virtual void Observe(int type, 96 const content::NotificationSource& source, 97 const content::NotificationDetails& details) OVERRIDE; 98 99 // Show the install dialog to the user. 100 void ShowInstallUI(); 101 102 // The UI for showing the install dialog when enabling. 103 scoped_ptr<ExtensionInstallPrompt> install_ui_; 104 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt_; 105 106 Browser* browser_; 107 base::WeakPtr<ExtensionService> service_weak_; 108 scoped_ptr<WebstoreDataFetcher> webstore_data_fetcher_; 109 content::NotificationRegistrar registrar_; 110 std::string extension_id_; 111 bool use_global_error_; 112 113 DISALLOW_COPY_AND_ASSIGN(ExternalInstallDialogDelegate); 114 }; 115 116 // Only shows a menu item, no bubble. Clicking the menu item shows 117 // an external install dialog. 118 class ExternalInstallMenuAlert : public GlobalErrorWithStandardBubble, 119 public content::NotificationObserver, 120 public ExtensionRegistryObserver { 121 public: 122 ExternalInstallMenuAlert(ExtensionService* service, 123 const Extension* extension); 124 virtual ~ExternalInstallMenuAlert(); 125 126 // GlobalError implementation. 127 virtual Severity GetSeverity() OVERRIDE; 128 virtual bool HasMenuItem() OVERRIDE; 129 virtual int MenuItemCommandID() OVERRIDE; 130 virtual base::string16 MenuItemLabel() OVERRIDE; 131 virtual void ExecuteMenuItem(Browser* browser) OVERRIDE; 132 virtual bool HasBubbleView() OVERRIDE; 133 virtual base::string16 GetBubbleViewTitle() OVERRIDE; 134 virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE; 135 virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE; 136 virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE; 137 virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE; 138 virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE; 139 virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE; 140 141 protected: 142 ExtensionService* service_; 143 const Extension* extension_; 144 145 private: 146 // Delete this instance after cleaning jobs. 147 void Clean(); 148 149 // content::NotificationObserver implementation. 150 virtual void Observe(int type, 151 const content::NotificationSource& source, 152 const content::NotificationDetails& details) OVERRIDE; 153 154 // ExtensionRegistryObserver implementation. 155 virtual void OnExtensionLoaded(content::BrowserContext* browser_context, 156 const Extension* extension) OVERRIDE; 157 158 content::NotificationRegistrar registrar_; 159 160 // Listen to extension load notifications. 161 ScopedObserver<ExtensionRegistry, ExtensionRegistryObserver> 162 extension_registry_observer_; 163 164 DISALLOW_COPY_AND_ASSIGN(ExternalInstallMenuAlert); 165 }; 166 167 // Shows a menu item and a global error bubble, replacing the install dialog. 168 class ExternalInstallGlobalError : public ExternalInstallMenuAlert { 169 public: 170 ExternalInstallGlobalError( 171 ExtensionService* service, 172 const Extension* extension, 173 ExternalInstallDialogDelegate* delegate, 174 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt); 175 virtual ~ExternalInstallGlobalError(); 176 177 virtual void ExecuteMenuItem(Browser* browser) OVERRIDE; 178 virtual bool HasBubbleView() OVERRIDE; 179 virtual gfx::Image GetBubbleViewIcon() OVERRIDE; 180 virtual base::string16 GetBubbleViewTitle() OVERRIDE; 181 virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE; 182 virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE; 183 virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE; 184 virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE; 185 virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE; 186 virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE; 187 188 protected: 189 // Ref-counted, but needs to be disposed of if we are dismissed without 190 // having been clicked (perhaps because the user enabled the extension 191 // manually). 192 ExternalInstallDialogDelegate* delegate_; 193 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt_; 194 195 private: 196 DISALLOW_COPY_AND_ASSIGN(ExternalInstallGlobalError); 197 }; 198 199 static void CreateExternalInstallGlobalError( 200 base::WeakPtr<ExtensionService> service, 201 const std::string& extension_id, 202 const ExtensionInstallPrompt::ShowParams& show_params, 203 ExtensionInstallPrompt::Delegate* prompt_delegate, 204 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) { 205 if (!service.get()) 206 return; 207 const Extension* extension = service->GetInstalledExtension(extension_id); 208 if (!extension) 209 return; 210 GlobalErrorService* error_service = 211 GlobalErrorServiceFactory::GetForProfile(service->profile()); 212 if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId)) 213 return; 214 215 ExternalInstallDialogDelegate* delegate = 216 static_cast<ExternalInstallDialogDelegate*>(prompt_delegate); 217 ExternalInstallGlobalError* error_bubble = new ExternalInstallGlobalError( 218 service.get(), extension, delegate, prompt); 219 error_service->AddGlobalError(error_bubble); 220 // Show bubble immediately if possible. 221 if (delegate->browser()) 222 error_bubble->ShowBubbleView(delegate->browser()); 223 } 224 225 static void ShowExternalInstallDialog( 226 ExtensionService* service, 227 Browser* browser, 228 const Extension* extension) { 229 // This object manages its own lifetime. 230 new ExternalInstallDialogDelegate(browser, service, extension, false); 231 } 232 233 // ExternalInstallDialogDelegate -------------------------------------------- 234 235 ExternalInstallDialogDelegate::ExternalInstallDialogDelegate( 236 Browser* browser, 237 ExtensionService* service, 238 const Extension* extension, 239 bool use_global_error) 240 : browser_(browser), 241 service_weak_(service->AsWeakPtr()), 242 extension_id_(extension->id()), 243 use_global_error_(use_global_error) { 244 AddRef(); // Balanced in Proceed or Abort. 245 246 prompt_ = new ExtensionInstallPrompt::Prompt( 247 ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT); 248 249 // If we don't have a browser, we can't go to the webstore to fetch data. 250 // This should only happen in tests. 251 if (!browser) { 252 ShowInstallUI(); 253 return; 254 } 255 256 // Make sure to be notified if the owning profile is destroyed. 257 registrar_.Add(this, 258 chrome::NOTIFICATION_PROFILE_DESTROYED, 259 content::Source<Profile>(browser->profile())); 260 261 webstore_data_fetcher_.reset(new WebstoreDataFetcher( 262 this, 263 browser->profile()->GetRequestContext(), 264 GURL::EmptyGURL(), 265 extension->id())); 266 webstore_data_fetcher_->Start(); 267 } 268 269 void ExternalInstallDialogDelegate::OnWebstoreRequestFailure() { 270 ShowInstallUI(); 271 } 272 273 void ExternalInstallDialogDelegate::OnWebstoreResponseParseSuccess( 274 scoped_ptr<base::DictionaryValue> webstore_data) { 275 std::string localized_user_count; 276 double average_rating; 277 int rating_count; 278 if (!webstore_data->GetString(kUsersKey, &localized_user_count) || 279 !webstore_data->GetDouble(kAverageRatingKey, &average_rating) || 280 !webstore_data->GetInteger(kRatingCountKey, &rating_count)) { 281 // If we don't get a valid webstore response, short circuit, and continue 282 // to show a prompt without webstore data. 283 ShowInstallUI(); 284 return; 285 } 286 287 bool show_user_count = true; 288 webstore_data->GetBoolean(kShowUserCountKey, &show_user_count); 289 290 prompt_->SetWebstoreData(localized_user_count, 291 show_user_count, 292 average_rating, 293 rating_count); 294 295 ShowInstallUI(); 296 } 297 298 void ExternalInstallDialogDelegate::OnWebstoreResponseParseFailure( 299 const std::string& error) { 300 ShowInstallUI(); 301 } 302 303 void ExternalInstallDialogDelegate::Observe( 304 int type, 305 const content::NotificationSource& source, 306 const content::NotificationDetails& details) { 307 DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED); 308 // If the owning profile is destroyed, we need to abort so that we don't leak. 309 InstallUIAbort(false); // Not user initiated. 310 } 311 312 void ExternalInstallDialogDelegate::ShowInstallUI() { 313 const Extension* extension = NULL; 314 if (!service_weak_.get() || 315 !(extension = service_weak_->GetInstalledExtension(extension_id_))) { 316 return; 317 } 318 install_ui_.reset( 319 ExtensionInstallUI::CreateInstallPromptWithBrowser(browser_)); 320 321 const ExtensionInstallPrompt::ShowDialogCallback callback = 322 use_global_error_ ? 323 base::Bind(&CreateExternalInstallGlobalError, 324 service_weak_, 325 extension_id_) : 326 ExtensionInstallPrompt::GetDefaultShowDialogCallback(); 327 328 install_ui_->ConfirmExternalInstall(this, extension, callback, prompt_); 329 } 330 331 ExternalInstallDialogDelegate::~ExternalInstallDialogDelegate() { 332 } 333 334 void ExternalInstallDialogDelegate::InstallUIProceed() { 335 const Extension* extension = NULL; 336 if (service_weak_.get() && 337 (extension = service_weak_->GetInstalledExtension(extension_id_))) { 338 service_weak_->GrantPermissionsAndEnableExtension(extension); 339 } 340 Release(); 341 } 342 343 void ExternalInstallDialogDelegate::InstallUIAbort(bool user_initiated) { 344 const Extension* extension = NULL; 345 346 // Uninstall the extension if the abort was user initiated (and not, e.g., the 347 // result of the window closing). 348 // Otherwise, the extension will remain installed, but unacknowledged, so it 349 // will be prompted again. 350 if (user_initiated && 351 service_weak_.get() && 352 (extension = service_weak_->GetInstalledExtension(extension_id_))) { 353 service_weak_->UninstallExtension(extension_id_, false, NULL); 354 } 355 Release(); 356 } 357 358 // ExternalInstallMenuAlert ------------------------------------------------- 359 360 ExternalInstallMenuAlert::ExternalInstallMenuAlert(ExtensionService* service, 361 const Extension* extension) 362 : service_(service), 363 extension_(extension), 364 extension_registry_observer_(this) { 365 extension_registry_observer_.Add(ExtensionRegistry::Get(service->profile())); 366 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_REMOVED, 367 content::Source<Profile>(service->profile())); 368 } 369 370 ExternalInstallMenuAlert::~ExternalInstallMenuAlert() { 371 } 372 373 GlobalError::Severity ExternalInstallMenuAlert::GetSeverity() { 374 return SEVERITY_LOW; 375 } 376 377 bool ExternalInstallMenuAlert::HasMenuItem() { 378 return true; 379 } 380 381 int ExternalInstallMenuAlert::MenuItemCommandID() { 382 return kMenuCommandId; 383 } 384 385 base::string16 ExternalInstallMenuAlert::MenuItemLabel() { 386 int id = -1; 387 if (extension_->is_app()) 388 id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_APP; 389 else if (extension_->is_theme()) 390 id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_THEME; 391 else 392 id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_EXTENSION; 393 return l10n_util::GetStringFUTF16(id, base::UTF8ToUTF16(extension_->name())); 394 } 395 396 void ExternalInstallMenuAlert::ExecuteMenuItem(Browser* browser) { 397 ShowExternalInstallDialog(service_, browser, extension_); 398 } 399 400 bool ExternalInstallMenuAlert::HasBubbleView() { 401 return false; 402 } 403 base::string16 ExternalInstallMenuAlert::GetBubbleViewTitle() { 404 return base::string16(); 405 } 406 407 std::vector<base::string16> ExternalInstallMenuAlert::GetBubbleViewMessages() { 408 return std::vector<base::string16>(); 409 } 410 411 base::string16 ExternalInstallMenuAlert::GetBubbleViewAcceptButtonLabel() { 412 return base::string16(); 413 } 414 415 base::string16 ExternalInstallMenuAlert::GetBubbleViewCancelButtonLabel() { 416 return base::string16(); 417 } 418 419 void ExternalInstallMenuAlert::OnBubbleViewDidClose(Browser* browser) { 420 NOTREACHED(); 421 } 422 423 void ExternalInstallMenuAlert::BubbleViewAcceptButtonPressed( 424 Browser* browser) { 425 NOTREACHED(); 426 } 427 428 void ExternalInstallMenuAlert::BubbleViewCancelButtonPressed( 429 Browser* browser) { 430 NOTREACHED(); 431 } 432 433 void ExternalInstallMenuAlert::OnExtensionLoaded( 434 content::BrowserContext* browser_context, 435 const Extension* extension) { 436 if (extension == extension_) 437 Clean(); 438 } 439 440 void ExternalInstallMenuAlert::Observe( 441 int type, 442 const content::NotificationSource& source, 443 const content::NotificationDetails& details) { 444 // The error is invalidated if the extension has been loaded or removed. 445 DCHECK_EQ(type, chrome::NOTIFICATION_EXTENSION_REMOVED); 446 const Extension* extension = content::Details<const Extension>(details).ptr(); 447 if (extension == extension_) 448 Clean(); 449 } 450 451 void ExternalInstallMenuAlert::Clean() { 452 GlobalErrorService* error_service = 453 GlobalErrorServiceFactory::GetForProfile(service_->profile()); 454 error_service->RemoveGlobalError(this); 455 service_->AcknowledgeExternalExtension(extension_->id()); 456 delete this; 457 } 458 459 // ExternalInstallGlobalError ----------------------------------------------- 460 461 ExternalInstallGlobalError::ExternalInstallGlobalError( 462 ExtensionService* service, 463 const Extension* extension, 464 ExternalInstallDialogDelegate* delegate, 465 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) 466 : ExternalInstallMenuAlert(service, extension), 467 delegate_(delegate), 468 prompt_(prompt) { 469 } 470 471 ExternalInstallGlobalError::~ExternalInstallGlobalError() { 472 if (delegate_) 473 delegate_->Release(); 474 } 475 476 void ExternalInstallGlobalError::ExecuteMenuItem(Browser* browser) { 477 ShowBubbleView(browser); 478 } 479 480 bool ExternalInstallGlobalError::HasBubbleView() { 481 return true; 482 } 483 484 gfx::Image ExternalInstallGlobalError::GetBubbleViewIcon() { 485 if (prompt_->icon().IsEmpty()) 486 return GlobalErrorWithStandardBubble::GetBubbleViewIcon(); 487 // Scale icon to a reasonable size. 488 return gfx::Image(gfx::ImageSkiaOperations::CreateResizedImage( 489 *prompt_->icon().ToImageSkia(), 490 skia::ImageOperations::RESIZE_BEST, 491 gfx::Size(extension_misc::EXTENSION_ICON_SMALL, 492 extension_misc::EXTENSION_ICON_SMALL))); 493 } 494 495 base::string16 ExternalInstallGlobalError::GetBubbleViewTitle() { 496 return prompt_->GetDialogTitle(); 497 } 498 499 std::vector<base::string16> 500 ExternalInstallGlobalError::GetBubbleViewMessages() { 501 std::vector<base::string16> messages; 502 messages.push_back(prompt_->GetHeading()); 503 if (prompt_->GetPermissionCount()) { 504 messages.push_back(prompt_->GetPermissionsHeading()); 505 for (size_t i = 0; i < prompt_->GetPermissionCount(); ++i) { 506 messages.push_back(l10n_util::GetStringFUTF16( 507 IDS_EXTENSION_PERMISSION_LINE, 508 prompt_->GetPermission(i))); 509 } 510 } 511 // TODO(yoz): OAuth issue advice? 512 return messages; 513 } 514 515 base::string16 ExternalInstallGlobalError::GetBubbleViewAcceptButtonLabel() { 516 return prompt_->GetAcceptButtonLabel(); 517 } 518 519 base::string16 ExternalInstallGlobalError::GetBubbleViewCancelButtonLabel() { 520 return prompt_->GetAbortButtonLabel(); 521 } 522 523 void ExternalInstallGlobalError::OnBubbleViewDidClose(Browser* browser) { 524 } 525 526 void ExternalInstallGlobalError::BubbleViewAcceptButtonPressed( 527 Browser* browser) { 528 ExternalInstallDialogDelegate* delegate = delegate_; 529 delegate_ = NULL; 530 delegate->InstallUIProceed(); 531 } 532 533 void ExternalInstallGlobalError::BubbleViewCancelButtonPressed( 534 Browser* browser) { 535 ExternalInstallDialogDelegate* delegate = delegate_; 536 delegate_ = NULL; 537 delegate->InstallUIAbort(true); 538 } 539 540 // Public interface --------------------------------------------------------- 541 542 void AddExternalInstallError(ExtensionService* service, 543 const Extension* extension, 544 bool is_new_profile) { 545 GlobalErrorService* error_service = 546 GlobalErrorServiceFactory::GetForProfile(service->profile()); 547 if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId)) 548 return; 549 550 if (UseBubbleInstall(extension, is_new_profile)) { 551 Browser* browser = NULL; 552 #if !defined(OS_ANDROID) 553 browser = chrome::FindTabbedBrowser(service->profile(), 554 true, 555 chrome::GetActiveDesktop()); 556 #endif 557 new ExternalInstallDialogDelegate(browser, service, extension, true); 558 } else { 559 error_service->AddGlobalError( 560 new ExternalInstallMenuAlert(service, extension)); 561 } 562 } 563 564 void RemoveExternalInstallError(ExtensionService* service) { 565 GlobalErrorService* error_service = 566 GlobalErrorServiceFactory::GetForProfile(service->profile()); 567 GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID( 568 kMenuCommandId); 569 if (error) { 570 error_service->RemoveGlobalError(error); 571 delete error; 572 } 573 } 574 575 bool HasExternalInstallError(ExtensionService* service) { 576 GlobalErrorService* error_service = 577 GlobalErrorServiceFactory::GetForProfile(service->profile()); 578 GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID( 579 kMenuCommandId); 580 return !!error; 581 } 582 583 bool HasExternalInstallBubble(ExtensionService* service) { 584 GlobalErrorService* error_service = 585 GlobalErrorServiceFactory::GetForProfile(service->profile()); 586 GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID( 587 kMenuCommandId); 588 return error && error->HasBubbleView(); 589 } 590 591 } // namespace extensions 592