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_context_menu_api.h"
6
7#include <string>
8
9#include "base/values.h"
10#include "base/string_number_conversions.h"
11#include "base/string_util.h"
12#include "chrome/browser/extensions/extension_service.h"
13#include "chrome/browser/profiles/profile.h"
14#include "chrome/common/extensions/extension_error_utils.h"
15
16const char kCheckedKey[] = "checked";
17const char kContextsKey[] = "contexts";
18const char kDocumentUrlPatternsKey[] = "documentUrlPatterns";
19const char kGeneratedIdKey[] = "generatedId";
20const char kParentIdKey[] = "parentId";
21const char kTargetUrlPatternsKey[] = "targetUrlPatterns";
22const char kTitleKey[] = "title";
23const char kTypeKey[] = "type";
24
25const char kCannotFindItemError[] = "Cannot find menu item with id *";
26const char kCheckedError[] =
27    "Only items with type \"radio\" or \"checkbox\" can be checked";
28const char kInvalidURLPatternError[] = "Invalid url pattern '*'";
29const char kInvalidValueError[] = "Invalid value for *";
30const char kInvalidTypeStringError[] = "Invalid type string '*'";
31const char kParentsMustBeNormalError[] =
32    "Parent items must have type \"normal\"";
33const char kTitleNeededError[] =
34    "All menu items except for separators must have a title";
35
36
37bool ExtensionContextMenuFunction::ParseContexts(
38    const DictionaryValue& properties,
39    const char* key,
40    ExtensionMenuItem::ContextList* result) {
41  ListValue* list = NULL;
42  if (!properties.GetList(key, &list)) {
43    return true;
44  }
45  ExtensionMenuItem::ContextList tmp_result;
46
47  std::string value;
48  for (size_t i = 0; i < list->GetSize(); i++) {
49    if (!list->GetString(i, &value))
50      return false;
51
52    if (value == "all") {
53      tmp_result.Add(ExtensionMenuItem::ALL);
54    } else if (value == "page") {
55      tmp_result.Add(ExtensionMenuItem::PAGE);
56    } else if (value == "selection") {
57      tmp_result.Add(ExtensionMenuItem::SELECTION);
58    } else if (value == "link") {
59      tmp_result.Add(ExtensionMenuItem::LINK);
60    } else if (value == "editable") {
61      tmp_result.Add(ExtensionMenuItem::EDITABLE);
62    } else if (value == "image") {
63      tmp_result.Add(ExtensionMenuItem::IMAGE);
64    } else if (value == "video") {
65      tmp_result.Add(ExtensionMenuItem::VIDEO);
66    } else if (value == "audio") {
67      tmp_result.Add(ExtensionMenuItem::AUDIO);
68    } else if (value == "frame") {
69      tmp_result.Add(ExtensionMenuItem::FRAME);
70    } else {
71      error_ = ExtensionErrorUtils::FormatErrorMessage(kInvalidValueError, key);
72      return false;
73    }
74  }
75  *result = tmp_result;
76  return true;
77}
78
79bool ExtensionContextMenuFunction::ParseType(
80    const DictionaryValue& properties,
81    const ExtensionMenuItem::Type& default_value,
82    ExtensionMenuItem::Type* result) {
83  DCHECK(result);
84  if (!properties.HasKey(kTypeKey)) {
85    *result = default_value;
86    return true;
87  }
88
89  std::string type_string;
90  if (!properties.GetString(kTypeKey, &type_string))
91    return false;
92
93  if (type_string == "normal") {
94    *result = ExtensionMenuItem::NORMAL;
95  } else if (type_string == "checkbox") {
96    *result = ExtensionMenuItem::CHECKBOX;
97  } else if (type_string == "radio") {
98    *result = ExtensionMenuItem::RADIO;
99  } else if (type_string == "separator") {
100    *result = ExtensionMenuItem::SEPARATOR;
101  } else {
102    error_ = ExtensionErrorUtils::FormatErrorMessage(kInvalidTypeStringError,
103                                                     type_string);
104    return false;
105  }
106  return true;
107}
108
109bool ExtensionContextMenuFunction::ParseChecked(
110    ExtensionMenuItem::Type type,
111    const DictionaryValue& properties,
112    bool default_value,
113    bool* checked) {
114  if (!properties.HasKey(kCheckedKey)) {
115    *checked = default_value;
116    return true;
117  }
118  if (!properties.GetBoolean(kCheckedKey, checked))
119    return false;
120  if (checked && type != ExtensionMenuItem::CHECKBOX &&
121      type != ExtensionMenuItem::RADIO) {
122    error_ = kCheckedError;
123    return false;
124  }
125  return true;
126}
127
128bool ExtensionContextMenuFunction::ParseURLPatterns(
129    const DictionaryValue& properties,
130    const char* key,
131    ExtensionExtent* result) {
132  if (!properties.HasKey(key))
133    return true;
134  ListValue* list = NULL;
135  if (!properties.GetList(key, &list))
136    return false;
137  for (ListValue::iterator i = list->begin(); i != list->end(); ++i) {
138    std::string tmp;
139    if (!(*i)->GetAsString(&tmp))
140      return false;
141
142    URLPattern pattern(ExtensionMenuManager::kAllowedSchemes);
143    // TODO(skerner):  Consider enabling strict pattern parsing
144    // if this extension's location indicates that it is under development.
145    if (URLPattern::PARSE_SUCCESS != pattern.Parse(tmp,
146                                                   URLPattern::PARSE_LENIENT)) {
147      error_ = ExtensionErrorUtils::FormatErrorMessage(kInvalidURLPatternError,
148                                                       tmp);
149      return false;
150    }
151    result->AddPattern(pattern);
152  }
153  return true;
154}
155
156bool ExtensionContextMenuFunction::SetURLPatterns(
157    const DictionaryValue& properties,
158    ExtensionMenuItem* item) {
159  // Process the documentUrlPattern value.
160  ExtensionExtent document_url_patterns;
161  if (!ParseURLPatterns(properties, kDocumentUrlPatternsKey,
162                        &document_url_patterns))
163    return false;
164
165  if (!document_url_patterns.is_empty()) {
166    item->set_document_url_patterns(document_url_patterns);
167  }
168
169  // Process the targetUrlPattern value.
170  ExtensionExtent target_url_patterns;
171  if (!ParseURLPatterns(properties, kTargetUrlPatternsKey,
172                        &target_url_patterns))
173    return false;
174
175  if (!target_url_patterns.is_empty()) {
176    item->set_target_url_patterns(target_url_patterns);
177  }
178
179  return true;
180}
181
182bool ExtensionContextMenuFunction::GetParent(
183    const DictionaryValue& properties,
184    const ExtensionMenuManager& manager,
185    ExtensionMenuItem** result) {
186  if (!properties.HasKey(kParentIdKey))
187    return true;
188  ExtensionMenuItem::Id parent_id(profile(), extension_id(), 0);
189  if (properties.HasKey(kParentIdKey) &&
190      !properties.GetInteger(kParentIdKey, &parent_id.uid))
191    return false;
192
193  ExtensionMenuItem* parent = manager.GetItemById(parent_id);
194  if (!parent) {
195    error_ = "Cannot find menu item with id " +
196        base::IntToString(parent_id.uid);
197    return false;
198  }
199  if (parent->type() != ExtensionMenuItem::NORMAL) {
200    error_ = kParentsMustBeNormalError;
201    return false;
202  }
203  *result = parent;
204  return true;
205}
206
207bool CreateContextMenuFunction::RunImpl() {
208  DictionaryValue* properties;
209  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &properties));
210  EXTENSION_FUNCTION_VALIDATE(properties != NULL);
211
212  ExtensionMenuItem::Id id(profile(), extension_id(), 0);
213  EXTENSION_FUNCTION_VALIDATE(properties->GetInteger(kGeneratedIdKey,
214                                                     &id.uid));
215  std::string title;
216  if (properties->HasKey(kTitleKey) &&
217      !properties->GetString(kTitleKey, &title))
218    return false;
219
220  ExtensionMenuManager* menu_manager =
221      profile()->GetExtensionService()->menu_manager();
222
223  ExtensionMenuItem::ContextList contexts(ExtensionMenuItem::PAGE);
224  if (!ParseContexts(*properties, kContextsKey, &contexts))
225    return false;
226
227  ExtensionMenuItem::Type type;
228  if (!ParseType(*properties, ExtensionMenuItem::NORMAL, &type))
229    return false;
230
231  if (title.empty() && type != ExtensionMenuItem::SEPARATOR) {
232    error_ = kTitleNeededError;
233    return false;
234  }
235
236  bool checked;
237  if (!ParseChecked(type, *properties, false, &checked))
238    return false;
239
240  scoped_ptr<ExtensionMenuItem> item(
241      new ExtensionMenuItem(id, title, checked, type, contexts));
242
243  if (!SetURLPatterns(*properties, item.get()))
244    return false;
245
246  bool success = true;
247  if (properties->HasKey(kParentIdKey)) {
248    ExtensionMenuItem::Id parent_id(profile(), extension_id(), 0);
249    EXTENSION_FUNCTION_VALIDATE(properties->GetInteger(kParentIdKey,
250                                                       &parent_id.uid));
251    ExtensionMenuItem* parent = menu_manager->GetItemById(parent_id);
252    if (!parent) {
253      error_ = ExtensionErrorUtils::FormatErrorMessage(
254          kCannotFindItemError, base::IntToString(parent_id.uid));
255      return false;
256    }
257    if (parent->type() != ExtensionMenuItem::NORMAL) {
258      error_ = kParentsMustBeNormalError;
259      return false;
260    }
261    success = menu_manager->AddChildItem(parent_id, item.release());
262  } else {
263    success = menu_manager->AddContextItem(GetExtension(), item.release());
264  }
265
266  if (!success)
267    return false;
268
269  return true;
270}
271
272bool UpdateContextMenuFunction::RunImpl() {
273  ExtensionMenuItem::Id item_id(profile(), extension_id(), 0);
274  EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &item_id.uid));
275
276  ExtensionService* service = profile()->GetExtensionService();
277  ExtensionMenuManager* manager = service->menu_manager();
278  ExtensionMenuItem* item = manager->GetItemById(item_id);
279  if (!item || item->extension_id() != extension_id()) {
280    error_ = ExtensionErrorUtils::FormatErrorMessage(
281        kCannotFindItemError, base::IntToString(item_id.uid));
282    return false;
283  }
284
285  DictionaryValue *properties = NULL;
286  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(1, &properties));
287  EXTENSION_FUNCTION_VALIDATE(properties != NULL);
288
289  ExtensionMenuManager* menu_manager =
290      profile()->GetExtensionService()->menu_manager();
291
292  // Type.
293  ExtensionMenuItem::Type type;
294  if (!ParseType(*properties, item->type(), &type))
295    return false;
296  if (type != item->type())
297    item->set_type(type);
298
299  // Title.
300  if (properties->HasKey(kTitleKey)) {
301    std::string title;
302    EXTENSION_FUNCTION_VALIDATE(properties->GetString(kTitleKey, &title));
303    if (title.empty() && type != ExtensionMenuItem::SEPARATOR) {
304      error_ = kTitleNeededError;
305      return false;
306    }
307    item->set_title(title);
308  }
309
310  // Checked state.
311  bool checked;
312  if (!ParseChecked(item->type(), *properties, item->checked(), &checked))
313    return false;
314  if (checked != item->checked()) {
315    if (!item->SetChecked(checked))
316      return false;
317  }
318
319  // Contexts.
320  ExtensionMenuItem::ContextList contexts(item->contexts());
321  if (!ParseContexts(*properties, kContextsKey, &contexts))
322    return false;
323  if (contexts != item->contexts())
324    item->set_contexts(contexts);
325
326  // Parent id.
327  ExtensionMenuItem* parent = NULL;
328  if (!GetParent(*properties, *menu_manager, &parent))
329    return false;
330  if (parent && !menu_manager->ChangeParent(item->id(), &parent->id()))
331    return false;
332
333  if (!SetURLPatterns(*properties, item))
334    return false;
335
336  return true;
337}
338
339bool RemoveContextMenuFunction::RunImpl() {
340  ExtensionMenuItem::Id id(profile(), extension_id(), 0);
341  EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &id.uid));
342  ExtensionService* service = profile()->GetExtensionService();
343  ExtensionMenuManager* manager = service->menu_manager();
344
345  ExtensionMenuItem* item = manager->GetItemById(id);
346  // Ensure one extension can't remove another's menu items.
347  if (!item || item->extension_id() != extension_id()) {
348    error_ = ExtensionErrorUtils::FormatErrorMessage(
349        kCannotFindItemError, base::IntToString(id.uid));
350    return false;
351  }
352
353  return manager->RemoveContextMenuItem(id);
354}
355
356bool RemoveAllContextMenusFunction::RunImpl() {
357  ExtensionService* service = profile()->GetExtensionService();
358  ExtensionMenuManager* manager = service->menu_manager();
359  manager->RemoveAllContextItems(extension_id());
360  return true;
361}
362