summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbartfab@chromium.org <bartfab@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-10-09 02:49:58 +0000
committerbartfab@chromium.org <bartfab@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-10-09 02:49:58 +0000
commit1a643611aac9777e310859b1b535796f7ade8424 (patch)
treea7d128ef5fe760d44bde3af7000e57adc6d413d3
parent5906629ae1144b74c0a89700304f8a30d4461ef8 (diff)
downloadchromium_src-1a643611aac9777e310859b1b535796f7ade8424.zip
chromium_src-1a643611aac9777e310859b1b535796f7ade8424.tar.gz
chromium_src-1a643611aac9777e310859b1b535796f7ade8424.tar.bz2
Allow explicitly whitelisted apps/extensions in public sessions
This CL adds an extension management policy provider that allows explicitly whitelisted apps/extensions to be installed in public sessions. Right now, QuickOffice and all hosted apps are whitelisted. BUG=296868 TEST=New browser and unit tests Committed: https://src.chromium.org/viewvc/chrome?view=rev&revision=226494 Review URL: https://codereview.chromium.org/24261010 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@227641 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/app/generated_resources.grd3
-rw-r--r--chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.cc79
-rw-r--r--chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.h38
-rw-r--r--chrome/browser/chromeos/extensions/device_local_account_management_policy_provider_unittest.cc124
-rw-r--r--chrome/browser/chromeos/login/user_manager_impl.cc12
-rw-r--r--chrome/browser/chromeos/policy/device_local_account.cc39
-rw-r--r--chrome/browser/chromeos/policy/device_local_account.h7
-rw-r--r--chrome/browser/chromeos/policy/device_local_account_browsertest.cc220
-rw-r--r--chrome/browser/chromeos/policy/device_local_account_policy_service_unittest.cc10
-rw-r--r--chrome/browser/chromeos/policy/device_local_account_policy_store.cc11
-rw-r--r--chrome/browser/extensions/extension_system.cc28
-rw-r--r--chrome/browser/extensions/extension_system.h11
-rw-r--r--chrome/browser/policy/browser_policy_connector.cc2
-rw-r--r--chrome/chrome_browser_chromeos.gypi2
-rw-r--r--chrome/chrome_tests_unit.gypi1
-rw-r--r--content/public/test/test_utils.cc23
-rw-r--r--content/public/test/test_utils.h16
-rw-r--r--net/base/url_util.cc81
-rw-r--r--net/base/url_util.h26
-rw-r--r--net/base/url_util_unittest.cc51
20 files changed, 721 insertions, 63 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 7aea90f..ad4b6bb 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -4403,6 +4403,9 @@ Make sure you do not expose any sensitive information.
<message name="IDS_EXTENSION_CANT_INSTALL_POLICY_BLOCKED" desc="Error message when a user tries to install an extension that is blocked by administrator policy.">
<ph name="EXTENSION_NAME">$1<ex>Google Talk</ex></ph> (extension ID "<ph name="EXTENSION_ID">$2<ex>nckgahadagoaajjgafhacjanaoiihapd</ex></ph>") is blocked by the administrator.
</message>
+ <message name="IDS_EXTENSION_CANT_INSTALL_IN_DEVICE_LOCAL_ACCOUNT" desc="Error message when a user tries to install or the administrator tries to force-install through policy an extension that is not allowed in a device-local account.">
+ <ph name="EXTENSION_NAME">$1<ex>Google Talk</ex></ph> (extension ID "<ph name="EXTENSION_ID">$2<ex>nckgahadagoaajjgafhacjanaoiihapd</ex></ph>") is not allowed in this type of session.
+ </message>
<message name="IDS_EXTENSION_CANT_MODIFY_POLICY_REQUIRED" desc="Error message when a user tries to remove or change an extension that is required by administrator policy.">
The administrator of this machine requires <ph name="EXTENSION_NAME">$1<ex>Google Talk</ex></ph> to be installed. It cannot be removed or modified.
</message>
diff --git a/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.cc b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.cc
new file mode 100644
index 0000000..fd25e9c
--- /dev/null
+++ b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.cc
@@ -0,0 +1,79 @@
+// Copyright 2013 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/chromeos/extensions/device_local_account_management_policy_provider.h"
+
+#include <string>
+
+#include "base/logging.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/common/extensions/extension.h"
+#include "extensions/common/manifest.h"
+#include "grit/generated_resources.h"
+#include "ui/base/l10n/l10n_util.h"
+
+namespace chromeos {
+
+namespace {
+
+// Apps/extensions explicitly whitelisted for use in device-local accounts.
+const char* kDeviceLocalAccountWhitelist[] = {
+ "bpmcpldpdmajfigpchkicefoigmkfalc", // QuickOffice
+};
+
+} // namespace
+
+DeviceLocalAccountManagementPolicyProvider::
+ DeviceLocalAccountManagementPolicyProvider(
+ policy::DeviceLocalAccount::Type account_type)
+ : account_type_(account_type) {
+}
+
+DeviceLocalAccountManagementPolicyProvider::
+ ~DeviceLocalAccountManagementPolicyProvider() {
+}
+
+std::string DeviceLocalAccountManagementPolicyProvider::
+ GetDebugPolicyProviderName() const {
+#if defined(NDEBUG)
+ NOTREACHED();
+ return std::string();
+#else
+ return "whitelist for device-local accounts";
+#endif
+}
+
+bool DeviceLocalAccountManagementPolicyProvider::UserMayLoad(
+ const extensions::Extension* extension,
+ string16* error) const {
+ if (account_type_ == policy::DeviceLocalAccount::TYPE_KIOSK_APP) {
+ // For single-app kiosk sessions, allow only platform apps.
+ if (extension->GetType() == extensions::Manifest::TYPE_PLATFORM_APP)
+ return true;
+
+ } else {
+ // Allow extension if its type is whitelisted for use in device-local
+ // accounts.
+ if (extension->GetType() == extensions::Manifest::TYPE_HOSTED_APP)
+ return true;
+
+ // Allow extension if its specific ID is whitelisted for use in device-local
+ // accounts.
+ for (size_t i = 0; i < arraysize(kDeviceLocalAccountWhitelist); ++i) {
+ if (extension->id() == kDeviceLocalAccountWhitelist[i])
+ return true;
+ }
+ }
+
+ // Disallow all other extensions.
+ if (error) {
+ *error = l10n_util::GetStringFUTF16(
+ IDS_EXTENSION_CANT_INSTALL_IN_DEVICE_LOCAL_ACCOUNT,
+ UTF8ToUTF16(extension->name()),
+ UTF8ToUTF16(extension->id()));
+ }
+ return false;
+}
+
+} // namespace chromeos
diff --git a/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.h b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.h
new file mode 100644
index 0000000..c19fe1a
--- /dev/null
+++ b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.h
@@ -0,0 +1,38 @@
+// Copyright 2013 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_CHROMEOS_EXTENSIONS_DEVICE_LOCAL_ACCOUNT_MANAGEMENT_POLICY_PROVIDER_H_
+#define CHROME_BROWSER_CHROMEOS_EXTENSIONS_DEVICE_LOCAL_ACCOUNT_MANAGEMENT_POLICY_PROVIDER_H_
+
+#include "base/basictypes.h"
+#include "base/compiler_specific.h"
+#include "chrome/browser/chromeos/policy/device_local_account.h"
+#include "chrome/browser/extensions/management_policy.h"
+
+namespace chromeos {
+
+// A managed policy for device-local accounts that ensures only extensions whose
+// type or ID has been whitelisted for use in device-local accounts can be
+// installed.
+class DeviceLocalAccountManagementPolicyProvider
+ : public extensions::ManagementPolicy::Provider {
+ public:
+ explicit DeviceLocalAccountManagementPolicyProvider(
+ policy::DeviceLocalAccount::Type account_type);
+ virtual ~DeviceLocalAccountManagementPolicyProvider();
+
+ // extensions::ManagementPolicy::Provider:
+ virtual std::string GetDebugPolicyProviderName() const OVERRIDE;
+ virtual bool UserMayLoad(const extensions::Extension* extension,
+ string16* error) const OVERRIDE;
+
+ private:
+ const policy::DeviceLocalAccount::Type account_type_;
+
+ DISALLOW_COPY_AND_ASSIGN(DeviceLocalAccountManagementPolicyProvider);
+};
+
+} // namespace chromeos
+
+#endif // CHROME_BROWSER_CHROMEOS_EXTENSIONS_DEVICE_LOCAL_ACCOUNT_MANAGEMENT_POLICY_PROVIDER_H_
diff --git a/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider_unittest.cc b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider_unittest.cc
new file mode 100644
index 0000000..00802bf
--- /dev/null
+++ b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider_unittest.cc
@@ -0,0 +1,124 @@
+// Copyright 2013 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/chromeos/extensions/device_local_account_management_policy_provider.h"
+
+#include <string>
+
+#include "base/files/file_path.h"
+#include "base/memory/ref_counted.h"
+#include "base/values.h"
+#include "chrome/common/extensions/extension.h"
+#include "extensions/common/manifest.h"
+#include "extensions/common/manifest_constants.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace chromeos {
+
+namespace {
+
+const char kWhitelistedId[] = "bpmcpldpdmajfigpchkicefoigmkfalc";
+
+scoped_refptr<const extensions::Extension> CreateExtensionFromValues(
+ const std::string& id,
+ base::DictionaryValue* values) {
+ values->SetString(extensions::manifest_keys::kName, "test");
+ values->SetString(extensions::manifest_keys::kVersion, "0.1");
+ std::string error;
+ return extensions::Extension::Create(base::FilePath(),
+ extensions::Manifest::INTERNAL,
+ *values,
+ extensions::Extension::NO_FLAGS,
+ id,
+ &error);
+}
+
+scoped_refptr<const extensions::Extension> CreateExtension(
+ const std::string& id) {
+ base::DictionaryValue values;
+ return CreateExtensionFromValues(id, &values);
+}
+
+scoped_refptr<const extensions::Extension> CreateHostedApp() {
+ base::DictionaryValue values;
+ values.Set(extensions::manifest_keys::kApp, new base::DictionaryValue);
+ values.Set(extensions::manifest_keys::kWebURLs, new base::ListValue);
+ return CreateExtensionFromValues(std::string(), &values);
+}
+
+scoped_refptr<const extensions::Extension> CreatePlatformApp() {
+ base::DictionaryValue values;
+ values.Set(extensions::manifest_keys::kApp, new base::DictionaryValue);
+ values.Set(extensions::manifest_keys::kPlatformAppBackground,
+ new base::DictionaryValue);
+ values.Set(extensions::manifest_keys::kPlatformAppBackgroundPage,
+ new base::StringValue("background.html"));
+ return CreateExtensionFromValues(std::string(), &values);
+}
+
+} // namespace
+
+TEST(DeviceLocalAccountManagementPolicyProviderTest, PublicSession) {
+ DeviceLocalAccountManagementPolicyProvider
+ provider(policy::DeviceLocalAccount::TYPE_PUBLIC_SESSION);
+
+ // Verify that if an extension's type has been whitelisted for use in
+ // device-local accounts, the extension can be installed.
+ scoped_refptr<const extensions::Extension> extension = CreateHostedApp();
+ ASSERT_TRUE(extension);
+ string16 error;
+ EXPECT_TRUE(provider.UserMayLoad(extension.get(), &error));
+ EXPECT_EQ(string16(), error);
+ error.clear();
+
+ // Verify that if an extension's ID has been explicitly whitelisted for use in
+ // device-local accounts, the extension can be installed.
+ extension = CreateExtension(kWhitelistedId);
+ ASSERT_TRUE(extension);
+ EXPECT_TRUE(provider.UserMayLoad(extension.get(), &error));
+ EXPECT_EQ(string16(), error);
+ error.clear();
+
+ // Verify that if neither the type nor the ID of an extension have been
+ // whitelisted for use in device-local accounts, the extension cannot be
+ // installed.
+ extension = CreateExtension(std::string());
+ ASSERT_TRUE(extension);
+ EXPECT_FALSE(provider.UserMayLoad(extension.get(), &error));
+ EXPECT_NE(string16(), error);
+ error.clear();
+}
+
+TEST(DeviceLocalAccountManagementPolicyProviderTest, KioskAppSession) {
+ DeviceLocalAccountManagementPolicyProvider
+ provider(policy::DeviceLocalAccount::TYPE_KIOSK_APP);
+
+ // Verify that a platform app can be installed.
+ scoped_refptr<const extensions::Extension> extension = CreatePlatformApp();
+ ASSERT_TRUE(extension);
+ string16 error;
+ EXPECT_TRUE(provider.UserMayLoad(extension.get(), &error));
+ EXPECT_EQ(string16(), error);
+ error.clear();
+
+ // Verify that an extension whose type has been whitelisted for use in other
+ // types of device-local accounts cannot be installed in a single-app kiosk
+ // session.
+ extension = CreateHostedApp();
+ ASSERT_TRUE(extension);
+ EXPECT_FALSE(provider.UserMayLoad(extension.get(), &error));
+ EXPECT_NE(string16(), error);
+ error.clear();
+
+ // Verify that an extension whose ID has been whitelisted for use in other
+ // types of device-local accounts cannot be installed in a single-app kiosk
+ // session.
+ extension = CreateExtension(kWhitelistedId);
+ ASSERT_TRUE(extension);
+ EXPECT_FALSE(provider.UserMayLoad(extension.get(), &error));
+ EXPECT_NE(string16(), error);
+ error.clear();
+}
+
+} // namespace chromeos
diff --git a/chrome/browser/chromeos/login/user_manager_impl.cc b/chrome/browser/chromeos/login/user_manager_impl.cc
index 68a4d38..c705639 100644
--- a/chrome/browser/chromeos/login/user_manager_impl.cc
+++ b/chrome/browser/chromeos/login/user_manager_impl.cc
@@ -362,11 +362,15 @@ void UserManagerImpl::UserLoggedIn(const std::string& email,
return;
}
+ policy::DeviceLocalAccount::Type device_local_account_type;
if (email == UserManager::kGuestUserName) {
GuestUserLoggedIn();
} else if (email == UserManager::kRetailModeUserName) {
RetailModeUserLoggedIn();
- } else if (policy::IsKioskAppUser(email)) {
+ } else if (policy::IsDeviceLocalAccountUser(email,
+ &device_local_account_type) &&
+ device_local_account_type ==
+ policy::DeviceLocalAccount::TYPE_KIOSK_APP) {
KioskAppLoggedIn(email);
} else {
EnsureUsersLoaded();
@@ -1402,7 +1406,11 @@ void UserManagerImpl::PublicAccountUserLoggedIn(User* user) {
void UserManagerImpl::KioskAppLoggedIn(const std::string& username) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
- DCHECK(policy::IsKioskAppUser(username));
+ policy::DeviceLocalAccount::Type device_local_account_type;
+ DCHECK(policy::IsDeviceLocalAccountUser(username,
+ &device_local_account_type));
+ DCHECK_EQ(policy::DeviceLocalAccount::TYPE_KIOSK_APP,
+ device_local_account_type);
active_user_ = User::CreateKioskAppUser(username);
active_user_->SetStubImage(User::kInvalidImageIndex, false);
diff --git a/chrome/browser/chromeos/policy/device_local_account.cc b/chrome/browser/chromeos/policy/device_local_account.cc
index 2ad53ea..a614a9d 100644
--- a/chrome/browser/chromeos/policy/device_local_account.cc
+++ b/chrome/browser/chromeos/policy/device_local_account.cc
@@ -6,11 +6,13 @@
#include <set>
+#include "base/basictypes.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/values.h"
+#include "chrome/browser/chromeos/login/user_manager.h"
#include "chrome/browser/chromeos/settings/cros_settings.h"
#include "chrome/browser/chromeos/settings/cros_settings_names.h"
#include "google_apis/gaia/gaia_auth_util.h"
@@ -56,16 +58,35 @@ std::string GenerateDeviceLocalAccountUserId(const std::string& account_id,
domain_prefix + kDeviceLocalAccountDomainSuffix);
}
-bool IsDeviceLocalAccountUser(const std::string& user_id) {
- return EndsWith(gaia::ExtractDomainName(user_id),
- kDeviceLocalAccountDomainSuffix,
- true);
-}
+bool IsDeviceLocalAccountUser(const std::string& user_id,
+ DeviceLocalAccount::Type* type) {
+ // For historical reasons, the guest user ID does not contain an @ symbol and
+ // therefore, cannot be parsed by gaia::ExtractDomainName().
+ if (user_id == chromeos::UserManager::kGuestUserName)
+ return false;
+ const std::string domain = gaia::ExtractDomainName(user_id);
+ if (!EndsWith(domain, kDeviceLocalAccountDomainSuffix, true))
+ return false;
+
+ const std::string domain_prefix = domain.substr(
+ 0, domain.size() - arraysize(kDeviceLocalAccountDomainSuffix) + 1);
+
+ if (domain_prefix == kPublicAccountDomainPrefix) {
+ if (type)
+ *type = DeviceLocalAccount::TYPE_PUBLIC_SESSION;
+ return true;
+ }
+ if (domain_prefix == kKioskAppAccountDomainPrefix) {
+ if (type)
+ *type = DeviceLocalAccount::TYPE_KIOSK_APP;
+ return true;
+ }
-bool IsKioskAppUser(const std::string& user_id) {
- return gaia::ExtractDomainName(user_id) ==
- std::string(kKioskAppAccountDomainPrefix) +
- kDeviceLocalAccountDomainSuffix;
+ // |user_id| is a device-local account but its type is not recognized.
+ NOTREACHED();
+ if (type)
+ *type = DeviceLocalAccount::TYPE_COUNT;
+ return true;
}
void SetDeviceLocalAccounts(
diff --git a/chrome/browser/chromeos/policy/device_local_account.h b/chrome/browser/chromeos/policy/device_local_account.h
index 1e3e318..ff83461 100644
--- a/chrome/browser/chromeos/policy/device_local_account.h
+++ b/chrome/browser/chromeos/policy/device_local_account.h
@@ -40,9 +40,10 @@ struct DeviceLocalAccount {
std::string GenerateDeviceLocalAccountUserId(const std::string& account_id,
DeviceLocalAccount::Type type);
-bool IsDeviceLocalAccountUser(const std::string& user_id);
-
-bool IsKioskAppUser(const std::string& user_id);
+// Determines whether |user_id| belongs to a device-local account and if so,
+// returns the type of device-local account in |type| unless |type| is NULL.
+bool IsDeviceLocalAccountUser(const std::string& user_id,
+ DeviceLocalAccount::Type* type);
// Stores a list of device-local accounts in |cros_settings|. The accounts are
// stored as a list of dictionaries with each dictionary containing the
diff --git a/chrome/browser/chromeos/policy/device_local_account_browsertest.cc b/chrome/browser/chromeos/policy/device_local_account_browsertest.cc
index 59eb85b..aae8978 100644
--- a/chrome/browser/chromeos/policy/device_local_account_browsertest.cc
+++ b/chrome/browser/chromeos/policy/device_local_account_browsertest.cc
@@ -7,6 +7,7 @@
#include "base/basictypes.h"
#include "base/bind.h"
+#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/file_util.h"
@@ -17,6 +18,7 @@
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
@@ -33,6 +35,8 @@
#include "chrome/browser/chromeos/policy/device_local_account.h"
#include "chrome/browser/chromeos/policy/device_policy_builder.h"
#include "chrome/browser/chromeos/policy/device_policy_cros_browser_test.h"
+#include "chrome/browser/extensions/extension_service.h"
+#include "chrome/browser/extensions/extension_system.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/browser/policy/cloud/cloud_policy_constants.h"
#include "chrome/browser/policy/cloud/policy_builder.h"
@@ -52,11 +56,14 @@
#include "chrome/browser/ui/webui/chromeos/login/oobe_ui.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
+#include "chrome/common/extensions/extension.h"
#include "chromeos/chromeos_switches.h"
#include "chromeos/dbus/cryptohome_client.h"
#include "chromeos/dbus/dbus_method_call_status.h"
#include "chromeos/dbus/fake_session_manager_client.h"
#include "chromeos/dbus/session_manager_client.h"
+#include "content/public/browser/notification_details.h"
+#include "content/public/browser/notification_source.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui.h"
#include "content/public/test/browser_test_utils.h"
@@ -64,7 +71,11 @@
#include "crypto/rsa_private_key.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
+#include "net/base/url_util.h"
+#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
#include "ui/base/l10n/l10n_util.h"
@@ -90,6 +101,129 @@ const char* kStartupURLs[] = {
};
const char kExistentTermsOfServicePath[] = "chromeos/enterprise/tos.txt";
const char kNonexistentTermsOfServicePath[] = "chromeos/enterprise/tos404.txt";
+const char kRelativeUpdateURL[] = "/service/update2/crx";
+const char kUpdateManifestHeader[] =
+ "<?xml version='1.0' encoding='UTF-8'?>\n"
+ "<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>\n";
+const char kUpdateManifestTemplate[] =
+ " <app appid='%s'>\n"
+ " <updatecheck codebase='%s' version='%s' />\n"
+ " </app>\n";
+const char kUpdateManifestFooter[] =
+ "</gupdate>\n";
+const char kHostedAppID[] = "kbmnembihfiondgfjekmnmcbddelicoi";
+const char kHostedAppCRXPath[] = "extensions/hosted_app.crx";
+const char kHostedAppVersion[] = "0.1";
+const char kGoodExtensionID[] = "ldnnhddmnhbkjipkidpdiheffobcpfmf";
+const char kGoodExtensionPath[] = "extensions/good.crx";
+const char kGoodExtensionVersion[] = "1.0";
+
+// Helper that serves extension update manifests to Chrome.
+class TestingUpdateManifestProvider {
+ public:
+
+ // Update manifests will be served at |relative_update_url|.
+ explicit TestingUpdateManifestProvider(
+ const std::string& relative_update_url);
+ ~TestingUpdateManifestProvider();
+
+ // When an update manifest is requested for the given extension |id|, indicate
+ // that |version| of the extension can be downloaded at |crx_url|.
+ void AddUpdate(const std::string& id,
+ const std::string& version,
+ const GURL& crx_url);
+
+ // This method must be registered with the test's EmbeddedTestServer to start
+ // serving update manifests.
+ scoped_ptr<net::test_server::HttpResponse> HandleRequest(
+ const net::test_server::HttpRequest& request);
+
+ private:
+ struct Update {
+ public:
+ Update(const std::string& version, const GURL& crx_url);
+ Update();
+
+ std::string version;
+ GURL crx_url;
+ };
+ typedef std::map<std::string, Update> UpdateMap;
+ UpdateMap updates_;
+
+ const std::string relative_update_url_;
+
+ DISALLOW_COPY_AND_ASSIGN(TestingUpdateManifestProvider);
+};
+
+TestingUpdateManifestProvider::Update::Update(const std::string& version,
+ const GURL& crx_url)
+ : version(version),
+ crx_url(crx_url) {
+}
+
+TestingUpdateManifestProvider::Update::Update() {
+}
+
+TestingUpdateManifestProvider::TestingUpdateManifestProvider(
+ const std::string& relative_update_url)
+ : relative_update_url_(relative_update_url) {
+}
+
+TestingUpdateManifestProvider::~TestingUpdateManifestProvider() {
+}
+
+void TestingUpdateManifestProvider::AddUpdate(const std::string& id,
+ const std::string& version,
+ const GURL& crx_url) {
+ updates_[id] = Update(version, crx_url);
+}
+
+scoped_ptr<net::test_server::HttpResponse>
+ TestingUpdateManifestProvider::HandleRequest(
+ const net::test_server::HttpRequest& request) {
+ const GURL url("http://localhost" + request.relative_url);
+ if (url.path() != relative_update_url_)
+ return scoped_ptr<net::test_server::HttpResponse>();
+
+ std::string content = kUpdateManifestHeader;
+ for (net::QueryIterator it(url); !it.IsAtEnd(); it.Advance()) {
+ if (it.GetKey() != "x")
+ continue;
+ // Extract the extension id from the subquery. Since GetValueForKeyInQuery()
+ // expects a complete URL, dummy scheme and host must be prepended.
+ std::string id;
+ net::GetValueForKeyInQuery(GURL("http://dummy?" + it.GetUnescapedValue()),
+ "id", &id);
+ UpdateMap::const_iterator entry = updates_.find(id);
+ if (entry != updates_.end()) {
+ content += base::StringPrintf(kUpdateManifestTemplate,
+ id.c_str(),
+ entry->second.crx_url.spec().c_str(),
+ entry->second.version.c_str());
+ }
+ }
+ content += kUpdateManifestFooter;
+ scoped_ptr<net::test_server::BasicHttpResponse>
+ http_response(new net::test_server::BasicHttpResponse);
+ http_response->set_code(net::HTTP_OK);
+ http_response->set_content(content);
+ http_response->set_content_type("text/xml");
+ return http_response.PassAs<net::test_server::HttpResponse>();
+}
+
+bool DoesInstallSuccessReferToId(const std::string& id,
+ const content::NotificationSource& source,
+ const content::NotificationDetails& details) {
+ return content::Details<const extensions::InstalledExtensionInfo>(details)->
+ extension->id() == id;
+}
+
+bool DoesInstallFailureReferToId(const std::string& id,
+ const content::NotificationSource& source,
+ const content::NotificationDetails& details) {
+ return content::Details<const string16>(details)->find(UTF8ToUTF16(id)) !=
+ string16::npos;
+}
} // namespace
@@ -420,6 +554,92 @@ IN_PROC_BROWSER_TEST_F(DeviceLocalAccountTest, FullscreenDisallowed) {
EXPECT_FALSE(browser_window->IsFullscreen());
}
+IN_PROC_BROWSER_TEST_F(DeviceLocalAccountTest, ExtensionWhitelist) {
+ // Make it possible to force-install a hosted app and an extension.
+ ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady());
+ TestingUpdateManifestProvider testing_update_manifest_provider(
+ kRelativeUpdateURL);
+ testing_update_manifest_provider.AddUpdate(
+ kHostedAppID,
+ kHostedAppVersion,
+ embedded_test_server()->GetURL(std::string("/") + kHostedAppCRXPath));
+ testing_update_manifest_provider.AddUpdate(
+ kGoodExtensionID,
+ kGoodExtensionVersion,
+ embedded_test_server()->GetURL(std::string("/") + kGoodExtensionPath));
+ embedded_test_server()->RegisterRequestHandler(
+ base::Bind(&TestingUpdateManifestProvider::HandleRequest,
+ base::Unretained(&testing_update_manifest_provider)));
+
+ // Specify policy to force-install the hosted app and the extension.
+ em::StringList* forcelist = device_local_account_policy_.payload()
+ .mutable_extensioninstallforcelist()->mutable_value();
+ forcelist->add_entries(base::StringPrintf(
+ "%s;%s",
+ kHostedAppID,
+ embedded_test_server()->GetURL(kRelativeUpdateURL).spec().c_str()));
+ forcelist->add_entries(base::StringPrintf(
+ "%s;%s",
+ kGoodExtensionID,
+ embedded_test_server()->GetURL(kRelativeUpdateURL).spec().c_str()));
+
+ UploadAndInstallDeviceLocalAccountPolicy();
+ AddPublicSessionToDevicePolicy(kAccountId1);
+
+ // This observes the display name becoming available as this indicates
+ // device-local account policy is fully loaded, which is a prerequisite for
+ // successful login.
+ content::WindowedNotificationObserver(
+ chrome::NOTIFICATION_USER_LIST_CHANGED,
+ base::Bind(&DisplayNameMatches, user_id_1_, kDisplayName)).Wait();
+
+ // Wait for the login UI to be ready.
+ chromeos::LoginDisplayHostImpl* host =
+ reinterpret_cast<chromeos::LoginDisplayHostImpl*>(
+ chromeos::LoginDisplayHostImpl::default_host());
+ ASSERT_TRUE(host);
+ chromeos::OobeUI* oobe_ui = host->GetOobeUI();
+ ASSERT_TRUE(oobe_ui);
+ base::RunLoop run_loop;
+ const bool oobe_ui_ready = oobe_ui->IsJSReady(run_loop.QuitClosure());
+ if (!oobe_ui_ready)
+ run_loop.Run();
+
+ // Ensure that the browser stays alive, even though no windows are opened
+ // during session start.
+ chrome::StartKeepAlive();
+
+ // Start listening for app/extension installation results.
+ content::WindowedNotificationObserver hosted_app_observer(
+ chrome::NOTIFICATION_EXTENSION_INSTALLED,
+ base::Bind(DoesInstallSuccessReferToId, kHostedAppID));
+ content::WindowedNotificationObserver extension_observer(
+ chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR,
+ base::Bind(DoesInstallFailureReferToId, kGoodExtensionID));
+
+ // Start login into the device-local account.
+ host->StartSignInScreen();
+ chromeos::ExistingUserController* controller =
+ chromeos::ExistingUserController::current_controller();
+ ASSERT_TRUE(controller);
+ controller->LoginAsPublicAccount(user_id_1_);
+
+ // Wait for the hosted app installation to succeed and the extension
+ // installation to fail.
+ hosted_app_observer.Wait();
+ extension_observer.Wait();
+
+ // Verify that the hosted app was installed.
+ Profile* profile = ProfileManager::GetDefaultProfile();
+ ASSERT_TRUE(profile);
+ ExtensionService* extension_service =
+ extensions::ExtensionSystem::Get(profile)->extension_service();
+ EXPECT_TRUE(extension_service->GetExtensionById(kHostedAppID, true));
+
+ // Verify that the extension was not installed.
+ EXPECT_FALSE(extension_service->GetExtensionById(kGoodExtensionID, true));
+}
+
class TermsOfServiceTest : public DeviceLocalAccountTest,
public testing::WithParamInterface<bool> {
};
diff --git a/chrome/browser/chromeos/policy/device_local_account_policy_service_unittest.cc b/chrome/browser/chromeos/policy/device_local_account_policy_service_unittest.cc
index ec3dc2b..8312db9 100644
--- a/chrome/browser/chromeos/policy/device_local_account_policy_service_unittest.cc
+++ b/chrome/browser/chromeos/policy/device_local_account_policy_service_unittest.cc
@@ -7,7 +7,6 @@
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
-#include "base/memory/scoped_ptr.h"
#include "chrome/browser/chromeos/policy/device_local_account.h"
#include "chrome/browser/chromeos/policy/device_local_account_policy_provider.h"
#include "chrome/browser/chromeos/settings/cros_settings.h"
@@ -80,13 +79,6 @@ class DeviceLocalAccountPolicyServiceTest
POLICY_SCOPE_USER,
Value::CreateBooleanValue(false),
NULL);
- scoped_ptr<base::ListValue> allowed_extension_types(new base::ListValue());
- allowed_extension_types->AppendString("hosted_app");
- expected_policy_map_.Set(key::kExtensionAllowedTypes,
- POLICY_LEVEL_MANDATORY,
- POLICY_SCOPE_USER,
- allowed_extension_types.release(),
- NULL);
// Explicitly set value.
expected_policy_map_.Set(key::kDisableSpdy,
@@ -502,8 +494,6 @@ TEST_F(DeviceLocalAccountPolicyProviderTest, Policy) {
set_value("Always");
device_local_account_policy_.payload().mutable_showlogoutbuttonintray()->
set_value(false);
- device_local_account_policy_.payload().mutable_extensionallowedtypes()->
- mutable_value()->mutable_entries()->Clear();
device_local_account_policy_.Build();
device_settings_test_helper_.set_device_local_account_policy_blob(
PolicyBuilder::kFakeUsername, device_local_account_policy_.GetBlob());
diff --git a/chrome/browser/chromeos/policy/device_local_account_policy_store.cc b/chrome/browser/chromeos/policy/device_local_account_policy_store.cc
index f3914cf..898733a 100644
--- a/chrome/browser/chromeos/policy/device_local_account_policy_store.cc
+++ b/chrome/browser/chromeos/policy/device_local_account_policy_store.cc
@@ -86,7 +86,6 @@ void DeviceLocalAccountPolicyStore::UpdatePolicy(
base::Value::CreateIntegerValue(
chromeos::PowerPolicyController::ACTION_STOP_SESSION),
NULL);
-
// Force the |ShelfAutoHideBehavior| policy to |Never|, ensuring that the ash
// shelf does not auto-hide.
policy_map_.Set(key::kShelfAutoHideBehavior,
@@ -108,16 +107,6 @@ void DeviceLocalAccountPolicyStore::UpdatePolicy(
POLICY_SCOPE_USER,
Value::CreateBooleanValue(false),
NULL);
- // Restrict device-local accounts to hosted apps for now (i.e. no extensions,
- // packaged apps etc.) for security/privacy reasons (i.e. we'd like to
- // prevent the admin from stealing private information from random people).
- scoped_ptr<base::ListValue> allowed_extension_types(new base::ListValue());
- allowed_extension_types->AppendString("hosted_app");
- policy_map_.Set(key::kExtensionAllowedTypes,
- POLICY_LEVEL_MANDATORY,
- POLICY_SCOPE_USER,
- allowed_extension_types.release(),
- NULL);
status_ = STATUS_OK;
NotifyStoreLoaded();
diff --git a/chrome/browser/extensions/extension_system.cc b/chrome/browser/extensions/extension_system.cc
index ef79b60..348c5ee 100644
--- a/chrome/browser/extensions/extension_system.cc
+++ b/chrome/browser/extensions/extension_system.cc
@@ -48,6 +48,10 @@
#if defined(OS_CHROMEOS)
#include "chrome/browser/app_mode/app_mode_utils.h"
+#include "chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.h"
+#include "chrome/browser/chromeos/login/user.h"
+#include "chrome/browser/chromeos/login/user_manager.h"
+#include "chrome/browser/chromeos/policy/device_local_account.h"
#include "chromeos/chromeos_switches.h"
#include "chromeos/login/login_state.h"
#endif
@@ -107,7 +111,19 @@ void ExtensionSystemImpl::Shared::InitPrefs() {
standard_management_policy_provider_.reset(
new StandardManagementPolicyProvider(ExtensionPrefs::Get(profile_)));
-#endif
+
+#if defined (OS_CHROMEOS)
+ const chromeos::User* user = chromeos::UserManager::Get()->GetActiveUser();
+ policy::DeviceLocalAccount::Type device_local_account_type;
+ if (user && policy::IsDeviceLocalAccountUser(user->email(),
+ &device_local_account_type)) {
+ device_local_account_management_policy_provider_.reset(
+ new chromeos::DeviceLocalAccountManagementPolicyProvider(
+ device_local_account_type));
+ }
+#endif // defined (OS_CHROMEOS)
+
+#endif // defined(ENABLE_EXTENSIONS)
}
void ExtensionSystemImpl::Shared::RegisterManagementPolicyProviders() {
@@ -116,7 +132,15 @@ void ExtensionSystemImpl::Shared::RegisterManagementPolicyProviders() {
DCHECK(standard_management_policy_provider_.get());
management_policy_->RegisterProvider(
standard_management_policy_provider_.get());
-#endif
+
+#if defined (OS_CHROMEOS)
+ if (device_local_account_management_policy_provider_) {
+ management_policy_->RegisterProvider(
+ device_local_account_management_policy_provider_.get());
+ }
+#endif // defined (OS_CHROMEOS)
+
+#endif // defined(ENABLE_EXTENSIONS)
}
void ExtensionSystemImpl::Shared::Init(bool extensions_enabled) {
diff --git a/chrome/browser/extensions/extension_system.h b/chrome/browser/extensions/extension_system.h
index 8a40070..c88fc74 100644
--- a/chrome/browser/extensions/extension_system.h
+++ b/chrome/browser/extensions/extension_system.h
@@ -18,6 +18,12 @@ class ExtensionProcessManager;
class ExtensionService;
class Profile;
+#if defined(OS_CHROMEOS)
+namespace chromeos {
+class DeviceLocalAccountManagementPolicyProvider;
+}
+#endif // defined(OS_CHROMEOS)
+
namespace extensions {
class Blacklist;
class ErrorConsole;
@@ -213,6 +219,11 @@ class ExtensionSystemImpl : public ExtensionSystem {
scoped_ptr<ExtensionWarningBadgeService> extension_warning_badge_service_;
scoped_ptr<ErrorConsole> error_console_;
+#if defined(OS_CHROMEOS)
+ scoped_ptr<chromeos::DeviceLocalAccountManagementPolicyProvider>
+ device_local_account_management_policy_provider_;
+#endif
+
OneShotEvent ready_;
};
diff --git a/chrome/browser/policy/browser_policy_connector.cc b/chrome/browser/policy/browser_policy_connector.cc
index d4d0f61..9132f43 100644
--- a/chrome/browser/policy/browser_policy_connector.cc
+++ b/chrome/browser/policy/browser_policy_connector.cc
@@ -415,7 +415,7 @@ UserAffiliation BrowserPolicyConnector::GetUserAffiliation(
if (install_attributes_ &&
(gaia::ExtractDomainName(gaia::CanonicalizeEmail(user_name)) ==
install_attributes_->GetDomain() ||
- policy::IsDeviceLocalAccountUser(user_name))) {
+ policy::IsDeviceLocalAccountUser(user_name, NULL))) {
return USER_AFFILIATION_MANAGED;
}
#endif
diff --git a/chrome/chrome_browser_chromeos.gypi b/chrome/chrome_browser_chromeos.gypi
index 71505cd..88619be 100644
--- a/chrome/chrome_browser_chromeos.gypi
+++ b/chrome/chrome_browser_chromeos.gypi
@@ -298,6 +298,8 @@
'browser/chromeos/enrollment_dialog_view.h',
'browser/chromeos/extensions/default_app_order.cc',
'browser/chromeos/extensions/default_app_order.h',
+ 'browser/chromeos/extensions/device_local_account_management_policy_provider.cc',
+ 'browser/chromeos/extensions/device_local_account_management_policy_provider.h',
'browser/chromeos/extensions/echo_private_api.cc',
'browser/chromeos/extensions/echo_private_api.h',
'browser/chromeos/extensions/external_cache.cc',
diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi
index 9742249..98f759f 100644
--- a/chrome/chrome_tests_unit.gypi
+++ b/chrome/chrome_tests_unit.gypi
@@ -655,6 +655,7 @@
'browser/chromeos/file_manager/mounted_disk_monitor_unittest.cc',
'browser/chromeos/file_manager/url_util_unittest.cc',
'browser/chromeos/file_manager/volume_manager_unittest.cc',
+ 'browser/chromeos/extensions/device_local_account_management_policy_provider_unittest.cc',
'browser/chromeos/extensions/wallpaper_private_api_unittest.cc',
'browser/chromeos/external_metrics_unittest.cc',
'browser/chromeos/fileapi/file_access_permissions_unittest.cc',
diff --git a/content/public/test/test_utils.cc b/content/public/test/test_utils.cc
index 92c6fdc..989437c 100644
--- a/content/public/test/test_utils.cc
+++ b/content/public/test/test_utils.cc
@@ -67,6 +67,17 @@ void ScriptCallback::ResultCallback(const base::Value* result) {
base::MessageLoop::current()->Quit();
}
+// Adapter that makes a WindowedNotificationObserver::ConditionTestCallback from
+// a WindowedNotificationObserver::ConditionTestCallbackWithoutSourceAndDetails
+// by ignoring the notification source and details.
+bool IgnoreSourceAndDetails(
+ const WindowedNotificationObserver::
+ ConditionTestCallbackWithoutSourceAndDetails& callback,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ return callback.Run();
+}
+
} // namespace
void RunMessageLoop() {
@@ -186,6 +197,16 @@ WindowedNotificationObserver::WindowedNotificationObserver(
AddNotificationType(notification_type, source_);
}
+WindowedNotificationObserver::WindowedNotificationObserver(
+ int notification_type,
+ const ConditionTestCallbackWithoutSourceAndDetails& callback)
+ : seen_(false),
+ running_(false),
+ callback_(base::Bind(&IgnoreSourceAndDetails, callback)),
+ source_(NotificationService::AllSources()) {
+ registrar_.Add(this, notification_type, source_);
+}
+
WindowedNotificationObserver::~WindowedNotificationObserver() {}
void WindowedNotificationObserver::AddNotificationType(
@@ -210,7 +231,7 @@ void WindowedNotificationObserver::Observe(
const NotificationDetails& details) {
source_ = source;
details_ = details;
- if (!callback_.is_null() && !callback_.Run())
+ if (!callback_.is_null() && !callback_.Run(source, details))
return;
seen_ = true;
diff --git a/content/public/test/test_utils.h b/content/public/test/test_utils.h
index 0b8fa76..c1aa189 100644
--- a/content/public/test/test_utils.h
+++ b/content/public/test/test_utils.h
@@ -100,7 +100,8 @@ class MessageLoopRunner : public base::RefCounted<MessageLoopRunner> {
// If the callback returns |true|, the condition is met. Otherwise, the
// condition is not yet met and the callback will be invoked again every time a
// notification of the expected type is received until the callback returns
-// |true|.
+// |true|. For convenience, two callback types are defined, one that is provided
+// with the notification source and details, and one that is not.
//
// This helper class exists to avoid the following common pattern in tests:
// PerformAction()
@@ -116,8 +117,14 @@ class MessageLoopRunner : public base::RefCounted<MessageLoopRunner> {
class WindowedNotificationObserver : public NotificationObserver {
public:
// Callback invoked on notifications. Should return |true| when the condition
- // being waited for is met.
- typedef base::Callback<bool(void)> ConditionTestCallback;
+ // being waited for is met. For convenience, there is a choice between two
+ // callback types, one that is provided with the notification source and
+ // details, and one that is not.
+ typedef base::Callback<bool(const NotificationSource&,
+ const NotificationDetails&)>
+ ConditionTestCallback;
+ typedef base::Callback<bool(void)>
+ ConditionTestCallbackWithoutSourceAndDetails;
// Set up to wait for a simple condition. The condition is met when a
// notification of the given |notification_type| from the given |source| is
@@ -131,6 +138,9 @@ class WindowedNotificationObserver : public NotificationObserver {
// of |notification_type| from any source is received.
WindowedNotificationObserver(int notification_type,
const ConditionTestCallback& callback);
+ WindowedNotificationObserver(
+ int notification_type,
+ const ConditionTestCallbackWithoutSourceAndDetails& callback);
virtual ~WindowedNotificationObserver();
diff --git a/net/base/url_util.cc b/net/base/url_util.cc
index c18fd17..a7c89e9 100644
--- a/net/base/url_util.cc
+++ b/net/base/url_util.cc
@@ -4,6 +4,9 @@
#include "net/base/url_util.h"
+#include <utility>
+
+#include "base/logging.h"
#include "base/strings/string_piece.h"
#include "net/base/escape.h"
#include "url/gurl.h"
@@ -68,30 +71,66 @@ GURL AppendOrReplaceQueryParameter(const GURL& url,
return url.ReplaceComponents(replacements);
}
+QueryIterator::QueryIterator(const GURL& url)
+ : url_(url),
+ at_end_(!url.is_valid()) {
+ if (!at_end_) {
+ query_ = url.parsed_for_possibly_invalid_spec().query;
+ Advance();
+ }
+}
+
+QueryIterator::~QueryIterator() {
+}
+
+std::string QueryIterator::GetKey() const {
+ DCHECK(!at_end_);
+ if (key_.is_nonempty())
+ return url_.spec().substr(key_.begin, key_.len);
+ return std::string();
+}
+
+std::string QueryIterator::GetValue() const {
+ DCHECK(!at_end_);
+ if (value_.is_nonempty())
+ return url_.spec().substr(value_.begin, value_.len);
+ return std::string();
+}
+
+const std::string& QueryIterator::GetUnescapedValue() {
+ DCHECK(!at_end_);
+ if (value_.is_nonempty() && unescaped_value_.empty()) {
+ unescaped_value_ = UnescapeURLComponent(
+ GetValue(),
+ UnescapeRule::SPACES |
+ UnescapeRule::URL_SPECIAL_CHARS |
+ UnescapeRule::REPLACE_PLUS_WITH_SPACE);
+ }
+ return unescaped_value_;
+}
+
+bool QueryIterator::IsAtEnd() const {
+ return at_end_;
+}
+
+void QueryIterator::Advance() {
+ DCHECK (!at_end_);
+ key_.reset();
+ value_.reset();
+ unescaped_value_.clear();
+ at_end_ = !url_parse::ExtractQueryKeyValue(url_.spec().c_str(),
+ &query_,
+ &key_,
+ &value_);
+}
+
bool GetValueForKeyInQuery(const GURL& url,
const std::string& search_key,
std::string* out_value) {
- if (!url.is_valid())
- return false;
-
- url_parse::Component query = url.parsed_for_possibly_invalid_spec().query;
- url_parse::Component key, value;
- while (url_parse::ExtractQueryKeyValue(
- url.spec().c_str(), &query, &key, &value)) {
- if (key.is_nonempty()) {
- std::string key_string = url.spec().substr(key.begin, key.len);
- if (key_string == search_key) {
- if (value.is_nonempty()) {
- *out_value = UnescapeURLComponent(
- url.spec().substr(value.begin, value.len),
- UnescapeRule::SPACES |
- UnescapeRule::URL_SPECIAL_CHARS |
- UnescapeRule::REPLACE_PLUS_WITH_SPACE);
- } else {
- *out_value = "";
- }
- return true;
- }
+ for (QueryIterator it(url); !it.IsAtEnd(); it.Advance()) {
+ if (it.GetKey() == search_key) {
+ *out_value = it.GetUnescapedValue();
+ return true;
}
}
return false;
diff --git a/net/base/url_util.h b/net/base/url_util.h
index 5d5e58b..83d0356 100644
--- a/net/base/url_util.h
+++ b/net/base/url_util.h
@@ -7,7 +7,9 @@
#include <string>
+#include "base/compiler_specific.h"
#include "net/base/net_export.h"
+#include "url/url_parse.h"
class GURL;
@@ -44,6 +46,30 @@ NET_EXPORT GURL AppendOrReplaceQueryParameter(const GURL& url,
const std::string& name,
const std::string& value);
+// Iterates over the key-value pairs in the query portion of |url|.
+class NET_EXPORT QueryIterator {
+ public:
+ explicit QueryIterator(const GURL& url);
+ ~QueryIterator();
+
+ std::string GetKey() const;
+ std::string GetValue() const;
+ const std::string& GetUnescapedValue();
+
+ bool IsAtEnd() const;
+ void Advance();
+
+ private:
+ const GURL& url_;
+ url_parse::Component query_;
+ bool at_end_;
+ url_parse::Component key_;
+ url_parse::Component value_;
+ std::string unescaped_value_;
+
+ DISALLOW_COPY_AND_ASSIGN(QueryIterator);
+};
+
// Looks for |search_key| in the query portion of |url|. Returns true if the
// key is found and sets |out_value| to the unescaped value for the key.
// Returns false if the key is not found.
diff --git a/net/base/url_util_unittest.cc b/net/base/url_util_unittest.cc
index 83ac258..158e274 100644
--- a/net/base/url_util_unittest.cc
+++ b/net/base/url_util_unittest.cc
@@ -108,5 +108,56 @@ TEST(UrlUtilTest, GetValueForKeyInQueryInvalidURL) {
EXPECT_FALSE(GetValueForKeyInQuery(url, "test", &value));
}
+TEST(UrlUtilTest, ParseQuery) {
+ const GURL url("http://example.com/path?name=value&boolParam&"
+ "url=http://test.com/q?n1%3Dv1%26n2&"
+ "multikey=value1&multikey=value2&multikey");
+ QueryIterator it(url);
+
+ ASSERT_FALSE(it.IsAtEnd());
+ EXPECT_EQ("name", it.GetKey());
+ EXPECT_EQ("value", it.GetValue());
+ EXPECT_EQ("value", it.GetUnescapedValue());
+ it.Advance();
+
+ ASSERT_FALSE(it.IsAtEnd());
+ EXPECT_EQ("boolParam", it.GetKey());
+ EXPECT_EQ("", it.GetValue());
+ EXPECT_EQ("", it.GetUnescapedValue());
+ it.Advance();
+
+ ASSERT_FALSE(it.IsAtEnd());
+ EXPECT_EQ("url", it.GetKey());
+ EXPECT_EQ("http://test.com/q?n1%3Dv1%26n2", it.GetValue());
+ EXPECT_EQ("http://test.com/q?n1=v1&n2", it.GetUnescapedValue());
+ it.Advance();
+
+ ASSERT_FALSE(it.IsAtEnd());
+ EXPECT_EQ("multikey", it.GetKey());
+ EXPECT_EQ("value1", it.GetValue());
+ EXPECT_EQ("value1", it.GetUnescapedValue());
+ it.Advance();
+
+ ASSERT_FALSE(it.IsAtEnd());
+ EXPECT_EQ("multikey", it.GetKey());
+ EXPECT_EQ("value2", it.GetValue());
+ EXPECT_EQ("value2", it.GetUnescapedValue());
+ it.Advance();
+
+ ASSERT_FALSE(it.IsAtEnd());
+ EXPECT_EQ("multikey", it.GetKey());
+ EXPECT_EQ("", it.GetValue());
+ EXPECT_EQ("", it.GetUnescapedValue());
+ it.Advance();
+
+ EXPECT_TRUE(it.IsAtEnd());
+}
+
+TEST(UrlUtilTest, ParseQueryInvalidURL) {
+ const GURL url("http://%01/?test");
+ QueryIterator it(url);
+ EXPECT_TRUE(it.IsAtEnd());
+}
+
} // namespace
} // namespace net