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