Add support to chrome://flags for command line flags that need to be treated as a list of origins

This CL adds support for command line flags that are lists of url::Origins. E.g. The value in --flag=value will now be modifyable from the chrome://flags page using a free form textbox, and will be treated as a comma separated list of origins (e.g. http://example1.test,http://example2.test)

The string from the textbox is transformed as follows:
- Continuous whitespace characters are collapsed into single a space
- String is tokenized using space and comma as delimiters
- Each token is parsed as a GURL. Invalid URLs or URLs with a scheme other than http and https are discarded.
- Remaining URLs are converted to url::Origins, then joined into a single, comma separated string.

The CL also adds --unsafely-treat-insecure-origin-as-secure as the first such flag to chrome://flags. Developers will now be able to modify the list of insecure origins treated as secure from the chrome://flags page on all platforms.

Bug: 834381
Change-Id: Iad44b5b2724687c7bea1ae45c23ccc910eb5cc9f
Reviewed-on: https://chromium-review.googlesource.com/1038152
Reviewed-by: Lei Zhang <[email protected]>
Reviewed-by: Mattias Nissler <[email protected]>
Reviewed-by: Mike West <[email protected]>
Reviewed-by: Edward Jung <[email protected]>
Reviewed-by: Alexei Svitkine <[email protected]>
Commit-Queue: Mustafa Emre Acer <[email protected]>
Cr-Commit-Position: refs/heads/master@{#560791}
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index c6360dd5..326032de 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -3885,6 +3885,11 @@
      flag_descriptions::kWebSocketHandshakeReuseConnectionDescription, kOsAll,
      FEATURE_VALUE_TYPE(net::WebSocketBasicHandshakeStream::
                             kWebSocketHandshakeReuseConnection)},
+    {"unsafely-treat-insecure-origin-as-secure",
+     flag_descriptions::kTreatInsecureOriginAsSecureName,
+     flag_descriptions::kTreatInsecureOriginAsSecureDescription, kOsAll,
+     ORIGIN_LIST_VALUE_TYPE(switches::kUnsafelyTreatInsecureOriginAsSecure,
+                            "")},
 
     // NOTE: Adding a new flag requires adding a corresponding entry to enum
     // "LoginCustomFlags" in tools/metrics/histograms/enums.xml. See "Flag
@@ -4052,6 +4057,13 @@
       flags_storage, internal_name, enable);
 }
 
+void SetOriginListFlag(const std::string& internal_name,
+                       const std::string& value,
+                       flags_ui::FlagsStorage* flags_storage) {
+  FlagsStateSingleton::GetFlagsState()->SetOriginListFlag(internal_name, value,
+                                                          flags_storage);
+}
+
 void RemoveFlagsSwitches(base::CommandLine::SwitchMap* switch_list) {
   FlagsStateSingleton::GetFlagsState()->RemoveFlagsSwitches(switch_list);
 }
diff --git a/chrome/browser/about_flags.h b/chrome/browser/about_flags.h
index 9714dea8..199f571 100644
--- a/chrome/browser/about_flags.h
+++ b/chrome/browser/about_flags.h
@@ -72,6 +72,17 @@
                             const std::string& internal_name,
                             bool enable);
 
+// Sets a flag value with a list of origins given by |value|. Origins in |value|
+// can be separated by a comma or whitespace. Invalid URLs will be dropped when
+// setting the command line flag.
+// E.g. SetOriginListFlag("test-flag",
+//                        "http://example.test1 http://example.test2",
+//                        flags_storage);
+// will add --test-flag=http://example.test to the command line.
+void SetOriginListFlag(const std::string& internal_name,
+                       const std::string& value,
+                       flags_ui::FlagsStorage* flags_storage);
+
 // Removes all switches that were added to a command line by a previous call to
 // |ConvertFlagsToSwitches()|.
 void RemoveFlagsSwitches(base::CommandLine::SwitchMap* switch_list);
diff --git a/chrome/browser/about_flags_browsertest.cc b/chrome/browser/about_flags_browsertest.cc
new file mode 100644
index 0000000..29197cf
--- /dev/null
+++ b/chrome/browser/about_flags_browsertest.cc
@@ -0,0 +1,141 @@
+// Copyright 2018 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 "base/command_line.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/interactive_test_utils.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_test_utils.h"
+#include "ui/base/window_open_disposition.h"
+
+namespace {
+
+const char kSwitchName[] = "unsafely-treat-insecure-origin-as-secure";
+
+void SimulateTextType(content::WebContents* contents,
+                      const char* experiment_id,
+                      const char* text) {
+  EXPECT_TRUE(content::ExecuteScript(
+      contents, base::StringPrintf(
+                    "var parent = document.getElementById('%s');"
+                    "var textarea = parent.getElementsByTagName('textarea')[0];"
+                    "textarea.focus();"
+                    "textarea.value = `%s`;"
+                    "textarea.onchange();",
+                    experiment_id, text)));
+}
+
+void ToggleEnableDropdown(content::WebContents* contents,
+                          const char* experiment_id,
+                          bool enable) {
+  EXPECT_TRUE(content::ExecuteScript(
+      contents,
+      base::StringPrintf(
+          "var k = "
+          "document.getElementById('%s');"
+          "var s = k.getElementsByClassName('experiment-enable-disable')[0];"
+          "s.focus();"
+          "s.selectedIndex = %d;"
+          "s.onchange();",
+          experiment_id, enable ? 1 : 0)));
+}
+
+void SetSwitch(base::CommandLine::SwitchMap* switch_map,
+               const std::string& switch_name,
+               const std::string& switch_value) {
+#if defined(OS_WIN)
+  (*switch_map)[switch_name] = base::ASCIIToUTF16(switch_value.c_str());
+#else
+  (*switch_map)[switch_name] = switch_value;
+#endif
+}
+
+class AboutFlagsBrowserTest : public InProcessBrowserTest {};
+
+// Tests experiments with origin values in chrome://flags page.
+IN_PROC_BROWSER_TEST_F(AboutFlagsBrowserTest, OriginFlag) {
+  ui_test_utils::NavigateToURL(browser(), GURL("chrome://flags"));
+
+  const base::CommandLine::SwitchMap switches =
+      base::CommandLine::ForCurrentProcess()->GetSwitches();
+
+  content::WebContents* contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  // Type a value in the experiment's textarea. Since the flag state is
+  // "Disabled" by default, command line shouldn't change.
+  SimulateTextType(contents, kSwitchName, "http://example.test");
+  EXPECT_EQ(switches, base::CommandLine::ForCurrentProcess()->GetSwitches());
+
+  // Enable the experiment. Command line should change.
+  ToggleEnableDropdown(contents, kSwitchName, true);
+  base::CommandLine::SwitchMap expected_switches = switches;
+  SetSwitch(&expected_switches, kSwitchName, "http://example.test");
+  EXPECT_EQ(expected_switches,
+            base::CommandLine::ForCurrentProcess()->GetSwitches());
+
+  // Typing while enabled should immediately change the flag.
+  SimulateTextType(contents, kSwitchName, "http://example.test.com");
+  SetSwitch(&expected_switches, kSwitchName, "http://example.test.com");
+  EXPECT_EQ(expected_switches,
+            base::CommandLine::ForCurrentProcess()->GetSwitches());
+
+  // Disable the experiment. Command line switch should be cleared.
+  ToggleEnableDropdown(contents, kSwitchName, false);
+  expected_switches.erase(kSwitchName);
+  EXPECT_EQ(expected_switches,
+            base::CommandLine::ForCurrentProcess()->GetSwitches());
+
+  // Enable again. Command line switch should be added back.
+  ToggleEnableDropdown(contents, kSwitchName, true);
+  SetSwitch(&expected_switches, kSwitchName, "http://example.test.com");
+  EXPECT_EQ(expected_switches,
+            base::CommandLine::ForCurrentProcess()->GetSwitches());
+
+  // Disable again and type. Command line switch should stay cleared.
+  ToggleEnableDropdown(contents, kSwitchName, false);
+  SimulateTextType(contents, kSwitchName, "http://example.test2.com");
+  expected_switches.erase(kSwitchName);
+  EXPECT_EQ(expected_switches,
+            base::CommandLine::ForCurrentProcess()->GetSwitches());
+
+  // Enable one last time. Command line should pick up the last typed value.
+  ToggleEnableDropdown(contents, kSwitchName, true);
+  SetSwitch(&expected_switches, kSwitchName, "http://example.test2.com");
+  EXPECT_EQ(expected_switches,
+            base::CommandLine::ForCurrentProcess()->GetSwitches());
+}
+
+// Tests that only valid http and https origins should be added to the command
+// line when modified from chrome://flags.
+IN_PROC_BROWSER_TEST_F(AboutFlagsBrowserTest, StringFlag) {
+  ui_test_utils::NavigateToURL(browser(), GURL("chrome://flags"));
+
+  const base::CommandLine::SwitchMap switches =
+      base::CommandLine::ForCurrentProcess()->GetSwitches();
+
+  content::WebContents* contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  const char kValue[] =
+      "http://example.test/path    http://example2.test/?query\n"
+      "invalid-value, filesystem:http://example.test.file, "
+      "ws://example3.test http://&^.com";
+
+  ToggleEnableDropdown(contents, kSwitchName, true);
+  SimulateTextType(contents, kSwitchName, kValue);
+  base::CommandLine::SwitchMap expected_switches = switches;
+  SetSwitch(&expected_switches, kSwitchName,
+            "http://example.test,http://example2.test,ws://example3.test");
+  EXPECT_EQ(expected_switches,
+            base::CommandLine::ForCurrentProcess()->GetSwitches());
+}
+
+}  // namespace
diff --git a/chrome/browser/about_flags_unittest.cc b/chrome/browser/about_flags_unittest.cc
index e9fbe31..77d24923 100644
--- a/chrome/browser/about_flags_unittest.cc
+++ b/chrome/browser/about_flags_unittest.cc
@@ -182,6 +182,9 @@
       case flags_ui::FeatureEntry::SINGLE_DISABLE_VALUE:
         result.insert(entry.command_line_switch);
         break;
+      case flags_ui::FeatureEntry::ORIGIN_LIST_VALUE:
+        // Do nothing, origin list values are not added as feature flags.
+        break;
       case flags_ui::FeatureEntry::MULTI_VALUE:
         for (int j = 0; j < entry.num_options; ++j) {
           result.insert(entry.ChoiceForOption(j).command_line_switch);
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index d816885..a52329b 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -1708,6 +1708,14 @@
     "Improved Translate UI triggering logic. TranslateRanker decides whether "
     "or not Translate UI should be triggered in a given context.";
 
+const char kTreatInsecureOriginAsSecureName[] =
+    "Insecure origins treated as secure";
+const char kTreatInsecureOriginAsSecureDescription[] =
+    "Treat given (insecure) origins as secure origins. Multiple origins can be "
+    "supplied as a comma-separated list. For the definition of secure "
+    "contexts, "
+    "see https://w3c.github.io/webappsec-secure-contexts/";
+
 const char kTrySupportedChannelLayoutsName[] =
     "Causes audio output streams to check if channel layouts other than the "
     "default hardware layout are available.";
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 82e23c59..e264c03 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1043,6 +1043,9 @@
 extern const char kTranslateRankerEnforcementName[];
 extern const char kTranslateRankerEnforcementDescription[];
 
+extern const char kTreatInsecureOriginAsSecureName[];
+extern const char kTreatInsecureOriginAsSecureDescription[];
+
 extern const char kTrySupportedChannelLayoutsName[];
 extern const char kTrySupportedChannelLayoutsDescription[];
 
diff --git a/chrome/browser/ui/webui/flags_ui.cc b/chrome/browser/ui/webui/flags_ui.cc
index 7bba630..eca2c98 100644
--- a/chrome/browser/ui/webui/flags_ui.cc
+++ b/chrome/browser/ui/webui/flags_ui.cc
@@ -113,6 +113,9 @@
   // Callback for the "enableExperimentalFeature" message.
   void HandleEnableExperimentalFeatureMessage(const base::ListValue* args);
 
+  // Callback for the "setOriginListFlag" message.
+  void HandleSetOriginListFlagMessage(const base::ListValue* args);
+
   // Callback for the "restartBrowser" message. Restores all tabs on restart.
   void HandleRestartBrowser(const base::ListValue* args);
 
@@ -138,6 +141,10 @@
           &FlagsDOMHandler::HandleEnableExperimentalFeatureMessage,
           base::Unretained(this)));
   web_ui()->RegisterMessageCallback(
+      flags_ui::kSetOriginListFlag,
+      base::BindRepeating(&FlagsDOMHandler::HandleSetOriginListFlagMessage,
+                          base::Unretained(this)));
+  web_ui()->RegisterMessageCallback(
       flags_ui::kRestartBrowser,
       base::BindRepeating(&FlagsDOMHandler::HandleRestartBrowser,
                           base::Unretained(this)));
@@ -210,6 +217,26 @@
                                       enable_str == "true");
 }
 
+void FlagsDOMHandler::HandleSetOriginListFlagMessage(
+    const base::ListValue* args) {
+  DCHECK(flags_storage_);
+  if (args->GetSize() != 2) {
+    NOTREACHED();
+    return;
+  }
+
+  std::string entry_internal_name;
+  std::string value_str;
+  if (!args->GetString(0, &entry_internal_name) ||
+      !args->GetString(1, &value_str) || entry_internal_name.empty()) {
+    NOTREACHED();
+    return;
+  }
+
+  about_flags::SetOriginListFlag(entry_internal_name, value_str,
+                                 flags_storage_.get());
+}
+
 void FlagsDOMHandler::HandleRestartBrowser(const base::ListValue* args) {
   DCHECK(flags_storage_);
 #if defined(OS_CHROMEOS)
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 317349ee..698aa1e3 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -4583,6 +4583,7 @@
 
   test("interactive_ui_tests") {
     sources = [
+      "../browser/about_flags_browsertest.cc",
       "../browser/apps/app_browsertest_util.cc",
       "../browser/apps/app_browsertest_util.h",
       "../browser/apps/app_pointer_lock_interactive_uitest.cc",
diff --git a/components/flags_ui/BUILD.gn b/components/flags_ui/BUILD.gn
index 9d085a1..e554054 100644
--- a/components/flags_ui/BUILD.gn
+++ b/components/flags_ui/BUILD.gn
@@ -26,6 +26,7 @@
     "//components/strings",
     "//components/variations",
     "//ui/base",
+    "//url",
   ]
 }
 
diff --git a/components/flags_ui/DEPS b/components/flags_ui/DEPS
index ad5532c..89103e5 100644
--- a/components/flags_ui/DEPS
+++ b/components/flags_ui/DEPS
@@ -4,4 +4,5 @@
   "+components/strings/grit/components_strings.h",
   "+components/variations",
   "+ui/base",
+  "+url",
 ]
diff --git a/components/flags_ui/feature_entry.h b/components/flags_ui/feature_entry.h
index 073ecea..15a2610 100644
--- a/components/flags_ui/feature_entry.h
+++ b/components/flags_ui/feature_entry.h
@@ -73,6 +73,10 @@
     // feature is overriden to be enabled and empty set of parameters is used
     // boiling down to the default behavior in the code.
     FEATURE_WITH_PARAMS_VALUE,
+
+    // Corresponds to a command line switch where the value is treatead as a
+    // list of url::Origins. Default state is disabled like SINGLE_VALUE.
+    ORIGIN_LIST_VALUE
   };
 
   // Describes state of a feature.
diff --git a/components/flags_ui/feature_entry_macros.h b/components/flags_ui/feature_entry_macros.h
index e474374..3d52071 100644
--- a/components/flags_ui/feature_entry_macros.h
+++ b/components/flags_ui/feature_entry_macros.h
@@ -15,6 +15,9 @@
       nullptr, nullptr, nullptr, 0, nullptr, nullptr, nullptr
 #define SINGLE_VALUE_TYPE(command_line_switch) \
   SINGLE_VALUE_TYPE_AND_VALUE(command_line_switch, "")
+#define ORIGIN_LIST_VALUE_TYPE(command_line_switch, switch_value) \
+  flags_ui::FeatureEntry::ORIGIN_LIST_VALUE, command_line_switch, \
+      switch_value, nullptr, nullptr, nullptr, 0, nullptr, nullptr, nullptr
 #define SINGLE_DISABLE_VALUE_TYPE_AND_VALUE(command_line_switch, switch_value) \
   flags_ui::FeatureEntry::SINGLE_DISABLE_VALUE, command_line_switch,           \
       switch_value, nullptr, nullptr, nullptr, 0, nullptr, nullptr, nullptr
diff --git a/components/flags_ui/flags_state.cc b/components/flags_ui/flags_state.cc
index b28bd45..7218bfd 100644
--- a/components/flags_ui/flags_state.cc
+++ b/components/flags_ui/flags_state.cc
@@ -16,6 +16,7 @@
 #include "base/metrics/field_trial.h"
 #include "base/stl_util.h"
 #include "base/strings/string_piece.h"
+#include "base/strings/string_tokenizer.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/values.h"
@@ -25,6 +26,8 @@
 #include "components/flags_ui/flags_ui_switches.h"
 #include "components/variations/variations_associated_data.h"
 #include "ui/base/l10n/l10n_util.h"
+#include "url/gurl.h"
+#include "url/origin.h"
 
 namespace flags_ui {
 
@@ -116,6 +119,7 @@
   switch (e.type) {
     case FeatureEntry::SINGLE_VALUE:
     case FeatureEntry::SINGLE_DISABLE_VALUE:
+    case FeatureEntry::ORIGIN_LIST_VALUE:
       names->insert(e.internal_name);
       break;
     case FeatureEntry::MULTI_VALUE:
@@ -134,6 +138,7 @@
   switch (e.type) {
     case FeatureEntry::SINGLE_VALUE:
     case FeatureEntry::SINGLE_DISABLE_VALUE:
+    case FeatureEntry::ORIGIN_LIST_VALUE:
       DCHECK_EQ(0, e.num_options);
       DCHECK(!e.choices);
       return true;
@@ -174,6 +179,7 @@
   switch (entry.type) {
     case FeatureEntry::SINGLE_VALUE:
     case FeatureEntry::SINGLE_DISABLE_VALUE:
+    case FeatureEntry::ORIGIN_LIST_VALUE:
       return enabled_entries.count(entry.internal_name) == 0;
     case FeatureEntry::MULTI_VALUE:
     case FeatureEntry::ENABLE_DISABLE_VALUE:
@@ -235,6 +241,51 @@
   return trial;
 }
 
+// Returns true if |value| is safe to include in a command line string in the
+// form of --flag=value.
+bool IsSafeValue(const std::string& value) {
+  // Punctuation characters at the end ("-", ".", ":", "/") are allowed because
+  // origins can contain those (e.g. http://example.test). Comma is allowed
+  // because it's used as the separator character.
+  static const char kAllowedChars[] =
+      "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+      "abcdefghijklmnopqrstuvwxyz"
+      "0123456789"
+      "-.:/,";
+  return value.find_first_not_of(kAllowedChars) == std::string::npos;
+}
+
+// Sanitizes |value| which contains a list of origins separated by whitespace
+// and/or comma. The sanitized value is added as a command line argument, so
+// this is a security critical operation: The sanitized value must have no
+// whitespaces, each individual origin must be separated by a comma, and each
+// origin must represent a url::Origin().
+std::string SanitizeOriginListFlag(const std::string& value) {
+  const std::string input = base::CollapseWhitespaceASCII(value, false);
+  const std::string delimiters = " ,";
+  base::StringTokenizer tokenizer(input, delimiters);
+  std::vector<std::string> origin_strings;
+  while (tokenizer.GetNext()) {
+    const std::string token = tokenizer.token();
+    if (token.empty()) {
+      continue;
+    }
+    const GURL url(token);
+    if (!url.is_valid() ||
+        (!url.SchemeIsHTTPOrHTTPS() && !url.SchemeIsWSOrWSS())) {
+      continue;
+    }
+    const std::string origin = url::Origin::Create(url).Serialize();
+    if (!IsSafeValue(origin)) {
+      continue;
+    }
+    origin_strings.push_back(origin);
+  }
+  const std::string result = base::JoinString(origin_strings, ",");
+  CHECK(IsSafeValue(result));
+  return result;
+}
+
 }  // namespace
 
 struct FlagsState::SwitchEntry {
@@ -347,20 +398,20 @@
   std::set<std::string> enabled_entries;
   GetSanitizedEnabledFlags(flags_storage, &enabled_entries);
 
-  const FeatureEntry* e = nullptr;
-  for (size_t i = 0; i < num_feature_entries_; ++i) {
-    if (feature_entries_[i].internal_name == internal_name) {
-      e = feature_entries_ + i;
-      break;
-    }
-  }
+  const FeatureEntry* e = FindFeatureEntryByName(internal_name);
   DCHECK(e);
 
-  if (e->type == FeatureEntry::SINGLE_VALUE) {
+  if (e->type == FeatureEntry::SINGLE_VALUE ||
+      e->type == FeatureEntry::ORIGIN_LIST_VALUE) {
     if (enable)
       needs_restart_ |= enabled_entries.insert(internal_name).second;
     else
       needs_restart_ |= (enabled_entries.erase(internal_name) > 0);
+
+    // If an origin list was enabled or disabled, update the command line flag.
+    if (e->type == FeatureEntry::ORIGIN_LIST_VALUE)
+      DidModifyOriginListFlag(*e, enable);
+
   } else if (e->type == FeatureEntry::SINGLE_DISABLE_VALUE) {
     if (!enable)
       needs_restart_ |= enabled_entries.insert(internal_name).second;
@@ -387,6 +438,21 @@
   flags_storage->SetFlags(enabled_entries);
 }
 
+void FlagsState::SetOriginListFlag(const std::string& internal_name,
+                                   const std::string& value,
+                                   FlagsStorage* flags_storage) {
+  const FeatureEntry* entry = FindFeatureEntryByName(internal_name);
+  DCHECK(entry);
+
+  std::set<std::string> enabled_entries;
+  GetSanitizedEnabledFlags(flags_storage, &enabled_entries);
+  // const bool enabled =
+  //    enabled_entries.find(entry->internal_name) != enabled_entries.end();
+  const bool enabled = base::ContainsKey(enabled_entries, entry->internal_name);
+  switch_values_[entry->command_line_switch] = value;
+  DidModifyOriginListFlag(*entry, enabled);
+}
+
 void FlagsState::RemoveFlagsSwitches(
     base::CommandLine::SwitchMap* switch_list) {
   for (const auto& entry : flags_switches_)
@@ -547,6 +613,14 @@
                 (is_default_value &&
                  entry.type == FeatureEntry::SINGLE_DISABLE_VALUE));
         break;
+      case FeatureEntry::ORIGIN_LIST_VALUE:
+        data->SetBoolean("enabled", !is_default_value);
+        switch_values_[entry.internal_name] =
+            base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
+                entry.command_line_switch);
+        data->SetString("origin_list_value",
+                        switch_values_[entry.internal_name]);
+        break;
       case FeatureEntry::MULTI_VALUE:
       case FeatureEntry::ENABLE_DISABLE_VALUE:
       case FeatureEntry::FEATURE_VALUE:
@@ -665,6 +739,14 @@
       continue;
     }
 
+    const FeatureEntry* feature_entry = FindFeatureEntryByName(entry_name);
+    if (feature_entry &&
+        feature_entry->type == FeatureEntry::ORIGIN_LIST_VALUE) {
+      // This is not a feature value that can be enabled/disabled, it's a
+      // command line argument that takes a list of origins. Skip it.
+      continue;
+    }
+
     const SwitchEntry& entry = entry_it->second;
     if (!entry.feature_name.empty()) {
       feature_switches[entry.feature_name] = entry.feature_state;
@@ -780,6 +862,16 @@
         AddSwitchMapping(e.internal_name, e.command_line_switch,
                          e.command_line_value, name_to_switch_map);
         break;
+
+      case FeatureEntry::ORIGIN_LIST_VALUE: {
+        const std::string value =
+            base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
+                e.command_line_switch);
+        AddSwitchMapping(e.internal_name, e.command_line_switch, value,
+                         name_to_switch_map);
+        break;
+      }
+
       case FeatureEntry::MULTI_VALUE:
         for (int j = 0; j < e.num_options; ++j) {
           AddSwitchMapping(
@@ -787,6 +879,7 @@
               e.ChoiceForOption(j).command_line_value, name_to_switch_map);
         }
         break;
+
       case FeatureEntry::ENABLE_DISABLE_VALUE:
         AddSwitchMapping(e.NameForOption(0), std::string(), std::string(),
                          name_to_switch_map);
@@ -795,6 +888,7 @@
         AddSwitchMapping(e.NameForOption(2), e.disable_command_line_switch,
                          e.disable_command_line_value, name_to_switch_map);
         break;
+
       case FeatureEntry::FEATURE_VALUE:
       case FeatureEntry::FEATURE_WITH_PARAMS_VALUE:
         for (int j = 0; j < e.num_options; ++j) {
@@ -813,4 +907,40 @@
   }
 }
 
+void FlagsState::DidModifyOriginListFlag(const FeatureEntry& entry,
+                                         bool enabled) {
+  // Remove the switch if it exists.
+  base::CommandLine* current_cl = base::CommandLine::ForCurrentProcess();
+  base::CommandLine new_cl(current_cl->GetProgram());
+  const base::CommandLine::SwitchMap switches = current_cl->GetSwitches();
+  for (const auto& it : switches) {
+    const auto& switch_name = it.first;
+    const auto& switch_value = it.second;
+    if (switch_name != entry.command_line_switch) {
+      if (switch_value.empty()) {
+        new_cl.AppendSwitch(switch_name);
+      } else {
+        new_cl.AppendSwitchNative(switch_name, switch_value);
+      }
+    }
+  }
+  *current_cl = new_cl;
+
+  if (enabled) {
+    current_cl->AppendSwitchASCII(
+        entry.command_line_switch,
+        SanitizeOriginListFlag(switch_values_[entry.command_line_switch]));
+  }
+}
+
+const FeatureEntry* FlagsState::FindFeatureEntryByName(
+    const std::string& internal_name) const {
+  for (size_t i = 0; i < num_feature_entries_; ++i) {
+    if (feature_entries_[i].internal_name == internal_name) {
+      return feature_entries_ + i;
+    }
+  }
+  return nullptr;
+}
+
 }  // namespace flags_ui
diff --git a/components/flags_ui/flags_state.h b/components/flags_ui/flags_state.h
index 8540ce8..0ec9b06 100644
--- a/components/flags_ui/flags_state.h
+++ b/components/flags_ui/flags_state.h
@@ -84,6 +84,15 @@
   void SetFeatureEntryEnabled(FlagsStorage* flags_storage,
                               const std::string& internal_name,
                               bool enable);
+
+  // Sets |value| as the command line switch for feature given by
+  // |internal_name|. |value| contains a list of origins (serialized form of
+  // url::Origin()) separated by whitespace and/or comma. Invalid values in this
+  // list are ignored.
+  void SetOriginListFlag(const std::string& internal_name,
+                         const std::string& value,
+                         FlagsStorage* flags_storage);
+
   void RemoveFlagsSwitches(base::CommandLine::SwitchMap* switch_list);
   void ResetAllFlags(FlagsStorage* flags_storage);
   void Reset();
@@ -190,6 +199,16 @@
       std::set<std::string>* enabled_entries,
       std::map<std::string, SwitchEntry>* name_to_switch_map);
 
+  // Called when the value of an entry with ORIGIN_LIST_VALUE is modified.
+  // Modifies the corresponding command line by adding or removing the switch
+  // based on the value of |enabled|.
+  void DidModifyOriginListFlag(const FeatureEntry& entry, bool enabled);
+
+  // Returns the FeatureEntry named |internal_name|. Returns null if no entry is
+  // matched.
+  const FeatureEntry* FindFeatureEntryByName(
+      const std::string& internal_name) const;
+
   const FeatureEntry* feature_entries_;
   size_t num_feature_entries_;
 
@@ -200,6 +219,10 @@
   // were appended to existing (list value) switches.
   std::map<std::string, std::set<std::string>> appended_switches_;
 
+  // Map from switch name to switch value. Only filled for features with
+  // ORIGIN_LIST_VALUE type.
+  std::map<std::string, std::string> switch_values_;
+
   DISALLOW_COPY_AND_ASSIGN(FlagsState);
 };
 
diff --git a/components/flags_ui/flags_state_unittest.cc b/components/flags_ui/flags_state_unittest.cc
index 9524d90..17a8a9e 100644
--- a/components/flags_ui/flags_state_unittest.cc
+++ b/components/flags_ui/flags_state_unittest.cc
@@ -46,6 +46,7 @@
 const char kFlags8[] = "flag8";
 const char kFlags9[] = "flag9";
 const char kFlags10[] = "flag10";
+const char kFlags11[] = "flag11";
 
 const char kSwitch1[] = "switch";
 const char kSwitch2[] = "switch2";
@@ -53,6 +54,9 @@
 const char kSwitch6[] = "switch6";
 const char kValueForSwitch2[] = "value_for_switch2";
 
+const char kStringSwitch[] = "string_switch";
+const char kValueForStringSwitch[] = "value_for_string_switch";
+
 const char kMultiSwitch1[] = "multi_switch1";
 const char kMultiSwitch2[] = "multi_switch2";
 const char kValueForMultiSwitch2[] = "value_for_multi_switch2";
@@ -143,7 +147,10 @@
      0,  // Ends up being mapped to the current platform.
      FeatureEntry::FEATURE_WITH_PARAMS_VALUE, nullptr, nullptr, nullptr,
      nullptr, &kTestFeature2, 4, nullptr, kTestVariations2, kTestTrial},
-};
+    {kFlags11, kDummyName, kDummyDescription,
+     0,  // Ends up being mapped to the current platform.
+     FeatureEntry::ORIGIN_LIST_VALUE, kStringSwitch, kValueForStringSwitch,
+     nullptr, nullptr, nullptr /* feature */, 0, nullptr, nullptr, nullptr}};
 
 class FlagsStateTest : public ::testing::Test {
  protected:
@@ -886,7 +893,7 @@
                                       &supported_entries, &unsupported_entries,
                                       base::Bind(&SkipFeatureEntry));
   // All |kEntries| except for |kFlags3| should be supported.
-  EXPECT_EQ(9u, supported_entries.GetSize());
+  EXPECT_EQ(10u, supported_entries.GetSize());
   EXPECT_EQ(1u, unsupported_entries.GetSize());
   EXPECT_EQ(arraysize(kEntries),
             supported_entries.GetSize() + unsupported_entries.GetSize());
diff --git a/components/flags_ui/flags_ui_constants.cc b/components/flags_ui/flags_ui_constants.cc
index 2b902245b..a3e2b1c 100644
--- a/components/flags_ui/flags_ui_constants.cc
+++ b/components/flags_ui/flags_ui_constants.cc
@@ -12,6 +12,7 @@
 // Message handlers.
 const char kEnableExperimentalFeature[] = "enableExperimentalFeature";
 const char kRequestExperimentalFeatures[] = "requestExperimentalFeatures";
+const char kSetOriginListFlag[] = "setOriginListFlag";
 const char kResetAllFlags[] = "resetAllFlags";
 const char kRestartBrowser[] = "restartBrowser";
 
diff --git a/components/flags_ui/flags_ui_constants.h b/components/flags_ui/flags_ui_constants.h
index 67442bc..5a3300a 100644
--- a/components/flags_ui/flags_ui_constants.h
+++ b/components/flags_ui/flags_ui_constants.h
@@ -15,6 +15,7 @@
 // Must match the constants used in the resource files.
 extern const char kEnableExperimentalFeature[];
 extern const char kRequestExperimentalFeatures[];
+extern const char kSetOriginListFlag[];
 extern const char kResetAllFlags[];
 extern const char kRestartBrowser[];
 
diff --git a/components/flags_ui/resources/flags.css b/components/flags_ui/resources/flags.css
index faca70f..b068942 100644
--- a/components/flags_ui/resources/flags.css
+++ b/components/flags_ui/resources/flags.css
@@ -260,6 +260,17 @@
   width: 100%;
 }
 
+.experiment-origin-list-value {
+  border: 1px solid var(--color-light-gray);
+  border-radius: 3px;
+  box-sizing: border-box;
+  font-size: 1em;
+  margin: 0;
+  min-height: 3em;
+  padding: 8px;
+  width: 100%;
+}
+
 .experiment-switched .experiment-actions select {
   background: var(--google-blue-700);
   color: white;
diff --git a/components/flags_ui/resources/flags.html b/components/flags_ui/resources/flags.html
index b19625a6..a91d6d6 100644
--- a/components/flags_ui/resources/flags.html
+++ b/components/flags_ui/resources/flags.html
@@ -87,6 +87,11 @@
                   <span jsvalues=".innerHTML:description"></span> –
                   <span class="platforms" jscontent="supported_platforms.join(', ')"></span>
                 </p>
+                <div jsdisplay="origin_list_value!==null">
+                  <textarea class="experiment-origin-list-value"
+                      jsvalues=".internal_name:internal_name; .value:origin_list_value"
+                      tabindex="7"></textarea>
+                </div>
                 <a class="permalink" jsvalues="href: '#' + internal_name"
                     jscontent="'#' + internal_name" tabindex="6"></a>
               </div>
@@ -126,6 +131,11 @@
                   <span jsvalues=".innerHTML:description"></span> –
                   <span class="platforms" jscontent="supported_platforms.join(', ')"></span>
                 </p>
+                <div jsdisplay="origin_list_value!==null">
+                  <textarea class="experiment-origin-list-value"
+                      jsvalues=".internal_name:internal_name; .value:origin_list_value"
+                      tabindex="7"></textarea>
+                </div>
                 <a class="permalink" jsvalues="href: '#' + internal_name"
                     jscontent="'#' + internal_name" tabindex="6"></a>
               </div>
@@ -177,7 +187,7 @@
                 </p>
                 <a class="permalink"
                     jsvalues="href: '#' + internal_name"
-                    jscontent="'#' + internal_name" tabindex="7"></a>
+                    jscontent="'#' + internal_name" tabindex="8"></a>
               </div>
               <div class="flex experiment-actions">Not available on your platform.</div>
             </div>
@@ -194,7 +204,7 @@
         <div class="flex restart-notice" jstcache="0">$i18n{flagsRestartNotice}</div>
         <div class="flex">
 <if expr="not is_ios">
-          <button class="experiment-restart-button" type="button" tabindex="8">
+          <button class="experiment-restart-button" type="button" tabindex="9">
 <if expr="not is_chromeos">
           Relaunch Now
 </if>
diff --git a/components/flags_ui/resources/flags.js b/components/flags_ui/resources/flags.js
index 807533b7..4508cfa 100644
--- a/components/flags_ui/resources/flags.js
+++ b/components/flags_ui/resources/flags.js
@@ -36,6 +36,14 @@
     };
   }
 
+  elements = document.getElementsByClassName('experiment-origin-list-value');
+  for (var i = 0; i < elements.length; ++i) {
+    elements[i].onchange = function() {
+      handleSetOriginListFlag(this, this.value);
+      return false;
+    };
+  }
+
   elements = document.getElementsByClassName('experiment-restart-button');
   for (var i = 0; i < elements.length; ++i) {
     elements[i].onclick = restartBrowser;
@@ -206,6 +214,11 @@
   experimentChangesUiUpdates(node, enable ? 1 : 0);
 }
 
+function handleSetOriginListFlag(node, value) {
+  chrome.send('setOriginListFlag', [String(node.internal_name), String(value)]);
+  $('needs-restart').classList.add('show');
+}
+
 /**
  * Invoked when the selection of a multi-value choice is changed to the
  * specified index.
@@ -286,6 +299,9 @@
           this.clearSearch.bind(this));
 
       window.addEventListener('keyup', function(e) {
+          if (document.activeElement.nodeName == "TEXTAREA") {
+            return;
+          }
           switch(e.key) {
             case '/':
               this.searchBox_.focus();