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