Add a promptBrowserLogin method to webstorePrivate extension API

Also change 'getSyncLogin' to 'getBrowserLogin', and add notion of the login
token to it (although this isn't actually sent yet).

BUG=58033
TEST=Should be covered by browser tests.


Review URL: http://codereview.chromium.org/3641001

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@62172 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/extensions/extension_function_dispatcher.cc b/chrome/browser/extensions/extension_function_dispatcher.cc
index b8643fb..c908c09 100644
--- a/chrome/browser/extensions/extension_function_dispatcher.cc
+++ b/chrome/browser/extensions/extension_function_dispatcher.cc
@@ -278,10 +278,11 @@
   RegisterFunction<UninstallFunction>();
 
   // WebstorePrivate.
-  RegisterFunction<GetSyncLoginFunction>();
+  RegisterFunction<GetBrowserLoginFunction>();
   RegisterFunction<GetStoreLoginFunction>();
   RegisterFunction<InstallFunction>();
   RegisterFunction<SetStoreLoginFunction>();
+  RegisterFunction<PromptBrowserLoginFunction>();
 }
 
 void FactoryRegistry::GetAllNames(std::vector<std::string>* names) {
diff --git a/chrome/browser/extensions/extension_webstore_private_api.cc b/chrome/browser/extensions/extension_webstore_private_api.cc
index f18c2d5..8f1a33b 100644
--- a/chrome/browser/extensions/extension_webstore_private_api.cc
+++ b/chrome/browser/extensions/extension_webstore_private_api.cc
@@ -18,6 +18,9 @@
 namespace {
 
 const char* install_base_url = extension_urls::kGalleryUpdateHttpsUrl;
+const char kAlreadyLoggedInError[] = "User already logged in";
+const char kLoginKey[] = "login";
+ProfileSyncService* test_sync_service = NULL;
 
 bool IsWebStoreURL(Profile* profile, const GURL& url) {
   ExtensionsService* service = profile->GetExtensionsService();
@@ -26,7 +29,7 @@
   return (service->GetExtensionByWebExtent(url) == store);
 }
 
-}
+}  // namespace
 
 // static
 void InstallFunction::SetTestingInstallBaseUrl(
@@ -67,12 +70,15 @@
   return true;
 }
 
-bool GetSyncLoginFunction::RunImpl() {
+bool GetBrowserLoginFunction::RunImpl() {
   if (!IsWebStoreURL(profile_, source_url()))
     return false;
   ProfileSyncService* sync_service = profile_->GetProfileSyncService();
   string16 username = sync_service->GetAuthenticatedUsername();
-  result_.reset(Value::CreateStringValue(username));
+  ListValue* list = new ListValue();
+  // TODO(asargent) - send the browser login token here too if available.
+  list->Append(Value::CreateStringValue(username));
+  result_.reset(list);
   return true;
 }
 
@@ -100,3 +106,67 @@
   prefs->SetWebStoreLogin(login);
   return true;
 }
+
+// static
+void PromptBrowserLoginFunction::SetTestingProfileSyncService(
+    ProfileSyncService* service) {
+  test_sync_service = service;
+}
+
+ProfileSyncService* PromptBrowserLoginFunction::profile_sync_service() {
+  if (test_sync_service)
+    return test_sync_service;
+  else
+    return profile_->GetProfileSyncService();
+}
+
+PromptBrowserLoginFunction::~PromptBrowserLoginFunction() {
+  if (observing_sync_state_)
+    profile_sync_service()->RemoveObserver(this);
+}
+
+bool PromptBrowserLoginFunction::RunImpl() {
+  if (!IsWebStoreURL(profile_, source_url()))
+    return false;
+
+  std::string preferred_email;
+  ProfileSyncService* sync_service = profile_sync_service();
+  if (args_->GetSize() > 0) {
+    EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &preferred_email));
+    if (!sync_service->GetAuthenticatedUsername().empty()) {
+      error_ = kAlreadyLoggedInError;
+      return false;
+    }
+  }
+
+  // We return the result asynchronously, so we addref to keep ourself alive.
+  // Matched with a Release in OnStateChanged().
+  AddRef();
+
+  observing_sync_state_ = true;
+  sync_service->AddObserver(this);
+  // TODO(mirandac/estade) - make use of |preferred_email| to pre-populate the
+  // browser login dialog if it was set to non-empty above.
+  sync_service->ShowLoginDialog(NULL);
+
+  // The response will be sent asynchronously in OnStateChanged().
+  return true;
+}
+
+void PromptBrowserLoginFunction::OnStateChanged() {
+  ProfileSyncService* sync_service = profile_sync_service();
+  // If the setup is finished, we'll report back what happened.
+  if (!sync_service->SetupInProgress()) {
+    DictionaryValue* dictionary = new DictionaryValue();
+
+    // TODO(asargent) - send the browser login token here too if available.
+    string16 username = sync_service->GetAuthenticatedUsername();
+    dictionary->SetString(kLoginKey, username);
+
+    result_.reset(dictionary);
+    SendResponse(true);
+
+    // Matches the AddRef in RunImpl().
+    Release();
+  }
+}
diff --git a/chrome/browser/extensions/extension_webstore_private_api.h b/chrome/browser/extensions/extension_webstore_private_api.h
index 0ba27aca..4da35cf 100644
--- a/chrome/browser/extensions/extension_webstore_private_api.h
+++ b/chrome/browser/extensions/extension_webstore_private_api.h
@@ -2,11 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_WEBSTORE_PRIVATE_API_H__
-#define CHROME_BROWSER_EXTENSIONS_EXTENSION_WEBSTORE_PRIVATE_API_H__
+#ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_WEBSTORE_PRIVATE_API_H_
+#define CHROME_BROWSER_EXTENSIONS_EXTENSION_WEBSTORE_PRIVATE_API_H_
 #pragma once
 
 #include "chrome/browser/extensions/extension_function.h"
+#include "chrome/browser/sync/profile_sync_service_observer.h"
+
+class ProfileSyncService;
 
 class InstallFunction : public SyncExtensionFunction {
  public:
@@ -18,9 +21,9 @@
   DECLARE_EXTENSION_FUNCTION_NAME("webstorePrivate.install");
 };
 
-class GetSyncLoginFunction : public SyncExtensionFunction {
+class GetBrowserLoginFunction : public SyncExtensionFunction {
   virtual bool RunImpl();
-  DECLARE_EXTENSION_FUNCTION_NAME("webstorePrivate.getSyncLogin");
+  DECLARE_EXTENSION_FUNCTION_NAME("webstorePrivate.getBrowserLogin");
 };
 
 class GetStoreLoginFunction : public SyncExtensionFunction {
@@ -33,4 +36,30 @@
   DECLARE_EXTENSION_FUNCTION_NAME("webstorePrivate.setStoreLogin");
 };
 
-#endif  // CHROME_BROWSER_EXTENSIONS_EXTENSION_WEBSTORE_PRIVATE_API_H__
+class PromptBrowserLoginFunction : public AsyncExtensionFunction,
+                                   public ProfileSyncServiceObserver {
+ public:
+  // Allows you to set the ProfileSyncService the function will use for
+  // testing purposes.
+  static void SetTestingProfileSyncService(ProfileSyncService* service);
+
+  // Implements ProfileSyncServiceObserver interface.
+  virtual void OnStateChanged();
+
+ protected:
+  virtual ~PromptBrowserLoginFunction();
+  virtual bool RunImpl();
+
+  // Returns either the actual ProfileSyncService or the test service if one
+  // was set.
+  ProfileSyncService* profile_sync_service();
+
+  DECLARE_EXTENSION_FUNCTION_NAME("webstorePrivate.promptBrowserLogin");
+
+ private:
+  // This indicates whether we're currently registered as an observer of the
+  // ProfileSyncService, and need to unregister ourselves at destruction.
+  bool observing_sync_state_;
+};
+
+#endif  // CHROME_BROWSER_EXTENSIONS_EXTENSION_WEBSTORE_PRIVATE_API_H_
diff --git a/chrome/browser/extensions/extension_webstore_private_browsertest.cc b/chrome/browser/extensions/extension_webstore_private_browsertest.cc
new file mode 100644
index 0000000..a0e03b40
--- /dev/null
+++ b/chrome/browser/extensions/extension_webstore_private_browsertest.cc
@@ -0,0 +1,109 @@
+// Copyright (c) 2010 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/string_number_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/extensions/extension_browsertest.h"
+#include "chrome/browser/extensions/extension_test_message_listener.h"
+#include "chrome/browser/extensions/extension_webstore_private_api.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/test/ui_test_utils.h"
+#include "googleurl/src/gurl.h"
+#include "net/base/mock_host_resolver.h"
+
+using chrome::kHttpScheme;
+using chrome::kStandardSchemeSeparator;
+
+namespace {
+
+const char kTestUrlHostname[] = "www.example.com";
+
+}  // namespace
+
+// A fake version of ProfileSyncService used for testing.
+class FakeProfileSyncService : public ProfileSyncService {
+ public:
+  // The |username_after_login| parameter determines what this fake
+  // ProfileSyncService will set the username to when ShowLoginDialog is called.
+  explicit FakeProfileSyncService(const std::string& username_after_login) :
+      username_after_login_(username_after_login), observer_(NULL) {}
+  virtual ~FakeProfileSyncService() {
+    EXPECT_TRUE(observer_ == NULL);
+  }
+
+  // Overrides of virtual methods in ProfileSyncService.
+  virtual string16 GetAuthenticatedUsername() const { return username_; }
+  virtual void ShowLoginDialog(gfx::NativeWindow) {
+    EXPECT_TRUE(observer_ != NULL);
+    username_ = ASCIIToUTF16(username_after_login_);
+    observer_->OnStateChanged();
+  }
+  virtual bool SetupInProgress() const {
+    return false;
+  }
+  virtual void AddObserver(ProfileSyncServiceObserver* observer) {
+    EXPECT_TRUE(observer_ == NULL);
+    observer_ = observer;
+  }
+  virtual void RemoveObserver(ProfileSyncServiceObserver* observer) {
+    EXPECT_TRUE(observer == observer_);
+    observer_ = NULL;
+  }
+
+ private:
+  std::string username_after_login_;
+  string16 username_;
+  ProfileSyncServiceObserver* observer_;
+};
+
+class ExtensionWebstorePrivateBrowserTest : public ExtensionBrowserTest {
+ public:
+  ExtensionWebstorePrivateBrowserTest() {
+    test_url_base_ = std::string() + kHttpScheme + kStandardSchemeSeparator +
+                     kTestUrlHostname;
+  }
+
+  void SetUpCommandLine(CommandLine* command_line) {
+    ExtensionBrowserTest::SetUpCommandLine(command_line);
+    command_line->AppendSwitchASCII(switches::kAppsGalleryURL, test_url_base_);
+  }
+
+  // This generates a regular test server url pointing to a test file at
+  // |relative_path|, but replaces the hostname with kTestUrlHostname so that
+  // we get the webstore private APIs injected (this happens because of the
+  // command line switch we added in SetupCommandLine).
+  GURL GetUrl(const std::string& relative_path) {
+    GURL base_url = test_server()->GetURL(
+        "files/extensions/webstore_private/" + relative_path);
+    GURL::Replacements replacements;
+    std::string replacement_host = std::string(kTestUrlHostname);
+    replacements.SetHostStr(replacement_host);
+    return base_url.ReplaceComponents(replacements);
+  }
+
+  void RunLoginTest(const std::string& relative_path,
+                    const std::string& login_result) {
+    FakeProfileSyncService sync_service(login_result);
+    PromptBrowserLoginFunction::SetTestingProfileSyncService(&sync_service);
+    ExtensionTestMessageListener listener("success", false);
+    GURL url = GetUrl(relative_path);
+    ui_test_utils::NavigateToURL(browser(), url);
+    EXPECT_TRUE(listener.WaitUntilSatisfied());
+    PromptBrowserLoginFunction::SetTestingProfileSyncService(NULL);
+  }
+
+ protected:
+  std::string test_url_base_;
+};
+
+IN_PROC_BROWSER_TEST_F(ExtensionWebstorePrivateBrowserTest, BrowserLogin) {
+  host_resolver()->AddRule(kTestUrlHostname, "127.0.0.1");
+  ASSERT_TRUE(test_server()->Start());
+
+  RunLoginTest("browser_login/no_preferred.html", "");
+  RunLoginTest("browser_login/preferred.html", "[email protected]");
+}
+
diff --git a/chrome/browser/sync/profile_sync_service.h b/chrome/browser/sync/profile_sync_service.h
index e68f554..5a889f18 100644
--- a/chrome/browser/sync/profile_sync_service.h
+++ b/chrome/browser/sync/profile_sync_service.h
@@ -214,11 +214,11 @@
   // progress, the sync system is already authenticated, or some error
   // occurred preventing the action. We make it the duty of ProfileSyncService
   // to open the dialog to easily ensure only one is ever showing.
-  bool SetupInProgress() const;
+  virtual bool SetupInProgress() const;
   bool WizardIsVisible() const {
     return wizard_.IsVisible();
   }
-  void ShowLoginDialog(gfx::NativeWindow parent_window);
+  virtual void ShowLoginDialog(gfx::NativeWindow parent_window);
 
   void ShowChooseDataTypes(gfx::NativeWindow parent_window);
 
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index 603f6ecd..55ba15f 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -2750,6 +2750,7 @@
         'browser/sync/profile_sync_factory_impl.h',
         'browser/sync/profile_sync_service.cc',
         'browser/sync/profile_sync_service.h',
+        'browser/sync/profile_sync_service_observer.h',
         'browser/sync/signin_manager.cc',
         'browser/sync/signin_manager.h',
         'browser/sync/sync_constants.cc',
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index 12ee7834..51598a31 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -1963,6 +1963,7 @@
         'browser/extensions/extension_webnavigation_apitest.cc',
         'browser/extensions/extension_webrequest_apitest.cc',
         'browser/extensions/extension_websocket_apitest.cc',
+        'browser/extensions/extension_webstore_private_browsertest.cc',
         'browser/extensions/fragment_navigation_apitest.cc',
         'browser/extensions/isolated_world_apitest.cc',
         'browser/extensions/notifications_apitest.cc',
@@ -2095,6 +2096,7 @@
         ['OS=="linux"', {
           'dependencies': [
             '../build/linux/system.gyp:gtk',
+            '../build/linux/system.gyp:nss',
             '../tools/xdisplaycheck/xdisplaycheck.gyp:xdisplaycheck',
           ],
         }],
diff --git a/chrome/common/extensions/api/extension_api.json b/chrome/common/extensions/api/extension_api.json
index 860f94d..34ab416 100644
--- a/chrome/common/extensions/api/extension_api.json
+++ b/chrome/common/extensions/api/extension_api.json
@@ -4119,7 +4119,7 @@
         ]
       },
       {
-        "name": "getSyncLogin",
+        "name": "getBrowserLogin",
         "description": "Returns the logged-in sync user login if there is one, or the empty string otherwise.",
         "parameters": [
           {
@@ -4127,7 +4127,8 @@
             "type": "function",
             "optional": "false",
             "parameters": [
-              { "name": "login", "type": "string" }
+              { "name": "login", "type": "string" },
+              { "name": "token", "type": "string", "optional": true }
             ]
           }
         ]
@@ -4153,6 +4154,32 @@
           { "name": "login", "type": "string" },
           { "name": "callback", "type": "function", "optional": "true" }
         ]
+      },
+      {
+        "name": "promptBrowserLogin",
+        "description": "Causes the browser to bring up the browser login UI.",
+        "parameters": [
+          {
+            "name": "preferred_email",
+            "type": "string",
+            "description": "The email address to use to pre-populate the login dialog (can be an empty string)."
+          },
+          { 
+            "name": "callback", 
+            "type": "function", 
+            "optional": "true",
+            "parameters": [
+              {
+                "name": "info",
+                "type": "object",
+                "properties": {
+                  "login": { "type": "string", "optional": true },
+                  "token": { "type": "string", "optional": true }
+                }
+              }
+            ]
+          }
+        ]
       }
     ]
   }
diff --git a/chrome/test/data/extensions/webstore_private/browser_login/no_preferred.html b/chrome/test/data/extensions/webstore_private/browser_login/no_preferred.html
new file mode 100644
index 0000000..b2391e87
--- /dev/null
+++ b/chrome/test/data/extensions/webstore_private/browser_login/no_preferred.html
@@ -0,0 +1,7 @@
+<script>
+chrome.webstorePrivate.promptBrowserLogin("", function(result) {
+  if (result.login == "") {
+    chrome.test.sendMessage("success");
+  }
+});
+</script>
diff --git a/chrome/test/data/extensions/webstore_private/browser_login/preferred.html b/chrome/test/data/extensions/webstore_private/browser_login/preferred.html
new file mode 100644
index 0000000..ffbe66e
--- /dev/null
+++ b/chrome/test/data/extensions/webstore_private/browser_login/preferred.html
@@ -0,0 +1,9 @@
+<script>
+chrome.webstorePrivate.promptBrowserLogin("[email protected]", 
+                                          function(result) {
+  if (result.login == "[email protected]") {
+    chrome.test.sendMessage("success");
+  }
+});
+</script>
+