1 // Copyright (c) 2012 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/api/bookmark_manager_private/bookmark_manager_private_api.h" 6 7 #include <vector> 8 9 #include "base/json/json_writer.h" 10 #include "base/prefs/pref_service.h" 11 #include "base/strings/string_number_conversions.h" 12 #include "base/values.h" 13 #include "chrome/browser/bookmarks/bookmark_model.h" 14 #include "chrome/browser/bookmarks/bookmark_model_factory.h" 15 #include "chrome/browser/bookmarks/bookmark_node_data.h" 16 #include "chrome/browser/bookmarks/bookmark_utils.h" 17 #include "chrome/browser/extensions/api/bookmark_manager_private/bookmark_manager_private_api_constants.h" 18 #include "chrome/browser/extensions/api/bookmarks/bookmark_api_constants.h" 19 #include "chrome/browser/extensions/api/bookmarks/bookmark_api_helpers.h" 20 #include "chrome/browser/extensions/event_router.h" 21 #include "chrome/browser/extensions/extension_function_dispatcher.h" 22 #include "chrome/browser/extensions/extension_system.h" 23 #include "chrome/browser/extensions/extension_web_ui.h" 24 #include "chrome/browser/profiles/profile.h" 25 #include "chrome/browser/ui/bookmarks/bookmark_drag_drop.h" 26 #include "chrome/common/extensions/api/bookmark_manager_private.h" 27 #include "chrome/common/pref_names.h" 28 #include "components/user_prefs/user_prefs.h" 29 #include "content/public/browser/render_view_host.h" 30 #include "content/public/browser/web_contents.h" 31 #include "content/public/browser/web_contents_view.h" 32 #include "content/public/browser/web_ui.h" 33 #include "extensions/browser/view_type_utils.h" 34 #include "grit/generated_resources.h" 35 #include "ui/base/l10n/l10n_util.h" 36 #include "ui/webui/web_ui_util.h" 37 38 #if defined(OS_WIN) 39 #include "win8/util/win8_util.h" 40 #endif // OS_WIN 41 42 namespace extensions { 43 44 namespace bookmark_keys = bookmark_api_constants; 45 namespace CanPaste = api::bookmark_manager_private::CanPaste; 46 namespace Copy = api::bookmark_manager_private::Copy; 47 namespace Cut = api::bookmark_manager_private::Cut; 48 namespace Drop = api::bookmark_manager_private::Drop; 49 namespace GetSubtree = api::bookmark_manager_private::GetSubtree; 50 namespace manager_keys = bookmark_manager_api_constants; 51 namespace Paste = api::bookmark_manager_private::Paste; 52 namespace RemoveTrees = api::bookmark_manager_private::RemoveTrees; 53 namespace SortChildren = api::bookmark_manager_private::SortChildren; 54 namespace StartDrag = api::bookmark_manager_private::StartDrag; 55 56 using content::WebContents; 57 58 namespace { 59 60 // Returns a single bookmark node from the argument ID. 61 // This returns NULL in case of failure. 62 const BookmarkNode* GetNodeFromString( 63 BookmarkModel* model, const std::string& id_string) { 64 int64 id; 65 if (!base::StringToInt64(id_string, &id)) 66 return NULL; 67 return model->GetNodeByID(id); 68 } 69 70 // Gets a vector of bookmark nodes from the argument list of IDs. 71 // This returns false in the case of failure. 72 bool GetNodesFromVector(BookmarkModel* model, 73 const std::vector<std::string>& id_strings, 74 std::vector<const BookmarkNode*>* nodes) { 75 76 if (id_strings.empty()) 77 return false; 78 79 for (size_t i = 0; i < id_strings.size(); ++i) { 80 const BookmarkNode* node = GetNodeFromString(model, id_strings[i]); 81 if (!node) 82 return false; 83 nodes->push_back(node); 84 } 85 86 return true; 87 } 88 89 // Recursively adds a node to a list. This is by used |BookmarkNodeDataToJSON| 90 // when the data comes from the current profile. In this case we have a 91 // BookmarkNode since we got the data from the current profile. 92 void AddNodeToList(base::ListValue* list, const BookmarkNode& node) { 93 base::DictionaryValue* dict = new base::DictionaryValue(); 94 95 // Add id and parentId so we can associate the data with existing nodes on the 96 // client side. 97 std::string id_string = base::Int64ToString(node.id()); 98 dict->SetString(bookmark_keys::kIdKey, id_string); 99 100 std::string parent_id_string = base::Int64ToString(node.parent()->id()); 101 dict->SetString(bookmark_keys::kParentIdKey, parent_id_string); 102 103 if (node.is_url()) 104 dict->SetString(bookmark_keys::kUrlKey, node.url().spec()); 105 106 dict->SetString(bookmark_keys::kTitleKey, node.GetTitle()); 107 108 base::ListValue* children = new base::ListValue(); 109 for (int i = 0; i < node.child_count(); ++i) 110 AddNodeToList(children, *node.GetChild(i)); 111 dict->Set(bookmark_keys::kChildrenKey, children); 112 113 list->Append(dict); 114 } 115 116 // Recursively adds an element to a list. This is used by 117 // |BookmarkNodeDataToJSON| when the data comes from a different profile. When 118 // the data comes from a different profile we do not have any IDs or parent IDs. 119 void AddElementToList(base::ListValue* list, 120 const BookmarkNodeData::Element& element) { 121 base::DictionaryValue* dict = new base::DictionaryValue(); 122 123 if (element.is_url) 124 dict->SetString(bookmark_keys::kUrlKey, element.url.spec()); 125 126 dict->SetString(bookmark_keys::kTitleKey, element.title); 127 128 base::ListValue* children = new base::ListValue(); 129 for (size_t i = 0; i < element.children.size(); ++i) 130 AddElementToList(children, element.children[i]); 131 dict->Set(bookmark_keys::kChildrenKey, children); 132 133 list->Append(dict); 134 } 135 136 // Builds the JSON structure based on the BookmarksDragData. 137 void BookmarkNodeDataToJSON(Profile* profile, const BookmarkNodeData& data, 138 base::ListValue* args) { 139 bool same_profile = data.IsFromProfile(profile); 140 base::DictionaryValue* value = new base::DictionaryValue(); 141 value->SetBoolean(manager_keys::kSameProfileKey, same_profile); 142 143 base::ListValue* list = new base::ListValue(); 144 if (same_profile) { 145 std::vector<const BookmarkNode*> nodes = data.GetNodes(profile); 146 for (size_t i = 0; i < nodes.size(); ++i) 147 AddNodeToList(list, *nodes[i]); 148 } else { 149 // We do not have an node IDs when the data comes from a different profile. 150 std::vector<BookmarkNodeData::Element> elements = data.elements; 151 for (size_t i = 0; i < elements.size(); ++i) 152 AddElementToList(list, elements[i]); 153 } 154 value->Set(manager_keys::kElementsKey, list); 155 156 args->Append(value); 157 } 158 159 } // namespace 160 161 BookmarkManagerPrivateEventRouter::BookmarkManagerPrivateEventRouter( 162 Profile* profile, 163 content::WebContents* web_contents) 164 : profile_(profile), 165 web_contents_(web_contents) { 166 BookmarkTabHelper* bookmark_tab_helper = 167 BookmarkTabHelper::FromWebContents(web_contents_); 168 bookmark_tab_helper->set_bookmark_drag_delegate(this); 169 } 170 171 BookmarkManagerPrivateEventRouter::~BookmarkManagerPrivateEventRouter() { 172 BookmarkTabHelper* bookmark_tab_helper = 173 BookmarkTabHelper::FromWebContents(web_contents_); 174 if (bookmark_tab_helper->bookmark_drag_delegate() == this) 175 bookmark_tab_helper->set_bookmark_drag_delegate(NULL); 176 } 177 178 void BookmarkManagerPrivateEventRouter::DispatchEvent( 179 const char* event_name, 180 scoped_ptr<base::ListValue> args) { 181 if (!ExtensionSystem::Get(profile_)->event_router()) 182 return; 183 184 scoped_ptr<Event> event(new Event(event_name, args.Pass())); 185 ExtensionSystem::Get(profile_)->event_router()->BroadcastEvent(event.Pass()); 186 } 187 188 void BookmarkManagerPrivateEventRouter::DispatchDragEvent( 189 const BookmarkNodeData& data, 190 const char* event_name) { 191 if (data.size() == 0) 192 return; 193 194 scoped_ptr<base::ListValue> args(new base::ListValue()); 195 BookmarkNodeDataToJSON(profile_, data, args.get()); 196 DispatchEvent(event_name, args.Pass()); 197 } 198 199 void BookmarkManagerPrivateEventRouter::OnDragEnter( 200 const BookmarkNodeData& data) { 201 DispatchDragEvent(data, manager_keys::kOnBookmarkDragEnter); 202 } 203 204 void BookmarkManagerPrivateEventRouter::OnDragOver( 205 const BookmarkNodeData& data) { 206 // Intentionally empty since these events happens too often and floods the 207 // message queue. We do not need this event for the bookmark manager anyway. 208 } 209 210 void BookmarkManagerPrivateEventRouter::OnDragLeave( 211 const BookmarkNodeData& data) { 212 DispatchDragEvent(data, manager_keys::kOnBookmarkDragLeave); 213 } 214 215 void BookmarkManagerPrivateEventRouter::OnDrop(const BookmarkNodeData& data) { 216 DispatchDragEvent(data, manager_keys::kOnBookmarkDrop); 217 218 // Make a copy that is owned by this instance. 219 ClearBookmarkNodeData(); 220 bookmark_drag_data_ = data; 221 } 222 223 const BookmarkNodeData* 224 BookmarkManagerPrivateEventRouter::GetBookmarkNodeData() { 225 if (bookmark_drag_data_.is_valid()) 226 return &bookmark_drag_data_; 227 return NULL; 228 } 229 230 void BookmarkManagerPrivateEventRouter::ClearBookmarkNodeData() { 231 bookmark_drag_data_.Clear(); 232 } 233 234 bool ClipboardBookmarkManagerFunction::CopyOrCut(bool cut, 235 const std::vector<std::string>& id_list) { 236 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); 237 std::vector<const BookmarkNode*> nodes; 238 EXTENSION_FUNCTION_VALIDATE(GetNodesFromVector(model, id_list, &nodes)); 239 bookmark_utils::CopyToClipboard(model, nodes, cut); 240 return true; 241 } 242 243 bool BookmarkManagerPrivateCopyFunction::RunImpl() { 244 scoped_ptr<Copy::Params> params(Copy::Params::Create(*args_)); 245 EXTENSION_FUNCTION_VALIDATE(params); 246 return CopyOrCut(false, params->id_list); 247 } 248 249 bool BookmarkManagerPrivateCutFunction::RunImpl() { 250 if (!EditBookmarksEnabled()) 251 return false; 252 253 scoped_ptr<Cut::Params> params(Cut::Params::Create(*args_)); 254 EXTENSION_FUNCTION_VALIDATE(params); 255 return CopyOrCut(true, params->id_list); 256 } 257 258 bool BookmarkManagerPrivatePasteFunction::RunImpl() { 259 if (!EditBookmarksEnabled()) 260 return false; 261 262 scoped_ptr<Paste::Params> params(Paste::Params::Create(*args_)); 263 EXTENSION_FUNCTION_VALIDATE(params); 264 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); 265 const BookmarkNode* parent_node = GetNodeFromString(model, params->parent_id); 266 if (!parent_node) { 267 error_ = bookmark_keys::kNoParentError; 268 return false; 269 } 270 bool can_paste = bookmark_utils::CanPasteFromClipboard(parent_node); 271 if (!can_paste) 272 return false; 273 274 // We want to use the highest index of the selected nodes as a destination. 275 std::vector<const BookmarkNode*> nodes; 276 // No need to test return value, if we got an empty list, we insert at end. 277 if (params->selected_id_list) 278 GetNodesFromVector(model, *params->selected_id_list, &nodes); 279 int highest_index = -1; // -1 means insert at end of list. 280 for (size_t i = 0; i < nodes.size(); ++i) { 281 // + 1 so that we insert after the selection. 282 int index = parent_node->GetIndexOf(nodes[i]) + 1; 283 if (index > highest_index) 284 highest_index = index; 285 } 286 287 bookmark_utils::PasteFromClipboard(model, parent_node, highest_index); 288 return true; 289 } 290 291 bool BookmarkManagerPrivateCanPasteFunction::RunImpl() { 292 if (!EditBookmarksEnabled()) 293 return false; 294 295 scoped_ptr<CanPaste::Params> params(CanPaste::Params::Create(*args_)); 296 EXTENSION_FUNCTION_VALIDATE(params); 297 298 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); 299 const BookmarkNode* parent_node = GetNodeFromString(model, params->parent_id); 300 if (!parent_node) { 301 error_ = bookmark_keys::kNoParentError; 302 return false; 303 } 304 bool can_paste = bookmark_utils::CanPasteFromClipboard(parent_node); 305 SetResult(new base::FundamentalValue(can_paste)); 306 return true; 307 } 308 309 bool BookmarkManagerPrivateSortChildrenFunction::RunImpl() { 310 if (!EditBookmarksEnabled()) 311 return false; 312 313 scoped_ptr<SortChildren::Params> params(SortChildren::Params::Create(*args_)); 314 EXTENSION_FUNCTION_VALIDATE(params); 315 316 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); 317 const BookmarkNode* parent_node = GetNodeFromString(model, params->parent_id); 318 if (!parent_node) { 319 error_ = bookmark_keys::kNoParentError; 320 return false; 321 } 322 model->SortChildren(parent_node); 323 return true; 324 } 325 326 bool BookmarkManagerPrivateGetStringsFunction::RunImpl() { 327 base::DictionaryValue* localized_strings = new base::DictionaryValue(); 328 329 localized_strings->SetString("title", 330 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_TITLE)); 331 localized_strings->SetString("search_button", 332 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_SEARCH_BUTTON)); 333 localized_strings->SetString("organize_menu", 334 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_ORGANIZE_MENU)); 335 localized_strings->SetString("show_in_folder", 336 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_SHOW_IN_FOLDER)); 337 localized_strings->SetString("sort", 338 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_SORT)); 339 localized_strings->SetString("import_menu", 340 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_IMPORT_MENU)); 341 localized_strings->SetString("export_menu", 342 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_EXPORT_MENU)); 343 localized_strings->SetString("rename_folder", 344 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_RENAME_FOLDER)); 345 localized_strings->SetString("edit", 346 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_EDIT)); 347 localized_strings->SetString("should_open_all", 348 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_SHOULD_OPEN_ALL)); 349 localized_strings->SetString("open_incognito", 350 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_OPEN_INCOGNITO)); 351 localized_strings->SetString("open_in_new_tab", 352 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_OPEN_IN_NEW_TAB)); 353 localized_strings->SetString("open_in_new_window", 354 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_OPEN_IN_NEW_WINDOW)); 355 localized_strings->SetString("add_new_bookmark", 356 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_ADD_NEW_BOOKMARK)); 357 localized_strings->SetString("new_folder", 358 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_NEW_FOLDER)); 359 localized_strings->SetString("open_all", 360 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_OPEN_ALL)); 361 localized_strings->SetString("open_all_new_window", 362 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_OPEN_ALL_NEW_WINDOW)); 363 localized_strings->SetString("open_all_incognito", 364 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_OPEN_ALL_INCOGNITO)); 365 localized_strings->SetString("remove", 366 l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_REMOVE)); 367 localized_strings->SetString("copy", 368 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_COPY)); 369 localized_strings->SetString("cut", 370 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_CUT)); 371 localized_strings->SetString("paste", 372 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_PASTE)); 373 localized_strings->SetString("delete", 374 l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_DELETE)); 375 localized_strings->SetString("undo_delete", 376 l10n_util::GetStringUTF16(IDS_UNDO_DELETE)); 377 localized_strings->SetString("new_folder_name", 378 l10n_util::GetStringUTF16(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME)); 379 localized_strings->SetString("name_input_placeholder", 380 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_NAME_INPUT_PLACE_HOLDER)); 381 localized_strings->SetString("url_input_placeholder", 382 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_URL_INPUT_PLACE_HOLDER)); 383 localized_strings->SetString("invalid_url", 384 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_INVALID_URL)); 385 localized_strings->SetString("recent", 386 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_RECENT)); 387 localized_strings->SetString("search", 388 l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_SEARCH)); 389 localized_strings->SetString("save", 390 l10n_util::GetStringUTF16(IDS_SAVE)); 391 localized_strings->SetString("cancel", 392 l10n_util::GetStringUTF16(IDS_CANCEL)); 393 394 webui::SetFontAndTextDirection(localized_strings); 395 396 SetResult(localized_strings); 397 398 // This is needed because unlike the rest of these functions, this class 399 // inherits from AsyncFunction directly, rather than BookmarkFunction. 400 SendResponse(true); 401 402 return true; 403 } 404 405 bool BookmarkManagerPrivateStartDragFunction::RunImpl() { 406 if (!EditBookmarksEnabled()) 407 return false; 408 409 scoped_ptr<StartDrag::Params> params(StartDrag::Params::Create(*args_)); 410 EXTENSION_FUNCTION_VALIDATE(params); 411 412 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); 413 std::vector<const BookmarkNode*> nodes; 414 EXTENSION_FUNCTION_VALIDATE( 415 GetNodesFromVector(model, params->id_list, &nodes)); 416 417 WebContents* web_contents = 418 WebContents::FromRenderViewHost(render_view_host_); 419 if (GetViewType(web_contents) == VIEW_TYPE_TAB_CONTENTS) { 420 WebContents* web_contents = 421 dispatcher()->delegate()->GetAssociatedWebContents(); 422 CHECK(web_contents); 423 chrome::DragBookmarks(profile(), nodes, 424 web_contents->GetView()->GetNativeView()); 425 426 return true; 427 } else { 428 NOTREACHED(); 429 return false; 430 } 431 } 432 433 bool BookmarkManagerPrivateDropFunction::RunImpl() { 434 if (!EditBookmarksEnabled()) 435 return false; 436 437 scoped_ptr<Drop::Params> params(Drop::Params::Create(*args_)); 438 EXTENSION_FUNCTION_VALIDATE(params); 439 440 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); 441 442 const BookmarkNode* drop_parent = GetNodeFromString(model, params->parent_id); 443 if (!drop_parent) { 444 error_ = bookmark_keys::kNoParentError; 445 return false; 446 } 447 448 int drop_index; 449 if (params->index) 450 drop_index = *params->index; 451 else 452 drop_index = drop_parent->child_count(); 453 454 WebContents* web_contents = 455 WebContents::FromRenderViewHost(render_view_host_); 456 if (GetViewType(web_contents) == VIEW_TYPE_TAB_CONTENTS) { 457 WebContents* web_contents = 458 dispatcher()->delegate()->GetAssociatedWebContents(); 459 CHECK(web_contents); 460 ExtensionWebUI* web_ui = 461 static_cast<ExtensionWebUI*>(web_contents->GetWebUI()->GetController()); 462 CHECK(web_ui); 463 BookmarkManagerPrivateEventRouter* router = 464 web_ui->bookmark_manager_private_event_router(); 465 466 DCHECK(router); 467 const BookmarkNodeData* drag_data = router->GetBookmarkNodeData(); 468 if (drag_data == NULL) { 469 NOTREACHED() <<"Somehow we're dropping null bookmark data"; 470 return false; 471 } 472 chrome::DropBookmarks(profile(), *drag_data, drop_parent, drop_index); 473 474 router->ClearBookmarkNodeData(); 475 return true; 476 } else { 477 NOTREACHED(); 478 return false; 479 } 480 } 481 482 bool BookmarkManagerPrivateGetSubtreeFunction::RunImpl() { 483 scoped_ptr<GetSubtree::Params> params(GetSubtree::Params::Create(*args_)); 484 EXTENSION_FUNCTION_VALIDATE(params); 485 486 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); 487 const BookmarkNode* node = NULL; 488 489 if (params->id == "") { 490 node = model->root_node(); 491 } else { 492 int64 id; 493 if (!base::StringToInt64(params->id, &id)) { 494 error_ = bookmark_keys::kInvalidIdError; 495 return false; 496 } 497 node = model->GetNodeByID(id); 498 } 499 500 if (!node) { 501 error_ = bookmark_keys::kNoNodeError; 502 return false; 503 } 504 505 scoped_ptr<base::ListValue> json(new base::ListValue()); 506 if (params->folders_only) 507 bookmark_api_helpers::AddNodeFoldersOnly(node, json.get(), true); 508 else 509 bookmark_api_helpers::AddNode(node, json.get(), true); 510 SetResult(json.release()); 511 return true; 512 } 513 514 bool BookmarkManagerPrivateCanEditFunction::RunImpl() { 515 PrefService* prefs = user_prefs::UserPrefs::Get(profile_); 516 SetResult(new base::FundamentalValue( 517 prefs->GetBoolean(prefs::kEditBookmarksEnabled))); 518 return true; 519 } 520 521 bool BookmarkManagerPrivateRecordLaunchFunction::RunImpl() { 522 bookmark_utils::RecordBookmarkLaunch(bookmark_utils::LAUNCH_MANAGER); 523 return true; 524 } 525 526 bool BookmarkManagerPrivateCanOpenNewWindowsFunction::RunImpl() { 527 bool can_open_new_windows = true; 528 529 #if defined(OS_WIN) 530 if (win8::IsSingleWindowMetroMode()) 531 can_open_new_windows = false; 532 #endif // OS_WIN 533 534 SetResult(new base::FundamentalValue(can_open_new_windows)); 535 return true; 536 } 537 538 bool BookmarkManagerPrivateRemoveTreesFunction::RunImpl() { 539 scoped_ptr<RemoveTrees::Params> params(RemoveTrees::Params::Create(*args_)); 540 EXTENSION_FUNCTION_VALIDATE(params); 541 542 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); 543 int64 id; 544 for (size_t i = 0; i < params->id_list.size(); ++i) { 545 if (!base::StringToInt64(params->id_list[i], &id)) { 546 error_ = bookmark_api_constants::kInvalidIdError; 547 return false; 548 } 549 if (!bookmark_api_helpers::RemoveNode(model, id, true, &error_)) 550 return false; 551 } 552 553 return true; 554 } 555 556 } // namespace extensions 557