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([