Change the web store private install API to accept a localized extension name.
In our initial design of the beginInstallWithManifest function, we forgot that
some extensions have a name which needs token substitution for i18n/l10n. This
change renames beginInstallWithManifest to beginInstallWithManifest2, and
modifies the signature to take optional localizedName and locale parameters.
I also refactored how we compare the passed in manifest used in the
pre-download confirmation dialog - we now keep a copy of the source from the
.crx file to compare against, since i18n substututions can happen on the
manifest we use to construct the Extension object.
BUG=75821
TEST=Covered by browser tests for now; will require web store server-side
changes before it can be manually tested.
Review URL: http://codereview.chromium.org/6992047
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@86780 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/extensions/crx_installer.cc b/chrome/browser/extensions/crx_installer.cc
index 8f13497c0..d289c1c 100644
--- a/chrome/browser/extensions/crx_installer.cc
+++ b/chrome/browser/extensions/crx_installer.cc
@@ -41,63 +41,65 @@
namespace {
-struct WhitelistedInstallData {
- WhitelistedInstallData() {}
+struct Whitelist {
+ Whitelist() {}
std::set<std::string> ids;
- std::map<std::string, linked_ptr<DictionaryValue> > manifests;
+ std::map<std::string, linked_ptr<CrxInstaller::WhitelistEntry> > entries;
};
-static base::LazyInstance<WhitelistedInstallData>
+static base::LazyInstance<Whitelist>
g_whitelisted_install_data(base::LINKER_INITIALIZED);
} // namespace
// static
void CrxInstaller::SetWhitelistedInstallId(const std::string& id) {
- DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+ CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
g_whitelisted_install_data.Get().ids.insert(id);
}
// static
-void CrxInstaller::SetWhitelistedManifest(const std::string& id,
- DictionaryValue* parsed_manifest) {
+void CrxInstaller::SetWhitelistEntry(const std::string& id,
+ CrxInstaller::WhitelistEntry* entry) {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
- WhitelistedInstallData& data = g_whitelisted_install_data.Get();
- data.manifests[id] = linked_ptr<DictionaryValue>(parsed_manifest);
+ Whitelist& data = g_whitelisted_install_data.Get();
+ data.entries[id] = linked_ptr<CrxInstaller::WhitelistEntry>(entry);
}
// static
-const DictionaryValue* CrxInstaller::GetWhitelistedManifest(
+const CrxInstaller::WhitelistEntry* CrxInstaller::GetWhitelistEntry(
const std::string& id) {
- WhitelistedInstallData& data = g_whitelisted_install_data.Get();
- if (ContainsKey(data.manifests, id))
- return data.manifests[id].get();
+ CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+ Whitelist& data = g_whitelisted_install_data.Get();
+ if (ContainsKey(data.entries, id))
+ return data.entries[id].get();
else
return NULL;
}
// static
-DictionaryValue* CrxInstaller::RemoveWhitelistedManifest(
+CrxInstaller::WhitelistEntry* CrxInstaller::RemoveWhitelistEntry(
const std::string& id) {
- WhitelistedInstallData& data = g_whitelisted_install_data.Get();
- if (ContainsKey(data.manifests, id)) {
- DictionaryValue* manifest = data.manifests[id].release();
- data.manifests.erase(id);
- return manifest;
+ CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+ Whitelist& data = g_whitelisted_install_data.Get();
+ if (ContainsKey(data.entries, id)) {
+ CrxInstaller::WhitelistEntry* entry = data.entries[id].release();
+ data.entries.erase(id);
+ return entry;
}
return NULL;
}
// static
bool CrxInstaller::IsIdWhitelisted(const std::string& id) {
- DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+ CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
std::set<std::string>& ids = g_whitelisted_install_data.Get().ids;
return ContainsKey(ids, id);
}
// static
bool CrxInstaller::ClearWhitelistedInstallId(const std::string& id) {
- DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+ CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
std::set<std::string>& ids = g_whitelisted_install_data.Get().ids;
if (ContainsKey(ids, id)) {
ids.erase(id);
@@ -185,7 +187,7 @@
return;
}
- OnUnpackSuccess(extension->path(), extension->path(), extension);
+ OnUnpackSuccess(extension->path(), extension->path(), NULL, extension);
}
void CrxInstaller::InstallWebApp(const WebApplicationInfo& web_app) {
@@ -209,7 +211,7 @@
// TODO(aa): conversion data gets lost here :(
- OnUnpackSuccess(extension->path(), extension->path(), extension);
+ OnUnpackSuccess(extension->path(), extension->path(), NULL, extension);
}
bool CrxInstaller::AllowInstall(const Extension* extension,
@@ -311,6 +313,7 @@
void CrxInstaller::OnUnpackSuccess(const FilePath& temp_dir,
const FilePath& extension_dir,
+ const DictionaryValue* original_manifest,
const Extension* extension) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
@@ -326,6 +329,9 @@
extension_ = extension;
temp_dir_ = temp_dir;
+ if (original_manifest)
+ original_manifest_.reset(original_manifest->DeepCopy());
+
// We don't have to delete the unpack dir explicity since it is a child of
// the temp dir.
unpacked_extension_root_ = extension_dir;
@@ -347,17 +353,6 @@
NOTREACHED();
}
-// Helper method to let us compare a whitelisted manifest with the actual
-// downloaded extension's manifest, but ignoring the kPublicKey since the
-// whitelisted manifest doesn't have that value.
-static bool EqualsIgnoringPublicKey(
- const DictionaryValue& extension_manifest,
- const DictionaryValue& whitelisted_manifest) {
- scoped_ptr<DictionaryValue> manifest_copy(extension_manifest.DeepCopy());
- manifest_copy->Remove(extension_manifest_keys::kPublicKey, NULL);
- return manifest_copy->Equals(&whitelisted_manifest);
-}
-
void CrxInstaller::ConfirmInstall() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (!frontend_weak_.get())
@@ -398,12 +393,11 @@
bool whitelisted = ClearWhitelistedInstallId(extension_->id()) &&
extension_->plugins().empty() && is_gallery_install_;
- // Now check if it's whitelisted by manifest.
- scoped_ptr<DictionaryValue> whitelisted_manifest(
- RemoveWhitelistedManifest(extension_->id()));
- if (is_gallery_install_ && whitelisted_manifest.get()) {
- if (!EqualsIgnoringPublicKey(*extension_->manifest_value(),
- *whitelisted_manifest)) {
+ // Now check if there's a WhitelistEntry.
+ scoped_ptr<CrxInstaller::WhitelistEntry> entry(
+ RemoveWhitelistEntry(extension_->id()));
+ if (is_gallery_install_ && entry.get() && original_manifest_.get()) {
+ if (!(original_manifest_->Equals(entry->parsed_manifest.get()))) {
ReportFailureFromUIThread(
l10n_util::GetStringUTF8(IDS_EXTENSION_MANIFEST_INVALID));
return;
diff --git a/chrome/browser/extensions/crx_installer.h b/chrome/browser/extensions/crx_installer.h
index 0cc9dc7..937fa4f 100644
--- a/chrome/browser/extensions/crx_installer.h
+++ b/chrome/browser/extensions/crx_installer.h
@@ -55,21 +55,25 @@
// crbug.com/54916
static void SetWhitelistedInstallId(const std::string& id);
+ struct WhitelistEntry {
+ scoped_ptr<DictionaryValue> parsed_manifest;
+ std::string localized_name;
+ };
+
// Exempt the next extension install with |id| from displaying a confirmation
// prompt, since the user already agreed to the install via
// beginInstallWithManifest. We require that the extension manifest matches
- // |parsed_manifest| which is what was used to prompt with. Ownership of
- // |parsed_manifest| is transferred here.
- static void SetWhitelistedManifest(const std::string& id,
- DictionaryValue* parsed_manifest);
+ // the manifest in |entry|, which is what was used to prompt with. Ownership
+ // of |entry| is transferred here.
+ static void SetWhitelistEntry(const std::string& id, WhitelistEntry* entry);
- // Returns the previously stored manifest from a call to
- // SetWhitelistedManifest.
- static const DictionaryValue* GetWhitelistedManifest(const std::string& id);
+ // Returns the previously stored manifest from a call to SetWhitelistEntry.
+ static const CrxInstaller::WhitelistEntry* GetWhitelistEntry(
+ const std::string& id);
- // Removes any whitelisted manifest for |id| and returns it. The caller owns
+ // Removes any whitelist data for |id| and returns it. The caller owns
// the return value and is responsible for deleting it.
- static DictionaryValue* RemoveWhitelistedManifest(const std::string& id);
+ static WhitelistEntry* RemoveWhitelistEntry(const std::string& id);
// Returns whether |id| is whitelisted - only call this on the UI thread.
static bool IsIdWhitelisted(const std::string& id);
@@ -159,6 +163,7 @@
virtual void OnUnpackFailure(const std::string& error_message);
virtual void OnUnpackSuccess(const FilePath& temp_dir,
const FilePath& extension_dir,
+ const DictionaryValue* original_manifest,
const Extension* extension);
// Returns true if we can skip confirmation because the install was
@@ -225,6 +230,10 @@
// ExtensionService on success, or delete it on failure.
scoped_refptr<const Extension> extension_;
+ // A parsed copy of the unmodified original manifest, before any
+ // transformations like localization have taken place.
+ scoped_ptr<DictionaryValue> original_manifest_;
+
// If non-empty, contains the current version of the extension we're
// installing (for upgrades).
std::string current_version_;
diff --git a/chrome/browser/extensions/extension_webstore_private_api.cc b/chrome/browser/extensions/extension_webstore_private_api.cc
index 1dead6c..a38d867e 100644
--- a/chrome/browser/extensions/extension_webstore_private_api.cc
+++ b/chrome/browser/extensions/extension_webstore_private_api.cc
@@ -21,6 +21,7 @@
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/extensions/extension_error_utils.h"
+#include "chrome/common/extensions/extension_l10n_util.h"
#include "chrome/common/net/gaia/gaia_constants.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/common/notification_details.h"
@@ -33,8 +34,13 @@
namespace {
+const char kIconDataKey[] = "iconData";
+const char kIdKey[] = "id";
+const char kLocalizedNameKey[] = "localizedName";
const char kLoginKey[] = "login";
+const char kManifestKey[] = "manifest";
const char kTokenKey[] = "token";
+
const char kImageDecodeError[] = "Image decode failed";
const char kInvalidIdError[] = "Invalid id";
const char kInvalidManifestError[] = "Invalid manifest";
@@ -283,15 +289,25 @@
return false;
}
- EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &id_));
+ DictionaryValue* details = NULL;
+ EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &details));
+ CHECK(details);
+
+ EXTENSION_FUNCTION_VALIDATE(details->GetString(kIdKey, &id_));
if (!Extension::IdIsValid(id_)) {
SetResult(INVALID_ID);
error_ = kInvalidIdError;
return false;
}
- EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &icon_data_));
- EXTENSION_FUNCTION_VALIDATE(args_->GetString(2, &manifest_));
+ EXTENSION_FUNCTION_VALIDATE(details->GetString(kManifestKey, &manifest_));
+
+ if (details->HasKey(kIconDataKey))
+ EXTENSION_FUNCTION_VALIDATE(details->GetString(kIconDataKey, &icon_data_));
+
+ if (details->HasKey(kLocalizedNameKey))
+ EXTENSION_FUNCTION_VALIDATE(details->GetString(kLocalizedNameKey,
+ &localized_name_));
scoped_refptr<SafeBeginInstallHelper> helper =
new SafeBeginInstallHelper(this, icon_data_, manifest_);
@@ -354,19 +370,29 @@
icon_ = icon;
parsed_manifest_.reset(parsed_manifest);
+ // If we were passed a localized name to use in the dialog, create a copy
+ // of the original manifest and replace the name in it.
+ scoped_ptr<DictionaryValue> localized_manifest;
+ if (!localized_name_.empty()) {
+ localized_manifest.reset(parsed_manifest->DeepCopy());
+ localized_manifest->SetString(extension_manifest_keys::kName,
+ localized_name_);
+ }
+
// Create a dummy extension and show the extension install confirmation
// dialog.
std::string init_errors;
dummy_extension_ = Extension::Create(
FilePath(),
Extension::INTERNAL,
- *static_cast<DictionaryValue*>(parsed_manifest_.get()),
+ localized_manifest.get() ? *localized_manifest.get() : *parsed_manifest,
Extension::NO_FLAGS,
&init_errors);
if (!dummy_extension_.get()) {
OnParseFailure(MANIFEST_ERROR, std::string(kInvalidManifestError));
return;
}
+
if (icon_.empty())
icon_ = Extension::GetDefaultIcon(dummy_extension_->is_app());
@@ -401,7 +427,10 @@
}
void BeginInstallWithManifestFunction::InstallUIProceed() {
- CrxInstaller::SetWhitelistedManifest(id_, parsed_manifest_.release());
+ CrxInstaller::WhitelistEntry* entry = new CrxInstaller::WhitelistEntry;
+ entry->parsed_manifest.reset(parsed_manifest_.release());
+ entry->localized_name = localized_name_;
+ CrxInstaller::SetWhitelistEntry(id_, entry);
SetResult(ERROR_NONE);
SendResponse(true);
@@ -430,7 +459,7 @@
}
if (!CrxInstaller::IsIdWhitelisted(id) &&
- !CrxInstaller::GetWhitelistedManifest(id)) {
+ !CrxInstaller::GetWhitelistEntry(id)) {
error_ = ExtensionErrorUtils::FormatErrorMessage(
kNoPreviousBeginInstallError, id);
return false;
diff --git a/chrome/browser/extensions/extension_webstore_private_api.h b/chrome/browser/extensions/extension_webstore_private_api.h
index 4acc7f6..3c909e1 100644
--- a/chrome/browser/extensions/extension_webstore_private_api.h
+++ b/chrome/browser/extensions/extension_webstore_private_api.h
@@ -106,6 +106,7 @@
std::string id_;
std::string manifest_;
std::string icon_data_;
+ std::string localized_name_;
// The results of parsing manifest_ and icon_data_ go into these two.
scoped_ptr<DictionaryValue> parsed_manifest_;
@@ -114,7 +115,7 @@
// A dummy Extension object we create for the purposes of using
// ExtensionInstallUI to prompt for confirmation of the install.
scoped_refptr<Extension> dummy_extension_;
- DECLARE_EXTENSION_FUNCTION_NAME("webstorePrivate.beginInstallWithManifest");
+ DECLARE_EXTENSION_FUNCTION_NAME("webstorePrivate.beginInstallWithManifest2");
};
class CompleteInstallFunction : public SyncExtensionFunction {
diff --git a/chrome/browser/extensions/extension_webstore_private_apitest.cc b/chrome/browser/extensions/extension_webstore_private_apitest.cc
index cdd465e..2638beb 100644
--- a/chrome/browser/extensions/extension_webstore_private_apitest.cc
+++ b/chrome/browser/extensions/extension_webstore_private_apitest.cc
@@ -51,9 +51,10 @@
return url.ReplaceComponents(replace_host);
}
- // Navigates to |page| and runs the Extension API test there.
- bool RunInstallTest(const std::string& page) {
- GURL crx_url = GetTestServerURL("extension.crx");
+ // Navigates to |page| and runs the Extension API test there. Any downloads
+ // of extensions will return the contents of |crx_file|.
+ bool RunInstallTest(const std::string& page, const std::string& crx_file) {
+ GURL crx_url = GetTestServerURL(crx_file);
CommandLine::ForCurrentProcess()->AppendSwitchASCII(
switches::kAppsGalleryUpdateURL, crx_url.spec());
@@ -69,18 +70,23 @@
// TODO(asargent) - flaky; see crbug.com/80606.
// Test cases where the user accepts the install confirmation dialog.
IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, FLAKY_InstallAccepted) {
- ASSERT_TRUE(RunInstallTest("accepted.html"));
+ ASSERT_TRUE(RunInstallTest("accepted.html", "extension.crx"));
+}
+
+// Tests passing a localized name.
+IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, InstallLocalized) {
+ ASSERT_TRUE(RunInstallTest("localized.html", "localized_extension.crx"));
}
// Now test the case where the user cancels the confirmation dialog.
IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, InstallCancelled) {
BeginInstallWithManifestFunction::SetAutoConfirmForTests(false);
- ASSERT_TRUE(RunInstallTest("cancelled.html"));
+ ASSERT_TRUE(RunInstallTest("cancelled.html", "extension.crx"));
}
IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest, InstallNoGesture) {
BeginInstallFunction::SetIgnoreUserGestureForTests(false);
- ASSERT_TRUE(RunInstallTest("no_user_gesture.html"));
+ ASSERT_TRUE(RunInstallTest("no_user_gesture.html", "extension.crx"));
}
IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateApiTest,
@@ -88,7 +94,7 @@
ui_test_utils::WindowedNotificationObserver observer(
NotificationType::EXTENSION_INSTALL_ERROR,
NotificationService::AllSources());
- ASSERT_TRUE(RunInstallTest("incorrect_manifest1.html"));
+ ASSERT_TRUE(RunInstallTest("incorrect_manifest1.html", "extension.crx"));
observer.Wait();
}
@@ -97,6 +103,6 @@
ui_test_utils::WindowedNotificationObserver observer(
NotificationType::EXTENSION_INSTALL_ERROR,
NotificationService::AllSources());
- ASSERT_TRUE(RunInstallTest("incorrect_manifest2.html"));
+ ASSERT_TRUE(RunInstallTest("incorrect_manifest2.html", "extension.crx"));
observer.Wait();
}
diff --git a/chrome/browser/extensions/sandboxed_extension_unpacker.cc b/chrome/browser/extensions/sandboxed_extension_unpacker.cc
index 8e31ddf..b8eb88c 100644
--- a/chrome/browser/extensions/sandboxed_extension_unpacker.cc
+++ b/chrome/browser/extensions/sandboxed_extension_unpacker.cc
@@ -284,7 +284,7 @@
if (!RewriteCatalogFiles())
return;
- ReportSuccess();
+ ReportSuccess(manifest);
}
void SandboxedExtensionUnpacker::OnUnpackExtensionFailed(
@@ -480,14 +480,18 @@
client_->OnUnpackFailure(error);
}
-void SandboxedExtensionUnpacker::ReportSuccess() {
+void SandboxedExtensionUnpacker::ReportSuccess(
+ const DictionaryValue& original_manifest) {
UMA_HISTOGRAM_COUNTS("Extensions.SandboxUnpackSuccess", 1);
RecordSuccessfulUnpackTimeHistograms(
crx_path_, base::TimeTicks::Now() - unpack_start_time_);
// Client takes ownership of temporary directory and extension.
- client_->OnUnpackSuccess(temp_dir_.Take(), extension_root_, extension_);
+ client_->OnUnpackSuccess(temp_dir_.Take(),
+ extension_root_,
+ &original_manifest,
+ extension_);
extension_ = NULL;
}
diff --git a/chrome/browser/extensions/sandboxed_extension_unpacker.h b/chrome/browser/extensions/sandboxed_extension_unpacker.h
index 7294b39..9c10886 100644
--- a/chrome/browser/extensions/sandboxed_extension_unpacker.h
+++ b/chrome/browser/extensions/sandboxed_extension_unpacker.h
@@ -25,10 +25,14 @@
//
// extension_root - The path to the extension root inside of temp_dir.
//
+ // original_manifest - The parsed but unmodified version of the manifest,
+ // with no modifications such as localization, etc.
+ //
// extension - The extension that was unpacked. The client is responsible
// for deleting this memory.
virtual void OnUnpackSuccess(const FilePath& temp_dir,
const FilePath& extension_root,
+ const DictionaryValue* original_manifest,
const Extension* extension) = 0;
virtual void OnUnpackFailure(const std::string& error) = 0;
@@ -189,7 +193,7 @@
virtual void OnProcessCrashed(int exit_code);
void ReportFailure(FailureReason reason, const std::string& message);
- void ReportSuccess();
+ void ReportSuccess(const DictionaryValue& original_manifest);
// Overwrites original manifest with safe result from utility process.
// Returns NULL on error. Caller owns the returned object.
diff --git a/chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc b/chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc
index bc31c45..165f571 100644
--- a/chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc
+++ b/chrome/browser/extensions/sandboxed_extension_unpacker_unittest.cc
@@ -27,6 +27,7 @@
void OnUnpackSuccess(const FilePath& temp_dir,
const FilePath& extension_root,
+ const DictionaryValue* original_manifest,
const Extension* extension) {
// Don't delete temp_dir here, we need to do some post op checking.
}
@@ -38,16 +39,17 @@
public:
virtual ~MockSandboxedExtensionUnpackerClient() {}
- MOCK_METHOD3(OnUnpackSuccess,
+ MOCK_METHOD4(OnUnpackSuccess,
void(const FilePath& temp_dir,
const FilePath& extension_root,
+ const DictionaryValue* original_manifest,
const Extension* extension));
MOCK_METHOD1(OnUnpackFailure,
void(const std::string& error));
void DelegateToFake() {
- ON_CALL(*this, OnUnpackSuccess(_, _, _))
+ ON_CALL(*this, OnUnpackSuccess(_, _, _, _))
.WillByDefault(Invoke(::OnUnpackSuccess));
}
};
@@ -157,7 +159,7 @@
};
TEST_F(SandboxedExtensionUnpackerTest, NoCatalogsSuccess) {
- EXPECT_CALL(*client_, OnUnpackSuccess(_, _, _));
+ EXPECT_CALL(*client_, OnUnpackSuccess(_, _, _, _));
EXPECT_CALL(*client_, OnUnpackFailure(_)).Times(0);
SetupUnpacker("no_l10n.crx");
@@ -179,7 +181,7 @@
}
TEST_F(SandboxedExtensionUnpackerTest, WithCatalogsSuccess) {
- EXPECT_CALL(*client_, OnUnpackSuccess(_, _, _));
+ EXPECT_CALL(*client_, OnUnpackSuccess(_, _, _, _));
EXPECT_CALL(*client_, OnUnpackFailure(_)).Times(0);
SetupUnpacker("good_l10n.crx");