extension_bookmarks_module.cc revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/extensions/extension_bookmarks_module.h"
6
7#include "base/file_path.h"
8#include "base/i18n/file_util_icu.h"
9#include "base/i18n/time_formatting.h"
10#include "base/json/json_writer.h"
11#include "base/sha1.h"
12#include "base/stl_util-inl.h"
13#include "base/string16.h"
14#include "base/string_number_conversions.h"
15#include "base/string_util.h"
16#include "base/time.h"
17#include "base/utf_string_conversions.h"
18#include "chrome/browser/bookmarks/bookmark_codec.h"
19#include "chrome/browser/bookmarks/bookmark_html_writer.h"
20#include "chrome/browser/bookmarks/bookmark_model.h"
21#include "chrome/browser/bookmarks/bookmark_utils.h"
22#include "chrome/browser/browser_list.h"
23#include "chrome/browser/extensions/extension_bookmark_helpers.h"
24#include "chrome/browser/extensions/extension_bookmarks_module_constants.h"
25#include "chrome/browser/extensions/extension_event_router.h"
26#include "chrome/browser/extensions/extensions_quota_service.h"
27#include "chrome/browser/importer/importer_data_types.h"
28#include "chrome/browser/prefs/pref_service.h"
29#include "chrome/browser/profiles/profile.h"
30#include "chrome/common/notification_service.h"
31#include "chrome/common/pref_names.h"
32#include "grit/generated_resources.h"
33#include "ui/base/l10n/l10n_util.h"
34
35namespace keys = extension_bookmarks_module_constants;
36
37using base::TimeDelta;
38typedef QuotaLimitHeuristic::Bucket Bucket;
39typedef QuotaLimitHeuristic::Config Config;
40typedef QuotaLimitHeuristic::BucketList BucketList;
41typedef ExtensionsQuotaService::TimedLimit TimedLimit;
42typedef ExtensionsQuotaService::SustainedLimit SustainedLimit;
43typedef QuotaLimitHeuristic::BucketMapper BucketMapper;
44
45namespace {
46
47// Generates a default filename that will be used for pre-populating
48// the "Export Bookmarks" file chooser dialog box.
49FilePath::StringType GetDefaultFilenameForBookmarkExport() {
50  base::Time time = base::Time::Now();
51
52  // Concatenate a date stamp to the filename.
53#if defined(OS_POSIX)
54  FilePath::StringType filename =
55      l10n_util::GetStringFUTF8(EXPORT_BOOKMARKS_DEFAULT_FILENAME,
56                                base::TimeFormatShortDateNumeric(time));
57#elif defined(OS_WIN)
58  FilePath::StringType filename =
59      l10n_util::GetStringFUTF16(EXPORT_BOOKMARKS_DEFAULT_FILENAME,
60                                 base::TimeFormatShortDateNumeric(time));
61#endif
62
63  file_util::ReplaceIllegalCharactersInPath(&filename, '_');
64  return filename;
65}
66
67}  // namespace
68
69void BookmarksFunction::Run() {
70  BookmarkModel* model = profile()->GetBookmarkModel();
71  if (!model->IsLoaded()) {
72    // Bookmarks are not ready yet.  We'll wait.
73    registrar_.Add(this, NotificationType::BOOKMARK_MODEL_LOADED,
74                   NotificationService::AllSources());
75    AddRef();  // Balanced in Observe().
76    return;
77  }
78
79  bool success = RunImpl();
80  if (success) {
81    NotificationService::current()->Notify(
82        NotificationType::EXTENSION_BOOKMARKS_API_INVOKED,
83        Source<const Extension>(GetExtension()),
84        Details<const BookmarksFunction>(this));
85  }
86  SendResponse(success);
87}
88
89bool BookmarksFunction::GetBookmarkIdAsInt64(
90    const std::string& id_string, int64* id) {
91  if (base::StringToInt64(id_string, id))
92    return true;
93
94  error_ = keys::kInvalidIdError;
95  return false;
96}
97
98void BookmarksFunction::Observe(NotificationType type,
99                                const NotificationSource& source,
100                                const NotificationDetails& details) {
101  DCHECK(type == NotificationType::BOOKMARK_MODEL_LOADED);
102  DCHECK(profile()->GetBookmarkModel()->IsLoaded());
103  Run();
104  Release();  // Balanced in Run().
105}
106
107// static
108ExtensionBookmarkEventRouter* ExtensionBookmarkEventRouter::GetInstance() {
109  return Singleton<ExtensionBookmarkEventRouter>::get();
110}
111
112ExtensionBookmarkEventRouter::ExtensionBookmarkEventRouter() {
113}
114
115ExtensionBookmarkEventRouter::~ExtensionBookmarkEventRouter() {
116}
117
118void ExtensionBookmarkEventRouter::Observe(BookmarkModel* model) {
119  if (models_.find(model) == models_.end()) {
120    model->AddObserver(this);
121    models_.insert(model);
122  }
123}
124
125void ExtensionBookmarkEventRouter::DispatchEvent(Profile *profile,
126                                                 const char* event_name,
127                                                 const std::string json_args) {
128  if (profile->GetExtensionEventRouter()) {
129    profile->GetExtensionEventRouter()->DispatchEventToRenderers(
130        event_name, json_args, NULL, GURL());
131  }
132}
133
134void ExtensionBookmarkEventRouter::Loaded(BookmarkModel* model) {
135  // TODO(erikkay): Perhaps we should send this event down to the extension
136  // so they know when it's safe to use the API?
137}
138
139void ExtensionBookmarkEventRouter::BookmarkNodeMoved(
140    BookmarkModel* model,
141    const BookmarkNode* old_parent,
142    int old_index,
143    const BookmarkNode* new_parent,
144    int new_index) {
145  ListValue args;
146  const BookmarkNode* node = new_parent->GetChild(new_index);
147  args.Append(new StringValue(base::Int64ToString(node->id())));
148  DictionaryValue* object_args = new DictionaryValue();
149  object_args->SetString(keys::kParentIdKey,
150                         base::Int64ToString(new_parent->id()));
151  object_args->SetInteger(keys::kIndexKey, new_index);
152  object_args->SetString(keys::kOldParentIdKey,
153                         base::Int64ToString(old_parent->id()));
154  object_args->SetInteger(keys::kOldIndexKey, old_index);
155  args.Append(object_args);
156
157  std::string json_args;
158  base::JSONWriter::Write(&args, false, &json_args);
159  DispatchEvent(model->profile(), keys::kOnBookmarkMoved, json_args);
160}
161
162void ExtensionBookmarkEventRouter::BookmarkNodeAdded(BookmarkModel* model,
163                                                     const BookmarkNode* parent,
164                                                     int index) {
165  ListValue args;
166  const BookmarkNode* node = parent->GetChild(index);
167  args.Append(new StringValue(base::Int64ToString(node->id())));
168  DictionaryValue* obj =
169      extension_bookmark_helpers::GetNodeDictionary(node, false, false);
170  args.Append(obj);
171
172  std::string json_args;
173  base::JSONWriter::Write(&args, false, &json_args);
174  DispatchEvent(model->profile(), keys::kOnBookmarkCreated, json_args);
175}
176
177void ExtensionBookmarkEventRouter::BookmarkNodeRemoved(
178    BookmarkModel* model,
179    const BookmarkNode* parent,
180    int index,
181    const BookmarkNode* node) {
182  ListValue args;
183  args.Append(new StringValue(base::Int64ToString(node->id())));
184  DictionaryValue* object_args = new DictionaryValue();
185  object_args->SetString(keys::kParentIdKey,
186                         base::Int64ToString(parent->id()));
187  object_args->SetInteger(keys::kIndexKey, index);
188  args.Append(object_args);
189
190  std::string json_args;
191  base::JSONWriter::Write(&args, false, &json_args);
192  DispatchEvent(model->profile(), keys::kOnBookmarkRemoved, json_args);
193}
194
195void ExtensionBookmarkEventRouter::BookmarkNodeChanged(
196    BookmarkModel* model, const BookmarkNode* node) {
197  ListValue args;
198  args.Append(new StringValue(base::Int64ToString(node->id())));
199
200  // TODO(erikkay) The only three things that BookmarkModel sends this
201  // notification for are title, url and favicon.  Since we're currently
202  // ignoring favicon and since the notification doesn't say which one anyway,
203  // for now we only include title and url.  The ideal thing would be to change
204  // BookmarkModel to indicate what changed.
205  DictionaryValue* object_args = new DictionaryValue();
206  object_args->SetString(keys::kTitleKey, node->GetTitle());
207  if (node->is_url())
208    object_args->SetString(keys::kUrlKey, node->GetURL().spec());
209  args.Append(object_args);
210
211  std::string json_args;
212  base::JSONWriter::Write(&args, false, &json_args);
213  DispatchEvent(model->profile(), keys::kOnBookmarkChanged, json_args);
214}
215
216void ExtensionBookmarkEventRouter::BookmarkNodeFavIconLoaded(
217    BookmarkModel* model, const BookmarkNode* node) {
218  // TODO(erikkay) anything we should do here?
219}
220
221void ExtensionBookmarkEventRouter::BookmarkNodeChildrenReordered(
222    BookmarkModel* model, const BookmarkNode* node) {
223  ListValue args;
224  args.Append(new StringValue(base::Int64ToString(node->id())));
225  int childCount = node->GetChildCount();
226  ListValue* children = new ListValue();
227  for (int i = 0; i < childCount; ++i) {
228    const BookmarkNode* child = node->GetChild(i);
229    Value* child_id = new StringValue(base::Int64ToString(child->id()));
230    children->Append(child_id);
231  }
232  DictionaryValue* reorder_info = new DictionaryValue();
233  reorder_info->Set(keys::kChildIdsKey, children);
234  args.Append(reorder_info);
235
236  std::string json_args;
237  base::JSONWriter::Write(&args, false, &json_args);
238  DispatchEvent(model->profile(),
239                keys::kOnBookmarkChildrenReordered,
240                json_args);
241}
242
243void ExtensionBookmarkEventRouter::
244    BookmarkImportBeginning(BookmarkModel* model) {
245  ListValue args;
246  std::string json_args;
247  base::JSONWriter::Write(&args, false, &json_args);
248  DispatchEvent(model->profile(),
249                keys::kOnBookmarkImportBegan,
250                json_args);
251}
252
253void ExtensionBookmarkEventRouter::BookmarkImportEnding(BookmarkModel* model) {
254  ListValue args;
255  std::string json_args;
256  base::JSONWriter::Write(&args, false, &json_args);
257  DispatchEvent(model->profile(),
258                keys::kOnBookmarkImportEnded,
259                json_args);
260}
261
262bool GetBookmarksFunction::RunImpl() {
263  BookmarkModel* model = profile()->GetBookmarkModel();
264  scoped_ptr<ListValue> json(new ListValue());
265  Value* arg0;
266  EXTENSION_FUNCTION_VALIDATE(args_->Get(0, &arg0));
267  if (arg0->IsType(Value::TYPE_LIST)) {
268    const ListValue* ids = static_cast<const ListValue*>(arg0);
269    size_t count = ids->GetSize();
270    EXTENSION_FUNCTION_VALIDATE(count > 0);
271    for (size_t i = 0; i < count; ++i) {
272      int64 id;
273      std::string id_string;
274      EXTENSION_FUNCTION_VALIDATE(ids->GetString(i, &id_string));
275      if (!GetBookmarkIdAsInt64(id_string, &id))
276        return false;
277      const BookmarkNode* node = model->GetNodeByID(id);
278      if (!node) {
279        error_ = keys::kNoNodeError;
280        return false;
281      } else {
282        extension_bookmark_helpers::AddNode(node, json.get(), false);
283      }
284    }
285  } else {
286    int64 id;
287    std::string id_string;
288    EXTENSION_FUNCTION_VALIDATE(arg0->GetAsString(&id_string));
289    if (!GetBookmarkIdAsInt64(id_string, &id))
290      return false;
291    const BookmarkNode* node = model->GetNodeByID(id);
292    if (!node) {
293      error_ = keys::kNoNodeError;
294      return false;
295    }
296    extension_bookmark_helpers::AddNode(node, json.get(), false);
297  }
298
299  result_.reset(json.release());
300  return true;
301}
302
303bool GetBookmarkChildrenFunction::RunImpl() {
304  BookmarkModel* model = profile()->GetBookmarkModel();
305  int64 id;
306  std::string id_string;
307  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &id_string));
308  if (!GetBookmarkIdAsInt64(id_string, &id))
309    return false;
310  scoped_ptr<ListValue> json(new ListValue());
311  const BookmarkNode* node = model->GetNodeByID(id);
312  if (!node) {
313    error_ = keys::kNoNodeError;
314    return false;
315  }
316  int child_count = node->GetChildCount();
317  for (int i = 0; i < child_count; ++i) {
318    const BookmarkNode* child = node->GetChild(i);
319    extension_bookmark_helpers::AddNode(child, json.get(), false);
320  }
321
322  result_.reset(json.release());
323  return true;
324}
325
326bool GetBookmarkRecentFunction::RunImpl() {
327  int number_of_items;
328  EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(0, &number_of_items));
329  if (number_of_items < 1)
330    return false;
331
332  BookmarkModel* model = profile()->GetBookmarkModel();
333  ListValue* json = new ListValue();
334  std::vector<const BookmarkNode*> nodes;
335  bookmark_utils::GetMostRecentlyAddedEntries(model, number_of_items, &nodes);
336  std::vector<const BookmarkNode*>::iterator i = nodes.begin();
337  for (; i != nodes.end(); ++i) {
338    const BookmarkNode* node = *i;
339    extension_bookmark_helpers::AddNode(node, json, false);
340  }
341  result_.reset(json);
342  return true;
343}
344
345bool GetBookmarkTreeFunction::RunImpl() {
346  BookmarkModel* model = profile()->GetBookmarkModel();
347  scoped_ptr<ListValue> json(new ListValue());
348  const BookmarkNode* node = model->root_node();
349  extension_bookmark_helpers::AddNode(node, json.get(), true);
350  result_.reset(json.release());
351  return true;
352}
353
354bool SearchBookmarksFunction::RunImpl() {
355  string16 query;
356  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &query));
357
358  BookmarkModel* model = profile()->GetBookmarkModel();
359  ListValue* json = new ListValue();
360  std::string lang = profile()->GetPrefs()->GetString(prefs::kAcceptLanguages);
361  std::vector<const BookmarkNode*> nodes;
362  bookmark_utils::GetBookmarksContainingText(model, query,
363                                             std::numeric_limits<int>::max(),
364                                             lang, &nodes);
365  std::vector<const BookmarkNode*>::iterator i = nodes.begin();
366  for (; i != nodes.end(); ++i) {
367    const BookmarkNode* node = *i;
368    extension_bookmark_helpers::AddNode(node, json, false);
369  }
370
371  result_.reset(json);
372  return true;
373}
374
375// static
376bool RemoveBookmarkFunction::ExtractIds(const ListValue* args,
377                                        std::list<int64>* ids,
378                                        bool* invalid_id) {
379  std::string id_string;
380  if (!args->GetString(0, &id_string))
381    return false;
382  int64 id;
383  if (base::StringToInt64(id_string, &id))
384    ids->push_back(id);
385  else
386    *invalid_id = true;
387  return true;
388}
389
390bool RemoveBookmarkFunction::RunImpl() {
391  std::list<int64> ids;
392  bool invalid_id = false;
393  EXTENSION_FUNCTION_VALIDATE(ExtractIds(args_.get(), &ids, &invalid_id));
394  if (invalid_id) {
395    error_ = keys::kInvalidIdError;
396    return false;
397  }
398  bool recursive = false;
399  if (name() == RemoveTreeBookmarkFunction::function_name())
400    recursive = true;
401
402  BookmarkModel* model = profile()->GetBookmarkModel();
403  size_t count = ids.size();
404  EXTENSION_FUNCTION_VALIDATE(count > 0);
405  for (std::list<int64>::iterator it = ids.begin(); it != ids.end(); ++it) {
406    if (!extension_bookmark_helpers::RemoveNode(model, *it, recursive, &error_))
407      return false;
408  }
409  return true;
410}
411
412bool CreateBookmarkFunction::RunImpl() {
413  DictionaryValue* json;
414  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(0, &json));
415  EXTENSION_FUNCTION_VALIDATE(json != NULL);
416
417  BookmarkModel* model = profile()->GetBookmarkModel();
418  int64 parentId;
419  if (!json->HasKey(keys::kParentIdKey)) {
420    // Optional, default to "other bookmarks".
421    parentId = model->other_node()->id();
422  } else {
423    std::string parentId_string;
424    EXTENSION_FUNCTION_VALIDATE(json->GetString(keys::kParentIdKey,
425                                                &parentId_string));
426    if (!GetBookmarkIdAsInt64(parentId_string, &parentId))
427      return false;
428  }
429  const BookmarkNode* parent = model->GetNodeByID(parentId);
430  if (!parent) {
431    error_ = keys::kNoParentError;
432    return false;
433  }
434  if (parent->GetParent() == NULL) {  // Can't create children of the root.
435    error_ = keys::kNoParentError;
436    return false;
437  }
438
439  int index;
440  if (!json->HasKey(keys::kIndexKey)) {  // Optional (defaults to end).
441    index = parent->GetChildCount();
442  } else {
443    EXTENSION_FUNCTION_VALIDATE(json->GetInteger(keys::kIndexKey, &index));
444    if (index > parent->GetChildCount() || index < 0) {
445      error_ = keys::kInvalidIndexError;
446      return false;
447    }
448  }
449
450  string16 title;
451  json->GetString(keys::kTitleKey, &title);  // Optional.
452  std::string url_string;
453  json->GetString(keys::kUrlKey, &url_string);  // Optional.
454  GURL url(url_string);
455  if (!url.is_empty() && !url.is_valid()) {
456    error_ = keys::kInvalidUrlError;
457    return false;
458  }
459
460  const BookmarkNode* node;
461  if (url_string.length())
462    node = model->AddURL(parent, index, title, url);
463  else
464    node = model->AddGroup(parent, index, title);
465  DCHECK(node);
466  if (!node) {
467    error_ = keys::kNoNodeError;
468    return false;
469  }
470
471  DictionaryValue* ret =
472      extension_bookmark_helpers::GetNodeDictionary(node, false, false);
473  result_.reset(ret);
474
475  return true;
476}
477
478// static
479bool MoveBookmarkFunction::ExtractIds(const ListValue* args,
480                                      std::list<int64>* ids,
481                                      bool* invalid_id) {
482  // For now, Move accepts ID parameters in the same way as an Update.
483  return UpdateBookmarkFunction::ExtractIds(args, ids, invalid_id);
484}
485
486bool MoveBookmarkFunction::RunImpl() {
487  std::list<int64> ids;
488  bool invalid_id = false;
489  EXTENSION_FUNCTION_VALIDATE(ExtractIds(args_.get(), &ids, &invalid_id));
490  if (invalid_id) {
491    error_ = keys::kInvalidIdError;
492    return false;
493  }
494  EXTENSION_FUNCTION_VALIDATE(ids.size() == 1);
495
496  DictionaryValue* destination;
497  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(1, &destination));
498
499  BookmarkModel* model = profile()->GetBookmarkModel();
500  const BookmarkNode* node = model->GetNodeByID(ids.front());
501  if (!node) {
502    error_ = keys::kNoNodeError;
503    return false;
504  }
505  if (node == model->root_node() ||
506      node == model->other_node() ||
507      node == model->GetBookmarkBarNode()) {
508    error_ = keys::kModifySpecialError;
509    return false;
510  }
511
512  const BookmarkNode* parent = NULL;
513  if (!destination->HasKey(keys::kParentIdKey)) {
514    // Optional, defaults to current parent.
515    parent = node->GetParent();
516  } else {
517    std::string parentId_string;
518    EXTENSION_FUNCTION_VALIDATE(destination->GetString(keys::kParentIdKey,
519        &parentId_string));
520    int64 parentId;
521    if (!GetBookmarkIdAsInt64(parentId_string, &parentId))
522      return false;
523
524    parent = model->GetNodeByID(parentId);
525  }
526  if (!parent) {
527    error_ = keys::kNoParentError;
528    // TODO(erikkay) return an error message.
529    return false;
530  }
531  if (parent == model->root_node()) {
532    error_ = keys::kModifySpecialError;
533    return false;
534  }
535
536  int index;
537  if (destination->HasKey(keys::kIndexKey)) {  // Optional (defaults to end).
538    EXTENSION_FUNCTION_VALIDATE(destination->GetInteger(keys::kIndexKey,
539                                                        &index));
540    if (index > parent->GetChildCount() || index < 0) {
541      error_ = keys::kInvalidIndexError;
542      return false;
543    }
544  } else {
545    index = parent->GetChildCount();
546  }
547
548  model->Move(node, parent, index);
549
550  DictionaryValue* ret =
551      extension_bookmark_helpers::GetNodeDictionary(node, false, false);
552  result_.reset(ret);
553
554  return true;
555}
556
557// static
558bool UpdateBookmarkFunction::ExtractIds(const ListValue* args,
559                                        std::list<int64>* ids,
560                                        bool* invalid_id) {
561  // For now, Update accepts ID parameters in the same way as an Remove.
562  return RemoveBookmarkFunction::ExtractIds(args, ids, invalid_id);
563}
564
565bool UpdateBookmarkFunction::RunImpl() {
566  std::list<int64> ids;
567  bool invalid_id = false;
568  EXTENSION_FUNCTION_VALIDATE(ExtractIds(args_.get(), &ids, &invalid_id));
569  if (invalid_id) {
570    error_ = keys::kInvalidIdError;
571    return false;
572  }
573  EXTENSION_FUNCTION_VALIDATE(ids.size() == 1);
574
575  DictionaryValue* updates;
576  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(1, &updates));
577
578  // Optional but we need to distinguish non present from an empty title.
579  string16 title;
580  const bool has_title = updates->GetString(keys::kTitleKey, &title);
581
582  // Optional.
583  std::string url_string;
584  updates->GetString(keys::kUrlKey, &url_string);
585  GURL url(url_string);
586  if (!url_string.empty() && !url.is_valid()) {
587    error_ = keys::kInvalidUrlError;
588    return false;
589  }
590
591  BookmarkModel* model = profile()->GetBookmarkModel();
592  const BookmarkNode* node = model->GetNodeByID(ids.front());
593  if (!node) {
594    error_ = keys::kNoNodeError;
595    return false;
596  }
597  if (node == model->root_node() ||
598      node == model->other_node() ||
599      node == model->GetBookmarkBarNode()) {
600    error_ = keys::kModifySpecialError;
601    return false;
602  }
603  if (has_title)
604    model->SetTitle(node, title);
605  if (!url.is_empty())
606    model->SetURL(node, url);
607
608  DictionaryValue* ret =
609      extension_bookmark_helpers::GetNodeDictionary(node, false, false);
610  result_.reset(ret);
611
612  return true;
613}
614
615// Mapper superclass for BookmarkFunctions.
616template <typename BucketIdType>
617class BookmarkBucketMapper : public BucketMapper {
618 public:
619  virtual ~BookmarkBucketMapper() { STLDeleteValues(&buckets_); }
620 protected:
621  Bucket* GetBucket(const BucketIdType& id) {
622    Bucket* b = buckets_[id];
623    if (b == NULL) {
624      b = new Bucket();
625      buckets_[id] = b;
626    }
627    return b;
628  }
629 private:
630  std::map<BucketIdType, Bucket*> buckets_;
631};
632
633// Mapper for 'bookmarks.create'.  Maps "same input to bookmarks.create" to a
634// unique bucket.
635class CreateBookmarkBucketMapper : public BookmarkBucketMapper<std::string> {
636 public:
637  explicit CreateBookmarkBucketMapper(Profile* profile) : profile_(profile) {}
638  // TODO(tim): This should share code with CreateBookmarkFunction::RunImpl,
639  // but I can't figure out a good way to do that with all the macros.
640  virtual void GetBucketsForArgs(const ListValue* args, BucketList* buckets) {
641    DictionaryValue* json;
642    if (!args->GetDictionary(0, &json))
643      return;
644
645    std::string parent_id;
646    if (json->HasKey(keys::kParentIdKey)) {
647      if (!json->GetString(keys::kParentIdKey, &parent_id))
648        return;
649    }
650    BookmarkModel* model = profile_->GetBookmarkModel();
651
652    int64 parent_id_int64;
653    base::StringToInt64(parent_id, &parent_id_int64);
654    const BookmarkNode* parent = model->GetNodeByID(parent_id_int64);
655    if (!parent)
656      return;
657
658    std::string bucket_id = UTF16ToUTF8(parent->GetTitle());
659    std::string title;
660    json->GetString(keys::kTitleKey, &title);
661    std::string url_string;
662    json->GetString(keys::kUrlKey, &url_string);
663
664    bucket_id += title;
665    bucket_id += url_string;
666    // 20 bytes (SHA1 hash length) is very likely less than most of the
667    // |bucket_id| strings we construct here, so we hash it to save space.
668    buckets->push_back(GetBucket(base::SHA1HashString(bucket_id)));
669  }
670 private:
671  Profile* profile_;
672};
673
674// Mapper for 'bookmarks.remove'.
675class RemoveBookmarksBucketMapper : public BookmarkBucketMapper<std::string> {
676 public:
677  explicit RemoveBookmarksBucketMapper(Profile* profile) : profile_(profile) {}
678  virtual void GetBucketsForArgs(const ListValue* args, BucketList* buckets) {
679    typedef std::list<int64> IdList;
680    IdList ids;
681    bool invalid_id = false;
682    if (!RemoveBookmarkFunction::ExtractIds(args, &ids, &invalid_id) ||
683        invalid_id) {
684      return;
685    }
686
687    for (IdList::iterator it = ids.begin(); it != ids.end(); ++it) {
688      BookmarkModel* model = profile_->GetBookmarkModel();
689      const BookmarkNode* node = model->GetNodeByID(*it);
690      if (!node || !node->GetParent())
691        return;
692
693      std::string bucket_id;
694      bucket_id += UTF16ToUTF8(node->GetParent()->GetTitle());
695      bucket_id += UTF16ToUTF8(node->GetTitle());
696      bucket_id += node->GetURL().spec();
697      buckets->push_back(GetBucket(base::SHA1HashString(bucket_id)));
698    }
699  }
700 private:
701  Profile* profile_;
702};
703
704// Mapper for any bookmark function accepting bookmark IDs as parameters, where
705// a distinct ID corresponds to a single item in terms of quota limiting.  This
706// is inappropriate for bookmarks.remove, for example, since repeated removals
707// of the same item will actually have a different ID each time.
708template <class FunctionType>
709class BookmarkIdMapper : public BookmarkBucketMapper<int64> {
710 public:
711  typedef std::list<int64> IdList;
712  virtual void GetBucketsForArgs(const ListValue* args, BucketList* buckets) {
713    IdList ids;
714    bool invalid_id = false;
715    if (!FunctionType::ExtractIds(args, &ids, &invalid_id) || invalid_id)
716      return;
717    for (IdList::iterator it = ids.begin(); it != ids.end(); ++it)
718      buckets->push_back(GetBucket(*it));
719  }
720};
721
722// Builds heuristics for all BookmarkFunctions using specialized BucketMappers.
723class BookmarksQuotaLimitFactory {
724 public:
725  // For id-based bookmark functions.
726  template <class FunctionType>
727  static void Build(QuotaLimitHeuristics* heuristics) {
728    BuildWithMappers(heuristics, new BookmarkIdMapper<FunctionType>(),
729                                 new BookmarkIdMapper<FunctionType>());
730  }
731
732  // For bookmarks.create.
733  static void BuildForCreate(QuotaLimitHeuristics* heuristics,
734                             Profile* profile) {
735    BuildWithMappers(heuristics, new CreateBookmarkBucketMapper(profile),
736                                 new CreateBookmarkBucketMapper(profile));
737  }
738
739  // For bookmarks.remove.
740  static void BuildForRemove(QuotaLimitHeuristics* heuristics,
741                             Profile* profile) {
742    BuildWithMappers(heuristics, new RemoveBookmarksBucketMapper(profile),
743                                 new RemoveBookmarksBucketMapper(profile));
744  }
745
746 private:
747  static void BuildWithMappers(QuotaLimitHeuristics* heuristics,
748      BucketMapper* short_mapper, BucketMapper* long_mapper) {
749    TimedLimit* timed = new TimedLimit(kLongLimitConfig, long_mapper);
750    // A max of two operations per minute, sustained over 10 minutes.
751    SustainedLimit* sustained = new SustainedLimit(TimeDelta::FromMinutes(10),
752        kShortLimitConfig, short_mapper);
753    heuristics->push_back(timed);
754    heuristics->push_back(sustained);
755  }
756
757  // The quota configurations used for all BookmarkFunctions.
758  static const Config kShortLimitConfig;
759  static const Config kLongLimitConfig;
760
761  DISALLOW_IMPLICIT_CONSTRUCTORS(BookmarksQuotaLimitFactory);
762};
763
764const Config BookmarksQuotaLimitFactory::kShortLimitConfig = {
765  2,                         // 2 tokens per interval.
766  TimeDelta::FromMinutes(1)  // 1 minute long refill interval.
767};
768
769const Config BookmarksQuotaLimitFactory::kLongLimitConfig = {
770  100,                       // 100 tokens per interval.
771  TimeDelta::FromHours(1)    // 1 hour long refill interval.
772};
773
774// And finally, building the individual heuristics for each function.
775void RemoveBookmarkFunction::GetQuotaLimitHeuristics(
776    QuotaLimitHeuristics* heuristics) const {
777  BookmarksQuotaLimitFactory::BuildForRemove(heuristics, profile());
778}
779
780void MoveBookmarkFunction::GetQuotaLimitHeuristics(
781    QuotaLimitHeuristics* heuristics) const {
782  BookmarksQuotaLimitFactory::Build<MoveBookmarkFunction>(heuristics);
783}
784
785void UpdateBookmarkFunction::GetQuotaLimitHeuristics(
786    QuotaLimitHeuristics* heuristics) const {
787  BookmarksQuotaLimitFactory::Build<UpdateBookmarkFunction>(heuristics);
788};
789
790void CreateBookmarkFunction::GetQuotaLimitHeuristics(
791    QuotaLimitHeuristics* heuristics) const {
792  BookmarksQuotaLimitFactory::BuildForCreate(heuristics, profile());
793}
794
795BookmarksIOFunction::BookmarksIOFunction() {}
796
797BookmarksIOFunction::~BookmarksIOFunction() {}
798
799void BookmarksIOFunction::SelectFile(SelectFileDialog::Type type) {
800  // Balanced in one of the three callbacks of SelectFileDialog:
801  // either FileSelectionCanceled, MultiFilesSelected, or FileSelected
802  AddRef();
803  select_file_dialog_ = SelectFileDialog::Create(this);
804  SelectFileDialog::FileTypeInfo file_type_info;
805  file_type_info.extensions.resize(1);
806  file_type_info.extensions[0].push_back(FILE_PATH_LITERAL("html"));
807
808  // Pre-populating the filename field in case this is a SELECT_SAVEAS_FILE
809  // dialog. If not, there is no filename field in the dialog box.
810  FilePath default_path;
811  if (type == SelectFileDialog::SELECT_SAVEAS_FILE)
812    default_path = FilePath(GetDefaultFilenameForBookmarkExport());
813  else
814    DCHECK(type == SelectFileDialog::SELECT_OPEN_FILE);
815
816  select_file_dialog_->SelectFile(type,
817                                  string16(),
818                                  default_path,
819                                  &file_type_info,
820                                  0,
821                                  FILE_PATH_LITERAL(""),
822                                  NULL,
823                                  NULL);
824}
825
826void BookmarksIOFunction::FileSelectionCanceled(void* params) {
827  Release();  // Balanced in BookmarksIOFunction::SelectFile()
828}
829
830void BookmarksIOFunction::MultiFilesSelected(
831    const std::vector<FilePath>& files, void* params) {
832  Release();  // Balanced in BookmarsIOFunction::SelectFile()
833  NOTREACHED() << "Should not be able to select multiple files";
834}
835
836bool ImportBookmarksFunction::RunImpl() {
837  SelectFile(SelectFileDialog::SELECT_OPEN_FILE);
838  return true;
839}
840
841void ImportBookmarksFunction::FileSelected(const FilePath& path,
842                                           int index,
843                                           void* params) {
844  scoped_refptr<ImporterHost> importer_host(new ImporterHost);
845  importer::ProfileInfo profile_info;
846  profile_info.browser_type = importer::BOOKMARKS_HTML;
847  profile_info.source_path = path;
848  importer_host->StartImportSettings(profile_info,
849                                     profile(),
850                                     importer::FAVORITES,
851                                     new ProfileWriter(profile()),
852                                     true);
853  Release();  // Balanced in BookmarksIOFunction::SelectFile()
854}
855
856bool ExportBookmarksFunction::RunImpl() {
857  SelectFile(SelectFileDialog::SELECT_SAVEAS_FILE);
858  return true;
859}
860
861void ExportBookmarksFunction::FileSelected(const FilePath& path,
862                                           int index,
863                                           void* params) {
864  bookmark_html_writer::WriteBookmarks(profile(), path, NULL);
865  Release();  // Balanced in BookmarksIOFunction::SelectFile()
866}
867