Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/extensions/extension_menu_manager.h"
      6 
      7 #include <algorithm>
      8 
      9 #include "base/json/json_writer.h"
     10 #include "base/logging.h"
     11 #include "base/stl_util-inl.h"
     12 #include "base/string_util.h"
     13 #include "base/utf_string_conversions.h"
     14 #include "base/values.h"
     15 #include "chrome/browser/extensions/extension_event_router.h"
     16 #include "chrome/browser/extensions/extension_tabs_module.h"
     17 #include "chrome/browser/profiles/profile.h"
     18 #include "chrome/common/extensions/extension.h"
     19 #include "content/common/notification_service.h"
     20 #include "ui/base/l10n/l10n_util.h"
     21 #include "ui/gfx/favicon_size.h"
     22 #include "webkit/glue/context_menu.h"
     23 
     24 ExtensionMenuItem::ExtensionMenuItem(const Id& id,
     25                                      const std::string& title,
     26                                      bool checked,
     27                                      Type type,
     28                                      const ContextList& contexts)
     29     : id_(id),
     30       title_(title),
     31       type_(type),
     32       checked_(checked),
     33       contexts_(contexts),
     34       parent_id_(0) {
     35 }
     36 
     37 ExtensionMenuItem::~ExtensionMenuItem() {
     38   STLDeleteElements(&children_);
     39 }
     40 
     41 ExtensionMenuItem* ExtensionMenuItem::ReleaseChild(const Id& child_id,
     42                                                    bool recursive) {
     43   for (List::iterator i = children_.begin(); i != children_.end(); ++i) {
     44     ExtensionMenuItem* child = NULL;
     45     if ((*i)->id() == child_id) {
     46       child = *i;
     47       children_.erase(i);
     48       return child;
     49     } else if (recursive) {
     50       child = (*i)->ReleaseChild(child_id, recursive);
     51       if (child)
     52         return child;
     53     }
     54   }
     55   return NULL;
     56 }
     57 
     58 std::set<ExtensionMenuItem::Id> ExtensionMenuItem::RemoveAllDescendants() {
     59   std::set<Id> result;
     60   for (List::iterator i = children_.begin(); i != children_.end(); ++i) {
     61     ExtensionMenuItem* child = *i;
     62     result.insert(child->id());
     63     std::set<Id> removed = child->RemoveAllDescendants();
     64     result.insert(removed.begin(), removed.end());
     65   }
     66   STLDeleteElements(&children_);
     67   return result;
     68 }
     69 
     70 string16 ExtensionMenuItem::TitleWithReplacement(
     71     const string16& selection, size_t max_length) const {
     72   string16 result = UTF8ToUTF16(title_);
     73   // TODO(asargent) - Change this to properly handle %% escaping so you can
     74   // put "%s" in titles that won't get substituted.
     75   ReplaceSubstringsAfterOffset(&result, 0, ASCIIToUTF16("%s"), selection);
     76 
     77   if (result.length() > max_length)
     78     result = l10n_util::TruncateString(result, max_length);
     79   return result;
     80 }
     81 
     82 bool ExtensionMenuItem::SetChecked(bool checked) {
     83   if (type_ != CHECKBOX && type_ != RADIO)
     84     return false;
     85   checked_ = checked;
     86   return true;
     87 }
     88 
     89 void ExtensionMenuItem::AddChild(ExtensionMenuItem* item) {
     90   item->parent_id_.reset(new Id(id_));
     91   children_.push_back(item);
     92 }
     93 
     94 const int ExtensionMenuManager::kAllowedSchemes =
     95     URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS;
     96 
     97 ExtensionMenuManager::ExtensionMenuManager() {
     98   registrar_.Add(this, NotificationType::EXTENSION_UNLOADED,
     99                  NotificationService::AllSources());
    100 }
    101 
    102 ExtensionMenuManager::~ExtensionMenuManager() {
    103   MenuItemMap::iterator i;
    104   for (i = context_items_.begin(); i != context_items_.end(); ++i) {
    105     STLDeleteElements(&(i->second));
    106   }
    107 }
    108 
    109 std::set<std::string> ExtensionMenuManager::ExtensionIds() {
    110   std::set<std::string> id_set;
    111   for (MenuItemMap::const_iterator i = context_items_.begin();
    112        i != context_items_.end(); ++i) {
    113     id_set.insert(i->first);
    114   }
    115   return id_set;
    116 }
    117 
    118 const ExtensionMenuItem::List* ExtensionMenuManager::MenuItems(
    119     const std::string& extension_id) {
    120   MenuItemMap::iterator i = context_items_.find(extension_id);
    121   if (i != context_items_.end()) {
    122     return &(i->second);
    123   }
    124   return NULL;
    125 }
    126 
    127 bool ExtensionMenuManager::AddContextItem(const Extension* extension,
    128                                           ExtensionMenuItem* item) {
    129   const std::string& extension_id = item->extension_id();
    130   // The item must have a non-empty extension id, and not have already been
    131   // added.
    132   if (extension_id.empty() || ContainsKey(items_by_id_, item->id()))
    133     return false;
    134 
    135   DCHECK_EQ(extension->id(), extension_id);
    136 
    137   bool first_item = !ContainsKey(context_items_, extension_id);
    138   context_items_[extension_id].push_back(item);
    139   items_by_id_[item->id()] = item;
    140 
    141   if (item->type() == ExtensionMenuItem::RADIO && item->checked())
    142     RadioItemSelected(item);
    143 
    144   // If this is the first item for this extension, start loading its icon.
    145   if (first_item)
    146     icon_manager_.LoadIcon(extension);
    147 
    148   return true;
    149 }
    150 
    151 bool ExtensionMenuManager::AddChildItem(const ExtensionMenuItem::Id& parent_id,
    152                                         ExtensionMenuItem* child) {
    153   ExtensionMenuItem* parent = GetItemById(parent_id);
    154   if (!parent || parent->type() != ExtensionMenuItem::NORMAL ||
    155       parent->extension_id() != child->extension_id() ||
    156       ContainsKey(items_by_id_, child->id()))
    157     return false;
    158   parent->AddChild(child);
    159   items_by_id_[child->id()] = child;
    160   return true;
    161 }
    162 
    163 bool ExtensionMenuManager::DescendantOf(
    164     ExtensionMenuItem* item,
    165     const ExtensionMenuItem::Id& ancestor_id) {
    166   // Work our way up the tree until we find the ancestor or NULL.
    167   ExtensionMenuItem::Id* id = item->parent_id();
    168   while (id != NULL) {
    169     DCHECK(*id != item->id());  // Catch circular graphs.
    170     if (*id == ancestor_id)
    171       return true;
    172     ExtensionMenuItem* next = GetItemById(*id);
    173     if (!next) {
    174       NOTREACHED();
    175       return false;
    176     }
    177     id = next->parent_id();
    178   }
    179   return false;
    180 }
    181 
    182 bool ExtensionMenuManager::ChangeParent(
    183     const ExtensionMenuItem::Id& child_id,
    184     const ExtensionMenuItem::Id* parent_id) {
    185   ExtensionMenuItem* child = GetItemById(child_id);
    186   ExtensionMenuItem* new_parent = parent_id ? GetItemById(*parent_id) : NULL;
    187   if ((parent_id && (child_id == *parent_id)) || !child ||
    188       (!new_parent && parent_id != NULL) ||
    189       (new_parent && (DescendantOf(new_parent, child_id) ||
    190                       child->extension_id() != new_parent->extension_id())))
    191     return false;
    192 
    193   ExtensionMenuItem::Id* old_parent_id = child->parent_id();
    194   if (old_parent_id != NULL) {
    195     ExtensionMenuItem* old_parent = GetItemById(*old_parent_id);
    196     if (!old_parent) {
    197       NOTREACHED();
    198       return false;
    199     }
    200     ExtensionMenuItem* taken =
    201       old_parent->ReleaseChild(child_id, false /* non-recursive search*/);
    202     DCHECK(taken == child);
    203   } else {
    204     // This is a top-level item, so we need to pull it out of our list of
    205     // top-level items.
    206     MenuItemMap::iterator i = context_items_.find(child->extension_id());
    207     if (i == context_items_.end()) {
    208       NOTREACHED();
    209       return false;
    210     }
    211     ExtensionMenuItem::List& list = i->second;
    212     ExtensionMenuItem::List::iterator j = std::find(list.begin(), list.end(),
    213                                                     child);
    214     if (j == list.end()) {
    215       NOTREACHED();
    216       return false;
    217     }
    218     list.erase(j);
    219   }
    220 
    221   if (new_parent) {
    222     new_parent->AddChild(child);
    223   } else {
    224     context_items_[child->extension_id()].push_back(child);
    225     child->parent_id_.reset(NULL);
    226   }
    227   return true;
    228 }
    229 
    230 bool ExtensionMenuManager::RemoveContextMenuItem(
    231     const ExtensionMenuItem::Id& id) {
    232   if (!ContainsKey(items_by_id_, id))
    233     return false;
    234 
    235   ExtensionMenuItem* menu_item = GetItemById(id);
    236   DCHECK(menu_item);
    237   std::string extension_id = menu_item->extension_id();
    238   MenuItemMap::iterator i = context_items_.find(extension_id);
    239   if (i == context_items_.end()) {
    240     NOTREACHED();
    241     return false;
    242   }
    243 
    244   bool result = false;
    245   std::set<ExtensionMenuItem::Id> items_removed;
    246   ExtensionMenuItem::List& list = i->second;
    247   ExtensionMenuItem::List::iterator j;
    248   for (j = list.begin(); j < list.end(); ++j) {
    249     // See if the current top-level item is a match.
    250     if ((*j)->id() == id) {
    251       items_removed = (*j)->RemoveAllDescendants();
    252       items_removed.insert(id);
    253       delete *j;
    254       list.erase(j);
    255       result = true;
    256       break;
    257     } else {
    258       // See if the item to remove was found as a descendant of the current
    259       // top-level item.
    260       ExtensionMenuItem* child = (*j)->ReleaseChild(id, true /* recursive */);
    261       if (child) {
    262         items_removed = child->RemoveAllDescendants();
    263         items_removed.insert(id);
    264         delete child;
    265         result = true;
    266         break;
    267       }
    268     }
    269   }
    270   DCHECK(result);  // The check at the very top should have prevented this.
    271 
    272   // Clear entries from the items_by_id_ map.
    273   std::set<ExtensionMenuItem::Id>::iterator removed_iter;
    274   for (removed_iter = items_removed.begin();
    275        removed_iter != items_removed.end();
    276        ++removed_iter) {
    277     items_by_id_.erase(*removed_iter);
    278   }
    279 
    280   if (list.empty()) {
    281     context_items_.erase(extension_id);
    282     icon_manager_.RemoveIcon(extension_id);
    283   }
    284 
    285   return result;
    286 }
    287 
    288 void ExtensionMenuManager::RemoveAllContextItems(
    289     const std::string& extension_id) {
    290   ExtensionMenuItem::List::iterator i;
    291   for (i = context_items_[extension_id].begin();
    292        i != context_items_[extension_id].end(); ++i) {
    293     ExtensionMenuItem* item = *i;
    294     items_by_id_.erase(item->id());
    295 
    296     // Remove descendants from this item and erase them from the lookup cache.
    297     std::set<ExtensionMenuItem::Id> removed_ids = item->RemoveAllDescendants();
    298     std::set<ExtensionMenuItem::Id>::const_iterator j;
    299     for (j = removed_ids.begin(); j != removed_ids.end(); ++j) {
    300       items_by_id_.erase(*j);
    301     }
    302   }
    303   STLDeleteElements(&context_items_[extension_id]);
    304   context_items_.erase(extension_id);
    305   icon_manager_.RemoveIcon(extension_id);
    306 }
    307 
    308 ExtensionMenuItem* ExtensionMenuManager::GetItemById(
    309     const ExtensionMenuItem::Id& id) const {
    310   std::map<ExtensionMenuItem::Id, ExtensionMenuItem*>::const_iterator i =
    311       items_by_id_.find(id);
    312   if (i != items_by_id_.end())
    313     return i->second;
    314   else
    315     return NULL;
    316 }
    317 
    318 void ExtensionMenuManager::RadioItemSelected(ExtensionMenuItem* item) {
    319   // If this is a child item, we need to get a handle to the list from its
    320   // parent. Otherwise get a handle to the top-level list.
    321   const ExtensionMenuItem::List* list = NULL;
    322   if (item->parent_id()) {
    323     ExtensionMenuItem* parent = GetItemById(*item->parent_id());
    324     if (!parent) {
    325       NOTREACHED();
    326       return;
    327     }
    328     list = &(parent->children());
    329   } else {
    330     if (context_items_.find(item->extension_id()) == context_items_.end()) {
    331       NOTREACHED();
    332       return;
    333     }
    334     list = &context_items_[item->extension_id()];
    335   }
    336 
    337   // Find where |item| is in the list.
    338   ExtensionMenuItem::List::const_iterator item_location;
    339   for (item_location = list->begin(); item_location != list->end();
    340        ++item_location) {
    341     if (*item_location == item)
    342       break;
    343   }
    344   if (item_location == list->end()) {
    345     NOTREACHED();  // We should have found the item.
    346     return;
    347   }
    348 
    349   // Iterate backwards from |item| and uncheck any adjacent radio items.
    350   ExtensionMenuItem::List::const_iterator i;
    351   if (item_location != list->begin()) {
    352     i = item_location;
    353     do {
    354       --i;
    355       if ((*i)->type() != ExtensionMenuItem::RADIO)
    356         break;
    357       (*i)->SetChecked(false);
    358     } while (i != list->begin());
    359   }
    360 
    361   // Now iterate forwards from |item| and uncheck any adjacent radio items.
    362   for (i = item_location + 1; i != list->end(); ++i) {
    363     if ((*i)->type() != ExtensionMenuItem::RADIO)
    364       break;
    365     (*i)->SetChecked(false);
    366   }
    367 }
    368 
    369 static void AddURLProperty(DictionaryValue* dictionary,
    370                            const std::string& key, const GURL& url) {
    371   if (!url.is_empty())
    372     dictionary->SetString(key, url.possibly_invalid_spec());
    373 }
    374 
    375 void ExtensionMenuManager::ExecuteCommand(
    376     Profile* profile,
    377     TabContents* tab_contents,
    378     const ContextMenuParams& params,
    379     const ExtensionMenuItem::Id& menuItemId) {
    380   ExtensionEventRouter* event_router = profile->GetExtensionEventRouter();
    381   if (!event_router)
    382     return;
    383 
    384   ExtensionMenuItem* item = GetItemById(menuItemId);
    385   if (!item)
    386     return;
    387 
    388   if (item->type() == ExtensionMenuItem::RADIO)
    389     RadioItemSelected(item);
    390 
    391   ListValue args;
    392 
    393   DictionaryValue* properties = new DictionaryValue();
    394   properties->SetInteger("menuItemId", item->id().uid);
    395   if (item->parent_id())
    396     properties->SetInteger("parentMenuItemId", item->parent_id()->uid);
    397 
    398   switch (params.media_type) {
    399     case WebKit::WebContextMenuData::MediaTypeImage:
    400       properties->SetString("mediaType", "image");
    401       break;
    402     case WebKit::WebContextMenuData::MediaTypeVideo:
    403       properties->SetString("mediaType", "video");
    404       break;
    405     case WebKit::WebContextMenuData::MediaTypeAudio:
    406       properties->SetString("mediaType", "audio");
    407       break;
    408     default:  {}  // Do nothing.
    409   }
    410 
    411   AddURLProperty(properties, "linkUrl", params.unfiltered_link_url);
    412   AddURLProperty(properties, "srcUrl", params.src_url);
    413   AddURLProperty(properties, "pageUrl", params.page_url);
    414   AddURLProperty(properties, "frameUrl", params.frame_url);
    415 
    416   if (params.selection_text.length() > 0)
    417     properties->SetString("selectionText", params.selection_text);
    418 
    419   properties->SetBoolean("editable", params.is_editable);
    420 
    421   args.Append(properties);
    422 
    423   // Add the tab info to the argument list.
    424   if (tab_contents) {
    425     args.Append(ExtensionTabUtil::CreateTabValue(tab_contents));
    426   } else {
    427     args.Append(new DictionaryValue());
    428   }
    429 
    430   if (item->type() == ExtensionMenuItem::CHECKBOX ||
    431       item->type() == ExtensionMenuItem::RADIO) {
    432     bool was_checked = item->checked();
    433     properties->SetBoolean("wasChecked", was_checked);
    434 
    435     // RADIO items always get set to true when you click on them, but CHECKBOX
    436     // items get their state toggled.
    437     bool checked =
    438         (item->type() == ExtensionMenuItem::RADIO) ? true : !was_checked;
    439 
    440     item->SetChecked(checked);
    441     properties->SetBoolean("checked", item->checked());
    442   }
    443 
    444   std::string json_args;
    445   base::JSONWriter::Write(&args, false, &json_args);
    446   std::string event_name = "contextMenus";
    447   event_router->DispatchEventToExtension(
    448       item->extension_id(), event_name, json_args, profile, GURL());
    449 }
    450 
    451 void ExtensionMenuManager::Observe(NotificationType type,
    452                                    const NotificationSource& source,
    453                                    const NotificationDetails& details) {
    454   // Remove menu items for disabled/uninstalled extensions.
    455   if (type != NotificationType::EXTENSION_UNLOADED) {
    456     NOTREACHED();
    457     return;
    458   }
    459   const Extension* extension =
    460       Details<UnloadedExtensionInfo>(details)->extension;
    461   if (ContainsKey(context_items_, extension->id())) {
    462     RemoveAllContextItems(extension->id());
    463   }
    464 }
    465 
    466 const SkBitmap& ExtensionMenuManager::GetIconForExtension(
    467     const std::string& extension_id) {
    468   return icon_manager_.GetIcon(extension_id);
    469 }
    470 
    471 // static
    472 bool ExtensionMenuManager::HasAllowedScheme(const GURL& url) {
    473   URLPattern pattern(kAllowedSchemes);
    474   return pattern.SetScheme(url.scheme());
    475 }
    476 
    477 ExtensionMenuItem::Id::Id()
    478     : profile(NULL), uid(0) {
    479 }
    480 
    481 ExtensionMenuItem::Id::Id(Profile* profile,
    482                           const std::string& extension_id,
    483                           int uid)
    484     : profile(profile), extension_id(extension_id), uid(uid) {
    485 }
    486 
    487 ExtensionMenuItem::Id::~Id() {
    488 }
    489 
    490 bool ExtensionMenuItem::Id::operator==(const Id& other) const {
    491   return (profile == other.profile &&
    492           extension_id == other.extension_id &&
    493           uid == other.uid);
    494 }
    495 
    496 bool ExtensionMenuItem::Id::operator!=(const Id& other) const {
    497   return !(*this == other);
    498 }
    499 
    500 bool ExtensionMenuItem::Id::operator<(const Id& other) const {
    501   if (profile < other.profile)
    502     return true;
    503   if (profile == other.profile) {
    504     if (extension_id < other.extension_id)
    505       return true;
    506     if (extension_id == other.extension_id)
    507       return uid < other.uid;
    508   }
    509   return false;
    510 }
    511