Add a webstore API for installing bundles of extensions.

This does not include any of the UI.

Re-landing due to trouble on debug bots (parameters section was missing from the callback spec).

BUG=112096
TEST=*InstallBundle*

Review URL: https://chromiumcodereview.appspot.com/9414013

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@123253 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/extensions/bundle_installer.cc b/chrome/browser/extensions/bundle_installer.cc
new file mode 100644
index 0000000..8772b0b4
--- /dev/null
+++ b/chrome/browser/extensions/bundle_installer.cc
@@ -0,0 +1,279 @@
+// 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 <string>
+#include <vector>
+
+#include "base/command_line.h"
+#include "base/values.h"
+#include "chrome/browser/extensions/crx_installer.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/common/chrome_switches.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/web_contents.h"
+
+using content::NavigationController;
+
+namespace extensions {
+
+namespace {
+
+enum AutoApproveForTest {
+  DO_NOT_SKIP = 0,
+  PROCEED,
+  ABORT
+};
+
+AutoApproveForTest g_auto_approve_for_test = DO_NOT_SKIP;
+
+// Creates a dummy extension and sets the manifest's name to the item's
+// localized name.
+scoped_refptr<Extension> CreateDummyExtension(BundleInstaller::Item item,
+                                              DictionaryValue* manifest) {
+  // We require localized names so we can have nice error messages when we can't
+  // parse an extension manifest.
+  CHECK(!item.localized_name.empty());
+
+  manifest->SetString(extension_manifest_keys::kName, item.localized_name);
+
+  std::string error;
+  return Extension::Create(FilePath(),
+                           Extension::INTERNAL,
+                           *manifest,
+                           Extension::NO_FLAGS,
+                           item.id,
+                           &error);
+}
+
+}  // namespace
+
+// static
+void BundleInstaller::SetAutoApproveForTesting(bool auto_approve) {
+  CHECK(CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType));
+  g_auto_approve_for_test = auto_approve ? PROCEED : ABORT;
+}
+
+BundleInstaller::Item::Item() : state(STATE_PENDING) {}
+
+BundleInstaller::BundleInstaller(Profile* profile,
+                                 const BundleInstaller::ItemList& items)
+    : approved_(false),
+      browser_(NULL),
+      profile_(profile),
+      delegate_(NULL) {
+  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 (ItemMap::const_iterator i = items_.begin(); i != items_.end(); ++i) {
+    if (i->second.state == state)
+      list.push_back(i->second);
+  }
+
+  return list;
+}
+
+void BundleInstaller::PromptForApproval(Delegate* delegate) {
+  delegate_ = delegate;
+
+  AddRef();  // Balanced in ReportApproved() and ReportCanceled().
+
+  ParseManifests();
+}
+
+void BundleInstaller::CompleteInstall(NavigationController* controller,
+                                      Browser* browser,
+                                      Delegate* delegate) {
+  CHECK(approved_);
+
+  browser_ = browser;
+  delegate_ = delegate;
+
+  AddRef();  // Balanced in ReportComplete();
+
+  if (GetItemsWithState(Item::STATE_PENDING).empty()) {
+    ReportComplete();
+    return;
+  }
+
+  // Start each WebstoreInstaller.
+  for (ItemMap::iterator i = items_.begin(); i != items_.end(); ++i) {
+    if (i->second.state != Item::STATE_PENDING)
+      continue;
+
+    scoped_refptr<WebstoreInstaller> installer = new WebstoreInstaller(
+        profile_,
+        this,
+        controller,
+        i->first,
+        WebstoreInstaller::FLAG_NONE);
+    installer->Start();
+  }
+}
+
+// static
+void BundleInstaller::ShowInstalledBubble(
+    const BundleInstaller* bundle, Browser* browser) {
+  // TODO(jstritar): provide platform specific implementations.
+}
+
+void BundleInstaller::ParseManifests() {
+  if (items_.empty()) {
+    ReportCanceled(false);
+    return;
+  }
+
+  for (ItemMap::iterator i = items_.begin(); i != items_.end(); ++i) {
+    scoped_refptr<WebstoreInstallHelper> helper = new WebstoreInstallHelper(
+        this, i->first, i->second.manifest, "", GURL(), NULL);
+    helper->Start();
+  }
+}
+
+void BundleInstaller::ReportApproved() {
+  if (delegate_)
+    delegate_->OnBundleInstallApproved();
+
+  Release();  // Balanced in PromptForApproval().
+}
+
+void BundleInstaller::ReportCanceled(bool user_initiated) {
+  if (delegate_)
+    delegate_->OnBundleInstallCanceled(user_initiated);
+
+  Release();  // Balanced in PromptForApproval().
+}
+
+void BundleInstaller::ReportComplete() {
+  if (delegate_)
+    delegate_->OnBundleInstallCompleted();
+
+  Release();  // Balanced in CompleteInstall().
+}
+
+void BundleInstaller::ShowPromptIfDoneParsing() {
+  // We don't prompt until all the manifests have been parsed.
+  ItemList pending_items = GetItemsWithState(Item::STATE_PENDING);
+  if (pending_items.size() != dummy_extensions_.size())
+    return;
+
+  ShowPrompt();
+}
+
+void BundleInstaller::ShowPrompt() {
+  // Abort if we couldn't create any Extensions out of the manifests.
+  if (dummy_extensions_.empty()) {
+    ReportCanceled(false);
+    return;
+  }
+
+  scoped_refptr<ExtensionPermissionSet> permissions;
+  for (size_t i = 0; i < dummy_extensions_.size(); ++i) {
+    permissions = ExtensionPermissionSet::CreateUnion(
+          permissions, dummy_extensions_[i]->required_permission_set());
+  }
+
+  // TODO(jstritar): show the actual prompt.
+  if (g_auto_approve_for_test == PROCEED)
+    InstallUIProceed();
+  else if (g_auto_approve_for_test == ABORT)
+    InstallUIAbort(true);
+  else
+    InstallUIAbort(false);
+}
+
+void BundleInstaller::ShowInstalledBubbleIfDone() {
+  // We're ready to show the installed bubble when no items are pending.
+  if (!GetItemsWithState(Item::STATE_PENDING).empty())
+    return;
+
+  if (browser_)
+    ShowInstalledBubble(this, browser_);
+
+  ReportComplete();
+}
+
+void BundleInstaller::OnWebstoreParseSuccess(
+    const std::string& id,
+    const SkBitmap& icon,
+    DictionaryValue* manifest) {
+  dummy_extensions_.push_back(CreateDummyExtension(items_[id], manifest));
+  parsed_manifests_[id] = linked_ptr<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;
+  for (ItemMap::iterator i = items_.begin(); i != items_.end(); ++i) {
+    if (i->second.state != Item::STATE_PENDING)
+      continue;
+
+    // Create a whitelist entry for each of the approved extensions.
+    CrxInstaller::WhitelistEntry* entry = new CrxInstaller::WhitelistEntry;
+    entry->parsed_manifest.reset(parsed_manifests_[i->first]->DeepCopy());
+    entry->localized_name = i->second.localized_name;
+    entry->use_app_installed_bubble = false;
+    entry->skip_post_install_ui = true;
+    CrxInstaller::SetWhitelistEntry(i->first, entry);
+  }
+  ReportApproved();
+}
+
+void BundleInstaller::InstallUIAbort(bool user_initiated) {
+  for (ItemMap::iterator i = items_.begin(); i != items_.end(); ++i)
+    i->second.state = Item::STATE_FAILED;
+
+  ReportCanceled(user_initiated);
+}
+
+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) {
+  items_[id].state = Item::STATE_FAILED;
+
+  ShowInstalledBubbleIfDone();
+}
+
+void BundleInstaller::OnBrowserAdded(const Browser* browser) {
+}
+
+void BundleInstaller::OnBrowserRemoved(const Browser* browser) {
+  if (browser_ == browser)
+    browser_ = NULL;
+}
+
+void BundleInstaller::OnBrowserSetLastActive(const Browser* browser) {
+}
+
+}  // namespace extensions
diff --git a/chrome/browser/extensions/bundle_installer.h b/chrome/browser/extensions/bundle_installer.h
new file mode 100644
index 0000000..dec681d
--- /dev/null
+++ b/chrome/browser/extensions/bundle_installer.h
@@ -0,0 +1,181 @@
+// 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.
+
+#ifndef CHROME_BROWSER_EXTENSIONS_BUNDLE_INSTALLER_H_
+#define CHROME_BROWSER_EXTENSIONS_BUNDLE_INSTALLER_H_
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "base/memory/linked_ptr.h"
+#include "chrome/browser/extensions/extension_install_ui.h"
+#include "chrome/browser/extensions/webstore_installer.h"
+#include "chrome/browser/extensions/webstore_install_helper.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/common/extensions/extension.h"
+
+namespace base {
+class DictionaryValue;
+}  // namespace base
+
+namespace content {
+class NavigationController;
+}  // namespace content
+
+class Browser;
+class Profile;
+
+namespace extensions {
+
+// Manages the installation life cycle for extension bundles.
+//
+// We install bundles in two steps:
+//  1) PromptForApproval: parse manifests and prompt the user
+//  2) CompleteInstall: install the CRXs and show confirmation bubble
+//
+class BundleInstaller : public WebstoreInstallHelper::Delegate,
+                        public ExtensionInstallUI::Delegate,
+                        public WebstoreInstaller::Delegate,
+                        public BrowserList::Observer,
+                        public base::RefCountedThreadSafe<BundleInstaller> {
+ public:
+  // Auto approve or cancel the permission prompt.
+  static void SetAutoApproveForTesting(bool approve);
+
+  class Delegate {
+   public:
+    virtual void OnBundleInstallApproved() {}
+    virtual void OnBundleInstallCanceled(bool user_initiated) {}
+    virtual void OnBundleInstallCompleted() {}
+  };
+
+  // Represents an individual member of the bundle.
+  struct Item {
+    // Items are in the PENDING state until they've been installed, or the
+    // install has failed or been canceled.
+    enum State {
+      STATE_PENDING,
+      STATE_INSTALLED,
+      STATE_FAILED
+    };
+
+    Item();
+
+    std::string id;
+    std::string manifest;
+    std::string localized_name;
+    State state;
+  };
+
+  typedef std::vector<Item> ItemList;
+
+  BundleInstaller(Profile* profile, const ItemList& items);
+  virtual ~BundleInstaller();
+
+  // Returns true if the user has approved the bundle's permissions.
+  bool approved() const { return approved_; }
+
+  // Gets the items in the given state.
+  ItemList GetItemsWithState(Item::State state) const;
+
+  // Parses the extension manifests and then prompts the user to approve their
+  // permissions. One of OnBundleInstallApproved or OnBundleInstallCanceled
+  // will be called when complete if |delegate| is not NULL.
+  // Note: the |delegate| must stay alive until receiving the callback.
+  void PromptForApproval(Delegate* delegate);
+
+  // If the bundle has been approved, this downloads and installs the member
+  // extensions. OnBundleInstallComplete will be called when the process is
+  // complete and |delegate| is not NULL. The download process uses the
+  // specified |controller|. When complete, we show a confirmation bubble in
+  // the specified |browser|.
+  // Note: the |delegate| must stay alive until receiving the callback.
+  void CompleteInstall(content::NavigationController* controller,
+                       Browser* browser,
+                       Delegate* delegate);
+
+ private:
+  friend class base::RefCountedThreadSafe<BundleInstaller>;
+
+  typedef std::map<std::string, Item> ItemMap;
+  typedef std::map<std::string, linked_ptr<base::DictionaryValue> > ManifestMap;
+
+  // Displays the install bubble for |bundle| on |browser|.
+  // Note: this is a platform specific implementation.
+  static void ShowInstalledBubble(const BundleInstaller* bundle,
+                                  Browser* browser);
+
+  // Parses the manifests using WebstoreInstallHelper.
+  void ParseManifests();
+
+  // Notifies the delegate that the installation has been approved.
+  void ReportApproved();
+
+  // Notifies the delegate that the installation was canceled.
+  void ReportCanceled(bool user_initiated);
+
+  // Notifies the delegate that the installation is complete.
+  void ReportComplete();
+
+  // Prompts the user to install the bundle once we have dummy extensions for
+  // all the pending items.
+  void ShowPromptIfDoneParsing();
+
+  // Prompts the user to install the bundle.
+  void ShowPrompt();
+
+  // Displays the installed bubble once all items have installed or failed.
+  void ShowInstalledBubbleIfDone();
+
+  // WebstoreInstallHelper::Delegate implementation:
+  virtual void OnWebstoreParseSuccess(
+      const std::string& id,
+      const SkBitmap& icon,
+      base::DictionaryValue* parsed_manifest) OVERRIDE;
+  virtual void OnWebstoreParseFailure(
+      const std::string& id,
+      InstallHelperResultCode result_code,
+      const std::string& error_message) OVERRIDE;
+
+  // ExtensionInstallUI::Delegate implementation:
+  virtual void InstallUIProceed() OVERRIDE;
+  virtual void InstallUIAbort(bool user_initiated) OVERRIDE;
+
+  // WebstoreInstaller::Delegate implementation:
+  virtual void OnExtensionInstallSuccess(const std::string& id) OVERRIDE;
+  virtual void OnExtensionInstallFailure(const std::string& id,
+                                         const std::string& error) OVERRIDE;
+
+  // BrowserList::observer implementation:
+  virtual void OnBrowserAdded(const Browser* browser) OVERRIDE;
+  virtual void OnBrowserRemoved(const Browser* browser) OVERRIDE;
+  virtual void OnBrowserSetLastActive(const Browser* browser) OVERRIDE;
+
+  // Holds the Extensions used to generate the permission warnings.
+  ExtensionList dummy_extensions_;
+
+  // Holds the parsed manifests, indexed by the extension ids.
+  ManifestMap parsed_manifests_;
+
+  // True if the user has approved the bundle.
+  bool approved_;
+
+  // Holds the bundle's Items, indexed by their ids.
+  ItemMap items_;
+
+  // The browser to show the confirmation bubble for.
+  Browser* browser_;
+
+  // The profile that the bundle should be installed in.
+  Profile* profile_;
+
+  Delegate* delegate_;
+
+  DISALLOW_COPY_AND_ASSIGN(BundleInstaller);
+};
+
+}  // namespace extensions
+
+#endif  // CHROME_BROWSER_EXTENSIONS_BUNDLE_INSTALLER_H_
diff --git a/chrome/browser/extensions/crx_installer.cc b/chrome/browser/extensions/crx_installer.cc
index 41b9760..4b923e9 100644
--- a/chrome/browser/extensions/crx_installer.cc
+++ b/chrome/browser/extensions/crx_installer.cc
@@ -50,6 +50,7 @@
 
 namespace {
 
+// TODO(jstritar): this whitelist is not profile aware.
 struct Whitelist {
   Whitelist() {}
   std::set<std::string> ids;
diff --git a/chrome/browser/extensions/extension_function_dispatcher.cc b/chrome/browser/extensions/extension_function_dispatcher.cc
index 2e06ca20..148ea696 100644
--- a/chrome/browser/extensions/extension_function_dispatcher.cc
+++ b/chrome/browser/extensions/extension_function_dispatcher.cc
@@ -366,6 +366,7 @@
   RegisterFunction<GetBrowserLoginFunction>();
   RegisterFunction<GetStoreLoginFunction>();
   RegisterFunction<SetStoreLoginFunction>();
+  RegisterFunction<InstallBundleFunction>();
   RegisterFunction<BeginInstallWithManifestFunction>();
   RegisterFunction<CompleteInstallFunction>();
   RegisterFunction<SilentlyInstallFunction>();
diff --git a/chrome/browser/extensions/extension_webstore_private_api.cc b/chrome/browser/extensions/extension_webstore_private_api.cc
index 5517250..f0c2834 100644
--- a/chrome/browser/extensions/extension_webstore_private_api.cc
+++ b/chrome/browser/extensions/extension_webstore_private_api.cc
@@ -36,6 +36,7 @@
 #include "ui/base/l10n/l10n_util.h"
 
 using content::GpuDataManager;
+using extensions::BundleInstaller;
 
 namespace {
 
@@ -137,6 +138,69 @@
   trust_test_ids = allow;
 }
 
+InstallBundleFunction::InstallBundleFunction() {}
+InstallBundleFunction::~InstallBundleFunction() {}
+
+bool InstallBundleFunction::RunImpl() {
+  ListValue* extensions = NULL;
+  EXTENSION_FUNCTION_VALIDATE(args_->GetList(0, &extensions));
+
+  BundleInstaller::ItemList items;
+  if (!ReadBundleInfo(extensions, &items))
+    return false;
+
+  bundle_ = new BundleInstaller(profile(), items);
+
+  AddRef();  // Balanced in OnBundleInstallCompleted / OnBundleInstallCanceled.
+
+  bundle_->PromptForApproval(this);
+  return true;
+}
+
+bool InstallBundleFunction::ReadBundleInfo(ListValue* extensions,
+                                           BundleInstaller::ItemList* items) {
+  for (size_t i = 0; i < extensions->GetSize(); ++i) {
+    DictionaryValue* details = NULL;
+    EXTENSION_FUNCTION_VALIDATE(extensions->GetDictionary(i, &details));
+
+    BundleInstaller::Item item;
+    EXTENSION_FUNCTION_VALIDATE(details->GetString(
+        kIdKey, &item.id));
+    EXTENSION_FUNCTION_VALIDATE(details->GetString(
+        kManifestKey, &item.manifest));
+    EXTENSION_FUNCTION_VALIDATE(details->GetString(
+        kLocalizedNameKey, &item.localized_name));
+
+    items->push_back(item);
+  }
+
+  return true;
+}
+
+void InstallBundleFunction::OnBundleInstallApproved() {
+  bundle_->CompleteInstall(
+      &(dispatcher()->delegate()->GetAssociatedWebContents()->GetController()),
+      GetCurrentBrowser(),
+      this);
+}
+
+void InstallBundleFunction::OnBundleInstallCanceled(bool user_initiated) {
+  if (user_initiated)
+    error_ = "user_canceled";
+  else
+    error_ = "unknown_error";
+
+  SendResponse(false);
+
+  Release();  // Balanced in RunImpl().
+}
+
+void InstallBundleFunction::OnBundleInstallCompleted() {
+  SendResponse(true);
+
+  Release();  // Balanced in RunImpl().
+}
+
 BeginInstallWithManifestFunction::BeginInstallWithManifestFunction()
   : use_app_installed_bubble_(false) {}
 
diff --git a/chrome/browser/extensions/extension_webstore_private_api.h b/chrome/browser/extensions/extension_webstore_private_api.h
index 0c1f09db..8a9945f8 100644
--- a/chrome/browser/extensions/extension_webstore_private_api.h
+++ b/chrome/browser/extensions/extension_webstore_private_api.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// 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.
 
@@ -8,6 +8,7 @@
 
 #include <string>
 
+#include "chrome/browser/extensions/bundle_installer.h"
 #include "chrome/browser/extensions/extension_function.h"
 #include "chrome/browser/extensions/extension_install_ui.h"
 #include "chrome/browser/extensions/webstore_install_helper.h"
@@ -38,6 +39,29 @@
   static void SetTrustTestIDsForTesting(bool allow);
 };
 
+class InstallBundleFunction : public AsyncExtensionFunction,
+                              public extensions::BundleInstaller::Delegate {
+ public:
+  InstallBundleFunction();
+
+  // BundleInstaller::Delegate implementation.
+  virtual void OnBundleInstallApproved() OVERRIDE;
+  virtual void OnBundleInstallCanceled(bool user_initiated) OVERRIDE;
+  virtual void OnBundleInstallCompleted() OVERRIDE;
+
+ protected:
+  virtual ~InstallBundleFunction();
+  virtual bool RunImpl() OVERRIDE;
+
+  // Reads the extension |details| into |items|.
+  bool ReadBundleInfo(base::ListValue* details,
+                      extensions::BundleInstaller::ItemList* items);
+
+ private:
+  scoped_refptr<extensions::BundleInstaller> bundle_;
+  DECLARE_EXTENSION_FUNCTION_NAME("webstorePrivate.installBundle");
+};
+
 class BeginInstallWithManifestFunction
     : public AsyncExtensionFunction,
       public ExtensionInstallUI::Delegate,
diff --git a/chrome/browser/extensions/extension_webstore_private_apitest.cc b/chrome/browser/extensions/extension_webstore_private_apitest.cc
index cf46462..77aa755d 100644
--- a/chrome/browser/extensions/extension_webstore_private_apitest.cc
+++ b/chrome/browser/extensions/extension_webstore_private_apitest.cc
@@ -7,6 +7,7 @@
 #include "base/file_path.h"
 #include "base/file_util.h"
 #include "base/stringprintf.h"
+#include "chrome/browser/extensions/bundle_installer.h"
 #include "chrome/browser/extensions/extension_apitest.h"
 #include "chrome/browser/extensions/extension_function_test_utils.h"
 #include "chrome/browser/extensions/extension_install_dialog.h"
@@ -38,10 +39,35 @@
   WebstoreInstallListener()
       : received_failure_(false), received_success_(false), waiting_(false) {}
 
-  void OnExtensionInstallSuccess(const std::string& id) OVERRIDE;
+  void OnExtensionInstallSuccess(const std::string& id) OVERRIDE {
+    received_success_ = true;
+    id_ = id;
+
+    if (waiting_) {
+      waiting_ = false;
+      MessageLoopForUI::current()->Quit();
+    }
+  }
+
   void OnExtensionInstallFailure(const std::string& id,
-                                 const std::string& error) OVERRIDE;
-  void Wait();
+                                 const std::string& error) OVERRIDE {
+    received_failure_ = true;
+    id_ = id;
+    error_ = error;
+
+    if (waiting_) {
+      waiting_ = false;
+      MessageLoopForUI::current()->Quit();
+    }
+  }
+
+  void Wait() {
+    if (received_success_ || received_failure_)
+      return;
+
+    waiting_ = true;
+    ui_test_utils::RunMessageLoop();
+  }
 
   bool received_failure() const { return received_failure_; }
   bool received_success() const { return received_success_; }
@@ -56,36 +82,6 @@
   std::string error_;
 };
 
-void WebstoreInstallListener::OnExtensionInstallSuccess(const std::string& id) {
-  received_success_ = true;
-  id_ = id;
-
-  if (waiting_) {
-    waiting_ = false;
-    MessageLoopForUI::current()->Quit();
-  }
-}
-
-void WebstoreInstallListener::OnExtensionInstallFailure(
-    const std::string& id, const std::string& error) {
-  received_failure_ = true;
-  id_ = id;
-  error_ = error;
-
-  if (waiting_) {
-    waiting_ = false;
-    MessageLoopForUI::current()->Quit();
-  }
-}
-
-void WebstoreInstallListener::Wait() {
-  if (received_success_ || received_failure_)
-    return;
-
-  waiting_ = true;
-  ui_test_utils::RunMessageLoop();
-}
-
 }  // namespace
 
 // A base class for tests below.
@@ -150,10 +146,6 @@
     CommandLine::ForCurrentProcess()->AppendSwitchASCII(
         switches::kAppsGalleryDownloadURL,
         GetTestServerURL("bundle/%s.crx").spec());
-
-    PackCRX("begfmnajjkbjdgmffnjaojchoncnmngg");
-    PackCRX("bmfoocgfinpmkmlbjhcbofejhkhlbchk");
-    PackCRX("mpneghmdnmaolkljkipbhaienajcflfe");
   }
 
   void TearDownInProcessBrowserTestFixture() OVERRIDE {
@@ -162,14 +154,39 @@
       ASSERT_TRUE(file_util::Delete(test_crx_[i], false));
   }
 
- private:
+ protected:
   void PackCRX(const std::string& id) {
+    FilePath dir_path = test_data_dir_
+        .AppendASCII("webstore_private/bundle")
+        .AppendASCII(id);
+
+    PackCRX(id, dir_path);
+  }
+
+  // Packs the |manifest| file into a CRX using |id|'s PEM key.
+  void PackCRX(const std::string& id, const std::string& manifest) {
+    // Move the extension to a temporary directory.
+    ScopedTempDir tmp;
+    ASSERT_TRUE(tmp.CreateUniqueTempDir());
+    ASSERT_TRUE(file_util::CreateDirectory(tmp.path()));
+
+    FilePath tmp_manifest = tmp.path().AppendASCII("manifest.json");
     FilePath data_path = test_data_dir_.AppendASCII("webstore_private/bundle");
-    FilePath dir_path = data_path.AppendASCII(id);
+    FilePath manifest_path = data_path.AppendASCII(manifest);
+
+    ASSERT_TRUE(file_util::PathExists(manifest_path));
+    ASSERT_TRUE(file_util::CopyFile(manifest_path, tmp_manifest));
+
+    PackCRX(id, tmp.path());
+  }
+
+  // Packs the extension at |ext_path| using |id|'s PEM key.
+  void PackCRX(const std::string& id, const FilePath& ext_path) {
+    FilePath data_path = test_data_dir_.AppendASCII("webstore_private/bundle");
     FilePath pem_path = data_path.AppendASCII(id + ".pem");
     FilePath crx_path = data_path.AppendASCII(id + ".crx");
     FilePath destination = PackExtensionWithOptions(
-        dir_path, crx_path, pem_path, FilePath());
+        ext_path, crx_path, pem_path, FilePath());
 
     ASSERT_FALSE(destination.empty());
     ASSERT_EQ(destination, crx_path);
@@ -177,6 +194,21 @@
     test_crx_.push_back(destination);
   }
 
+  // Creates an invalid CRX.
+  void PackInvalidCRX(const std::string& id) {
+    FilePath contents = test_data_dir_
+        .AppendASCII("webstore_private")
+        .AppendASCII("install_bundle_invalid.html");
+    FilePath crx_path = test_data_dir_
+        .AppendASCII("webstore_private/bundle")
+        .AppendASCII(id + ".crx");
+
+    ASSERT_TRUE(file_util::CopyFile(contents, crx_path));
+
+    test_crx_.push_back(crx_path);
+  }
+
+ private:
   std::vector<FilePath> test_crx_;
 };
 
@@ -282,9 +314,59 @@
 // Tests using silentlyInstall to install extensions.
 IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateBundleTest, SilentlyInstall) {
   WebstorePrivateApi::SetTrustTestIDsForTesting(true);
+
+  PackCRX("bmfoocgfinpmkmlbjhcbofejhkhlbchk", "extension1.json");
+  PackCRX("mpneghmdnmaolkljkipbhaienajcflfe", "extension2.json");
+  PackCRX("begfmnajjkbjdgmffnjaojchoncnmngg", "app2.json");
+
   ASSERT_TRUE(RunPageTest(GetTestServerURL("silently_install.html").spec()));
 }
 
+// Tests successfully installing a bundle of 2 apps and 2 extensions.
+IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateBundleTest, InstallBundle) {
+  extensions::BundleInstaller::SetAutoApproveForTesting(true);
+
+  PackCRX("bmfoocgfinpmkmlbjhcbofejhkhlbchk", "extension1.json");
+  PackCRX("pkapffpjmiilhlhbibjhamlmdhfneidj", "extension2.json");
+  PackCRX("begfmnajjkbjdgmffnjaojchoncnmngg", "app1.json");
+  PackCRX("mpneghmdnmaolkljkipbhaienajcflfe", "app2.json");
+
+  ASSERT_TRUE(RunPageTest(GetTestServerURL("install_bundle.html").spec()));
+}
+
+// Tests the user canceling the bundle install prompt.
+IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateBundleTest,
+                       InstallBundleCancel) {
+  // We don't need to create the CRX files since we are aborting the install.
+  extensions::BundleInstaller::SetAutoApproveForTesting(false);
+  ASSERT_TRUE(RunPageTest(GetTestServerURL(
+      "install_bundle_cancel.html").spec()));
+}
+
+// Tests partially installing a bundle (2 succeed, 1 fails due to an invalid
+// CRX, and 1 fails due to the manifests not matching).
+IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateBundleTest,
+                       InstallBundleInvalid) {
+  extensions::BundleInstaller::SetAutoApproveForTesting(true);
+
+  PackInvalidCRX("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+  PackCRX("bmfoocgfinpmkmlbjhcbofejhkhlbchk", "extension1.json");
+  PackCRX("pkapffpjmiilhlhbibjhamlmdhfneidj", "extension2.json");
+  PackCRX("begfmnajjkbjdgmffnjaojchoncnmngg", "app1.json");
+
+  ASSERT_TRUE(RunPageTest(GetTestServerURL(
+      "install_bundle_invalid.html").spec()));
+
+  ASSERT_TRUE(service()->GetExtensionById(
+      "begfmnajjkbjdgmffnjaojchoncnmngg", false));
+  ASSERT_TRUE(service()->GetExtensionById(
+      "pkapffpjmiilhlhbibjhamlmdhfneidj", false));
+  ASSERT_FALSE(service()->GetExtensionById(
+      "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true));
+  ASSERT_FALSE(service()->GetExtensionById(
+      "bmfoocgfinpmkmlbjhcbofejhkhlbchk", true));
+}
+
 // Tests getWebGLStatus function when WebGL is allowed.
 IN_PROC_BROWSER_TEST_F(ExtensionWebstoreGetWebGLStatusTest, Allowed) {
   bool webgl_allowed = true;
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index dee8edb..f71805a5 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -1043,6 +1043,8 @@
         'browser/extensions/browser_action_test_util_gtk.cc',
         'browser/extensions/browser_action_test_util_mac.mm',
         'browser/extensions/browser_action_test_util_views.cc',
+        'browser/extensions/bundle_installer.cc',
+        'browser/extensions/bundle_installer.h',
         'browser/extensions/component_loader.cc',
         'browser/extensions/component_loader.h',
         'browser/extensions/convert_user_script.cc',
diff --git a/chrome/common/extensions/api/webstorePrivate.json b/chrome/common/extensions/api/webstorePrivate.json
index da19da6..a3839de9 100644
--- a/chrome/common/extensions/api/webstorePrivate.json
+++ b/chrome/common/extensions/api/webstorePrivate.json
@@ -21,6 +21,44 @@
         ]
       },
       {
+        "name": "installBundle",
+        "description": "Initiates the install process for the given bundle of extensions.",
+        "parameters": [
+          {
+            "name": "details",
+            "description": "An array of extension details to be installed.",
+            "type": "array",
+            "items": {
+              "type": "object",
+              "properties": {
+                "id": {
+                  "type": "string",
+                  "description": "The id of the extension to be installed.",
+                  "minLength": 32,
+                  "maxLength": 32
+                },
+                "manifest": {
+                  "type": "string",
+                  "description": "A string with the contents of the extension's manifest.json file. During the install process, the browser will check that the downloaded extension's manifest matches what was passed in here.",
+                  "minLength": 1
+                },
+                "localizedName": {
+                  "type": "string",
+                  "description": "A string to use instead of the raw value of the 'name' key from manifest.json."
+                }
+              }
+            }
+          },
+          {
+            "name": "callback",
+            "type": "function",
+            "description": "Called when the install process completes. Upon failures, chrome.extension.lastError will be set to 'user_canceled' or 'unknown_error'.",
+            "optional": "true",
+            "parameters": []
+          }
+        ]
+      },
+      {
         "name": "beginInstallWithManifest3",
         "description": "Initiates the install process for the given extension.",
         "parameters": [
diff --git a/chrome/common/extensions/docs/tabs.html b/chrome/common/extensions/docs/tabs.html
index 89e5f50b..6a680d0e 100644
--- a/chrome/common/extensions/docs/tabs.html
+++ b/chrome/common/extensions/docs/tabs.html
@@ -2712,7 +2712,7 @@
                     <span class="optional">optional</span>
                     <span id="typeTemplate">
                       <span>
-                        <span>string</span>
+                        <span>undefined</span>
                       </span>
                     </span>
                   )
diff --git a/chrome/test/data/extensions/api_test/webstore_private/bundle/app1.json b/chrome/test/data/extensions/api_test/webstore_private/bundle/app1.json
new file mode 100644
index 0000000..cc6c678
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/webstore_private/bundle/app1.json
@@ -0,0 +1,9 @@
+{
+  "name": "app.1",
+  "version": "1",
+  "app": {
+    "urls": [ "http://www.testapp.com" ],
+    "launch": { "web_url": "http://www.testapp.com" }
+  },
+  "permissions": [ "clipboardRead" ]
+}
diff --git a/chrome/test/data/extensions/api_test/webstore_private/bundle/begfmnajjkbjdgmffnjaojchoncnmngg/manifest.json b/chrome/test/data/extensions/api_test/webstore_private/bundle/app2.json
similarity index 85%
rename from chrome/test/data/extensions/api_test/webstore_private/bundle/begfmnajjkbjdgmffnjaojchoncnmngg/manifest.json
rename to chrome/test/data/extensions/api_test/webstore_private/bundle/app2.json
index 2c2ca61..4618194 100644
--- a/chrome/test/data/extensions/api_test/webstore_private/bundle/begfmnajjkbjdgmffnjaojchoncnmngg/manifest.json
+++ b/chrome/test/data/extensions/api_test/webstore_private/bundle/app2.json
@@ -1,5 +1,5 @@
 {
-  "name": "Bundle App 2",
+  "name": "app.2",
   "version": "1",
   "manifest_version": 2,
   "app": {
diff --git a/chrome/test/data/extensions/api_test/webstore_private/bundle/bmfoocgfinpmkmlbjhcbofejhkhlbchk/manifest.json b/chrome/test/data/extensions/api_test/webstore_private/bundle/extension1.json
similarity index 69%
rename from chrome/test/data/extensions/api_test/webstore_private/bundle/bmfoocgfinpmkmlbjhcbofejhkhlbchk/manifest.json
rename to chrome/test/data/extensions/api_test/webstore_private/bundle/extension1.json
index 9e4b6c7..2c84382 100644
--- a/chrome/test/data/extensions/api_test/webstore_private/bundle/bmfoocgfinpmkmlbjhcbofejhkhlbchk/manifest.json
+++ b/chrome/test/data/extensions/api_test/webstore_private/bundle/extension1.json
@@ -1,5 +1,5 @@
 {
-  "name": "Extension Bundle 1",
+  "name": "extension.1",
   "version": "1",
   "manifest_version": 2,
   "permissions": [ "tabs" ]
diff --git a/chrome/test/data/extensions/api_test/webstore_private/bundle/mpneghmdnmaolkljkipbhaienajcflfe/manifest.json b/chrome/test/data/extensions/api_test/webstore_private/bundle/extension2.json
similarity index 87%
rename from chrome/test/data/extensions/api_test/webstore_private/bundle/mpneghmdnmaolkljkipbhaienajcflfe/manifest.json
rename to chrome/test/data/extensions/api_test/webstore_private/bundle/extension2.json
index c4c5b8b..09ced87 100644
--- a/chrome/test/data/extensions/api_test/webstore_private/bundle/mpneghmdnmaolkljkipbhaienajcflfe/manifest.json
+++ b/chrome/test/data/extensions/api_test/webstore_private/bundle/extension2.json
@@ -1,5 +1,5 @@
 {
-  "name": "Extension Bundle 2",
+  "name": "extension.2",
   "version": "1",
   "manifest_version": 2,
   "permissions": ["management", "http://google.com" ],
diff --git a/chrome/test/data/extensions/api_test/webstore_private/bundle/mpneghmdnmaolkljkipbhaienajcflfe/content_script.js b/chrome/test/data/extensions/api_test/webstore_private/bundle/mpneghmdnmaolkljkipbhaienajcflfe/content_script.js
deleted file mode 100644
index 1ea18e2..0000000
--- a/chrome/test/data/extensions/api_test/webstore_private/bundle/mpneghmdnmaolkljkipbhaienajcflfe/content_script.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright (c) 2011 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.
-
-var x = 0;
-x + 1;
diff --git a/chrome/test/data/extensions/api_test/webstore_private/bundle/pkapffpjmiilhlhbibjhamlmdhfneidj.pem b/chrome/test/data/extensions/api_test/webstore_private/bundle/pkapffpjmiilhlhbibjhamlmdhfneidj.pem
new file mode 100644
index 0000000..9e0d783
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/webstore_private/bundle/pkapffpjmiilhlhbibjhamlmdhfneidj.pem
@@ -0,0 +1,15 @@
+-----BEGIN PRIVATE KEY-----
+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBANnVMj1o/ie45vXd3
+IhQEy+UNJ/CeZME9TJDMFPiOzrGqeMI6Ku9SdNo5EhS+EwA51/Zgfjt5lZl1BIa+B
+2wNammaJg2V2Yl9zfOj/ye5kyCr0z5QRcWwIG/YGhwVl9V+Hls+KHSV7ZP5ivhyGn
+Uts3ft/dL1YSF5pRgEK78YeBbAgMBAAECgYBxZ7vTGsEOXwXmxI1WbhG++HJ5Jd7z
+OmaIt1AGq8XYMKsrZmzzVAWGSZpnSMK5ltLeJLe0p+391t+UWXQIyL72UJ9Nm8e8h
+AFDwYC7Lyjlf2NDqi8/ylL0RfWM4y0EGzsVGprymWkQIdFZd932p0LnKNFPSsI1p4
+hqBpKyiiPd6QJBAPBfaOcJeeRwhWt5XN6mdl37K3BSdpXAG9ph1kwElTxN2QBk22a
+FxcaPlPJe0yKql5AUVDuNlXnwTSPanSFPQc8CQQDn/qYN1E1Vay1T82vtcp7SDPug
+J7BJRw7nVEfPaiCnngD8CX9706szZL7yqHf51WvF7EBrTjIgd7X/LsLGFVe1AkBTN
+NO9VhxppUGqCGLLd9f1hGJvCTyfbda2a7OgsN1v+Iqrhj4kaR4jM8SdeZGgqGi6qS
+7XRpV9ll89kAlgZG0lAkBxOx3rNArGvTfzeKTd0QrpdMK/qX9mVJNWnxEpkB/+D6V
+lXnFli6tMu0hjgYyFWQBwKt5KQXE/3Y3rzfPs4G/dAkAKjwU3x8/1MLD6mBZwp60s
+M3xunjgzglYKg55Grn3+C+S09TEr/OEMqIZPEQ5f4pAnIlWqOyQTDNBFutM3c6/j
+-----END PRIVATE KEY-----
diff --git a/chrome/test/data/extensions/api_test/webstore_private/common.js b/chrome/test/data/extensions/api_test/webstore_private/common.js
index 17025a42b..bc0805a 100644
--- a/chrome/test/data/extensions/api_test/webstore_private/common.js
+++ b/chrome/test/data/extensions/api_test/webstore_private/common.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// 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.
 
@@ -9,6 +9,7 @@
 var appId = "iladmdjkfniedhfhcfoefgojhgaiaccc";
 
 var assertEq = chrome.test.assertEq;
+var assertFalse = chrome.test.assertFalse;
 var assertNoLastError = chrome.test.assertNoLastError;
 var assertTrue = chrome.test.assertTrue;
 var callbackFail = chrome.test.callbackFail;
@@ -58,13 +59,8 @@
   img.src = "extension/icon.png";
 }
 
-var cachedManifest = null;
-
 // This returns the string contents of the extension's manifest file.
 function getManifest(alternativePath) {
-  if (cachedManifest)
-    return cachedManifest;
-
   // Do a synchronous XHR to get the manifest.
   var xhr = new XMLHttpRequest();
   xhr.open("GET",
diff --git a/chrome/test/data/extensions/api_test/webstore_private/install_bundle.html b/chrome/test/data/extensions/api_test/webstore_private/install_bundle.html
new file mode 100644
index 0000000..d9859dd
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/webstore_private/install_bundle.html
@@ -0,0 +1,45 @@
+<!--
+ * 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.
+-->
+<script src="common.js"></script>
+<script>
+
+var bundleItems = [
+  {
+    id: 'begfmnajjkbjdgmffnjaojchoncnmngg',
+    manifest: getManifest('bundle/app1.json'),
+    localizedName: 'app.1'
+  },
+  {
+    id: 'mpneghmdnmaolkljkipbhaienajcflfe',
+    manifest: getManifest('bundle/app2.json'),
+    localizedName: 'app.2'
+  },
+  {
+    id: 'bmfoocgfinpmkmlbjhcbofejhkhlbchk',
+    manifest: getManifest('bundle/extension1.json'),
+    localizedName: 'extension.1'
+  },
+  {
+    id: 'pkapffpjmiilhlhbibjhamlmdhfneidj',
+    manifest: getManifest('bundle/extension2.json'),
+    localizedName: 'extension.2'
+  }
+];
+
+runTests([
+  function successfulInstall() {
+    chrome.webstorePrivate.installBundle(
+        bundleItems, callbackPass(function() {
+      bundleItems.forEach(function(item) {
+        checkItemInstalled(
+            item.id,
+            callbackPass(function(result) { assertTrue(result); }));
+      });
+    }));
+  }
+]);
+
+</script>
diff --git a/chrome/test/data/extensions/api_test/webstore_private/install_bundle_cancel.html b/chrome/test/data/extensions/api_test/webstore_private/install_bundle_cancel.html
new file mode 100644
index 0000000..4453f92
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/webstore_private/install_bundle_cancel.html
@@ -0,0 +1,39 @@
+<!--
+ * 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.
+-->
+<script src="common.js"></script>
+<script>
+
+var bundleItems = [
+  {
+    id: 'begfmnajjkbjdgmffnjaojchoncnmngg',
+    manifest: getManifest('bundle/app1.json'),
+    localizedName: 'app.1'
+  },
+  {
+    id: 'mpneghmdnmaolkljkipbhaienajcflfe',
+    manifest: getManifest('bundle/app2.json'),
+    localizedName: 'app.2'
+  },
+  {
+    id: 'bmfoocgfinpmkmlbjhcbofejhkhlbchk',
+    manifest: getManifest('bundle/extension1.json'),
+    localizedName: 'extension.1'
+  },
+  {
+    id: 'pkapffpjmiilhlhbibjhamlmdhfneidj',
+    manifest: getManifest('bundle/extension2.json'),
+    localizedName: 'extension.2'
+  }
+];
+
+runTests([
+  function installCanceled() {
+    chrome.webstorePrivate.installBundle(
+        bundleItems, callbackFail("user_canceled"));
+  }
+]);
+
+</script>
diff --git a/chrome/test/data/extensions/api_test/webstore_private/install_bundle_invalid.html b/chrome/test/data/extensions/api_test/webstore_private/install_bundle_invalid.html
new file mode 100644
index 0000000..a144718c
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/webstore_private/install_bundle_invalid.html
@@ -0,0 +1,73 @@
+<!--
+ * 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.
+-->
+<script src="common.js"></script>
+<script>
+
+var bundleItems = [
+  {
+    id: 'begfmnajjkbjdgmffnjaojchoncnmngg',
+    manifest: getManifest('bundle/app1.json'),
+    localizedName: 'app.1'
+  },
+  {
+    id: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',  // Invalid CRX.
+    manifest: getManifest('bundle/app2.json'),
+    localizedName: 'app.2'
+  },
+  {
+    id: 'bmfoocgfinpmkmlbjhcbofejhkhlbchk',  // Wrong manifest.
+    manifest: getManifest('bundle/extension2.json'),
+    localizedName: 'extension.1'
+  },
+  {
+    id: 'pkapffpjmiilhlhbibjhamlmdhfneidj',
+    manifest: getManifest('bundle/extension2.json'),
+    localizedName: 'extension.2'
+  }
+];
+
+var installed = [
+  'begfmnajjkbjdgmffnjaojchoncnmngg',
+  'pkapffpjmiilhlhbibjhamlmdhfneidj'
+];
+
+var failed = [
+  'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+  'bmfoocgfinpmkmlbjhcbofejhkhlbchk'
+];
+
+runTests([
+  function successfulInstall() {
+    chrome.webstorePrivate.installBundle(
+        bundleItems, callbackPass(function() {
+      installed.forEach(function(id) {
+        checkItemInstalled(
+            id,
+            callbackPass(function(result) { assertTrue(result); }));
+      });
+      failed.forEach(function(id) {
+        checkItemInstalled(
+            id,
+            callbackPass(function(result) { assertFalse(result); }));
+      });
+    }));
+  },
+
+  function allItemsFail() {
+    chrome.webstorePrivate.installBundle(
+        [bundleItems[2]], callbackPass(function() {
+      checkItemInstalled(
+           bundleItems[2].id,
+           callbackPass(function(result) { assertFalse(result); }));
+    }));
+  },
+
+  function noItems() {
+    chrome.webstorePrivate.installBundle([], callbackFail("unknown_error"));
+  }
+]);
+
+</script>
diff --git a/chrome/test/data/extensions/api_test/webstore_private/silently_install.html b/chrome/test/data/extensions/api_test/webstore_private/silently_install.html
index 602cdb8..0f39777 100644
--- a/chrome/test/data/extensions/api_test/webstore_private/silently_install.html
+++ b/chrome/test/data/extensions/api_test/webstore_private/silently_install.html
@@ -1,45 +1,24 @@
+<!--
+ * 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.
+-->
 <script src="common.js"></script>
 <script>
 
 var extension1 = {
   'id': 'bmfoocgfinpmkmlbjhcbofejhkhlbchk',
-  'manifest':
-    '{\
-      "name": "Extension Bundle 1",\
-      "version": "1",\
-      "manifest_version": 2,\
-      "permissions": [ "tabs" ]\
-     }'
+  'manifest': getManifest('bundle/extension1.json')
 };
 
 var extension2 = {
   'id': 'mpneghmdnmaolkljkipbhaienajcflfe',
-  'manifest':
-    '{\
-      "name": "Extension Bundle 2",\
-      "version": "1",\
-      "manifest_version": 2,\
-      "permissions": ["management", "http://google.com" ],\
-      "content_script": [{\
-        "matches": [ "http://www.example.com/*" ],\
-        "js": [ "content_script.js" ],\
-        "run_at": "document_start"\
-       }]\
-    }'
+  'manifest': getManifest('bundle/extension2.json')
 };
 
 var extension3 = {
   'id': 'begfmnajjkbjdgmffnjaojchoncnmngg',
-  'manifest':
-    '{\
-      "name": "Bundle App 2",\
-      "version": "1",\
-      "manifest_version": 2,\
-      "app": {\
-        "urls": [ "http://www.testapp2.com" ],\
-        "launch": { "web_url": "http://www.testapp2.com" }\
-      }\
-    }'
+  'manifest': getManifest('bundle/app2.json')
 };
 
 runTests([