Add option of string item IDs (given at create() time) to contextMenus API.

This will be (but is not yet) required for event pages.

BUG=123366
TEST=automated tests

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

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@137323 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/extensions/api/context_menu/context_menu_api.cc b/chrome/browser/extensions/api/context_menu/context_menu_api.cc
index 83e48763..695abb7 100644
--- a/chrome/browser/extensions/api/context_menu/context_menu_api.cc
+++ b/chrome/browser/extensions/api/context_menu/context_menu_api.cc
@@ -18,6 +18,7 @@
 const char kDocumentUrlPatternsKey[] = "documentUrlPatterns";
 const char kEnabledKey[] = "enabled";
 const char kGeneratedIdKey[] = "generatedId";
+const char kIdKey[] = "id";
 const char kParentIdKey[] = "parentId";
 const char kTargetUrlPatternsKey[] = "targetUrlPatterns";
 const char kTitleKey[] = "title";
@@ -26,6 +27,8 @@
 const char kCannotFindItemError[] = "Cannot find menu item with id *";
 const char kCheckedError[] =
     "Only items with type \"radio\" or \"checkbox\" can be checked";
+const char kDuplicateIDError[] =
+    "Cannot create item with duplicate id *";
 const char kInvalidURLPatternError[] = "Invalid url pattern '*'";
 const char kInvalidValueError[] = "Invalid value for *";
 const char kInvalidTypeStringError[] = "Invalid type string '*'";
@@ -36,6 +39,17 @@
 
 namespace extensions {
 
+namespace {
+
+std::string GetIDString(const ExtensionMenuItem::Id& id) {
+  if (id.uid == 0)
+    return id.string_uid;
+  else
+    return base::IntToString(id.uid);
+}
+
+}  // namespace
+
 bool ExtensionContextMenuFunction::ParseContexts(
     const DictionaryValue& properties,
     const char* key,
@@ -180,21 +194,33 @@
   return true;
 }
 
+bool ExtensionContextMenuFunction::ParseID(
+    const Value* value,
+    ExtensionMenuItem::Id* result) {
+  if (value->GetAsInteger(&result->uid) ||
+      value->GetAsString(&result->string_uid)) {
+    return true;
+  } else {
+    return false;
+  }
+}
+
 bool ExtensionContextMenuFunction::GetParent(
     const DictionaryValue& properties,
     const ExtensionMenuManager& manager,
     ExtensionMenuItem** result) {
   if (!properties.HasKey(kParentIdKey))
     return true;
-  ExtensionMenuItem::Id parent_id(profile(), extension_id(), 0);
-  if (properties.HasKey(kParentIdKey) &&
-      !properties.GetInteger(kParentIdKey, &parent_id.uid))
+  ExtensionMenuItem::Id parent_id(profile(), extension_id());
+  Value* parent_id_value = NULL;
+  if (properties.Get(kParentIdKey, &parent_id_value) &&
+      !ParseID(parent_id_value, &parent_id))
     return false;
 
   ExtensionMenuItem* parent = manager.GetItemById(parent_id);
   if (!parent) {
-    error_ = "Cannot find menu item with id " +
-        base::IntToString(parent_id.uid);
+    error_ = ExtensionErrorUtils::FormatErrorMessage(
+        kCannotFindItemError, GetIDString(parent_id));
     return false;
   }
   if (parent->type() != ExtensionMenuItem::NORMAL) {
@@ -210,9 +236,15 @@
   EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &properties));
   EXTENSION_FUNCTION_VALIDATE(properties != NULL);
 
-  ExtensionMenuItem::Id id(profile(), extension_id(), 0);
-  EXTENSION_FUNCTION_VALIDATE(properties->GetInteger(kGeneratedIdKey,
-                                                     &id.uid));
+  ExtensionMenuItem::Id id(profile(), extension_id());
+  if (properties->HasKey(kIdKey)) {
+    EXTENSION_FUNCTION_VALIDATE(properties->GetString(kIdKey,
+                                                      &id.string_uid));
+  } else {
+    EXTENSION_FUNCTION_VALIDATE(properties->GetInteger(kGeneratedIdKey,
+                                                       &id.uid));
+  }
+
   std::string title;
   if (properties->HasKey(kTitleKey) &&
       !properties->GetString(kTitleKey, &title))
@@ -221,6 +253,12 @@
   ExtensionMenuManager* menu_manager =
       profile()->GetExtensionService()->menu_manager();
 
+  if (menu_manager->GetItemById(id)) {
+    error_ = ExtensionErrorUtils::FormatErrorMessage(kDuplicateIDError,
+                                                     GetIDString(id));
+    return false;
+  }
+
   ExtensionMenuItem::ContextList contexts(ExtensionMenuItem::PAGE);
   if (!ParseContexts(*properties, kContextsKey, &contexts))
     return false;
@@ -250,20 +288,10 @@
 
   bool success = true;
   if (properties->HasKey(kParentIdKey)) {
-    ExtensionMenuItem::Id parent_id(profile(), extension_id(), 0);
-    EXTENSION_FUNCTION_VALIDATE(properties->GetInteger(kParentIdKey,
-                                                       &parent_id.uid));
-    ExtensionMenuItem* parent = menu_manager->GetItemById(parent_id);
-    if (!parent) {
-      error_ = ExtensionErrorUtils::FormatErrorMessage(
-          kCannotFindItemError, base::IntToString(parent_id.uid));
+    ExtensionMenuItem* parent = NULL;
+    if (!GetParent(*properties, *menu_manager, &parent))
       return false;
-    }
-    if (parent->type() != ExtensionMenuItem::NORMAL) {
-      error_ = kParentsMustBeNormalError;
-      return false;
-    }
-    success = menu_manager->AddChildItem(parent_id, item.release());
+    success = menu_manager->AddChildItem(parent->id(), item.release());
   } else {
     success = menu_manager->AddContextItem(GetExtension(), item.release());
   }
@@ -276,15 +304,17 @@
 
 bool UpdateContextMenuFunction::RunImpl() {
   bool radioItemUpdated = false;
-  ExtensionMenuItem::Id item_id(profile(), extension_id(), 0);
-  EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &item_id.uid));
+  ExtensionMenuItem::Id item_id(profile(), extension_id());
+  Value* id_value = NULL;
+  EXTENSION_FUNCTION_VALIDATE(args_->Get(0, &id_value));
+  EXTENSION_FUNCTION_VALIDATE(ParseID(id_value, &item_id));
 
   ExtensionService* service = profile()->GetExtensionService();
   ExtensionMenuManager* manager = service->menu_manager();
   ExtensionMenuItem* item = manager->GetItemById(item_id);
   if (!item || item->extension_id() != extension_id()) {
     error_ = ExtensionErrorUtils::FormatErrorMessage(
-        kCannotFindItemError, base::IntToString(item_id.uid));
+        kCannotFindItemError, GetIDString(item_id));
     return false;
   }
 
@@ -358,8 +388,10 @@
 }
 
 bool RemoveContextMenuFunction::RunImpl() {
-  ExtensionMenuItem::Id id(profile(), extension_id(), 0);
-  EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &id.uid));
+  ExtensionMenuItem::Id id(profile(), extension_id());
+  Value* id_value = NULL;
+  EXTENSION_FUNCTION_VALIDATE(args_->Get(0, &id_value));
+  EXTENSION_FUNCTION_VALIDATE(ParseID(id_value, &id));
   ExtensionService* service = profile()->GetExtensionService();
   ExtensionMenuManager* manager = service->menu_manager();
 
@@ -367,7 +399,7 @@
   // Ensure one extension can't remove another's menu items.
   if (!item || item->extension_id() != extension_id()) {
     error_ = ExtensionErrorUtils::FormatErrorMessage(
-        kCannotFindItemError, base::IntToString(id.uid));
+        kCannotFindItemError, GetIDString(id));
     return false;
   }
 
diff --git a/chrome/browser/extensions/api/context_menu/context_menu_api.h b/chrome/browser/extensions/api/context_menu/context_menu_api.h
index 2f88137..6117e3e 100644
--- a/chrome/browser/extensions/api/context_menu/context_menu_api.h
+++ b/chrome/browser/extensions/api/context_menu/context_menu_api.h
@@ -52,6 +52,11 @@
   bool SetURLPatterns(const base::DictionaryValue& properties,
                       ExtensionMenuItem* item);
 
+  // Helper to read an ID from the Value*. The ID can be either a string or
+  // integer.
+  bool ParseID(const Value* value,
+               ExtensionMenuItem::Id* result);
+
   // If the parentId key was specified in properties, this will try looking up
   // an ExtensionMenuItem with that id and set it into |result|. Returns false
   // on error, with an explanation written into error_. Note that if the
diff --git a/chrome/browser/extensions/api/context_menu/context_menu_apitest.cc b/chrome/browser/extensions/api/context_menu/context_menu_apitest.cc
index 973a299..094df01 100644
--- a/chrome/browser/extensions/api/context_menu/context_menu_apitest.cc
+++ b/chrome/browser/extensions/api/context_menu/context_menu_apitest.cc
@@ -15,6 +15,7 @@
 IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContextMenus) {
   ASSERT_TRUE(RunExtensionTest("context_menus/basics")) << message_;
   ASSERT_TRUE(RunExtensionTest("context_menus/no_perms")) << message_;
+  ASSERT_TRUE(RunExtensionTest("context_menus/item_ids")) << message_;
 }
 
 // crbug.com/51436 -- creating context menus from multiple script contexts
diff --git a/chrome/browser/extensions/extension_menu_manager.cc b/chrome/browser/extensions/extension_menu_manager.cc
index d12989c..8f320056 100644
--- a/chrome/browser/extensions/extension_menu_manager.cc
+++ b/chrome/browser/extensions/extension_menu_manager.cc
@@ -25,6 +25,19 @@
 
 using content::WebContents;
 
+namespace {
+
+void SetIdKeyValue(base::DictionaryValue* properties,
+                   const char* key,
+                   ExtensionMenuItem::Id id) {
+  if (id.uid == 0)
+    properties->SetString(key, id.string_uid);
+  else
+    properties->SetInteger(key, id.uid);
+}
+
+}  // namespace
+
 ExtensionMenuItem::ExtensionMenuItem(const Id& id,
                                      const std::string& title,
                                      bool checked,
@@ -408,9 +421,9 @@
   ListValue args;
 
   DictionaryValue* properties = new DictionaryValue();
-  properties->SetInteger("menuItemId", item->id().uid);
+  SetIdKeyValue(properties, "menuItemId", item->id());
   if (item->parent_id())
-    properties->SetInteger("parentMenuItemId", item->parent_id()->uid);
+    SetIdKeyValue(properties, "parentMenuItemId", item->id());
 
   switch (params.media_type) {
     case WebKit::WebContextMenuData::MediaTypeImage:
@@ -464,6 +477,7 @@
   event_router->DispatchEventToExtension(
       item->extension_id(), event_name, json_args, profile, GURL(),
       ExtensionEventRouter::USER_GESTURE_ENABLED);
+  // TODO(yoz): dispatch another event onClicked.
 }
 
 void ExtensionMenuManager::SanitizeRadioList(
@@ -543,13 +557,13 @@
 }
 
 ExtensionMenuItem::Id::Id()
-    : profile(NULL), uid(0) {
+    : profile(NULL), extension_id(""), uid(0), string_uid("") {
 }
 
 ExtensionMenuItem::Id::Id(Profile* profile,
-                          const std::string& extension_id,
-                          int uid)
-    : profile(profile), extension_id(extension_id), uid(uid) {
+                          const std::string& extension_id)
+    : profile(profile), extension_id(extension_id), uid(0),
+      string_uid("") {
 }
 
 ExtensionMenuItem::Id::~Id() {
@@ -558,7 +572,8 @@
 bool ExtensionMenuItem::Id::operator==(const Id& other) const {
   return (profile == other.profile &&
           extension_id == other.extension_id &&
-          uid == other.uid);
+          uid == other.uid &&
+          string_uid == other.string_uid);
 }
 
 bool ExtensionMenuItem::Id::operator!=(const Id& other) const {
@@ -571,8 +586,12 @@
   if (profile == other.profile) {
     if (extension_id < other.extension_id)
       return true;
-    if (extension_id == other.extension_id)
-      return uid < other.uid;
+    if (extension_id == other.extension_id) {
+      if (uid < other.uid)
+        return true;
+      if (uid == other.uid)
+        return string_uid < other.string_uid;
+    }
   }
   return false;
 }
diff --git a/chrome/browser/extensions/extension_menu_manager.h b/chrome/browser/extensions/extension_menu_manager.h
index 299fa05..30a8b35 100644
--- a/chrome/browser/extensions/extension_menu_manager.h
+++ b/chrome/browser/extensions/extension_menu_manager.h
@@ -40,7 +40,10 @@
   // An Id uniquely identifies a context menu item registered by an extension.
   struct Id {
     Id();
-    Id(Profile* profile, const std::string& extension_id, int uid);
+    // Since the unique ID (uid or string_uid) is parsed from API arguments,
+    // the normal usage is to set the uid or string_uid immediately after
+    // construction.
+    Id(Profile* profile, const std::string& extension_id);
     ~Id();
 
     bool operator==(const Id& other) const;
@@ -49,7 +52,9 @@
 
     Profile* profile;
     std::string extension_id;
+    // Only one of uid or string_uid will be defined.
     int uid;
+    std::string string_uid;
   };
 
   // For context menus, these are the contexts where an item can appear.
diff --git a/chrome/browser/extensions/extension_menu_manager_unittest.cc b/chrome/browser/extensions/extension_menu_manager_unittest.cc
index cae9a9b..a506545 100644
--- a/chrome/browser/extensions/extension_menu_manager_unittest.cc
+++ b/chrome/browser/extensions/extension_menu_manager_unittest.cc
@@ -45,7 +45,18 @@
   ExtensionMenuItem* CreateTestItem(Extension* extension) {
     ExtensionMenuItem::Type type = ExtensionMenuItem::NORMAL;
     ExtensionMenuItem::ContextList contexts(ExtensionMenuItem::ALL);
-    ExtensionMenuItem::Id id(NULL, extension->id(), next_id_++);
+    ExtensionMenuItem::Id id(NULL, extension->id());
+    id.uid = next_id_++;
+    return new ExtensionMenuItem(id, "test", false, true, type, contexts);
+  }
+
+  // Returns a test item with the given string ID.
+  ExtensionMenuItem* CreateTestItemWithID(Extension* extension,
+                                          const std::string& string_id) {
+    ExtensionMenuItem::Type type = ExtensionMenuItem::NORMAL;
+    ExtensionMenuItem::ContextList contexts(ExtensionMenuItem::ALL);
+    ExtensionMenuItem::Id id(NULL, extension->id());
+    id.string_uid = string_id;
     return new ExtensionMenuItem(id, "test", false, true, type, contexts);
   }
 
@@ -87,7 +98,7 @@
   ASSERT_EQ(item1, items->at(0));
 
   // Add a second item, make sure it comes back too.
-  ExtensionMenuItem* item2 = CreateTestItem(extension);
+  ExtensionMenuItem* item2 = CreateTestItemWithID(extension, "id2");
   ASSERT_TRUE(manager_.AddContextItem(extension, item2));
   ASSERT_EQ(item2, manager_.GetItemById(item2->id()));
   items = manager_.MenuItems(item2->extension_id());
@@ -107,8 +118,19 @@
   ASSERT_EQ(2u, manager_.MenuItems(extension_id)->size());
 
   // Make sure removing a non-existent item returns false.
-  ExtensionMenuItem::Id id(NULL, extension->id(), id3.uid + 50);
+  ExtensionMenuItem::Id id(NULL, extension->id());
+  id.uid = id3.uid + 50;
   ASSERT_FALSE(manager_.RemoveContextMenuItem(id));
+
+  // Make sure adding an item with the same string ID returns false.
+  scoped_ptr<ExtensionMenuItem> item2too(
+      CreateTestItemWithID(extension, "id2"));
+  ASSERT_FALSE(manager_.AddContextItem(extension, item2too.get()));
+
+  // But the same string ID should not collide with another extension.
+  Extension* extension2 = AddExtension("test2");
+  ExtensionMenuItem* item2other = CreateTestItemWithID(extension2, "id2");
+  ASSERT_TRUE(manager_.AddContextItem(extension2, item2other));
 }
 
 // Test adding/removing child items.
@@ -119,7 +141,7 @@
 
   ExtensionMenuItem* item1 = CreateTestItem(extension1);
   ExtensionMenuItem* item2 = CreateTestItem(extension2);
-  ExtensionMenuItem* item2_child = CreateTestItem(extension2);
+  ExtensionMenuItem* item2_child = CreateTestItemWithID(extension2, "2child");
   ExtensionMenuItem* item2_grandchild = CreateTestItem(extension2);
 
   // This third item we expect to fail inserting, so we use a scoped_ptr to make
@@ -173,8 +195,8 @@
   // Set up 5 items to add.
   ExtensionMenuItem* item1 = CreateTestItem(extension);
   ExtensionMenuItem* item2 = CreateTestItem(extension);
-  ExtensionMenuItem* item3 = CreateTestItem(extension);
-  ExtensionMenuItem* item4 = CreateTestItem(extension);
+  ExtensionMenuItem* item3 = CreateTestItemWithID(extension, "id3");
+  ExtensionMenuItem* item4 = CreateTestItemWithID(extension, "id4");
   ExtensionMenuItem* item5 = CreateTestItem(extension);
   ExtensionMenuItem* item6 = CreateTestItem(extension);
   ExtensionMenuItem::Id item1_id = item1->id();
@@ -370,7 +392,6 @@
  public:
   MockTestingProfile() {}
   MOCK_METHOD0(GetExtensionEventRouter, ExtensionEventRouter*());
-  MOCK_CONST_METHOD0(IsOffTheRecord, bool());
 
  private:
   DISALLOW_COPY_AND_ASSIGN(MockTestingProfile);
@@ -414,11 +435,14 @@
   Extension* extension1 = AddExtension("1111");
   ExtensionMenuItem* item1 = CreateTestItem(extension1);
   ExtensionMenuItem* item2 = CreateTestItem(extension1);
+  ExtensionMenuItem* item3 = CreateTestItemWithID(extension1, "id3");
   ASSERT_TRUE(manager_.AddContextItem(extension1, item1));
   ASSERT_TRUE(manager_.AddContextItem(extension1, item2));
+  ASSERT_TRUE(manager_.AddContextItem(extension1, item3));
 
   ASSERT_FALSE(manager_.context_items_.empty());
 
+  manager_.RemoveContextMenuItem(item3->id());
   manager_.RemoveContextMenuItem(item1->id());
   manager_.RemoveContextMenuItem(item2->id());