| OLD | NEW |
| (Empty) |
| 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/bookmarks/bookmarks_api.h" | |
| 6 | |
| 7 #include "base/bind.h" | |
| 8 #include "base/file_path.h" | |
| 9 #include "base/i18n/file_util_icu.h" | |
| 10 #include "base/i18n/time_formatting.h" | |
| 11 #include "base/json/json_writer.h" | |
| 12 #include "base/memory/scoped_ptr.h" | |
| 13 #include "base/path_service.h" | |
| 14 #include "base/prefs/public/pref_service_base.h" | |
| 15 #include "base/sha1.h" | |
| 16 #include "base/stl_util.h" | |
| 17 #include "base/string16.h" | |
| 18 #include "base/string_number_conversions.h" | |
| 19 #include "base/string_util.h" | |
| 20 #include "base/time.h" | |
| 21 #include "base/utf_string_conversions.h" | |
| 22 #include "chrome/browser/bookmarks/bookmark_codec.h" | |
| 23 #include "chrome/browser/bookmarks/bookmark_html_writer.h" | |
| 24 #include "chrome/browser/bookmarks/bookmark_model.h" | |
| 25 #include "chrome/browser/bookmarks/bookmark_model_factory.h" | |
| 26 #include "chrome/browser/bookmarks/bookmark_utils.h" | |
| 27 #include "chrome/browser/extensions/api/bookmarks/bookmark_api_constants.h" | |
| 28 #include "chrome/browser/extensions/api/bookmarks/bookmark_api_helpers.h" | |
| 29 #include "chrome/browser/extensions/event_router.h" | |
| 30 #include "chrome/browser/extensions/extension_function_dispatcher.h" | |
| 31 #include "chrome/browser/extensions/extension_system.h" | |
| 32 #include "chrome/browser/extensions/extensions_quota_service.h" | |
| 33 #include "chrome/browser/importer/importer_data_types.h" | |
| 34 #include "chrome/browser/importer/importer_host.h" | |
| 35 #include "chrome/browser/profiles/profile.h" | |
| 36 #include "chrome/browser/ui/chrome_select_file_policy.h" | |
| 37 #include "chrome/common/chrome_notification_types.h" | |
| 38 #include "chrome/common/chrome_paths.h" | |
| 39 #include "chrome/common/extensions/api/bookmarks.h" | |
| 40 #include "chrome/common/pref_names.h" | |
| 41 #include "content/public/browser/notification_service.h" | |
| 42 #include "grit/generated_resources.h" | |
| 43 #include "ui/base/l10n/l10n_util.h" | |
| 44 | |
| 45 namespace extensions { | |
| 46 | |
| 47 namespace keys = bookmark_api_constants; | |
| 48 namespace bookmarks = api::bookmarks; | |
| 49 | |
| 50 using base::TimeDelta; | |
| 51 using bookmarks::BookmarkTreeNode; | |
| 52 using content::BrowserThread; | |
| 53 using content::WebContents; | |
| 54 | |
| 55 typedef QuotaLimitHeuristic::Bucket Bucket; | |
| 56 typedef QuotaLimitHeuristic::Config Config; | |
| 57 typedef QuotaLimitHeuristic::BucketList BucketList; | |
| 58 typedef ExtensionsQuotaService::TimedLimit TimedLimit; | |
| 59 typedef ExtensionsQuotaService::SustainedLimit SustainedLimit; | |
| 60 typedef QuotaLimitHeuristic::BucketMapper BucketMapper; | |
| 61 | |
| 62 namespace { | |
| 63 | |
| 64 // Generates a default path (including a default filename) that will be | |
| 65 // used for pre-populating the "Export Bookmarks" file chooser dialog box. | |
| 66 FilePath GetDefaultFilepathForBookmarkExport() { | |
| 67 base::Time time = base::Time::Now(); | |
| 68 | |
| 69 // Concatenate a date stamp to the filename. | |
| 70 #if defined(OS_POSIX) | |
| 71 FilePath::StringType filename = | |
| 72 l10n_util::GetStringFUTF8(IDS_EXPORT_BOOKMARKS_DEFAULT_FILENAME, | |
| 73 base::TimeFormatShortDateNumeric(time)); | |
| 74 #elif defined(OS_WIN) | |
| 75 FilePath::StringType filename = | |
| 76 l10n_util::GetStringFUTF16(IDS_EXPORT_BOOKMARKS_DEFAULT_FILENAME, | |
| 77 base::TimeFormatShortDateNumeric(time)); | |
| 78 #endif | |
| 79 | |
| 80 file_util::ReplaceIllegalCharactersInPath(&filename, '_'); | |
| 81 | |
| 82 FilePath default_path; | |
| 83 PathService::Get(chrome::DIR_USER_DOCUMENTS, &default_path); | |
| 84 return default_path.Append(filename); | |
| 85 } | |
| 86 | |
| 87 } // namespace | |
| 88 | |
| 89 void BookmarksFunction::Run() { | |
| 90 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); | |
| 91 if (!model->IsLoaded()) { | |
| 92 // Bookmarks are not ready yet. We'll wait. | |
| 93 registrar_.Add( | |
| 94 this, chrome::NOTIFICATION_BOOKMARK_MODEL_LOADED, | |
| 95 content::NotificationService::AllBrowserContextsAndSources()); | |
| 96 AddRef(); // Balanced in Observe(). | |
| 97 return; | |
| 98 } | |
| 99 | |
| 100 bool success = RunImpl(); | |
| 101 if (success) { | |
| 102 content::NotificationService::current()->Notify( | |
| 103 chrome::NOTIFICATION_EXTENSION_BOOKMARKS_API_INVOKED, | |
| 104 content::Source<const Extension>(GetExtension()), | |
| 105 content::Details<const BookmarksFunction>(this)); | |
| 106 } | |
| 107 SendResponse(success); | |
| 108 } | |
| 109 | |
| 110 bool BookmarksFunction::GetBookmarkIdAsInt64( | |
| 111 const std::string& id_string, int64* id) { | |
| 112 if (base::StringToInt64(id_string, id)) | |
| 113 return true; | |
| 114 | |
| 115 error_ = keys::kInvalidIdError; | |
| 116 return false; | |
| 117 } | |
| 118 | |
| 119 bool BookmarksFunction::EditBookmarksEnabled() { | |
| 120 PrefServiceBase* prefs = PrefServiceBase::FromBrowserContext(profile_); | |
| 121 if (prefs->GetBoolean(prefs::kEditBookmarksEnabled)) | |
| 122 return true; | |
| 123 error_ = keys::kEditBookmarksDisabled; | |
| 124 return false; | |
| 125 } | |
| 126 | |
| 127 void BookmarksFunction::Observe(int type, | |
| 128 const content::NotificationSource& source, | |
| 129 const content::NotificationDetails& details) { | |
| 130 DCHECK(type == chrome::NOTIFICATION_BOOKMARK_MODEL_LOADED); | |
| 131 Profile* source_profile = content::Source<Profile>(source).ptr(); | |
| 132 if (!source_profile || !source_profile->IsSameProfile(profile())) | |
| 133 return; | |
| 134 | |
| 135 DCHECK(BookmarkModelFactory::GetForProfile(profile())->IsLoaded()); | |
| 136 Run(); | |
| 137 Release(); // Balanced in Run(). | |
| 138 } | |
| 139 | |
| 140 BookmarkEventRouter::BookmarkEventRouter(BookmarkModel* model) : model_(model) { | |
| 141 model_->AddObserver(this); | |
| 142 } | |
| 143 | |
| 144 BookmarkEventRouter::~BookmarkEventRouter() { | |
| 145 if (model_) { | |
| 146 model_->RemoveObserver(this); | |
| 147 } | |
| 148 } | |
| 149 | |
| 150 void BookmarkEventRouter::DispatchEvent( | |
| 151 Profile* profile, | |
| 152 const char* event_name, | |
| 153 scoped_ptr<ListValue> event_args) { | |
| 154 if (extensions::ExtensionSystem::Get(profile)->event_router()) { | |
| 155 extensions::ExtensionSystem::Get(profile)->event_router()->BroadcastEvent( | |
| 156 make_scoped_ptr(new extensions::Event(event_name, event_args.Pass()))); | |
| 157 } | |
| 158 } | |
| 159 | |
| 160 void BookmarkEventRouter::Loaded(BookmarkModel* model, bool ids_reassigned) { | |
| 161 // TODO(erikkay): Perhaps we should send this event down to the extension | |
| 162 // so they know when it's safe to use the API? | |
| 163 } | |
| 164 | |
| 165 void BookmarkEventRouter::BookmarkModelBeingDeleted(BookmarkModel* model) { | |
| 166 model_ = NULL; | |
| 167 } | |
| 168 | |
| 169 void BookmarkEventRouter::BookmarkNodeMoved(BookmarkModel* model, | |
| 170 const BookmarkNode* old_parent, | |
| 171 int old_index, | |
| 172 const BookmarkNode* new_parent, | |
| 173 int new_index) { | |
| 174 scoped_ptr<ListValue> args(new ListValue()); | |
| 175 const BookmarkNode* node = new_parent->GetChild(new_index); | |
| 176 args->Append(new StringValue(base::Int64ToString(node->id()))); | |
| 177 DictionaryValue* object_args = new DictionaryValue(); | |
| 178 object_args->SetString(keys::kParentIdKey, | |
| 179 base::Int64ToString(new_parent->id())); | |
| 180 object_args->SetInteger(keys::kIndexKey, new_index); | |
| 181 object_args->SetString(keys::kOldParentIdKey, | |
| 182 base::Int64ToString(old_parent->id())); | |
| 183 object_args->SetInteger(keys::kOldIndexKey, old_index); | |
| 184 args->Append(object_args); | |
| 185 | |
| 186 DispatchEvent(model->profile(), keys::kOnBookmarkMoved, args.Pass()); | |
| 187 } | |
| 188 | |
| 189 void BookmarkEventRouter::BookmarkNodeAdded(BookmarkModel* model, | |
| 190 const BookmarkNode* parent, | |
| 191 int index) { | |
| 192 scoped_ptr<ListValue> args(new ListValue()); | |
| 193 const BookmarkNode* node = parent->GetChild(index); | |
| 194 args->Append(new StringValue(base::Int64ToString(node->id()))); | |
| 195 scoped_ptr<BookmarkTreeNode> tree_node( | |
| 196 bookmark_api_helpers::GetBookmarkTreeNode(node, false, false)); | |
| 197 args->Append(tree_node->ToValue().release()); | |
| 198 | |
| 199 DispatchEvent(model->profile(), keys::kOnBookmarkCreated, args.Pass()); | |
| 200 } | |
| 201 | |
| 202 void BookmarkEventRouter::BookmarkNodeRemoved(BookmarkModel* model, | |
| 203 const BookmarkNode* parent, | |
| 204 int index, | |
| 205 const BookmarkNode* node) { | |
| 206 scoped_ptr<ListValue> args(new ListValue()); | |
| 207 args->Append(new StringValue(base::Int64ToString(node->id()))); | |
| 208 DictionaryValue* object_args = new DictionaryValue(); | |
| 209 object_args->SetString(keys::kParentIdKey, | |
| 210 base::Int64ToString(parent->id())); | |
| 211 object_args->SetInteger(keys::kIndexKey, index); | |
| 212 args->Append(object_args); | |
| 213 | |
| 214 DispatchEvent(model->profile(), keys::kOnBookmarkRemoved, args.Pass()); | |
| 215 } | |
| 216 | |
| 217 void BookmarkEventRouter::BookmarkNodeChanged(BookmarkModel* model, | |
| 218 const BookmarkNode* node) { | |
| 219 scoped_ptr<ListValue> args(new ListValue()); | |
| 220 args->Append(new StringValue(base::Int64ToString(node->id()))); | |
| 221 | |
| 222 // TODO(erikkay) The only three things that BookmarkModel sends this | |
| 223 // notification for are title, url and favicon. Since we're currently | |
| 224 // ignoring favicon and since the notification doesn't say which one anyway, | |
| 225 // for now we only include title and url. The ideal thing would be to change | |
| 226 // BookmarkModel to indicate what changed. | |
| 227 DictionaryValue* object_args = new DictionaryValue(); | |
| 228 object_args->SetString(keys::kTitleKey, node->GetTitle()); | |
| 229 if (node->is_url()) | |
| 230 object_args->SetString(keys::kUrlKey, node->url().spec()); | |
| 231 args->Append(object_args); | |
| 232 | |
| 233 DispatchEvent(model->profile(), keys::kOnBookmarkChanged, args.Pass()); | |
| 234 } | |
| 235 | |
| 236 void BookmarkEventRouter::BookmarkNodeFaviconChanged(BookmarkModel* model, | |
| 237 const BookmarkNode* node) { | |
| 238 // TODO(erikkay) anything we should do here? | |
| 239 } | |
| 240 | |
| 241 void BookmarkEventRouter::BookmarkNodeChildrenReordered( | |
| 242 BookmarkModel* model, | |
| 243 const BookmarkNode* node) { | |
| 244 scoped_ptr<ListValue> args(new ListValue()); | |
| 245 args->Append(new StringValue(base::Int64ToString(node->id()))); | |
| 246 int childCount = node->child_count(); | |
| 247 ListValue* children = new ListValue(); | |
| 248 for (int i = 0; i < childCount; ++i) { | |
| 249 const BookmarkNode* child = node->GetChild(i); | |
| 250 Value* child_id = new StringValue(base::Int64ToString(child->id())); | |
| 251 children->Append(child_id); | |
| 252 } | |
| 253 DictionaryValue* reorder_info = new DictionaryValue(); | |
| 254 reorder_info->Set(keys::kChildIdsKey, children); | |
| 255 args->Append(reorder_info); | |
| 256 | |
| 257 DispatchEvent(model->profile(), keys::kOnBookmarkChildrenReordered, | |
| 258 args.Pass()); | |
| 259 } | |
| 260 | |
| 261 void BookmarkEventRouter::ExtensiveBookmarkChangesBeginning( | |
| 262 BookmarkModel* model) { | |
| 263 scoped_ptr<ListValue> args(new ListValue()); | |
| 264 DispatchEvent(model->profile(), | |
| 265 keys::kOnBookmarkImportBegan, | |
| 266 args.Pass()); | |
| 267 } | |
| 268 | |
| 269 void BookmarkEventRouter::ExtensiveBookmarkChangesEnded(BookmarkModel* model) { | |
| 270 scoped_ptr<ListValue> args(new ListValue()); | |
| 271 DispatchEvent(model->profile(), | |
| 272 keys::kOnBookmarkImportEnded, | |
| 273 args.Pass()); | |
| 274 } | |
| 275 | |
| 276 BookmarkAPI::BookmarkAPI(Profile* profile) : profile_(profile) { | |
| 277 ExtensionSystem::Get(profile_)->event_router()->RegisterObserver( | |
| 278 this, keys::kOnBookmarkCreated); | |
| 279 ExtensionSystem::Get(profile_)->event_router()->RegisterObserver( | |
| 280 this, keys::kOnBookmarkRemoved); | |
| 281 ExtensionSystem::Get(profile_)->event_router()->RegisterObserver( | |
| 282 this, keys::kOnBookmarkChanged); | |
| 283 ExtensionSystem::Get(profile_)->event_router()->RegisterObserver( | |
| 284 this, keys::kOnBookmarkMoved); | |
| 285 ExtensionSystem::Get(profile_)->event_router()->RegisterObserver( | |
| 286 this, keys::kOnBookmarkChildrenReordered); | |
| 287 ExtensionSystem::Get(profile_)->event_router()->RegisterObserver( | |
| 288 this, keys::kOnBookmarkImportBegan); | |
| 289 ExtensionSystem::Get(profile_)->event_router()->RegisterObserver( | |
| 290 this, keys::kOnBookmarkImportEnded); | |
| 291 } | |
| 292 | |
| 293 BookmarkAPI::~BookmarkAPI() { | |
| 294 } | |
| 295 | |
| 296 void BookmarkAPI::Shutdown() { | |
| 297 ExtensionSystem::Get(profile_)->event_router()->UnregisterObserver(this); | |
| 298 } | |
| 299 | |
| 300 void BookmarkAPI::OnListenerAdded(const EventListenerInfo& details) { | |
| 301 bookmark_event_router_.reset(new BookmarkEventRouter( | |
| 302 BookmarkModelFactory::GetForProfile(profile_))); | |
| 303 ExtensionSystem::Get(profile_)->event_router()->UnregisterObserver(this); | |
| 304 } | |
| 305 | |
| 306 bool BookmarksGetTreeFunction::RunImpl() { | |
| 307 scoped_ptr<bookmarks::Get::Params> params( | |
| 308 bookmarks::Get::Params::Create(*args_)); | |
| 309 EXTENSION_FUNCTION_VALIDATE(params.get()); | |
| 310 | |
| 311 std::vector<linked_ptr<BookmarkTreeNode> > nodes; | |
| 312 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); | |
| 313 if (params->id_or_id_list_type == | |
| 314 bookmarks::Get::Params::ID_OR_ID_LIST_ARRAY) { | |
| 315 std::vector<std::string>* ids = params->id_or_id_list_array.get(); | |
| 316 size_t count = ids->size(); | |
| 317 EXTENSION_FUNCTION_VALIDATE(count > 0); | |
| 318 for (size_t i = 0; i < count; ++i) { | |
| 319 int64 id; | |
| 320 if (!GetBookmarkIdAsInt64(ids->at(i), &id)) | |
| 321 return false; | |
| 322 const BookmarkNode* node = model->GetNodeByID(id); | |
| 323 if (!node) { | |
| 324 error_ = keys::kNoNodeError; | |
| 325 return false; | |
| 326 } else { | |
| 327 bookmark_api_helpers::AddNode(node, &nodes, false); | |
| 328 } | |
| 329 } | |
| 330 } else { | |
| 331 int64 id; | |
| 332 if (!GetBookmarkIdAsInt64(*params->id_or_id_list_string, &id)) | |
| 333 return false; | |
| 334 const BookmarkNode* node = model->GetNodeByID(id); | |
| 335 if (!node) { | |
| 336 error_ = keys::kNoNodeError; | |
| 337 return false; | |
| 338 } | |
| 339 bookmark_api_helpers::AddNode(node, &nodes, false); | |
| 340 } | |
| 341 | |
| 342 results_ = bookmarks::Get::Results::Create(nodes); | |
| 343 return true; | |
| 344 } | |
| 345 | |
| 346 bool BookmarksGetChildrenFunction::RunImpl() { | |
| 347 scoped_ptr<bookmarks::GetChildren::Params> params( | |
| 348 bookmarks::GetChildren::Params::Create(*args_)); | |
| 349 EXTENSION_FUNCTION_VALIDATE(params.get()); | |
| 350 | |
| 351 int64 id; | |
| 352 if (!GetBookmarkIdAsInt64(params->id, &id)) | |
| 353 return false; | |
| 354 | |
| 355 std::vector<linked_ptr<BookmarkTreeNode> > nodes; | |
| 356 const BookmarkNode* node = | |
| 357 BookmarkModelFactory::GetForProfile(profile())->GetNodeByID(id); | |
| 358 if (!node) { | |
| 359 error_ = keys::kNoNodeError; | |
| 360 return false; | |
| 361 } | |
| 362 int child_count = node->child_count(); | |
| 363 for (int i = 0; i < child_count; ++i) { | |
| 364 const BookmarkNode* child = node->GetChild(i); | |
| 365 bookmark_api_helpers::AddNode(child, &nodes, false); | |
| 366 } | |
| 367 | |
| 368 results_ = bookmarks::GetChildren::Results::Create(nodes); | |
| 369 return true; | |
| 370 } | |
| 371 | |
| 372 bool BookmarksGetFunction::RunImpl() { | |
| 373 scoped_ptr<bookmarks::GetRecent::Params> params( | |
| 374 bookmarks::GetRecent::Params::Create(*args_)); | |
| 375 EXTENSION_FUNCTION_VALIDATE(params.get()); | |
| 376 if (params->number_of_items < 1) | |
| 377 return false; | |
| 378 | |
| 379 std::vector<const BookmarkNode*> nodes; | |
| 380 bookmark_utils::GetMostRecentlyAddedEntries( | |
| 381 BookmarkModelFactory::GetForProfile(profile()), | |
| 382 params->number_of_items, | |
| 383 &nodes); | |
| 384 | |
| 385 std::vector<linked_ptr<BookmarkTreeNode> > tree_nodes; | |
| 386 std::vector<const BookmarkNode*>::iterator i = nodes.begin(); | |
| 387 for (; i != nodes.end(); ++i) { | |
| 388 const BookmarkNode* node = *i; | |
| 389 bookmark_api_helpers::AddNode(node, &tree_nodes, false); | |
| 390 } | |
| 391 | |
| 392 results_ = bookmarks::GetRecent::Results::Create(tree_nodes); | |
| 393 return true; | |
| 394 } | |
| 395 | |
| 396 bool BookmarksGetSubTreeFunction::RunImpl() { | |
| 397 std::vector<linked_ptr<BookmarkTreeNode> > nodes; | |
| 398 const BookmarkNode* node = | |
| 399 BookmarkModelFactory::GetForProfile(profile())->root_node(); | |
| 400 bookmark_api_helpers::AddNode(node, &nodes, true); | |
| 401 results_ = bookmarks::GetTree::Results::Create(nodes); | |
| 402 return true; | |
| 403 } | |
| 404 | |
| 405 bool BookmarksGetRecentFunction::RunImpl() { | |
| 406 scoped_ptr<bookmarks::GetSubTree::Params> params( | |
| 407 bookmarks::GetSubTree::Params::Create(*args_)); | |
| 408 EXTENSION_FUNCTION_VALIDATE(params.get()); | |
| 409 | |
| 410 int64 id; | |
| 411 if (!GetBookmarkIdAsInt64(params->id, &id)) | |
| 412 return false; | |
| 413 | |
| 414 const BookmarkNode* node = | |
| 415 BookmarkModelFactory::GetForProfile(profile())->GetNodeByID(id); | |
| 416 if (!node) { | |
| 417 error_ = keys::kNoNodeError; | |
| 418 return false; | |
| 419 } | |
| 420 | |
| 421 std::vector<linked_ptr<BookmarkTreeNode> > nodes; | |
| 422 bookmark_api_helpers::AddNode(node, &nodes, true); | |
| 423 results_ = bookmarks::GetSubTree::Results::Create(nodes); | |
| 424 return true; | |
| 425 } | |
| 426 | |
| 427 bool BookmarksSearchFunction::RunImpl() { | |
| 428 scoped_ptr<bookmarks::Search::Params> params( | |
| 429 bookmarks::Search::Params::Create(*args_)); | |
| 430 EXTENSION_FUNCTION_VALIDATE(params.get()); | |
| 431 | |
| 432 PrefServiceBase* prefs = PrefServiceBase::FromBrowserContext(profile_); | |
| 433 std::string lang = prefs->GetString(prefs::kAcceptLanguages); | |
| 434 std::vector<const BookmarkNode*> nodes; | |
| 435 bookmark_utils::GetBookmarksContainingText( | |
| 436 BookmarkModelFactory::GetForProfile(profile()), | |
| 437 UTF8ToUTF16(params->query), | |
| 438 std::numeric_limits<int>::max(), | |
| 439 lang, | |
| 440 &nodes); | |
| 441 | |
| 442 std::vector<linked_ptr<BookmarkTreeNode> > tree_nodes; | |
| 443 for (std::vector<const BookmarkNode*>::iterator node_iter = nodes.begin(); | |
| 444 node_iter != nodes.end(); ++node_iter) { | |
| 445 bookmark_api_helpers::AddNode(*node_iter, &tree_nodes, false); | |
| 446 } | |
| 447 | |
| 448 results_ = bookmarks::Search::Results::Create(tree_nodes); | |
| 449 return true; | |
| 450 } | |
| 451 | |
| 452 // static | |
| 453 bool BookmarksRemoveFunction::ExtractIds(const ListValue* args, | |
| 454 std::list<int64>* ids, | |
| 455 bool* invalid_id) { | |
| 456 std::string id_string; | |
| 457 if (!args->GetString(0, &id_string)) | |
| 458 return false; | |
| 459 int64 id; | |
| 460 if (base::StringToInt64(id_string, &id)) | |
| 461 ids->push_back(id); | |
| 462 else | |
| 463 *invalid_id = true; | |
| 464 return true; | |
| 465 } | |
| 466 | |
| 467 bool BookmarksRemoveFunction::RunImpl() { | |
| 468 if (!EditBookmarksEnabled()) | |
| 469 return false; | |
| 470 | |
| 471 scoped_ptr<bookmarks::Remove::Params> params( | |
| 472 bookmarks::Remove::Params::Create(*args_)); | |
| 473 EXTENSION_FUNCTION_VALIDATE(params.get()); | |
| 474 | |
| 475 int64 id; | |
| 476 if (!base::StringToInt64(params->id, &id)) { | |
| 477 error_ = keys::kInvalidIdError; | |
| 478 return false; | |
| 479 } | |
| 480 | |
| 481 bool recursive = false; | |
| 482 if (name() == BookmarksRemoveTreeFunction::function_name()) | |
| 483 recursive = true; | |
| 484 | |
| 485 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); | |
| 486 if (!bookmark_api_helpers::RemoveNode(model, id, recursive, &error_)) | |
| 487 return false; | |
| 488 | |
| 489 return true; | |
| 490 } | |
| 491 | |
| 492 bool BookmarksCreateFunction::RunImpl() { | |
| 493 if (!EditBookmarksEnabled()) | |
| 494 return false; | |
| 495 | |
| 496 scoped_ptr<bookmarks::Create::Params> params( | |
| 497 bookmarks::Create::Params::Create(*args_)); | |
| 498 EXTENSION_FUNCTION_VALIDATE(params.get()); | |
| 499 | |
| 500 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); | |
| 501 int64 parentId; | |
| 502 | |
| 503 if (!params->bookmark.parent_id.get()) { | |
| 504 // Optional, default to "other bookmarks". | |
| 505 parentId = model->other_node()->id(); | |
| 506 } else { | |
| 507 if (!GetBookmarkIdAsInt64(*params->bookmark.parent_id, &parentId)) | |
| 508 return false; | |
| 509 } | |
| 510 const BookmarkNode* parent = model->GetNodeByID(parentId); | |
| 511 if (!parent) { | |
| 512 error_ = keys::kNoParentError; | |
| 513 return false; | |
| 514 } | |
| 515 if (parent->is_root()) { // Can't create children of the root. | |
| 516 error_ = keys::kModifySpecialError; | |
| 517 return false; | |
| 518 } | |
| 519 | |
| 520 int index; | |
| 521 if (!params->bookmark.index.get()) { // Optional (defaults to end). | |
| 522 index = parent->child_count(); | |
| 523 } else { | |
| 524 index = *params->bookmark.index; | |
| 525 if (index > parent->child_count() || index < 0) { | |
| 526 error_ = keys::kInvalidIndexError; | |
| 527 return false; | |
| 528 } | |
| 529 } | |
| 530 | |
| 531 string16 title; // Optional. | |
| 532 if (params->bookmark.title.get()) | |
| 533 title = UTF8ToUTF16(*params->bookmark.title.get()); | |
| 534 | |
| 535 std::string url_string; // Optional. | |
| 536 if (params->bookmark.url.get()) | |
| 537 url_string = *params->bookmark.url.get(); | |
| 538 | |
| 539 GURL url(url_string); | |
| 540 if (!url_string.empty() && !url.is_valid()) { | |
| 541 error_ = keys::kInvalidUrlError; | |
| 542 return false; | |
| 543 } | |
| 544 | |
| 545 const BookmarkNode* node; | |
| 546 if (url_string.length()) | |
| 547 node = model->AddURL(parent, index, title, url); | |
| 548 else | |
| 549 node = model->AddFolder(parent, index, title); | |
| 550 DCHECK(node); | |
| 551 if (!node) { | |
| 552 error_ = keys::kNoNodeError; | |
| 553 return false; | |
| 554 } | |
| 555 | |
| 556 scoped_ptr<BookmarkTreeNode> ret( | |
| 557 bookmark_api_helpers::GetBookmarkTreeNode(node, false, false)); | |
| 558 results_ = bookmarks::Create::Results::Create(*ret); | |
| 559 | |
| 560 return true; | |
| 561 } | |
| 562 | |
| 563 // static | |
| 564 bool BookmarksMoveFunction::ExtractIds(const ListValue* args, | |
| 565 std::list<int64>* ids, | |
| 566 bool* invalid_id) { | |
| 567 // For now, Move accepts ID parameters in the same way as an Update. | |
| 568 return BookmarksUpdateFunction::ExtractIds(args, ids, invalid_id); | |
| 569 } | |
| 570 | |
| 571 bool BookmarksMoveFunction::RunImpl() { | |
| 572 if (!EditBookmarksEnabled()) | |
| 573 return false; | |
| 574 | |
| 575 scoped_ptr<bookmarks::Move::Params> params( | |
| 576 bookmarks::Move::Params::Create(*args_)); | |
| 577 EXTENSION_FUNCTION_VALIDATE(params.get()); | |
| 578 | |
| 579 int64 id; | |
| 580 if (!base::StringToInt64(params->id, &id)) { | |
| 581 error_ = keys::kInvalidIdError; | |
| 582 return false; | |
| 583 } | |
| 584 | |
| 585 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); | |
| 586 const BookmarkNode* node = model->GetNodeByID(id); | |
| 587 if (!node) { | |
| 588 error_ = keys::kNoNodeError; | |
| 589 return false; | |
| 590 } | |
| 591 if (model->is_permanent_node(node)) { | |
| 592 error_ = keys::kModifySpecialError; | |
| 593 return false; | |
| 594 } | |
| 595 | |
| 596 const BookmarkNode* parent = NULL; | |
| 597 if (!params->destination.parent_id.get()) { | |
| 598 // Optional, defaults to current parent. | |
| 599 parent = node->parent(); | |
| 600 } else { | |
| 601 int64 parentId; | |
| 602 if (!GetBookmarkIdAsInt64(*params->destination.parent_id, &parentId)) | |
| 603 return false; | |
| 604 | |
| 605 parent = model->GetNodeByID(parentId); | |
| 606 } | |
| 607 if (!parent) { | |
| 608 error_ = keys::kNoParentError; | |
| 609 // TODO(erikkay) return an error message. | |
| 610 return false; | |
| 611 } | |
| 612 if (parent == model->root_node()) { | |
| 613 error_ = keys::kModifySpecialError; | |
| 614 return false; | |
| 615 } | |
| 616 | |
| 617 int index; | |
| 618 if (params->destination.index.get()) { // Optional (defaults to end). | |
| 619 index = *params->destination.index; | |
| 620 if (index > parent->child_count() || index < 0) { | |
| 621 error_ = keys::kInvalidIndexError; | |
| 622 return false; | |
| 623 } | |
| 624 } else { | |
| 625 index = parent->child_count(); | |
| 626 } | |
| 627 | |
| 628 model->Move(node, parent, index); | |
| 629 | |
| 630 scoped_ptr<BookmarkTreeNode> tree_node( | |
| 631 bookmark_api_helpers::GetBookmarkTreeNode(node, false, false)); | |
| 632 results_ = bookmarks::Move::Results::Create(*tree_node); | |
| 633 | |
| 634 return true; | |
| 635 } | |
| 636 | |
| 637 // static | |
| 638 bool BookmarksUpdateFunction::ExtractIds(const ListValue* args, | |
| 639 std::list<int64>* ids, | |
| 640 bool* invalid_id) { | |
| 641 // For now, Update accepts ID parameters in the same way as an Remove. | |
| 642 return BookmarksRemoveFunction::ExtractIds(args, ids, invalid_id); | |
| 643 } | |
| 644 | |
| 645 bool BookmarksUpdateFunction::RunImpl() { | |
| 646 if (!EditBookmarksEnabled()) | |
| 647 return false; | |
| 648 | |
| 649 scoped_ptr<bookmarks::Update::Params> params( | |
| 650 bookmarks::Update::Params::Create(*args_)); | |
| 651 EXTENSION_FUNCTION_VALIDATE(params.get()); | |
| 652 | |
| 653 int64 id; | |
| 654 if (!base::StringToInt64(params->id, &id)) { | |
| 655 error_ = keys::kInvalidIdError; | |
| 656 return false; | |
| 657 } | |
| 658 | |
| 659 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile()); | |
| 660 | |
| 661 // Optional but we need to distinguish non present from an empty title. | |
| 662 string16 title; | |
| 663 bool has_title = false; | |
| 664 if (params->changes.title.get()) { | |
| 665 title = UTF8ToUTF16(*params->changes.title); | |
| 666 has_title = true; | |
| 667 } | |
| 668 | |
| 669 // Optional. | |
| 670 std::string url_string; | |
| 671 if (params->changes.url.get()) | |
| 672 url_string = *params->changes.url; | |
| 673 GURL url(url_string); | |
| 674 if (!url_string.empty() && !url.is_valid()) { | |
| 675 error_ = keys::kInvalidUrlError; | |
| 676 return false; | |
| 677 } | |
| 678 | |
| 679 const BookmarkNode* node = model->GetNodeByID(id); | |
| 680 if (!node) { | |
| 681 error_ = keys::kNoNodeError; | |
| 682 return false; | |
| 683 } | |
| 684 if (model->is_permanent_node(node)) { | |
| 685 error_ = keys::kModifySpecialError; | |
| 686 return false; | |
| 687 } | |
| 688 if (has_title) | |
| 689 model->SetTitle(node, title); | |
| 690 if (!url.is_empty()) | |
| 691 model->SetURL(node, url); | |
| 692 | |
| 693 scoped_ptr<BookmarkTreeNode> tree_node( | |
| 694 bookmark_api_helpers::GetBookmarkTreeNode(node, false, false)); | |
| 695 results_ = bookmarks::Update::Results::Create(*tree_node); | |
| 696 return true; | |
| 697 } | |
| 698 | |
| 699 // Mapper superclass for BookmarkFunctions. | |
| 700 template <typename BucketIdType> | |
| 701 class BookmarkBucketMapper : public BucketMapper { | |
| 702 public: | |
| 703 virtual ~BookmarkBucketMapper() { STLDeleteValues(&buckets_); } | |
| 704 protected: | |
| 705 Bucket* GetBucket(const BucketIdType& id) { | |
| 706 Bucket* b = buckets_[id]; | |
| 707 if (b == NULL) { | |
| 708 b = new Bucket(); | |
| 709 buckets_[id] = b; | |
| 710 } | |
| 711 return b; | |
| 712 } | |
| 713 private: | |
| 714 std::map<BucketIdType, Bucket*> buckets_; | |
| 715 }; | |
| 716 | |
| 717 // Mapper for 'bookmarks.create'. Maps "same input to bookmarks.create" to a | |
| 718 // unique bucket. | |
| 719 class CreateBookmarkBucketMapper : public BookmarkBucketMapper<std::string> { | |
| 720 public: | |
| 721 explicit CreateBookmarkBucketMapper(Profile* profile) : profile_(profile) {} | |
| 722 // TODO(tim): This should share code with BookmarksCreateFunction::RunImpl, | |
| 723 // but I can't figure out a good way to do that with all the macros. | |
| 724 virtual void GetBucketsForArgs(const ListValue* args, BucketList* buckets) { | |
| 725 const DictionaryValue* json; | |
| 726 if (!args->GetDictionary(0, &json)) | |
| 727 return; | |
| 728 | |
| 729 std::string parent_id; | |
| 730 if (json->HasKey(keys::kParentIdKey)) { | |
| 731 if (!json->GetString(keys::kParentIdKey, &parent_id)) | |
| 732 return; | |
| 733 } | |
| 734 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_); | |
| 735 | |
| 736 int64 parent_id_int64; | |
| 737 base::StringToInt64(parent_id, &parent_id_int64); | |
| 738 const BookmarkNode* parent = model->GetNodeByID(parent_id_int64); | |
| 739 if (!parent) | |
| 740 return; | |
| 741 | |
| 742 std::string bucket_id = UTF16ToUTF8(parent->GetTitle()); | |
| 743 std::string title; | |
| 744 json->GetString(keys::kTitleKey, &title); | |
| 745 std::string url_string; | |
| 746 json->GetString(keys::kUrlKey, &url_string); | |
| 747 | |
| 748 bucket_id += title; | |
| 749 bucket_id += url_string; | |
| 750 // 20 bytes (SHA1 hash length) is very likely less than most of the | |
| 751 // |bucket_id| strings we construct here, so we hash it to save space. | |
| 752 buckets->push_back(GetBucket(base::SHA1HashString(bucket_id))); | |
| 753 } | |
| 754 private: | |
| 755 Profile* profile_; | |
| 756 }; | |
| 757 | |
| 758 // Mapper for 'bookmarks.remove'. | |
| 759 class RemoveBookmarksBucketMapper : public BookmarkBucketMapper<std::string> { | |
| 760 public: | |
| 761 explicit RemoveBookmarksBucketMapper(Profile* profile) : profile_(profile) {} | |
| 762 virtual void GetBucketsForArgs(const ListValue* args, BucketList* buckets) { | |
| 763 typedef std::list<int64> IdList; | |
| 764 IdList ids; | |
| 765 bool invalid_id = false; | |
| 766 if (!BookmarksRemoveFunction::ExtractIds(args, &ids, &invalid_id) || | |
| 767 invalid_id) { | |
| 768 return; | |
| 769 } | |
| 770 | |
| 771 for (IdList::iterator it = ids.begin(); it != ids.end(); ++it) { | |
| 772 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_); | |
| 773 const BookmarkNode* node = model->GetNodeByID(*it); | |
| 774 if (!node || node->is_root()) | |
| 775 return; | |
| 776 | |
| 777 std::string bucket_id; | |
| 778 bucket_id += UTF16ToUTF8(node->parent()->GetTitle()); | |
| 779 bucket_id += UTF16ToUTF8(node->GetTitle()); | |
| 780 bucket_id += node->url().spec(); | |
| 781 buckets->push_back(GetBucket(base::SHA1HashString(bucket_id))); | |
| 782 } | |
| 783 } | |
| 784 private: | |
| 785 Profile* profile_; | |
| 786 }; | |
| 787 | |
| 788 // Mapper for any bookmark function accepting bookmark IDs as parameters, where | |
| 789 // a distinct ID corresponds to a single item in terms of quota limiting. This | |
| 790 // is inappropriate for bookmarks.remove, for example, since repeated removals | |
| 791 // of the same item will actually have a different ID each time. | |
| 792 template <class FunctionType> | |
| 793 class BookmarkIdMapper : public BookmarkBucketMapper<int64> { | |
| 794 public: | |
| 795 typedef std::list<int64> IdList; | |
| 796 virtual void GetBucketsForArgs(const ListValue* args, BucketList* buckets) { | |
| 797 IdList ids; | |
| 798 bool invalid_id = false; | |
| 799 if (!FunctionType::ExtractIds(args, &ids, &invalid_id) || invalid_id) | |
| 800 return; | |
| 801 for (IdList::iterator it = ids.begin(); it != ids.end(); ++it) | |
| 802 buckets->push_back(GetBucket(*it)); | |
| 803 } | |
| 804 }; | |
| 805 | |
| 806 // Builds heuristics for all BookmarkFunctions using specialized BucketMappers. | |
| 807 class BookmarksQuotaLimitFactory { | |
| 808 public: | |
| 809 // For id-based bookmark functions. | |
| 810 template <class FunctionType> | |
| 811 static void Build(QuotaLimitHeuristics* heuristics) { | |
| 812 BuildWithMappers(heuristics, new BookmarkIdMapper<FunctionType>(), | |
| 813 new BookmarkIdMapper<FunctionType>()); | |
| 814 } | |
| 815 | |
| 816 // For bookmarks.create. | |
| 817 static void BuildForCreate(QuotaLimitHeuristics* heuristics, | |
| 818 Profile* profile) { | |
| 819 BuildWithMappers(heuristics, new CreateBookmarkBucketMapper(profile), | |
| 820 new CreateBookmarkBucketMapper(profile)); | |
| 821 } | |
| 822 | |
| 823 // For bookmarks.remove. | |
| 824 static void BuildForRemove(QuotaLimitHeuristics* heuristics, | |
| 825 Profile* profile) { | |
| 826 BuildWithMappers(heuristics, new RemoveBookmarksBucketMapper(profile), | |
| 827 new RemoveBookmarksBucketMapper(profile)); | |
| 828 } | |
| 829 | |
| 830 private: | |
| 831 static void BuildWithMappers(QuotaLimitHeuristics* heuristics, | |
| 832 BucketMapper* short_mapper, BucketMapper* long_mapper) { | |
| 833 const Config kSustainedLimitConfig = { | |
| 834 // See bookmarks.json for current value. | |
| 835 bookmarks::MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE, | |
| 836 TimeDelta::FromMinutes(1) | |
| 837 }; | |
| 838 heuristics->push_back(new SustainedLimit( | |
| 839 TimeDelta::FromMinutes(10), | |
| 840 kSustainedLimitConfig, | |
| 841 short_mapper, | |
| 842 "MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE")); | |
| 843 | |
| 844 const Config kTimedLimitConfig = { | |
| 845 // See bookmarks.json for current value. | |
| 846 bookmarks::MAX_WRITE_OPERATIONS_PER_HOUR, | |
| 847 TimeDelta::FromHours(1) | |
| 848 }; | |
| 849 heuristics->push_back(new TimedLimit( | |
| 850 kTimedLimitConfig, | |
| 851 long_mapper, | |
| 852 "MAX_WRITE_OPERATIONS_PER_HOUR")); | |
| 853 } | |
| 854 | |
| 855 DISALLOW_IMPLICIT_CONSTRUCTORS(BookmarksQuotaLimitFactory); | |
| 856 }; | |
| 857 | |
| 858 // And finally, building the individual heuristics for each function. | |
| 859 void BookmarksRemoveFunction::GetQuotaLimitHeuristics( | |
| 860 QuotaLimitHeuristics* heuristics) const { | |
| 861 BookmarksQuotaLimitFactory::BuildForRemove(heuristics, profile()); | |
| 862 } | |
| 863 | |
| 864 void BookmarksMoveFunction::GetQuotaLimitHeuristics( | |
| 865 QuotaLimitHeuristics* heuristics) const { | |
| 866 BookmarksQuotaLimitFactory::Build<BookmarksMoveFunction>(heuristics); | |
| 867 } | |
| 868 | |
| 869 void BookmarksUpdateFunction::GetQuotaLimitHeuristics( | |
| 870 QuotaLimitHeuristics* heuristics) const { | |
| 871 BookmarksQuotaLimitFactory::Build<BookmarksUpdateFunction>(heuristics); | |
| 872 }; | |
| 873 | |
| 874 void BookmarksCreateFunction::GetQuotaLimitHeuristics( | |
| 875 QuotaLimitHeuristics* heuristics) const { | |
| 876 BookmarksQuotaLimitFactory::BuildForCreate(heuristics, profile()); | |
| 877 } | |
| 878 | |
| 879 BookmarksIOFunction::BookmarksIOFunction() {} | |
| 880 | |
| 881 BookmarksIOFunction::~BookmarksIOFunction() { | |
| 882 // There may be pending file dialogs, we need to tell them that we've gone | |
| 883 // away so they don't try and call back to us. | |
| 884 if (select_file_dialog_.get()) | |
| 885 select_file_dialog_->ListenerDestroyed(); | |
| 886 } | |
| 887 | |
| 888 void BookmarksIOFunction::SelectFile(ui::SelectFileDialog::Type type) { | |
| 889 // GetDefaultFilepathForBookmarkExport() might have to touch the filesystem | |
| 890 // (stat or access, for example), so this requires a thread with IO allowed. | |
| 891 if (!BrowserThread::CurrentlyOn(BrowserThread::FILE)) { | |
| 892 BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE, | |
| 893 base::Bind(&BookmarksIOFunction::SelectFile, this, type)); | |
| 894 return; | |
| 895 } | |
| 896 | |
| 897 // Pre-populating the filename field in case this is a SELECT_SAVEAS_FILE | |
| 898 // dialog. If not, there is no filename field in the dialog box. | |
| 899 FilePath default_path; | |
| 900 if (type == ui::SelectFileDialog::SELECT_SAVEAS_FILE) | |
| 901 default_path = GetDefaultFilepathForBookmarkExport(); | |
| 902 else | |
| 903 DCHECK(type == ui::SelectFileDialog::SELECT_OPEN_FILE); | |
| 904 | |
| 905 // After getting the |default_path|, ask the UI to display the file dialog. | |
| 906 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, | |
| 907 base::Bind(&BookmarksIOFunction::ShowSelectFileDialog, this, | |
| 908 type, default_path)); | |
| 909 } | |
| 910 | |
| 911 void BookmarksIOFunction::ShowSelectFileDialog(ui::SelectFileDialog::Type type, | |
| 912 const FilePath& default_path) { | |
| 913 // Balanced in one of the three callbacks of SelectFileDialog: | |
| 914 // either FileSelectionCanceled, MultiFilesSelected, or FileSelected | |
| 915 AddRef(); | |
| 916 | |
| 917 WebContents* web_contents = dispatcher()->delegate()-> | |
| 918 GetAssociatedWebContents(); | |
| 919 | |
| 920 select_file_dialog_ = ui::SelectFileDialog::Create( | |
| 921 this, new ChromeSelectFilePolicy(web_contents)); | |
| 922 ui::SelectFileDialog::FileTypeInfo file_type_info; | |
| 923 file_type_info.extensions.resize(1); | |
| 924 file_type_info.extensions[0].push_back(FILE_PATH_LITERAL("html")); | |
| 925 // TODO(kinaba): http://crbug.com/140425. Turn file_type_info.support_gdata | |
| 926 // on for saving once Google Drive client on ChromeOS supports it. | |
| 927 if (type == ui::SelectFileDialog::SELECT_OPEN_FILE) | |
| 928 file_type_info.support_gdata = true; | |
| 929 // |web_contents| can be NULL (for background pages), which is fine. In such | |
| 930 // a case if file-selection dialogs are forbidden by policy, we will not | |
| 931 // show an InfoBar, which is better than letting one appear out of the blue. | |
| 932 select_file_dialog_->SelectFile(type, | |
| 933 string16(), | |
| 934 default_path, | |
| 935 &file_type_info, | |
| 936 0, | |
| 937 FILE_PATH_LITERAL(""), | |
| 938 NULL, | |
| 939 NULL); | |
| 940 } | |
| 941 | |
| 942 void BookmarksIOFunction::FileSelectionCanceled(void* params) { | |
| 943 Release(); // Balanced in BookmarksIOFunction::SelectFile() | |
| 944 } | |
| 945 | |
| 946 void BookmarksIOFunction::MultiFilesSelected( | |
| 947 const std::vector<FilePath>& files, void* params) { | |
| 948 Release(); // Balanced in BookmarsIOFunction::SelectFile() | |
| 949 NOTREACHED() << "Should not be able to select multiple files"; | |
| 950 } | |
| 951 | |
| 952 bool BookmarksImportFunction::RunImpl() { | |
| 953 if (!EditBookmarksEnabled()) | |
| 954 return false; | |
| 955 SelectFile(ui::SelectFileDialog::SELECT_OPEN_FILE); | |
| 956 return true; | |
| 957 } | |
| 958 | |
| 959 void BookmarksImportFunction::FileSelected(const FilePath& path, | |
| 960 int index, | |
| 961 void* params) { | |
| 962 #if !defined(OS_ANDROID) | |
| 963 // Android does not have support for the standard importers. | |
| 964 // TODO(jgreenwald): remove ifdef once extensions are no longer built on | |
| 965 // Android. | |
| 966 scoped_refptr<ImporterHost> importer_host(new ImporterHost); | |
| 967 importer::SourceProfile source_profile; | |
| 968 source_profile.importer_type = importer::TYPE_BOOKMARKS_FILE; | |
| 969 source_profile.source_path = path; | |
| 970 importer_host->StartImportSettings(source_profile, | |
| 971 profile(), | |
| 972 importer::FAVORITES, | |
| 973 new ProfileWriter(profile()), | |
| 974 true); | |
| 975 #endif | |
| 976 Release(); // Balanced in BookmarksIOFunction::SelectFile() | |
| 977 } | |
| 978 | |
| 979 bool BookmarksExportFunction::RunImpl() { | |
| 980 SelectFile(ui::SelectFileDialog::SELECT_SAVEAS_FILE); | |
| 981 return true; | |
| 982 } | |
| 983 | |
| 984 void BookmarksExportFunction::FileSelected(const FilePath& path, | |
| 985 int index, | |
| 986 void* params) { | |
| 987 #if !defined(OS_ANDROID) | |
| 988 // Android does not have support for the standard exporter. | |
| 989 // TODO(jgreenwald): remove ifdef once extensions are no longer built on | |
| 990 // Android. | |
| 991 bookmark_html_writer::WriteBookmarks(profile(), path, NULL); | |
| 992 #endif | |
| 993 Release(); // Balanced in BookmarksIOFunction::SelectFile() | |
| 994 } | |
| 995 | |
| 996 } // namespace extensions | |
| OLD | NEW |