Add support for Omaha cohorts to the component updater.

BUG=638633,639569

Review-Url: https://codereview.chromium.org/2252093002
Cr-Commit-Position: refs/heads/master@{#413809}
diff --git a/components/update_client/persisted_data.cc b/components/update_client/persisted_data.cc
index 673ba1e6..4f2c34a 100644
--- a/components/update_client/persisted_data.cc
+++ b/components/update_client/persisted_data.cc
@@ -28,33 +28,62 @@
   DCHECK(thread_checker_.CalledOnValidThread());
 }
 
-int PersistedData::GetDateLastRollCall(const std::string& id) const {
+int PersistedData::GetInt(const std::string& id,
+                          const std::string& key,
+                          int fallback) const {
   DCHECK(thread_checker_.CalledOnValidThread());
-  if (!pref_service_)
-    return kDateLastRollCallUnknown;
-  int dlrc = kDateLastRollCallUnknown;
-  const base::DictionaryValue* dict =
-      pref_service_->GetDictionary(kPersistedDataPreference);
   // We assume ids do not contain '.' characters.
   DCHECK_EQ(std::string::npos, id.find('.'));
-  if (!dict->GetInteger(base::StringPrintf("apps.%s.dlrc", id.c_str()), &dlrc))
-    return kDateLastRollCallUnknown;
-  return dlrc;
+  if (!pref_service_)
+    return fallback;
+  const base::DictionaryValue* dict =
+      pref_service_->GetDictionary(kPersistedDataPreference);
+  if (!dict)
+    return fallback;
+  int result = 0;
+  return dict->GetInteger(
+             base::StringPrintf("apps.%s.%s", id.c_str(), key.c_str()), &result)
+             ? result
+             : fallback;
+}
+
+std::string PersistedData::GetString(const std::string& id,
+                                     const std::string& key) const {
+  DCHECK(thread_checker_.CalledOnValidThread());
+  // We assume ids do not contain '.' characters.
+  DCHECK_EQ(std::string::npos, id.find('.'));
+  if (!pref_service_)
+    return std::string();
+  const base::DictionaryValue* dict =
+      pref_service_->GetDictionary(kPersistedDataPreference);
+  if (!dict)
+    return std::string();
+  std::string result;
+  return dict->GetString(
+             base::StringPrintf("apps.%s.%s", id.c_str(), key.c_str()), &result)
+             ? result
+             : std::string();
+}
+
+int PersistedData::GetDateLastRollCall(const std::string& id) const {
+  return GetInt(id, "dlrc", kDateLastRollCallUnknown);
 }
 
 std::string PersistedData::GetPingFreshness(const std::string& id) const {
-  DCHECK(thread_checker_.CalledOnValidThread());
-  if (!pref_service_)
-    return std::string();
-  std::string freshness;
-  const base::DictionaryValue* dict =
-      pref_service_->GetDictionary(kPersistedDataPreference);
-  // We assume ids do not contain '.' characters.
-  DCHECK_EQ(std::string::npos, id.find('.'));
-  if (!dict->GetString(base::StringPrintf("apps.%s.pf", id.c_str()),
-                       &freshness))
-    return std::string();
-  return base::StringPrintf("{%s}", freshness.c_str());
+  std::string result = GetString(id, "pf");
+  return !result.empty() ? base::StringPrintf("{%s}", result.c_str()) : result;
+}
+
+std::string PersistedData::GetCohort(const std::string& id) const {
+  return GetString(id, "cohort");
+}
+
+std::string PersistedData::GetCohortName(const std::string& id) const {
+  return GetString(id, "cohortname");
+}
+
+std::string PersistedData::GetCohortHint(const std::string& id) const {
+  return GetString(id, "cohorthint");
 }
 
 void PersistedData::SetDateLastRollCall(const std::vector<std::string>& ids,
@@ -72,6 +101,32 @@
   }
 }
 
+void PersistedData::SetString(const std::string& id,
+                              const std::string& key,
+                              const std::string& value) {
+  DCHECK(thread_checker_.CalledOnValidThread());
+  if (!pref_service_)
+    return;
+  DictionaryPrefUpdate update(pref_service_, kPersistedDataPreference);
+  update->SetString(base::StringPrintf("apps.%s.%s", id.c_str(), key.c_str()),
+                    value);
+}
+
+void PersistedData::SetCohort(const std::string& id,
+                              const std::string& cohort) {
+  SetString(id, "cohort", cohort);
+}
+
+void PersistedData::SetCohortName(const std::string& id,
+                                  const std::string& cohort_name) {
+  SetString(id, "cohortname", cohort_name);
+}
+
+void PersistedData::SetCohortHint(const std::string& id,
+                                  const std::string& cohort_hint) {
+  SetString(id, "cohorthint", cohort_hint);
+}
+
 void PersistedData::RegisterPrefs(PrefRegistrySimple* registry) {
   registry->RegisterDictionaryPref(kPersistedDataPreference);
 }
diff --git a/components/update_client/persisted_data.h b/components/update_client/persisted_data.h
index 6b24e57..06d4d0d2 100644
--- a/components/update_client/persisted_data.h
+++ b/components/update_client/persisted_data.h
@@ -52,7 +52,27 @@
   // This is called only via update_client's RegisterUpdateClientPreferences.
   static void RegisterPrefs(PrefRegistrySimple* registry);
 
+  // These functions return cohort data for the specified |id|. "Cohort"
+  // indicates the membership of the client in any release channels components
+  // have set up in a machine-readable format, while "CohortName" does so in a
+  // human-readable form. "CohortHint" indicates the client's channel selection
+  // preference.
+  std::string GetCohort(const std::string& id) const;
+  std::string GetCohortHint(const std::string& id) const;
+  std::string GetCohortName(const std::string& id) const;
+
+  // These functions set cohort data for the specified |id|.
+  void SetCohort(const std::string& id, const std::string& cohort);
+  void SetCohortHint(const std::string& id, const std::string& cohort_hint);
+  void SetCohortName(const std::string& id, const std::string& cohort_name);
+
  private:
+  int GetInt(const std::string& id, const std::string& key, int fallback) const;
+  std::string GetString(const std::string& id, const std::string& key) const;
+  void SetString(const std::string& id,
+                 const std::string& key,
+                 const std::string& value);
+
   base::ThreadChecker thread_checker_;
   PrefService* pref_service_;
 
diff --git a/components/update_client/persisted_data_unittest.cc b/components/update_client/persisted_data_unittest.cc
index dfe242c2..cc5f875 100644
--- a/components/update_client/persisted_data_unittest.cc
+++ b/components/update_client/persisted_data_unittest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <memory>
 #include <string>
 #include <vector>
 
@@ -50,4 +51,41 @@
   EXPECT_EQ(-2, metadata->GetDateLastRollCall("someotherappid"));
 }
 
+TEST(PersistedDataTest, SimpleCohort) {
+  std::unique_ptr<TestingPrefServiceSimple> pref(
+      new TestingPrefServiceSimple());
+  PersistedData::RegisterPrefs(pref->registry());
+  std::unique_ptr<PersistedData> metadata(new PersistedData(pref.get()));
+  EXPECT_EQ("", metadata->GetCohort("someappid"));
+  EXPECT_EQ("", metadata->GetCohort("someotherappid"));
+  EXPECT_EQ("", metadata->GetCohortHint("someappid"));
+  EXPECT_EQ("", metadata->GetCohortHint("someotherappid"));
+  EXPECT_EQ("", metadata->GetCohortName("someappid"));
+  EXPECT_EQ("", metadata->GetCohortName("someotherappid"));
+  metadata->SetCohort("someappid", "c1");
+  metadata->SetCohort("someotherappid", "c2");
+  metadata->SetCohortHint("someappid", "ch1");
+  metadata->SetCohortHint("someotherappid", "ch2");
+  metadata->SetCohortName("someappid", "cn1");
+  metadata->SetCohortName("someotherappid", "cn2");
+  EXPECT_EQ("c1", metadata->GetCohort("someappid"));
+  EXPECT_EQ("c2", metadata->GetCohort("someotherappid"));
+  EXPECT_EQ("ch1", metadata->GetCohortHint("someappid"));
+  EXPECT_EQ("ch2", metadata->GetCohortHint("someotherappid"));
+  EXPECT_EQ("cn1", metadata->GetCohortName("someappid"));
+  EXPECT_EQ("cn2", metadata->GetCohortName("someotherappid"));
+  metadata->SetCohort("someappid", "oc1");
+  metadata->SetCohort("someotherappid", "oc2");
+  metadata->SetCohortHint("someappid", "och1");
+  metadata->SetCohortHint("someotherappid", "och2");
+  metadata->SetCohortName("someappid", "ocn1");
+  metadata->SetCohortName("someotherappid", "ocn2");
+  EXPECT_EQ("oc1", metadata->GetCohort("someappid"));
+  EXPECT_EQ("oc2", metadata->GetCohort("someotherappid"));
+  EXPECT_EQ("och1", metadata->GetCohortHint("someappid"));
+  EXPECT_EQ("och2", metadata->GetCohortHint("someotherappid"));
+  EXPECT_EQ("ocn1", metadata->GetCohortName("someappid"));
+  EXPECT_EQ("ocn2", metadata->GetCohortName("someotherappid"));
+}
+
 }  // namespace update_client
diff --git a/components/update_client/update_checker.cc b/components/update_client/update_checker.cc
index a5ec266..c1cf4d7 100644
--- a/components/update_client/update_checker.cc
+++ b/components/update_client/update_checker.cc
@@ -91,6 +91,15 @@
       base::StringAppendF(&app, " %s=\"%s\"", attr.first.c_str(),
                           attr.second.c_str());
     }
+    const std::string cohort = metadata->GetCohort(item->id);
+    const std::string cohort_name = metadata->GetCohortName(item->id);
+    const std::string cohort_hint = metadata->GetCohortHint(item->id);
+    if (!cohort.empty())
+      base::StringAppendF(&app, " cohort=\"%s\"", cohort.c_str());
+    if (!cohort_name.empty())
+      base::StringAppendF(&app, " cohortname=\"%s\"", cohort_name.c_str());
+    if (!cohort_hint.empty())
+      base::StringAppendF(&app, " cohorthint=\"%s\"", cohort_hint.c_str());
     base::StringAppendF(&app, ">");
 
     base::StringAppendF(&app, "<updatecheck");
@@ -203,6 +212,17 @@
       int daynum = update_response.results().daystart_elapsed_days;
       if (daynum != UpdateResponse::kNoDaystart)
         metadata_->SetDateLastRollCall(*ids_checked, daynum);
+      for (const auto& result : update_response.results().list) {
+        auto entry = result.cohort_attrs.find(UpdateResponse::Result::kCohort);
+        if (entry != result.cohort_attrs.end())
+          metadata_->SetCohort(result.extension_id, entry->second);
+        entry = result.cohort_attrs.find(UpdateResponse::Result::kCohortName);
+        if (entry != result.cohort_attrs.end())
+          metadata_->SetCohortName(result.extension_id, entry->second);
+        entry = result.cohort_attrs.find(UpdateResponse::Result::kCohortHint);
+        if (entry != result.cohort_attrs.end())
+          metadata_->SetCohortHint(result.extension_id, entry->second);
+      }
       base::ThreadTaskRunnerHandle::Get()->PostTask(
           FROM_HERE, base::Bind(update_check_callback_, error,
                                 update_response.results(), retry_after_sec));
diff --git a/components/update_client/update_response.cc b/components/update_client/update_response.cc
index 453f6a2..41670d3 100644
--- a/components/update_client/update_response.cc
+++ b/components/update_client/update_response.cc
@@ -9,6 +9,7 @@
 #include <algorithm>
 #include <memory>
 
+#include "base/memory/ptr_util.h"
 #include "base/stl_util.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
@@ -20,6 +21,9 @@
 namespace update_client {
 
 static const char* kExpectedResponseProtocol = "3.0";
+const char UpdateResponse::Result::kCohort[] = "cohort";
+const char UpdateResponse::Result::kCohortHint[] = "cohorthint";
+const char UpdateResponse::Result::kCohortName[] = "cohortname";
 
 UpdateResponse::UpdateResponse() {
 }
@@ -32,8 +36,7 @@
 UpdateResponse::Results::~Results() {
 }
 
-UpdateResponse::Result::Result() {
-}
+UpdateResponse::Result::Result() {}
 UpdateResponse::Result::Result(const Result& other) = default;
 UpdateResponse::Result::~Result() {
 }
@@ -93,6 +96,21 @@
   return std::string();
 }
 
+// Returns the value of a named attribute, or nullptr .
+static std::unique_ptr<std::string> GetAttributePtr(
+    xmlNode* node,
+    const char* attribute_name) {
+  const xmlChar* name = reinterpret_cast<const xmlChar*>(attribute_name);
+  for (xmlAttr* attr = node->properties; attr != NULL; attr = attr->next) {
+    if (!xmlStrcmp(attr->name, name) && attr->children &&
+        attr->children->content) {
+      return base::MakeUnique<std::string>(
+          reinterpret_cast<const char*>(attr->children->content));
+    }
+  }
+  return nullptr;
+}
+
 // This is used for the xml parser to report errors. This assumes the context
 // is a pointer to a std::string where the error message should be appended.
 static void XmlErrorFunc(void* context, const char* message, ...) {
@@ -268,6 +286,17 @@
 bool ParseAppTag(xmlNode* app,
                  UpdateResponse::Result* result,
                  std::string* error) {
+  // Read cohort information.
+  auto cohort = GetAttributePtr(app, "cohort");
+  static const char* attrs[] = {UpdateResponse::Result::kCohort,
+                                UpdateResponse::Result::kCohortHint,
+                                UpdateResponse::Result::kCohortName};
+  for (const auto& attr : attrs) {
+    auto value = GetAttributePtr(app, attr);
+    if (value)
+      result->cohort_attrs.insert({attr, *value});
+  }
+
   // Read the crx id.
   result->extension_id = GetAttribute(app, "appid");
   if (result->extension_id.empty()) {
diff --git a/components/update_client/update_response.h b/components/update_client/update_response.h
index 6c0ea9bd..01b49aaa 100644
--- a/components/update_client/update_response.h
+++ b/components/update_client/update_response.h
@@ -5,6 +5,8 @@
 #ifndef COMPONENTS_UPDATE_CLIENT_UPDATE_RESPONSE_H_
 #define COMPONENTS_UPDATE_CLIENT_UPDATE_RESPONSE_H_
 
+#include <map>
+#include <memory>
 #include <string>
 #include <vector>
 
@@ -99,6 +101,15 @@
     std::vector<GURL> crx_diffurls;
 
     Manifest manifest;
+
+    // The server has instructed the client to set its [key] to [value] for each
+    // key-value pair in this string.
+    std::map<std::string, std::string> cohort_attrs;
+
+    // The following are the only allowed keys in |cohort_attrs|.
+    static const char kCohort[];
+    static const char kCohortHint[];
+    static const char kCohortName[];
   };
 
   static const int kNoDaystart = -1;
diff --git a/components/update_client/update_response_unittest.cc b/components/update_client/update_response_unittest.cc
index 20f08b07..b4160240 100644
--- a/components/update_client/update_response_unittest.cc
+++ b/components/update_client/update_response_unittest.cc
@@ -220,6 +220,27 @@
     " </app>"
     "</response>";
 
+// Includes two <app> tags, both of which set the cohort.
+const char* kTwoAppsSetCohort =
+    "<?xml version='1.0' encoding='UTF-8'?>"
+    "<response protocol='3.0'>"
+    " <app appid='aaaaaaaa' cohort='1:2q3/'>"
+    "  <updatecheck status='noupdate'/>"
+    " </app>"
+    " <app appid='bbbbbbbb' cohort='1:[email protected]' cohortname='cname'>"
+    "   <updatecheck status='ok'>"
+    "     <urls>"
+    "       <url codebase='http://example.com/'/>"
+    "     </urls>"
+    "     <manifest version='1.2.3.4' prodversionmin='2.0.143.0'>"
+    "       <packages>"
+    "         <package name='extension_1_2_3_4.crx'/>"
+    "       </packages>"
+    "     </manifest>"
+    "   </updatecheck>"
+    " </app>"
+    "</response>";
+
 TEST(ComponentUpdaterUpdateResponseTest, TestParser) {
   UpdateResponse parser;
 
@@ -312,6 +333,30 @@
   EXPECT_EQ(1u, parser.results().list.size());
   firstResult = &parser.results().list[0];
   EXPECT_EQ(firstResult->extension_id, "bbbbbbbb");
+
+  // Parse xml with two apps setting the cohort info.
+  EXPECT_TRUE(parser.Parse(kTwoAppsSetCohort));
+  EXPECT_TRUE(parser.errors().empty());
+  EXPECT_EQ(2u, parser.results().list.size());
+  firstResult = &parser.results().list[0];
+  EXPECT_EQ(firstResult->extension_id, "aaaaaaaa");
+  EXPECT_NE(firstResult->cohort_attrs.find("cohort"),
+            firstResult->cohort_attrs.end());
+  EXPECT_EQ(firstResult->cohort_attrs.find("cohort")->second, "1:2q3/");
+  EXPECT_EQ(firstResult->cohort_attrs.find("cohortname"),
+            firstResult->cohort_attrs.end());
+  EXPECT_EQ(firstResult->cohort_attrs.find("cohorthint"),
+            firstResult->cohort_attrs.end());
+  const UpdateResponse::Result* secondResult = &parser.results().list[1];
+  EXPECT_EQ(secondResult->extension_id, "bbbbbbbb");
+  EXPECT_NE(secondResult->cohort_attrs.find("cohort"),
+            secondResult->cohort_attrs.end());
+  EXPECT_EQ(secondResult->cohort_attrs.find("cohort")->second, "1:[email protected]");
+  EXPECT_NE(secondResult->cohort_attrs.find("cohortname"),
+            secondResult->cohort_attrs.end());
+  EXPECT_EQ(secondResult->cohort_attrs.find("cohortname")->second, "cname");
+  EXPECT_EQ(secondResult->cohort_attrs.find("cohorthint"),
+            secondResult->cohort_attrs.end());
 }
 
 }  // namespace update_client