Tabs Extension: Implementation of tabs.duplicate api.

BUG=36007
TEST=Manual: Tested with an extension with/without "tabs" permission.
Automated: unittest: ExtensionTabsTest.DuplicateTabNoPermission, ExtensionPermissionsTest.DefaultFunctionAccess, ExtensionTest.ApiPermissions
apitest: ExtensionApiTest.TabDuplicate

Review URL: https://chromiumcodereview.appspot.com/10697017

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@155731 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/extensions/api/tabs/tabs.cc b/chrome/browser/extensions/api/tabs/tabs.cc
index 76e34a9..115665196 100644
--- a/chrome/browser/extensions/api/tabs/tabs.cc
+++ b/chrome/browser/extensions/api/tabs/tabs.cc
@@ -1093,6 +1093,33 @@
   return true;
 }
 
+bool DuplicateTabFunction::RunImpl() {
+  int tab_id = -1;
+  EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &tab_id));
+
+  Browser* browser = NULL;
+  TabStripModel* tab_strip = NULL;
+  TabContents* contents = NULL;
+  int tab_index = -1;
+  if (!GetTabById(tab_id, profile(), include_incognito(),
+                  &browser, &tab_strip, &contents, &tab_index, &error_)) {
+    return false;
+  }
+
+  TabContents* new_contents = chrome::DuplicateTabAt(browser, tab_index);
+  if (!has_callback())
+    return true;
+
+  int new_index = tab_strip->GetIndexOfTabContents(new_contents);
+
+  // Return data about the newly created tab.
+  SetResult(ExtensionTabUtil::CreateTabValue(
+      new_contents->web_contents(),
+      tab_strip, new_index, GetExtension()));
+
+  return true;
+}
+
 bool GetTabFunction::RunImpl() {
   int tab_id = -1;
   EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &tab_id));
diff --git a/chrome/browser/extensions/api/tabs/tabs.h b/chrome/browser/extensions/api/tabs/tabs.h
index 26125ac2..b808950 100644
--- a/chrome/browser/extensions/api/tabs/tabs.h
+++ b/chrome/browser/extensions/api/tabs/tabs.h
@@ -109,6 +109,11 @@
   virtual bool RunImpl() OVERRIDE;
   DECLARE_EXTENSION_FUNCTION_NAME("tabs.create")
 };
+class DuplicateTabFunction : public SyncExtensionFunction {
+  virtual ~DuplicateTabFunction() {}
+  virtual bool RunImpl() OVERRIDE;
+  DECLARE_EXTENSION_FUNCTION_NAME("tabs.duplicate")
+};
 class HighlightTabsFunction : public SyncExtensionFunction {
   virtual ~HighlightTabsFunction() {}
   virtual bool RunImpl() OVERRIDE;
diff --git a/chrome/browser/extensions/api/tabs/tabs_test.cc b/chrome/browser/extensions/api/tabs/tabs_test.cc
index dfe8f62..e9c97488 100644
--- a/chrome/browser/extensions/api/tabs/tabs_test.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_test.cc
@@ -604,3 +604,86 @@
           browser()),
       keys::kInvalidWindowStateError));
 }
+
+IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DuplicateTab) {
+  static const char kNewBlankTabArgs[] ="about:blank";
+
+  content::OpenURLParams params(GURL(kNewBlankTabArgs), content::Referrer(),
+                                NEW_FOREGROUND_TAB,
+                                content::PAGE_TRANSITION_LINK, false);
+  content::WebContents* web_contents = browser()->OpenURL(params);
+  int tab_id = ExtensionTabUtil::GetTabId(web_contents);
+  int window_id = ExtensionTabUtil::GetWindowIdOfTab(web_contents);
+  int tab_index = -1;
+  TabStripModel* tab_strip;
+  ExtensionTabUtil::GetTabStripModel(web_contents, &tab_strip, &tab_index);
+
+  scoped_refptr<DuplicateTabFunction> duplicate_tab_function(
+      new DuplicateTabFunction());
+  scoped_ptr<base::DictionaryValue> test_extension_value(
+      utils::ParseDictionary(
+      "{\"name\": \"Test\", \"version\": \"1.0\", \"permissions\": [\"tabs\"]}"
+      ));
+  scoped_refptr<extensions::Extension> empty_tab_extension(
+      utils::CreateExtension(test_extension_value.get()));
+  duplicate_tab_function->set_extension(empty_tab_extension.get());
+  duplicate_tab_function->set_has_callback(true);
+
+  scoped_ptr<base::DictionaryValue> duplicate_result(utils::ToDictionary(
+      utils::RunFunctionAndReturnSingleResult(
+          duplicate_tab_function.get(), base::StringPrintf("[%u]", tab_id),
+          browser())));
+
+  int duplicate_tab_id = utils::GetInteger(duplicate_result.get(), "id");
+  int duplicate_tab_window_id = utils::GetInteger(duplicate_result.get(),
+                                                  "windowId");
+  int duplicate_tab_index = utils::GetInteger(duplicate_result.get(), "index");
+  EXPECT_EQ(base::Value::TYPE_DICTIONARY, duplicate_result->GetType());
+  // Duplicate tab id should be different from the original tab id.
+  EXPECT_NE(tab_id, duplicate_tab_id);
+  EXPECT_EQ(window_id, duplicate_tab_window_id);
+  EXPECT_EQ(tab_index + 1, duplicate_tab_index);
+  // The test empty tab extension has tabs permissions, therefore
+  // |duplicate_result| should contain url, title, and faviconUrl
+  // in the function result.
+  EXPECT_TRUE(utils::HasPrivacySensitiveFields(duplicate_result.get()));
+}
+
+IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DuplicateTabNoPermission) {
+  static const char kNewBlankTabArgs[] ="about:blank";
+
+  content::OpenURLParams params(GURL(kNewBlankTabArgs), content::Referrer(),
+                                NEW_FOREGROUND_TAB,
+                                content::PAGE_TRANSITION_LINK, false);
+  content::WebContents* web_contents = browser()->OpenURL(params);
+  int tab_id = ExtensionTabUtil::GetTabId(web_contents);
+  int window_id = ExtensionTabUtil::GetWindowIdOfTab(web_contents);
+  int tab_index = -1;
+  TabStripModel* tab_strip;
+  ExtensionTabUtil::GetTabStripModel(web_contents, &tab_strip, &tab_index);
+
+  scoped_refptr<DuplicateTabFunction> duplicate_tab_function(
+      new DuplicateTabFunction());
+  scoped_refptr<extensions::Extension> empty_extension(
+      utils::CreateEmptyExtension());
+  duplicate_tab_function->set_extension(empty_extension.get());
+  duplicate_tab_function->set_has_callback(true);
+
+  scoped_ptr<base::DictionaryValue> duplicate_result(utils::ToDictionary(
+      utils::RunFunctionAndReturnSingleResult(
+          duplicate_tab_function.get(), base::StringPrintf("[%u]", tab_id),
+          browser())));
+
+  int duplicate_tab_id = utils::GetInteger(duplicate_result.get(), "id");
+  int duplicate_tab_window_id = utils::GetInteger(duplicate_result.get(),
+                                                  "windowId");
+  int duplicate_tab_index = utils::GetInteger(duplicate_result.get(), "index");
+  EXPECT_EQ(base::Value::TYPE_DICTIONARY, duplicate_result->GetType());
+  // Duplicate tab id should be different from the original tab id.
+  EXPECT_NE(tab_id, duplicate_tab_id);
+  EXPECT_EQ(window_id, duplicate_tab_window_id);
+  EXPECT_EQ(tab_index + 1, duplicate_tab_index);
+  // The test empty extension has no permissions, therefore |duplicate_result|
+  // should not contain url, title, and faviconUrl in the function result.
+  EXPECT_FALSE(utils::HasPrivacySensitiveFields(duplicate_result.get()));
+}
diff --git a/chrome/browser/extensions/extension_function_registry.cc b/chrome/browser/extensions/extension_function_registry.cc
index 0817326..dcca8d2 100644
--- a/chrome/browser/extensions/extension_function_registry.cc
+++ b/chrome/browser/extensions/extension_function_registry.cc
@@ -97,21 +97,22 @@
   RegisterFunction<RemoveWindowFunction>();
 
   // Tabs
-  RegisterFunction<GetTabFunction>();
+  RegisterFunction<CaptureVisibleTabFunction>();
+  RegisterFunction<CreateTabFunction>();
+  RegisterFunction<DetectTabLanguageFunction>();
+  RegisterFunction<DuplicateTabFunction>();
+  RegisterFunction<GetAllTabsInWindowFunction>();
   RegisterFunction<GetCurrentTabFunction>();
   RegisterFunction<GetSelectedTabFunction>();
-  RegisterFunction<GetAllTabsInWindowFunction>();
-  RegisterFunction<QueryTabsFunction>();
+  RegisterFunction<GetTabFunction>();
   RegisterFunction<HighlightTabsFunction>();
-  RegisterFunction<CreateTabFunction>();
-  RegisterFunction<UpdateTabFunction>();
   RegisterFunction<MoveTabsFunction>();
+  RegisterFunction<QueryTabsFunction>();
   RegisterFunction<ReloadTabFunction>();
   RegisterFunction<RemoveTabsFunction>();
-  RegisterFunction<DetectTabLanguageFunction>();
-  RegisterFunction<CaptureVisibleTabFunction>();
   RegisterFunction<TabsExecuteScriptFunction>();
   RegisterFunction<TabsInsertCSSFunction>();
+  RegisterFunction<UpdateTabFunction>();
 
   // Page Actions.
   RegisterFunction<EnablePageActionsFunction>();
diff --git a/chrome/browser/extensions/extension_function_test_utils.cc b/chrome/browser/extensions/extension_function_test_utils.cc
index 820fbde..2f8bfc6 100644
--- a/chrome/browser/extensions/extension_function_test_utils.cc
+++ b/chrome/browser/extensions/extension_function_test_utils.cc
@@ -9,6 +9,7 @@
 #include "base/file_path.h"
 #include "base/json/json_reader.h"
 #include "base/values.h"
+#include "chrome/browser/extensions/api/tabs/tabs_constants.h"
 #include "chrome/browser/extensions/extension_function.h"
 #include "chrome/browser/extensions/extension_function_dispatcher.h"
 #include "chrome/browser/ui/browser.h"
@@ -18,6 +19,7 @@
 
 using content::WebContents;
 using extensions::Extension;
+namespace keys = extensions::tabs_constants;
 
 namespace {
 
@@ -105,20 +107,40 @@
 
 scoped_refptr<Extension> CreateEmptyExtensionWithLocation(
     Extension::Location location) {
-  std::string error;
-  const FilePath test_extension_path;
   scoped_ptr<base::DictionaryValue> test_extension_value(
       ParseDictionary("{\"name\": \"Test\", \"version\": \"1.0\"}"));
+  return CreateExtension(location, test_extension_value.get());
+}
+
+scoped_refptr<Extension> CreateExtension(
+    base::DictionaryValue* test_extension_value) {
+  return CreateExtension(Extension::INTERNAL, test_extension_value);
+}
+
+scoped_refptr<Extension> CreateExtension(
+    Extension::Location location,
+    base::DictionaryValue* test_extension_value) {
+  std::string error;
+  const FilePath test_extension_path;
   scoped_refptr<Extension> extension(Extension::Create(
       test_extension_path,
       location,
-      *test_extension_value.get(),
+      *test_extension_value,
       Extension::NO_FLAGS,
       &error));
   EXPECT_TRUE(error.empty()) << "Could not parse test extension " << error;
   return extension;
 }
 
+bool HasPrivacySensitiveFields(base::DictionaryValue* val) {
+  std::string result;
+  if (val->GetString(keys::kUrlKey, &result) ||
+      val->GetString(keys::kTitleKey, &result) ||
+      val->GetString(keys::kFaviconUrlKey, &result))
+    return true;
+  return false;
+}
+
 std::string RunFunctionAndReturnError(UIThreadExtensionFunction* function,
                                       const std::string& args,
                                       Browser* browser) {
diff --git a/chrome/browser/extensions/extension_function_test_utils.h b/chrome/browser/extensions/extension_function_test_utils.h
index 7bcf84d..cea6e78 100644
--- a/chrome/browser/extensions/extension_function_test_utils.h
+++ b/chrome/browser/extensions/extension_function_test_utils.h
@@ -54,6 +54,19 @@
 scoped_refptr<extensions::Extension> CreateEmptyExtensionWithLocation(
     extensions::Extension::Location location);
 
+// Creates an extension instance with a specified extension value that can be
+// attached to an ExtensionFunction before running.
+scoped_refptr<extensions::Extension> CreateExtension(
+    base::DictionaryValue* test_extension_value);
+
+scoped_refptr<extensions::Extension> CreateExtension(
+    extensions::Extension::Location location,
+    base::DictionaryValue* test_extension_value);
+
+// Returns true if |val| contains privacy information, e.g. url,
+// title, and faviconUrl.
+bool HasPrivacySensitiveFields(base::DictionaryValue* val);
+
 enum RunFunctionFlags {
   NONE = 0,
   INCLUDE_INCOGNITO = 1 << 0
diff --git a/chrome/browser/extensions/extension_tabs_apitest.cc b/chrome/browser/extensions/extension_tabs_apitest.cc
index 4c921aa5..53954665 100644
--- a/chrome/browser/extensions/extension_tabs_apitest.cc
+++ b/chrome/browser/extensions/extension_tabs_apitest.cc
@@ -79,6 +79,10 @@
   ASSERT_TRUE(RunExtensionSubtest("tabs/basics", "crud2.html")) << message_;
 }
 
+IN_PROC_BROWSER_TEST_F(ExtensionApiTest, TabDuplicate) {
+  ASSERT_TRUE(RunExtensionSubtest("tabs/basics", "duplicate.html")) << message_;
+}
+
 IN_PROC_BROWSER_TEST_F(ExtensionApiTest, TabUpdate) {
   ASSERT_TRUE(RunExtensionSubtest("tabs/basics", "update.html")) << message_;
 }
diff --git a/chrome/browser/ui/browser_commands.cc b/chrome/browser/ui/browser_commands.cc
index 5b166f5..3d88794 100644
--- a/chrome/browser/ui/browser_commands.cc
+++ b/chrome/browser/ui/browser_commands.cc
@@ -491,7 +491,7 @@
   return contents && contents->GetController().GetLastCommittedEntry();
 }
 
-void DuplicateTabAt(Browser* browser, int index) {
+TabContents* DuplicateTabAt(Browser* browser, int index) {
   TabContents* contents = GetTabContentsAt(browser, index);
   CHECK(contents);
   TabContents* contents_dupe =
@@ -541,6 +541,7 @@
       SessionServiceFactory::GetForProfileIfExisting(browser->profile());
   if (session_service)
     session_service->TabRestored(contents_dupe, pinned);
+  return contents_dupe;
 }
 
 bool CanDuplicateTabAt(Browser* browser, int index) {
diff --git a/chrome/browser/ui/browser_commands.h b/chrome/browser/ui/browser_commands.h
index 5ac5396..b69f5086 100644
--- a/chrome/browser/ui/browser_commands.h
+++ b/chrome/browser/ui/browser_commands.h
@@ -82,7 +82,7 @@
 void SelectLastTab(Browser* browser);
 void DuplicateTab(Browser* browser);
 bool CanDuplicateTab(const Browser* browser);
-void DuplicateTabAt(Browser* browser, int index);
+TabContents* DuplicateTabAt(Browser* browser, int index);
 bool CanDuplicateTabAt(Browser* browser, int index);
 void ConvertPopupToTabbedBrowser(Browser* browser);
 void Exit();
diff --git a/chrome/common/extensions/api/tabs.json b/chrome/common/extensions/api/tabs.json
index bacd2c9a..7f88a83e 100644
--- a/chrome/common/extensions/api/tabs.json
+++ b/chrome/common/extensions/api/tabs.json
@@ -274,6 +274,32 @@
         ]
       },
       {
+        "name": "duplicate",
+        "type": "function",
+        "description": "Duplicates a tab. Note: This function can be used without requesting the 'tabs' permission in the manifest.",
+        "parameters": [
+          {
+            "type": "integer",
+            "name": "tabId",
+            "minimum": 0,
+            "description": "The ID of the tab which is to be duplicated."
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": [
+              {
+                "name": "tab",
+                "optional": true,
+                "description": "Details about the duplicated tab. The Tab object doesn't contain url, title and faviconUrl if the 'tabs' permission has not been requested.",
+                "$ref": "Tab"
+              }
+            ]
+          }
+        ]
+      },
+      {
         "name": "query",
         "type": "function",
         "description": "Gets all tabs that have the specified properties, or all tabs if no properties are specified.",
diff --git a/chrome/common/extensions/extension_unittest.cc b/chrome/common/extensions/extension_unittest.cc
index ebe75be4..72d04c3 100644
--- a/chrome/common/extensions/extension_unittest.cc
+++ b/chrome/common/extensions/extension_unittest.cc
@@ -602,6 +602,7 @@
     { "browserAction.abcd.onClick",  true },
     // Test Tabs functions.
     { "tabs.create",      true},
+    { "tabs.duplicate",   true},
     { "tabs.onRemoved",   true},
     { "tabs.remove",      true},
     { "tabs.update",      true},
diff --git a/chrome/common/extensions/permissions/permission_set_unittest.cc b/chrome/common/extensions/permissions/permission_set_unittest.cc
index a884bd0..b87a8b3 100644
--- a/chrome/common/extensions/permissions/permission_set_unittest.cc
+++ b/chrome/common/extensions/permissions/permission_set_unittest.cc
@@ -770,6 +770,7 @@
     { "browserAction.abcd.onClick",  true },
     // Test Tabs functions.
     { "tabs.create",      true},
+    { "tabs.duplicate",   true},
     { "tabs.update",      true},
     { "tabs.getSelected", true},
     { "tabs.onUpdated",   true },
diff --git a/chrome/test/data/extensions/api_test/tabs/basics/duplicate.html b/chrome/test/data/extensions/api_test/tabs/basics/duplicate.html
new file mode 100644
index 0000000..37facd6
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tabs/basics/duplicate.html
@@ -0,0 +1,7 @@
+<!--
+ * Copyright (c) 2011 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.
+-->
+<script src="tabs_util.js"></script>
+<script src="duplicate.js"></script>
diff --git a/chrome/test/data/extensions/api_test/tabs/basics/duplicate.js b/chrome/test/data/extensions/api_test/tabs/basics/duplicate.js
new file mode 100644
index 0000000..78bd2e9
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tabs/basics/duplicate.js
@@ -0,0 +1,32 @@
+// Copyright (c) 2011 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.
+
+var firstTabId;
+var firstWindowId;
+
+chrome.test.runTests([
+  function setupWindow() {
+    createWindow([pageUrl("a")], {},
+                 pass(function(winId, tabIds) {
+      firstWindowId = winId;
+      firstTabId = tabIds[0];
+    }));
+  },
+
+  function duplicateTab() {
+    chrome.tabs.duplicate(firstTabId, pass(function(tab) {
+      assertEq(pageUrl("a"), tab.url);
+      assertEq(1, tab.index);
+    }));
+  },
+
+  function totalTab() {
+    chrome.tabs.getAllInWindow(firstWindowId,
+      pass(function(tabs) {
+        assertEq(tabs.length, 2);
+        assertEq(tabs[0].url, tabs[1].url);
+        assertEq(tabs[0].index + 1, tabs[1].index);
+      }));
+  }
+]);